vuetify eingebunden + erster Versuch Combobox

This commit is contained in:
2019-04-17 15:58:15 +02:00
parent 24642ca1a8
commit 56f4a3f940
1846 changed files with 219701 additions and 53848 deletions
+79
View File
@@ -0,0 +1,79 @@
import { factory as PositionableFactory } from './positionable'
import { TargetProp } from '../components/Vuetify/mixins/application'
// Util
import mixins from '../util/mixins'
export default function applicationable (value: TargetProp, events: string[] = []) {
/* @vue/component */
return mixins(PositionableFactory(['absolute', 'fixed'])).extend({
name: 'applicationable',
props: {
app: Boolean
},
computed: {
applicationProperty (): TargetProp {
return value
}
},
watch: {
// If previous value was app
// reset the provided prop
app (x: boolean, prev: boolean) {
prev
? this.removeApplication(true)
: this.callUpdate()
},
applicationProperty (newVal, oldVal) {
this.$vuetify.application.unbind(this._uid, oldVal)
}
},
activated () {
this.callUpdate()
},
created () {
for (let i = 0, length = events.length; i < length; i++) {
this.$watch(events[i], this.callUpdate)
}
this.callUpdate()
},
mounted () {
this.callUpdate()
},
deactivated () {
this.removeApplication()
},
destroyed () {
this.removeApplication()
},
methods: {
callUpdate () {
if (!this.app) return
this.$vuetify.application.bind(
this._uid,
this.applicationProperty,
this.updateApplication()
)
},
removeApplication (force = false) {
if (!force && !this.app) return
this.$vuetify.application.unbind(
this._uid,
this.applicationProperty
)
},
updateApplication: () => 0
}
})
}
+44
View File
@@ -0,0 +1,44 @@
import Vue, { VNode } from 'vue'
interface Toggleable extends Vue {
isActive?: boolean
}
/**
* Bootable
* @mixin
*
* Used to add lazy content functionality to components
* Looks for change in "isActive" to automatically boot
* Otherwise can be set manually
*/
/* @vue/component */
export default Vue.extend<Vue & Toggleable>().extend({
name: 'bootable',
props: {
lazy: Boolean
},
data: () => ({
isBooted: false
}),
computed: {
hasContent (): boolean | undefined {
return this.isBooted || !this.lazy || this.isActive
}
},
watch: {
isActive () {
this.isBooted = true
}
},
methods: {
showLazyContent (content?: VNode[]): VNode[] | undefined {
return this.hasContent ? content : undefined
}
}
})
+26
View File
@@ -0,0 +1,26 @@
// Extensions
import { BaseItemGroup } from '../components/VItemGroup/VItemGroup'
/* @vue/component */
export default BaseItemGroup.extend({
name: 'button-group',
provide (): object {
return {
btnToggle: this
}
},
props: {
activeClass: {
type: String,
default: 'v-btn--active'
}
},
computed: {
classes (): object {
return BaseItemGroup.options.computed.classes.call(this)
}
}
})
+53
View File
@@ -0,0 +1,53 @@
import Vue from 'vue'
import { VNodeData } from 'vue/types/vnode'
function isCssColor (color?: string | false): boolean {
return !!color && !!color.match(/^(#|(rgb|hsl)a?\()/)
}
export default Vue.extend({
name: 'colorable',
props: {
color: String
},
methods: {
setBackgroundColor (color?: string | false, data: VNodeData = {}): VNodeData {
if (isCssColor(color)) {
data.style = {
...data.style as object,
'background-color': `${color}`,
'border-color': `${color}`
}
} else if (color) {
data.class = {
...data.class,
[color]: true
}
}
return data
},
setTextColor (color?: string | false, data: VNodeData = {}): VNodeData {
if (isCssColor(color)) {
data.style = {
...data.style as object,
'color': `${color}`,
'caret-color': `${color}`
}
} else if (color) {
const [colorName, colorModifier] = color.toString().trim().split(' ', 2) as (string | undefined)[]
data.class = {
...data.class,
[colorName + '--text']: true
}
if (colorModifier) {
data.class['text--' + colorModifier] = true
}
}
return data
}
}
})
+13
View File
@@ -0,0 +1,13 @@
import Vue from 'vue'
import { PropValidator } from 'vue/types/options'
import { deepEqual } from '../util/helpers'
export default Vue.extend({
name: 'comparable',
props: {
valueComparator: {
type: Function,
default: deepEqual
} as any as PropValidator<typeof deepEqual>
}
})
+495
View File
@@ -0,0 +1,495 @@
import VBtn from '../components/VBtn'
import VIcon from '../components/VIcon'
import VSelect from '../components/VSelect'
import Filterable from './filterable'
import Themeable from './themeable'
import Loadable from './loadable'
import { getObjectValueByPath, isObject } from '../util/helpers'
import { consoleWarn } from '../util/console'
/**
* DataIterable
*
* @mixin
*
* Base behavior for data table and data iterator
* providing selection, pagination, sorting and filtering.
*
*/
/* @vue/component */
export default {
name: 'data-iterable',
mixins: [Filterable, Loadable, Themeable],
props: {
expand: Boolean,
hideActions: Boolean,
disableInitialSort: Boolean,
mustSort: Boolean,
noResultsText: {
type: String,
default: '$vuetify.dataIterator.noResultsText'
},
nextIcon: {
type: String,
default: '$vuetify.icons.next'
},
prevIcon: {
type: String,
default: '$vuetify.icons.prev'
},
rowsPerPageItems: {
type: Array,
default () {
return [
5,
10,
25,
{
text: '$vuetify.dataIterator.rowsPerPageAll',
value: -1
}
]
}
},
rowsPerPageText: {
type: String,
default: '$vuetify.dataIterator.rowsPerPageText'
},
selectAll: [Boolean, String],
search: {
required: false
},
filter: {
type: Function,
default: (val, search) => {
return val != null &&
typeof val !== 'boolean' &&
val.toString().toLowerCase().indexOf(search) !== -1
}
},
customFilter: {
type: Function,
default: (items, search, filter) => {
search = search.toString().toLowerCase()
if (search.trim() === '') return items
return items.filter(i => (
Object.keys(i).some(j => filter(i[j], search))
))
}
},
customSort: {
type: Function,
default: (items, index, isDescending) => {
if (index === null) return items
return items.sort((a, b) => {
let sortA = getObjectValueByPath(a, index)
let sortB = getObjectValueByPath(b, index)
if (isDescending) {
[sortA, sortB] = [sortB, sortA]
}
// Check if both are numbers
if (!isNaN(sortA) && !isNaN(sortB)) {
return sortA - sortB
}
// Check if both cannot be evaluated
if (sortA === null && sortB === null) {
return 0
}
[sortA, sortB] = [sortA, sortB]
.map(s => (
(s || '').toString().toLocaleLowerCase()
))
if (sortA > sortB) return 1
if (sortA < sortB) return -1
return 0
})
}
},
value: {
type: Array,
default: () => []
},
items: {
type: Array,
required: true,
default: () => []
},
totalItems: {
type: Number,
default: null
},
itemKey: {
type: String,
default: 'id'
},
pagination: {
type: Object,
default: () => {}
}
},
data: () => ({
searchLength: 0,
defaultPagination: {
descending: false,
page: 1,
rowsPerPage: 5,
sortBy: null,
totalItems: 0
},
expanded: {},
actionsClasses: 'v-data-iterator__actions',
actionsRangeControlsClasses: 'v-data-iterator__actions__range-controls',
actionsSelectClasses: 'v-data-iterator__actions__select',
actionsPaginationClasses: 'v-data-iterator__actions__pagination'
}),
computed: {
computedPagination () {
return this.hasPagination
? this.pagination
: this.defaultPagination
},
computedRowsPerPageItems () {
return this.rowsPerPageItems.map(item => {
return isObject(item)
? Object.assign({}, item, {
text: this.$vuetify.t(item.text)
})
: { value: item, text: Number(item).toLocaleString(this.$vuetify.lang.current) }
})
},
hasPagination () {
const pagination = this.pagination || {}
return Object.keys(pagination).length > 0
},
hasSelectAll () {
return this.selectAll !== undefined && this.selectAll !== false
},
itemsLength () {
if (this.hasSearch) return this.searchLength
return this.totalItems || this.items.length
},
indeterminate () {
return this.hasSelectAll && this.someItems && !this.everyItem
},
everyItem () {
return this.filteredItems.length &&
this.filteredItems.every(i => this.isSelected(i))
},
someItems () {
return this.filteredItems.some(i => this.isSelected(i))
},
getPage () {
const { rowsPerPage } = this.computedPagination
return rowsPerPage === Object(rowsPerPage)
? rowsPerPage.value
: rowsPerPage
},
pageStart () {
return this.getPage === -1
? 0
: (this.computedPagination.page - 1) * this.getPage
},
pageStop () {
return this.getPage === -1
? this.itemsLength
: this.computedPagination.page * this.getPage
},
filteredItems () {
return this.filteredItemsImpl()
},
selected () {
const selected = {}
for (let index = 0; index < this.value.length; index++) {
const key = getObjectValueByPath(this.value[index], this.itemKey)
selected[key] = true
}
return selected
},
hasSearch () {
return this.search != null
}
},
watch: {
items () {
if (this.pageStart >= this.itemsLength) {
this.resetPagination()
}
const newItemKeys = new Set(this.items.map(item => getObjectValueByPath(item, this.itemKey)))
const selection = this.value.filter(item => newItemKeys.has(getObjectValueByPath(item, this.itemKey)))
if (selection.length !== this.value.length) {
this.$emit('input', selection)
}
},
search () {
this.$nextTick(() => {
this.updatePagination({ page: 1, totalItems: this.itemsLength })
})
},
'computedPagination.sortBy': 'resetPagination',
'computedPagination.descending': 'resetPagination'
},
methods: {
initPagination () {
if (!this.rowsPerPageItems.length) {
consoleWarn(`The prop 'rows-per-page-items' can not be empty`, this)
} else {
this.defaultPagination.rowsPerPage = this.rowsPerPageItems[0]
}
this.defaultPagination.totalItems = this.items.length
this.updatePagination(
Object.assign({}, this.defaultPagination, this.pagination)
)
},
updatePagination (val) {
const pagination = this.hasPagination
? this.pagination
: this.defaultPagination
const updatedPagination = Object.assign({}, pagination, val)
this.$emit('update:pagination', updatedPagination)
if (!this.hasPagination) {
this.defaultPagination = updatedPagination
}
},
isSelected (item) {
return this.selected[getObjectValueByPath(item, this.itemKey)]
},
isExpanded (item) {
return this.expanded[getObjectValueByPath(item, this.itemKey)]
},
filteredItemsImpl (...additionalFilterArgs) {
if (this.totalItems) return this.items
let items = this.items.slice()
if (this.hasSearch) {
items = this.customFilter(items, this.search, this.filter, ...additionalFilterArgs)
this.searchLength = items.length
}
items = this.customSort(
items,
this.computedPagination.sortBy,
this.computedPagination.descending
)
return this.hideActions &&
!this.hasPagination
? items
: items.slice(this.pageStart, this.pageStop)
},
resetPagination () {
this.computedPagination.page !== 1 &&
this.updatePagination({ page: 1 })
},
sort (index) {
const { sortBy, descending } = this.computedPagination
if (sortBy === null) {
this.updatePagination({ sortBy: index, descending: false })
} else if (sortBy === index && !descending) {
this.updatePagination({ descending: true })
} else if (sortBy !== index) {
this.updatePagination({ sortBy: index, descending: false })
} else if (!this.mustSort) {
this.updatePagination({ sortBy: null, descending: null })
} else {
this.updatePagination({ sortBy: index, descending: false })
}
},
toggle (value) {
const selected = Object.assign({}, this.selected)
for (let index = 0; index < this.filteredItems.length; index++) {
const key = getObjectValueByPath(this.filteredItems[index], this.itemKey)
selected[key] = value
}
this.$emit('input', this.items.filter(i => {
const key = getObjectValueByPath(i, this.itemKey)
return selected[key]
}))
},
createProps (item, index) {
const props = { item, index }
const keyProp = this.itemKey
const itemKey = getObjectValueByPath(item, keyProp)
Object.defineProperty(props, 'selected', {
get: () => this.selected[itemKey],
set: value => {
if (itemKey == null) {
consoleWarn(`"${keyProp}" attribute must be defined for item`, this)
}
let selected = this.value.slice()
if (value) selected.push(item)
else selected = selected.filter(i => getObjectValueByPath(i, keyProp) !== itemKey)
this.$emit('input', selected)
}
})
Object.defineProperty(props, 'expanded', {
get: () => this.expanded[itemKey],
set: value => {
if (itemKey == null) {
consoleWarn(`"${keyProp}" attribute must be defined for item`, this)
}
if (!this.expand) {
for (const key in this.expanded) {
this.expanded.hasOwnProperty(key) && this.$set(this.expanded, key, false)
}
}
this.$set(this.expanded, itemKey, value)
}
})
return props
},
genItems () {
if (!this.itemsLength && !this.items.length) {
const noData = this.$slots['no-data'] || this.$vuetify.t(this.noDataText)
return [this.genEmptyItems(noData)]
}
if (!this.filteredItems.length) {
const noResults = this.$slots['no-results'] || this.$vuetify.t(this.noResultsText)
return [this.genEmptyItems(noResults)]
}
return this.genFilteredItems()
},
genPrevIcon () {
return this.$createElement(VBtn, {
props: {
disabled: this.computedPagination.page === 1,
icon: true,
flat: true
},
on: {
click: () => {
const page = this.computedPagination.page
this.updatePagination({ page: page - 1 })
}
},
attrs: {
'aria-label': this.$vuetify.t('$vuetify.dataIterator.prevPage')
}
}, [this.$createElement(VIcon, this.$vuetify.rtl ? this.nextIcon : this.prevIcon)])
},
genNextIcon () {
const pagination = this.computedPagination
const disabled = pagination.rowsPerPage < 0 ||
pagination.page * pagination.rowsPerPage >= this.itemsLength ||
this.pageStop < 0
return this.$createElement(VBtn, {
props: {
disabled,
icon: true,
flat: true
},
on: {
click: () => {
const page = this.computedPagination.page
this.updatePagination({ page: page + 1 })
}
},
attrs: {
'aria-label': this.$vuetify.t('$vuetify.dataIterator.nextPage')
}
}, [this.$createElement(VIcon, this.$vuetify.rtl ? this.prevIcon : this.nextIcon)])
},
genSelect () {
return this.$createElement('div', {
'class': this.actionsSelectClasses
}, [
this.$vuetify.t(this.rowsPerPageText),
this.$createElement(VSelect, {
attrs: {
'aria-label': this.$vuetify.t(this.rowsPerPageText)
},
props: {
items: this.computedRowsPerPageItems,
value: this.computedPagination.rowsPerPage,
hideDetails: true,
menuProps: {
auto: true,
dark: this.dark,
light: this.light,
minWidth: '75px'
}
},
on: {
input: val => {
this.updatePagination({
page: 1,
rowsPerPage: val
})
}
}
})
])
},
genPagination () {
let pagination = '–'
if (this.itemsLength) {
const stop = this.itemsLength < this.pageStop || this.pageStop < 0
? this.itemsLength
: this.pageStop
pagination = this.$scopedSlots.pageText
? this.$scopedSlots.pageText({
pageStart: this.pageStart + 1,
pageStop: stop,
itemsLength: this.itemsLength
})
: this.$vuetify.t('$vuetify.dataIterator.pageText',
...([this.pageStart + 1, stop, this.itemsLength].map(n => Number(n).toLocaleString(this.$vuetify.lang.current))))
}
return this.$createElement('div', {
'class': this.actionsPaginationClasses
}, [pagination])
},
genActions () {
const rangeControls = this.$createElement('div', {
'class': this.actionsRangeControlsClasses
}, [
this.genPagination(),
this.genPrevIcon(),
this.genNextIcon()
])
return [this.$createElement('div', {
'class': this.actionsClasses
}, [
this.$slots['actions-prepend'] ? this.$createElement('div', {}, this.$slots['actions-prepend']) : null,
this.rowsPerPageItems.length > 1 ? this.genSelect() : null,
rangeControls,
this.$slots['actions-append'] ? this.$createElement('div', {}, this.$slots['actions-append']) : null
])]
}
}
}
+50
View File
@@ -0,0 +1,50 @@
import Vue from 'vue'
/**
* Delayable
*
* @mixin
*
* Changes the open or close delay time for elements
*/
export default Vue.extend<Vue & { isActive?: boolean }>().extend({
name: 'delayable',
props: {
openDelay: {
type: [Number, String],
default: 0
},
closeDelay: {
type: [Number, String],
default: 0
}
},
data: () => ({
openTimeout: undefined as number | undefined,
closeTimeout: undefined as number | undefined
}),
methods: {
/**
* Clear any pending delay timers from executing
*/
clearDelay (): void {
clearTimeout(this.openTimeout)
clearTimeout(this.closeTimeout)
},
/**
* Runs callback after a specified delay
*/
runDelay (type: 'open' | 'close', cb?: () => void): void {
this.clearDelay()
const delay = parseInt((this as any)[`${type}Delay`], 10)
;(this as any)[`${type}Timeout`] = setTimeout(cb || (() => {
this.isActive = { open: true, close: false }[type]
}), delay)
}
}
})
+80
View File
@@ -0,0 +1,80 @@
import Vue from 'vue'
import mixins from '../util/mixins'
interface options extends Vue {
$el: HTMLElement
$refs: {
content: HTMLElement
}
overlay?: HTMLElement
}
interface DependentInstance extends Vue {
isActive?: boolean
isDependent?: boolean
}
function searchChildren (children: Vue[]): DependentInstance[] {
const results = []
for (let index = 0; index < children.length; index++) {
const child = children[index] as DependentInstance
if (child.isActive && child.isDependent) {
results.push(child)
} else {
results.push(...searchChildren(child.$children))
}
}
return results
}
/* @vue/component */
export default mixins<options>().extend({
name: 'dependent',
data () {
return {
closeDependents: true,
isActive: false,
isDependent: true
}
},
watch: {
isActive (val) {
if (val) return
const openDependents = this.getOpenDependents()
for (let index = 0; index < openDependents.length; index++) {
openDependents[index].isActive = false
}
}
},
methods: {
getOpenDependents (): any[] {
if (this.closeDependents) return searchChildren(this.$children)
return []
},
getOpenDependentElements (): HTMLElement[] {
const result = []
const openDependents = this.getOpenDependents()
for (let index = 0; index < openDependents.length; index++) {
result.push(...openDependents[index].getClickableDependentElements())
}
return result
},
getClickableDependentElements (): HTMLElement[] {
const result = [this.$el]
if (this.$refs.content) result.push(this.$refs.content)
if (this.overlay) result.push(this.overlay)
result.push(...this.getOpenDependentElements())
return result
}
}
})
+119
View File
@@ -0,0 +1,119 @@
import Bootable from './bootable'
import { consoleWarn } from '../util/console'
function validateAttachTarget (val) {
const type = typeof val
if (type === 'boolean' || type === 'string') return true
return val.nodeType === Node.ELEMENT_NODE
}
/* @vue/component */
export default {
name: 'detachable',
mixins: [Bootable],
props: {
attach: {
type: null,
default: false,
validator: validateAttachTarget
},
contentClass: {
default: ''
}
},
data: () => ({
hasDetached: false
}),
watch: {
attach () {
this.hasDetached = false
this.initDetach()
},
hasContent: 'initDetach'
},
beforeMount () {
this.$nextTick(() => {
if (this.activatorNode) {
const activator = Array.isArray(this.activatorNode) ? this.activatorNode : [this.activatorNode]
activator.forEach(node => {
node.elm && this.$el.parentNode.insertBefore(node.elm, this.$el)
})
}
})
},
mounted () {
!this.lazy && this.initDetach()
},
deactivated () {
this.isActive = false
},
beforeDestroy () {
// IE11 Fix
try {
if (this.$refs.content) {
this.$refs.content.parentNode.removeChild(this.$refs.content)
}
if (this.activatorNode) {
const activator = Array.isArray(this.activatorNode) ? this.activatorNode : [this.activatorNode]
activator.forEach(node => {
node.elm && node.elm.parentNode.removeChild(node.elm)
})
}
} catch (e) { console.log(e) }
},
methods: {
getScopeIdAttrs () {
const scopeId = this.$vnode && this.$vnode.context.$options._scopeId
return scopeId && {
[scopeId]: ''
}
},
initDetach () {
if (this._isDestroyed ||
!this.$refs.content ||
this.hasDetached ||
// Leave menu in place if attached
// and dev has not changed target
this.attach === '' || // If used as a boolean prop (<v-menu attach>)
this.attach === true || // If bound to a boolean (<v-menu :attach="true">)
this.attach === 'attach' // If bound as boolean prop in pug (v-menu(attach))
) return
let target
if (this.attach === false) {
// Default, detach to app
target = document.querySelector('[data-app]')
} else if (typeof this.attach === 'string') {
// CSS selector
target = document.querySelector(this.attach)
} else {
// DOM Element
target = this.attach
}
if (!target) {
consoleWarn(`Unable to locate target ${this.attach || '[data-app]'}`, this)
return
}
target.insertBefore(
this.$refs.content,
target.firstChild
)
this.hasDetached = true
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import Vue from 'vue'
export default Vue.extend({
name: 'elevatable',
props: {
elevation: [Number, String]
},
computed: {
computedElevation (): string | number | undefined {
return this.elevation
},
elevationClasses (): Record<string, boolean> {
if (!this.computedElevation) return {}
return { [`elevation-${this.computedElevation}`]: true }
}
}
})
+13
View File
@@ -0,0 +1,13 @@
import Vue from 'vue'
/* @vue/component */
export default Vue.extend({
name: 'filterable',
props: {
noDataText: {
type: String,
default: '$vuetify.noDataText'
}
}
})
+72
View File
@@ -0,0 +1,72 @@
// Mixins
import { Registrable, inject as RegistrableInject } from './registrable'
// Utilities
import { ExtractVue } from '../util/mixins'
import { PropValidator } from 'vue/types/options'
import { VueConstructor } from 'vue'
/* eslint-disable-next-line no-use-before-define */
export type Groupable<T extends string> = VueConstructor<ExtractVue<Registrable<T>> & {
activeClass: string
isActive: boolean
groupClasses: object
toggle (): void
}>
export function factory<T extends string> (
namespace: T,
child?: string,
parent?: string
): Groupable<T> {
return RegistrableInject(namespace, child, parent).extend({
name: 'groupable',
props: {
activeClass: {
type: String,
default (): string | undefined {
if (!this[namespace]) return undefined
return this[namespace].activeClass
}
} as any as PropValidator<string>,
disabled: Boolean
},
data () {
return {
isActive: false
}
},
computed: {
groupClasses (): object {
if (!this.activeClass) return {}
return {
[this.activeClass]: this.isActive
}
}
},
created () {
this[namespace] && (this[namespace] as any).register(this)
},
beforeDestroy () {
this[namespace] && (this[namespace] as any).unregister(this)
},
methods: {
toggle () {
this.$emit('change')
}
}
})
}
/* eslint-disable-next-line no-redeclare */
const Groupable = factory('itemGroup')
export default Groupable
+43
View File
@@ -0,0 +1,43 @@
import Vue, { VNode } from 'vue'
import VProgressLinear from '../components/VProgressLinear'
interface colorable extends Vue {
color?: string
}
/**
* Loadable
*
* @mixin
*
* Used to add linear progress bar to components
* Can use a default bar with a specific color
* or designate a custom progress linear bar
*/
/* @vue/component */
export default Vue.extend<colorable>().extend({
name: 'loadable',
props: {
loading: {
type: [Boolean, String],
default: false
}
},
methods: {
genProgress (): VNode | VNode[] | null {
if (this.loading === false) return null
return this.$slots.progress || this.$createElement(VProgressLinear, {
props: {
color: (this.loading === true || this.loading === '')
? (this.color || 'primary')
: this.loading,
height: 2,
indeterminate: true
}
})
}
}
})
+153
View File
@@ -0,0 +1,153 @@
/**
* Maskable
*
* @mixin
*
* Creates an input mask that is
* generated from a masked str
*
* Example: mask="#### #### #### ####"
*/
import {
isMaskDelimiter,
maskText,
unmaskText
} from '../util/mask'
/* @vue/component */
export default {
name: 'maskable',
props: {
dontFillMaskBlanks: Boolean,
mask: {
type: [Object, String],
default: null
},
returnMaskedValue: Boolean,
value: { required: false }
},
data: vm => ({
selection: 0,
lazySelection: 0,
lazyValue: vm.value,
preDefined: {
'credit-card': '#### - #### - #### - ####',
'date': '##/##/####',
'date-with-time': '##/##/#### ##:##',
'phone': '(###) ### - ####',
'social': '###-##-####',
'time': '##:##',
'time-with-seconds': '##:##:##'
}
}),
computed: {
masked () {
const preDefined = this.preDefined[this.mask]
const mask = preDefined || this.mask || ''
return mask.split('')
}
},
watch: {
/**
* Make sure the cursor is in the correct
* location when the mask changes
*/
mask () {
if (!this.$refs.input) return
const oldValue = this.$refs.input.value
const newValue = this.maskText(unmaskText(this.lazyValue))
let position = 0
let selection = this.selection
for (let index = 0; index < selection; index++) {
isMaskDelimiter(oldValue[index]) || position++
}
selection = 0
if (newValue) {
for (let index = 0; index < newValue.length; index++) {
isMaskDelimiter(newValue[index]) || position--
selection++
if (position <= 0) break
}
}
this.$nextTick(() => {
this.$refs.input.value = newValue
this.setCaretPosition(selection)
})
}
},
beforeMount () {
if (!this.mask ||
this.value == null ||
!this.returnMaskedValue
) return
const value = this.maskText(this.value)
// See if masked value does not
// match the user given value
if (value === this.value) return
this.$emit('input', value)
},
methods: {
setCaretPosition (selection) {
this.selection = selection
window.setTimeout(() => {
this.$refs.input && this.$refs.input.setSelectionRange(this.selection, this.selection)
}, 0)
},
updateRange () {
/* istanbul ignore next */
if (!this.$refs.input) return
const newValue = this.maskText(this.lazyValue)
let selection = 0
this.$refs.input.value = newValue
if (newValue) {
for (let index = 0; index < newValue.length; index++) {
if (this.lazySelection <= 0) break
isMaskDelimiter(newValue[index]) || this.lazySelection--
selection++
}
}
this.setCaretPosition(selection)
// this.$emit() must occur only when all internal values are correct
this.$emit('input', this.returnMaskedValue ? this.$refs.input.value : this.lazyValue)
},
maskText (text) {
return this.mask ? maskText(text, this.masked, this.dontFillMaskBlanks) : text
},
unmaskText (text) {
return this.mask && !this.returnMaskedValue ? unmaskText(text) : text
},
// When the input changes and is
// re-created, ensure that the
// caret location is correct
setSelectionRange () {
this.$nextTick(this.updateRange)
},
resetSelections (input) {
if (!input.selectionEnd) return
this.selection = input.selectionEnd
this.lazySelection = 0
for (let index = 0; index < this.selection; index++) {
isMaskDelimiter(input.value[index]) || this.lazySelection++
}
}
}
}
+43
View File
@@ -0,0 +1,43 @@
// Helpers
import { convertToUnit } from '../util/helpers'
// Types
import Vue from 'vue'
import { PropValidator } from 'vue/types/options'
export type NumberOrNumberString = PropValidator<string | number | undefined>
export default Vue.extend({
name: 'measurable',
props: {
height: [Number, String] as NumberOrNumberString,
maxHeight: [Number, String] as NumberOrNumberString,
maxWidth: [Number, String] as NumberOrNumberString,
minHeight: [Number, String] as NumberOrNumberString,
minWidth: [Number, String] as NumberOrNumberString,
width: [Number, String] as NumberOrNumberString
},
computed: {
measurableStyles (): object {
const styles: Record<string, string> = {}
const height = convertToUnit(this.height)
const minHeight = convertToUnit(this.minHeight)
const minWidth = convertToUnit(this.minWidth)
const maxHeight = convertToUnit(this.maxHeight)
const maxWidth = convertToUnit(this.maxWidth)
const width = convertToUnit(this.width)
if (height) styles.height = height
if (minHeight) styles.minHeight = minHeight
if (minWidth) styles.minWidth = minWidth
if (maxHeight) styles.maxHeight = maxHeight
if (maxWidth) styles.maxWidth = maxWidth
if (width) styles.width = width
return styles
}
}
})
+390
View File
@@ -0,0 +1,390 @@
import Vue from 'vue'
import Positionable from './positionable'
import Stackable from './stackable'
/* eslint-disable object-property-newline */
const dimensions = {
activator: {
top: 0, left: 0,
bottom: 0, right: 0,
width: 0, height: 0,
offsetTop: 0, scrollHeight: 0
},
content: {
top: 0, left: 0,
bottom: 0, right: 0,
width: 0, height: 0,
offsetTop: 0, scrollHeight: 0
},
hasWindow: false
}
/* eslint-enable object-property-newline */
/**
* Menuable
*
* @mixin
*
* Used for fixed or absolutely positioning
* elements within the DOM
* Can calculate X and Y axis overflows
* As well as be manually positioned
*/
/* @vue/component */
export default Vue.extend({
name: 'menuable',
mixins: [
Positionable,
Stackable
],
props: {
activator: {
default: null,
validator: val => {
return ['string', 'object'].includes(typeof val)
}
},
allowOverflow: Boolean,
inputActivator: Boolean,
light: Boolean,
dark: Boolean,
maxWidth: {
type: [Number, String],
default: 'auto'
},
minWidth: [Number, String],
nudgeBottom: {
type: [Number, String],
default: 0
},
nudgeLeft: {
type: [Number, String],
default: 0
},
nudgeRight: {
type: [Number, String],
default: 0
},
nudgeTop: {
type: [Number, String],
default: 0
},
nudgeWidth: {
type: [Number, String],
default: 0
},
offsetOverflow: Boolean,
positionX: {
type: Number,
default: null
},
positionY: {
type: Number,
default: null
},
zIndex: {
type: [Number, String],
default: null
}
},
data: () => ({
absoluteX: 0,
absoluteY: 0,
activatorFixed: false,
dimensions: Object.assign({}, dimensions),
isContentActive: false,
pageWidth: 0,
pageYOffset: 0,
stackClass: 'v-menu__content--active',
stackMinZIndex: 6
}),
computed: {
computedLeft () {
const a = this.dimensions.activator
const c = this.dimensions.content
const activatorLeft = (this.isAttached ? a.offsetLeft : a.left) || 0
const minWidth = Math.max(a.width, c.width)
let left = 0
left += this.left ? activatorLeft - (minWidth - a.width) : activatorLeft
if (this.offsetX) {
const maxWidth = isNaN(this.maxWidth)
? a.width
: Math.min(a.width, this.maxWidth)
left += this.left ? -maxWidth : a.width
}
if (this.nudgeLeft) left -= parseInt(this.nudgeLeft)
if (this.nudgeRight) left += parseInt(this.nudgeRight)
return left
},
computedTop () {
const a = this.dimensions.activator
const c = this.dimensions.content
let top = 0
if (this.top) top += a.height - c.height
if (this.isAttached) top += a.offsetTop
else top += a.top + this.pageYOffset
if (this.offsetY) top += this.top ? -a.height : a.height
if (this.nudgeTop) top -= parseInt(this.nudgeTop)
if (this.nudgeBottom) top += parseInt(this.nudgeBottom)
return top
},
hasActivator () {
return !!this.$slots.activator || !!this.$scopedSlots.activator || this.activator || this.inputActivator
},
isAttached () {
return this.attach !== false
}
},
watch: {
disabled (val) {
val && this.callDeactivate()
},
isActive (val) {
if (this.disabled) return
val ? this.callActivate() : this.callDeactivate()
},
positionX: 'updateDimensions',
positionY: 'updateDimensions'
},
beforeMount () {
this.checkForWindow()
},
methods: {
absolutePosition () {
return {
offsetTop: 0,
offsetLeft: 0,
scrollHeight: 0,
top: this.positionY || this.absoluteY,
bottom: this.positionY || this.absoluteY,
left: this.positionX || this.absoluteX,
right: this.positionX || this.absoluteX,
height: 0,
width: 0
}
},
activate () {},
calcLeft (menuWidth) {
return `${this.isAttached
? this.computedLeft
: this.calcXOverflow(this.computedLeft, menuWidth)
}px`
},
calcTop () {
return `${this.isAttached
? this.computedTop
: this.calcYOverflow(this.computedTop)
}px`
},
calcXOverflow (left, menuWidth) {
const xOverflow = left + menuWidth - this.pageWidth + 12
if ((!this.left || this.right) && xOverflow > 0) {
left = Math.max(left - xOverflow, 0)
} else {
left = Math.max(left, 12)
}
return left + this.getOffsetLeft()
},
calcYOverflow (top) {
const documentHeight = this.getInnerHeight()
const toTop = this.pageYOffset + documentHeight
const activator = this.dimensions.activator
const contentHeight = this.dimensions.content.height
const totalHeight = top + contentHeight
const isOverflowing = toTop < totalHeight
// If overflowing bottom and offset
// TODO: set 'bottom' position instead of 'top'
if (isOverflowing &&
this.offsetOverflow &&
// If we don't have enough room to offset
// the overflow, don't offset
activator.top > contentHeight
) {
top = this.pageYOffset + (activator.top - contentHeight)
// If overflowing bottom
} else if (isOverflowing && !this.allowOverflow) {
top = toTop - contentHeight - 12
// If overflowing top
} else if (top < this.pageYOffset && !this.allowOverflow) {
top = this.pageYOffset + 12
}
return top < 12 ? 12 : top
},
callActivate () {
if (!this.hasWindow) return
this.activate()
},
callDeactivate () {
this.isContentActive = false
this.deactivate()
},
checkForWindow () {
if (!this.hasWindow) {
this.hasWindow = typeof window !== 'undefined'
}
},
checkForPageYOffset () {
if (this.hasWindow) {
this.pageYOffset = this.activatorFixed ? 0 : this.getOffsetTop()
}
},
checkActivatorFixed () {
if (this.attach !== false) return
let el = this.getActivator()
while (el) {
if (window.getComputedStyle(el).position === 'fixed') {
this.activatorFixed = true
return
}
el = el.offsetParent
}
this.activatorFixed = false
},
deactivate () {},
getActivator (e) {
if (this.inputActivator) {
return this.$el.querySelector('.v-input__slot')
}
if (this.activator) {
return typeof this.activator === 'string'
? document.querySelector(this.activator)
: this.activator
}
if (this.$refs.activator) {
return this.$refs.activator.children.length > 0
? this.$refs.activator.children[0]
: this.$refs.activator
}
if (e) {
this.activatedBy = e.currentTarget || e.target
return this.activatedBy
}
if (this.activatedBy) return this.activatedBy
if (this.activatorNode) {
const activator = Array.isArray(this.activatorNode) ? this.activatorNode[0] : this.activatorNode
const el = activator && activator.elm
if (el) return el
}
},
getInnerHeight () {
if (!this.hasWindow) return 0
return window.innerHeight ||
document.documentElement.clientHeight
},
getOffsetLeft () {
if (!this.hasWindow) return 0
return window.pageXOffset ||
document.documentElement.scrollLeft
},
getOffsetTop () {
if (!this.hasWindow) return 0
return window.pageYOffset ||
document.documentElement.scrollTop
},
getRoundedBoundedClientRect (el) {
const rect = el.getBoundingClientRect()
return {
top: Math.round(rect.top),
left: Math.round(rect.left),
bottom: Math.round(rect.bottom),
right: Math.round(rect.right),
width: Math.round(rect.width),
height: Math.round(rect.height)
}
},
measure (el) {
if (!el || !this.hasWindow) return null
const rect = this.getRoundedBoundedClientRect(el)
// Account for activator margin
if (this.isAttached) {
const style = window.getComputedStyle(el)
rect.left = parseInt(style.marginLeft)
rect.top = parseInt(style.marginTop)
}
return rect
},
sneakPeek (cb) {
requestAnimationFrame(() => {
const el = this.$refs.content
if (!el || this.isShown(el)) return cb()
el.style.display = 'inline-block'
cb()
el.style.display = 'none'
})
},
startTransition () {
return new Promise(resolve => requestAnimationFrame(() => {
this.isContentActive = this.hasJustFocused = this.isActive
resolve()
}))
},
isShown (el) {
return el.style.display !== 'none'
},
updateDimensions () {
this.checkForWindow()
this.checkActivatorFixed()
this.checkForPageYOffset()
this.pageWidth = document.documentElement.clientWidth
const dimensions = {}
// Activator should already be shown
if (!this.hasActivator || this.absolute) {
dimensions.activator = this.absolutePosition()
} else {
const activator = this.getActivator()
dimensions.activator = this.measure(activator)
dimensions.activator.offsetLeft = activator.offsetLeft
if (this.isAttached) {
// account for css padding causing things to not line up
// this is mostly for v-autocomplete, hopefully it won't break anything
dimensions.activator.offsetTop = activator.offsetTop
} else {
dimensions.activator.offsetTop = 0
}
}
// Display and hide to get dimensions
this.sneakPeek(() => {
dimensions.content = this.measure(this.$refs.content)
this.dimensions = dimensions
})
}
}
})
+224
View File
@@ -0,0 +1,224 @@
// Styles
import '../stylus/components/_overlay.styl'
// Utilities
import { keyCodes, addPassiveEventListener } from '../util/helpers'
// Types
import Vue from 'vue'
interface Toggleable extends Vue {
isActive?: boolean
}
interface Stackable extends Vue {
activeZIndex: number
}
interface options {
absolute?: boolean
$refs: {
dialog?: HTMLElement
content?: HTMLElement
}
}
/* @vue/component */
export default Vue.extend<Vue & Toggleable & Stackable & options>().extend({
name: 'overlayable',
props: {
hideOverlay: Boolean
},
data () {
return {
overlay: null as HTMLElement | null,
overlayOffset: 0,
overlayTimeout: undefined as number | undefined,
overlayTransitionDuration: 500 + 150 // transition + delay
}
},
watch: {
hideOverlay (value) {
if (value) this.removeOverlay()
else this.genOverlay()
}
},
beforeDestroy () {
this.removeOverlay()
},
methods: {
genOverlay () {
// If fn is called and timeout is active
// or overlay already exists
// cancel removal of overlay and re-add active
if ((!this.isActive || this.hideOverlay) ||
(this.isActive && this.overlayTimeout) ||
this.overlay
) {
clearTimeout(this.overlayTimeout)
return this.overlay &&
this.overlay.classList.add('v-overlay--active')
}
this.overlay = document.createElement('div')
this.overlay.className = 'v-overlay'
if (this.absolute) this.overlay.className += ' v-overlay--absolute'
this.hideScroll()
const parent = this.absolute
? this.$el.parentNode
: document.querySelector('[data-app]')
parent && parent.insertBefore(this.overlay, parent.firstChild)
// eslint-disable-next-line no-unused-expressions
this.overlay.clientHeight // Force repaint
requestAnimationFrame(() => {
// https://github.com/vuetifyjs/vuetify/issues/4678
if (!this.overlay) return
this.overlay.className += ' v-overlay--active'
if (this.activeZIndex !== undefined) {
this.overlay.style.zIndex = String(this.activeZIndex - 1)
}
})
return true
},
/** removeOverlay(false) will not restore the scollbar afterwards */
removeOverlay (showScroll = true) {
if (!this.overlay) {
return showScroll && this.showScroll()
}
this.overlay.classList.remove('v-overlay--active')
this.overlayTimeout = window.setTimeout(() => {
// IE11 Fix
try {
if (this.overlay && this.overlay.parentNode) {
this.overlay.parentNode.removeChild(this.overlay)
}
this.overlay = null
showScroll && this.showScroll()
} catch (e) { console.log(e) }
clearTimeout(this.overlayTimeout)
this.overlayTimeout = undefined
}, this.overlayTransitionDuration)
},
scrollListener (e: WheelEvent & KeyboardEvent) {
if (e.type === 'keydown') {
if (
['INPUT', 'TEXTAREA', 'SELECT'].includes((e.target as Element).tagName) ||
// https://github.com/vuetifyjs/vuetify/issues/4715
(e.target as HTMLElement).isContentEditable
) return
const up = [keyCodes.up, keyCodes.pageup]
const down = [keyCodes.down, keyCodes.pagedown]
if (up.includes(e.keyCode)) {
(e as any).deltaY = -1
} else if (down.includes(e.keyCode)) {
(e as any).deltaY = 1
} else {
return
}
}
if (e.target === this.overlay ||
(e.type !== 'keydown' && e.target === document.body) ||
this.checkPath(e)) e.preventDefault()
},
hasScrollbar (el?: Element) {
if (!el || el.nodeType !== Node.ELEMENT_NODE) return false
const style = window.getComputedStyle(el)
return ['auto', 'scroll'].includes(style.overflowY!) && el.scrollHeight > el.clientHeight
},
shouldScroll (el: Element, delta: number) {
if (el.scrollTop === 0 && delta < 0) return true
return el.scrollTop + el.clientHeight === el.scrollHeight && delta > 0
},
isInside (el: Element, parent: Element): boolean {
if (el === parent) {
return true
} else if (el === null || el === document.body) {
return false
} else {
return this.isInside(el.parentNode as Element, parent)
}
},
checkPath (e: WheelEvent) {
const path = e.path || this.composedPath(e)
const delta = e.deltaY
if (e.type === 'keydown' && path[0] === document.body) {
const dialog = this.$refs.dialog
const selected = window.getSelection().anchorNode as Element
if (dialog && this.hasScrollbar(dialog) && this.isInside(selected, dialog)) {
return this.shouldScroll(dialog, delta)
}
return true
}
for (let index = 0; index < path.length; index++) {
const el = path[index]
if (el === document) return true
if (el === document.documentElement) return true
if (el === this.$refs.content) return true
if (this.hasScrollbar(el as Element)) return this.shouldScroll(el as Element, delta)
}
return true
},
/**
* Polyfill for Event.prototype.composedPath
*/
composedPath (e: WheelEvent): EventTarget[] {
if (e.composedPath) return e.composedPath()
const path = []
let el = e.target as Element
while (el) {
path.push(el)
if (el.tagName === 'HTML') {
path.push(document)
path.push(window)
return path
}
el = el.parentElement!
}
return path
},
hideScroll () {
if (this.$vuetify.breakpoint.smAndDown) {
document.documentElement!.classList.add('overflow-y-hidden')
} else {
addPassiveEventListener(window, 'wheel', this.scrollListener as EventHandlerNonNull, { passive: false })
window.addEventListener('keydown', this.scrollListener as EventHandlerNonNull)
}
},
showScroll () {
document.documentElement!.classList.remove('overflow-y-hidden')
window.removeEventListener('wheel', this.scrollListener as EventHandlerNonNull)
window.removeEventListener('keydown', this.scrollListener as EventHandlerNonNull)
}
}
})
+38
View File
@@ -0,0 +1,38 @@
// Mixins
import Colorable from './colorable'
// Utilities
import mixins from '../util/mixins'
// Types
import { VNodeChildren } from 'vue'
/* @vue/component */
export default mixins(
Colorable
).extend({
methods: {
genPickerButton (
prop: string,
value: any,
content: VNodeChildren,
readonly = false,
staticClass = ''
) {
const active = (this as any)[prop] === value
const click = (event: Event) => {
event.stopPropagation()
this.$emit(`update:${prop}`, value)
}
return this.$createElement('div', {
staticClass: `v-picker__title__btn ${staticClass}`.trim(),
'class': {
'v-picker__title__btn--active': active,
'v-picker__title__btn--readonly': readonly
},
on: (active || readonly) ? undefined : { click }
}, Array.isArray(content) ? content : [content])
}
}
})
+71
View File
@@ -0,0 +1,71 @@
// Components
import VPicker from '../components/VPicker'
// Mixins
import Colorable from './colorable'
import Themeable from './themeable'
// Utils
import mixins from '../util/mixins'
// Types
import { VNode } from 'vue'
export default mixins(
Colorable,
Themeable
/* @vue/component */
).extend({
name: 'picker',
props: {
fullWidth: Boolean,
headerColor: String,
landscape: Boolean,
noTitle: Boolean,
width: {
type: [Number, String],
default: 290
}
},
methods: {
genPickerTitle (): VNode | null {
return null
},
genPickerBody (): VNode | null {
return null
},
genPickerActionsSlot () {
return this.$scopedSlots.default ? this.$scopedSlots.default({
save: (this as any).save,
cancel: (this as any).cancel
}) : this.$slots.default
},
genPicker (staticClass: string) {
const children: VNode[] = []
if (!this.noTitle) {
const title = this.genPickerTitle()
title && children.push(title)
}
const body = this.genPickerBody()
body && children.push(body)
children.push(this.$createElement('template', { slot: 'actions' }, [this.genPickerActionsSlot()]))
return this.$createElement(VPicker, {
staticClass,
props: {
color: this.headerColor || this.color,
dark: this.dark,
fullWidth: this.fullWidth,
landscape: this.landscape,
light: this.light,
width: this.width
}
}, children)
}
}
})
+54
View File
@@ -0,0 +1,54 @@
import Vue from 'vue'
import { filterObjectOnKeys } from '../util/helpers'
import { OptionsVue, VueConstructor } from 'vue/types/vue'
const availableProps = {
absolute: Boolean,
bottom: Boolean,
fixed: Boolean,
left: Boolean,
right: Boolean,
top: Boolean
}
type props = Record<keyof typeof availableProps, boolean>
export type Positionable<S extends keyof props> = VueConstructor<Vue & { [P in S]: boolean }, { [P in S]: BooleanConstructor }>
export function factory <S extends keyof props> (selected?: S[]): Positionable<S>
export function factory (selected: undefined): OptionsVue<Vue, {}, {}, {}, props, typeof availableProps>
export function factory (selected: any[] = []): any {
return Vue.extend({
name: 'positionable',
props: selected.length ? filterObjectOnKeys(availableProps, selected) : availableProps
})
}
export default factory()
// Add a `*` before the second `/`
/* Tests /
let single = factory(['top']).extend({
created () {
this.top
this.bottom
this.absolute
}
})
let some = factory(['top', 'bottom']).extend({
created () {
this.top
this.bottom
this.absolute
}
})
let all = factory().extend({
created () {
this.top
this.bottom
this.absolute
this.foobar
}
})
/**/
+60
View File
@@ -0,0 +1,60 @@
import Vue, { VueConstructor } from 'vue'
/* eslint-disable-next-line no-use-before-define */
export type Proxyable<T extends string = 'value'> = VueConstructor<Vue & {
internalLazyValue: unknown
internalValue: unknown
} & Record<T, any>>
export function factory<T extends string = 'value'> (prop?: T, event?: string): Proxyable<T>
export function factory (
prop = 'value',
event = 'change'
) {
return Vue.extend({
name: 'proxyable',
model: {
prop,
event
},
props: {
[prop]: {
required: false
}
},
data () {
return {
internalLazyValue: this[prop] as unknown
}
},
computed: {
internalValue: {
get (): unknown {
return this.internalLazyValue
},
set (val: any) {
if (val === this.internalLazyValue) return
this.internalLazyValue = val
this.$emit(event, val)
}
}
},
watch: {
[prop] (val) {
this.internalLazyValue = val
}
}
})
}
/* eslint-disable-next-line no-redeclare */
const Proxyable = factory()
export default Proxyable
+50
View File
@@ -0,0 +1,50 @@
import Vue from 'vue'
import { VueConstructor } from 'vue/types/vue'
import { consoleWarn } from '../util/console'
function generateWarning (child: string, parent: string) {
return () => consoleWarn(`The ${child} component must be used inside a ${parent}`)
}
export type Registrable<T extends string> = VueConstructor<Vue & {
[K in T]: {
register (...props: any[]): void
unregister (self: any): void
}
}>
export function inject<T extends string> (namespace: T, child?: string, parent?: string): Registrable<T> {
const defaultImpl = child && parent ? {
register: generateWarning(child, parent),
unregister: generateWarning(child, parent)
} : null
return Vue.extend({
name: 'registrable-inject',
inject: {
[namespace]: {
default: defaultImpl
}
}
})
}
export function provide (namespace: string) {
return Vue.extend({
name: 'registrable-provide',
methods: {
register: null,
unregister: null
},
provide () {
return {
[namespace]: {
register: this.register,
unregister: this.unregister
}
}
}
})
}
+34
View File
@@ -0,0 +1,34 @@
import Vue from 'vue'
/* @vue/component */
export default Vue.extend({
name: 'returnable',
props: {
returnValue: null as any
},
data: () => ({
isActive: false,
originalValue: null as any
}),
watch: {
isActive (val) {
if (val) {
this.originalValue = this.returnValue
} else {
this.$emit('update:returnValue', this.originalValue)
}
}
},
methods: {
save (value: any) {
this.originalValue = value
setTimeout(() => {
this.isActive = false
})
}
}
})
+38
View File
@@ -0,0 +1,38 @@
// Directives
import Ripple from '../directives/ripple'
// Types
import Vue, { VNode, VNodeData, VNodeDirective } from 'vue'
export default Vue.extend({
name: 'rippleable',
directives: { Ripple },
props: {
ripple: {
type: [Boolean, Object],
default: true
}
},
methods: {
genRipple (data: VNodeData = {}): VNode | null {
if (!this.ripple) return null
data.staticClass = 'v-input--selection-controls__ripple'
data.directives = data.directives || []
data.directives.push({
name: 'ripple',
value: { center: true }
} as VNodeDirective)
data.on = Object.assign({
click: this.onChange
}, this.$listeners)
return this.$createElement('div', data)
},
onChange () {}
}
})
+96
View File
@@ -0,0 +1,96 @@
import Vue, { VNodeData } from 'vue'
import { PropValidator } from 'vue/types/options'
import Ripple, { RippleOptions } from '../directives/ripple'
export default Vue.extend({
name: 'routable',
directives: {
Ripple
},
props: {
activeClass: String,
append: Boolean,
disabled: Boolean,
exact: {
type: Boolean,
default: undefined
} as PropValidator<boolean | undefined>,
exactActiveClass: String,
href: [String, Object],
to: [String, Object],
nuxt: Boolean,
replace: Boolean,
ripple: [Boolean, Object],
tag: String,
target: String
},
computed: {
computedRipple (): RippleOptions | boolean {
return (this.ripple && !this.disabled) ? this.ripple : false
}
},
methods: {
click (e: MouseEvent): void {
this.$emit('click', e)
},
generateRouteLink (classes: any) {
let exact = this.exact
let tag
const data: VNodeData = {
attrs: { disabled: this.disabled },
class: classes,
props: {},
directives: [{
name: 'ripple',
value: this.computedRipple
}],
[this.to ? 'nativeOn' : 'on']: {
...this.$listeners,
click: this.click
}
}
if (typeof this.exact === 'undefined') {
exact = this.to === '/' ||
(this.to === Object(this.to) && this.to.path === '/')
}
if (this.to) {
// Add a special activeClass hook
// for component level styles
let activeClass = this.activeClass
let exactActiveClass = this.exactActiveClass || activeClass
// TODO: apply only in VListTile
if ((this as any).proxyClass) {
activeClass += ' ' + (this as any).proxyClass
exactActiveClass += ' ' + (this as any).proxyClass
}
tag = this.nuxt ? 'nuxt-link' : 'router-link'
Object.assign(data.props, {
to: this.to,
exact,
activeClass,
exactActiveClass,
append: this.append,
replace: this.replace
})
} else {
tag = (this.href && 'a') || this.tag || 'a'
if (tag === 'a' && this.href) data.attrs!.href = this.href
}
if (this.target) data.attrs!.target = this.target
return { tag, data }
}
}
})
+146
View File
@@ -0,0 +1,146 @@
// Components
import VInput from '../components/VInput'
// Mixins
import Rippleable from './rippleable'
import Comparable from './comparable'
/* @vue/component */
export default VInput.extend({
name: 'selectable',
mixins: [Rippleable, Comparable],
model: {
prop: 'inputValue',
event: 'change'
},
props: {
color: {
type: String,
default: 'accent'
},
id: String,
inputValue: null,
falseValue: null,
trueValue: null,
multiple: {
type: Boolean,
default: null
},
label: String
},
data: vm => ({
lazyValue: vm.inputValue
}),
computed: {
computedColor () {
return this.isActive ? this.color : this.validationState
},
isMultiple () {
return this.multiple === true || (this.multiple === null && Array.isArray(this.internalValue))
},
isActive () {
const value = this.value
const input = this.internalValue
if (this.isMultiple) {
if (!Array.isArray(input)) return false
return input.some(item => this.valueComparator(item, value))
}
if (this.trueValue === undefined || this.falseValue === undefined) {
return value
? this.valueComparator(value, input)
: Boolean(input)
}
return this.valueComparator(input, this.trueValue)
},
isDirty () {
return this.isActive
}
},
watch: {
inputValue (val) {
this.lazyValue = val
}
},
methods: {
genLabel () {
if (!this.hasLabel) return null
const label = VInput.options.methods.genLabel.call(this)
label.data.on = { click: this.onChange }
return label
},
genInput (type, attrs) {
return this.$createElement('input', {
attrs: Object.assign({
'aria-label': this.label,
'aria-checked': this.isActive.toString(),
disabled: this.isDisabled,
id: this.id,
role: type,
type
}, attrs),
domProps: {
value: this.value,
checked: this.isActive
},
on: {
blur: this.onBlur,
change: this.onChange,
focus: this.onFocus,
keydown: this.onKeydown
},
ref: 'input'
})
},
onBlur () {
this.isFocused = false
},
onChange () {
if (this.isDisabled) return
const value = this.value
let input = this.internalValue
if (this.isMultiple) {
if (!Array.isArray(input)) {
input = []
}
const length = input.length
input = input.filter(item => !this.valueComparator(item, value))
if (input.length === length) {
input.push(value)
}
} else if (this.trueValue !== undefined && this.falseValue !== undefined) {
input = this.valueComparator(input, this.trueValue) ? this.falseValue : this.trueValue
} else if (value) {
input = this.valueComparator(input, value) ? null : value
} else {
input = !input
}
this.validate(true, input)
this.internalValue = input
},
onFocus () {
this.isFocused = true
},
/** @abstract */
onKeydown (e) {}
}
})
+15
View File
@@ -0,0 +1,15 @@
import Vue from 'vue'
export default Vue.extend({
name: 'sizeable',
props: {
large: Boolean,
medium: Boolean,
size: {
type: [Number, String]
},
small: Boolean,
xLarge: Boolean
}
})
+27
View File
@@ -0,0 +1,27 @@
import Vue from 'vue'
/**
* SSRBootable
*
* @mixin
*
* Used in layout components (drawer, toolbar, content)
* to avoid an entry animation when using SSR
*/
export default Vue.extend({
name: 'ssr-bootable',
data: () => ({
isBooted: false
}),
mounted () {
// Use setAttribute instead of dataset
// because dataset does not work well
// with unit tests
window.requestAnimationFrame(() => {
this.$el.setAttribute('data-booted', 'true')
this.isBooted = true
})
}
})
+63
View File
@@ -0,0 +1,63 @@
import Vue from 'vue'
import { getZIndex } from '../util/helpers'
interface options extends Vue {
$refs: {
content: Element
}
}
/* @vue/component */
export default Vue.extend<options>().extend({
name: 'stackable',
data () {
return {
stackClass: 'unpecified',
stackElement: null as Element | null,
stackExclude: null as Element[] | null,
stackMinZIndex: 0,
isActive: false
}
},
computed: {
activeZIndex (): number {
if (typeof window === 'undefined') return 0
const content = this.stackElement || this.$refs.content
// Return current zindex if not active
const index = !this.isActive
? getZIndex(content)
: this.getMaxZIndex(this.stackExclude || [content]) + 2
if (index == null) return index
// Return max current z-index (excluding self) + 2
// (2 to leave room for an overlay below, if needed)
return parseInt(index)
}
},
methods: {
getMaxZIndex (exclude: Element[] = []) {
const base = this.$el
// Start with lowest allowed z-index or z-index of
// base component's element, whichever is greater
const zis = [this.stackMinZIndex, getZIndex(base)]
// Convert the NodeList to an array to
// prevent an Edge bug with Symbol.iterator
// https://github.com/vuetifyjs/vuetify/issues/2146
const activeElements = [...document.getElementsByClassName(this.stackClass)]
// Get z-index for all active dialogs
for (let index = 0; index < activeElements.length; index++) {
if (!exclude.includes(activeElements[index])) {
zis.push(getZIndex(activeElements[index]))
}
}
return Math.max(...zis)
}
}
})
+109
View File
@@ -0,0 +1,109 @@
import Vue from 'vue'
import { PropValidator, RenderContext } from 'vue/types/options'
/* eslint-disable-next-line no-use-before-define */
interface Themeable extends Vue {
theme: {
isDark: boolean
}
}
export function functionalThemeClasses (context: RenderContext): object {
const vm = {
...context.props,
...context.injections
}
const isDark = Themeable.options.computed.isDark.call(vm)
return Themeable.options.computed.themeClasses.call({ isDark })
}
/* @vue/component */
const Themeable = Vue.extend<Themeable>().extend({
name: 'themeable',
provide (): object {
return {
theme: this.themeableProvide
}
},
inject: {
theme: {
default: {
isDark: false
}
}
},
props: {
dark: {
type: Boolean,
default: null
} as PropValidator<boolean | null>,
light: {
type: Boolean,
default: null
} as PropValidator<boolean | null>
},
data () {
return {
themeableProvide: {
isDark: false
}
}
},
computed: {
isDark (): boolean {
if (this.dark === true) {
// explicitly dark
return true
} else if (this.light === true) {
// explicitly light
return false
} else {
// inherit from parent, or default false if there is none
return this.theme.isDark
}
},
themeClasses (): object {
return {
'theme--dark': this.isDark,
'theme--light': !this.isDark
}
},
/** Used by menus and dialogs, inherits from v-app instead of the parent */
rootIsDark (): boolean {
if (this.dark === true) {
// explicitly dark
return true
} else if (this.light === true) {
// explicitly light
return false
} else {
// inherit from v-app
return this.$vuetify.dark
}
},
rootThemeClasses (): Dictionary<boolean> {
return {
'theme--dark': this.rootIsDark,
'theme--light': !this.rootIsDark
}
}
},
watch: {
isDark: {
handler (newVal, oldVal) {
if (newVal !== oldVal) {
this.themeableProvide.isDark = this.isDark
}
},
immediate: true
}
}
})
export default Themeable
+37
View File
@@ -0,0 +1,37 @@
import Vue, { VueConstructor } from 'vue'
/* eslint-disable-next-line no-use-before-define */
export type Toggleable<T extends string = 'value'> = VueConstructor<Vue & { isActive: boolean } & Record<T, any>>
export function factory<T extends string = 'value'> (prop?: T, event?: string): Toggleable<T>
export function factory (prop = 'value', event = 'input') {
return Vue.extend({
name: 'toggleable',
model: { prop, event },
props: {
[prop]: { required: false }
},
data () {
return {
isActive: !!this[prop]
}
},
watch: {
[prop] (val) {
this.isActive = !!val
},
isActive (val) {
!!val !== this[prop] && this.$emit(event, val)
}
}
})
}
/* eslint-disable-next-line no-redeclare */
const Toggleable = factory()
export default Toggleable
+11
View File
@@ -0,0 +1,11 @@
import Vue from 'vue'
export default Vue.extend({
name: 'transitionable',
props: {
mode: String,
origin: String,
transition: String
}
})
+60
View File
@@ -0,0 +1,60 @@
import Vue from 'vue'
export default Vue.extend({
name: 'translatable',
props: {
height: Number
},
data: () => ({
elOffsetTop: 0,
parallax: 0,
parallaxDist: 0,
percentScrolled: 0,
scrollTop: 0,
windowHeight: 0,
windowBottom: 0
}),
computed: {
imgHeight (): number {
return this.objHeight()
}
},
beforeDestroy () {
window.removeEventListener('scroll', this.translate, false)
window.removeEventListener('resize', this.translate, false)
},
methods: {
calcDimensions () {
const offset = this.$el.getBoundingClientRect()
this.scrollTop = window.pageYOffset
this.parallaxDist = this.imgHeight - this.height
this.elOffsetTop = offset.top + this.scrollTop
this.windowHeight = window.innerHeight
this.windowBottom = this.scrollTop + this.windowHeight
},
listeners () {
window.addEventListener('scroll', this.translate, false)
window.addEventListener('resize', this.translate, false)
},
/** @abstract **/
objHeight (): number {
throw new Error('Not implemented !')
},
translate () {
this.calcDimensions()
this.percentScrolled = (
(this.windowBottom - this.elOffsetTop) /
(parseInt(this.height) + this.windowHeight)
)
this.parallax = Math.round(this.parallaxDist * this.percentScrolled)
}
}
})
+239
View File
@@ -0,0 +1,239 @@
// Mixins
import Colorable from './colorable'
import { inject as RegistrableInject } from './registrable'
// Utilities
import { deepEqual } from '../util/helpers'
import { consoleError } from '../util/console'
import mixins from '../util/mixins'
// Types
import { PropValidator } from 'vue/types/options'
export type VuetifyRuleValidator = (value: any) => string | false
export type VuetifyMessage = string | string[]
export type VuetifyRuleValidations = (VuetifyRuleValidator | string)[]
/* @vue/component */
export default mixins(
Colorable,
RegistrableInject('form')
).extend({
name: 'validatable',
props: {
disabled: Boolean,
error: Boolean,
errorCount: {
type: [Number, String],
default: 1
},
errorMessages: {
type: [String, Array],
default: () => []
} as PropValidator<VuetifyMessage>,
messages: {
type: [String, Array],
default: () => []
} as PropValidator<VuetifyMessage>,
readonly: Boolean,
rules: {
type: Array,
default: () => []
} as PropValidator<VuetifyRuleValidations>,
success: Boolean,
successMessages: {
type: [String, Array],
default: () => []
} as PropValidator<VuetifyMessage>,
validateOnBlur: Boolean,
value: { required: false }
},
data () {
return {
errorBucket: [] as string[],
hasColor: false,
hasFocused: false,
hasInput: false,
isFocused: false,
isResetting: false,
lazyValue: this.value,
valid: false
}
},
computed: {
hasError (): boolean {
return (
this.internalErrorMessages.length > 0 ||
this.errorBucket.length > 0 ||
this.error
)
},
// TODO: Add logic that allows the user to enable based
// upon a good validation
hasSuccess (): boolean {
return (
this.internalSuccessMessages.length > 0 ||
this.success
)
},
externalError (): boolean {
return this.internalErrorMessages.length > 0 || this.error
},
hasMessages (): boolean {
return this.validationTarget.length > 0
},
hasState (): boolean {
return (
this.hasSuccess ||
(this.shouldValidate && this.hasError)
)
},
internalErrorMessages (): VuetifyRuleValidations {
return this.genInternalMessages(this.errorMessages)
},
internalMessages (): VuetifyRuleValidations {
return this.genInternalMessages(this.messages)
},
internalSuccessMessages (): VuetifyRuleValidations {
return this.genInternalMessages(this.successMessages)
},
internalValue: {
get (): unknown {
return this.lazyValue
},
set (val: any) {
this.lazyValue = val
this.$emit('input', val)
}
},
shouldValidate (): boolean {
if (this.externalError) return true
if (this.isResetting) return false
return this.validateOnBlur
? this.hasFocused && !this.isFocused
: (this.hasInput || this.hasFocused)
},
validations (): VuetifyRuleValidations {
return this.validationTarget.slice(0, Number(this.errorCount))
},
validationState (): string | undefined {
if (this.hasError && this.shouldValidate) return 'error'
if (this.hasSuccess) return 'success'
if (this.hasColor) return this.color
return undefined
},
validationTarget (): VuetifyRuleValidations {
if (this.internalErrorMessages.length > 0) {
return this.internalErrorMessages
} else if (this.successMessages.length > 0) {
return this.internalSuccessMessages
} else if (this.messages.length > 0) {
return this.internalMessages
} else if (this.shouldValidate) {
return this.errorBucket
} else return []
}
},
watch: {
rules: {
handler (newVal, oldVal) {
if (deepEqual(newVal, oldVal)) return
this.validate()
},
deep: true
},
internalValue () {
// If it's the first time we're setting input,
// mark it with hasInput
this.hasInput = true
this.validateOnBlur || this.$nextTick(this.validate)
},
isFocused (val) {
// Should not check validation
// if disabled or readonly
if (
!val &&
!this.disabled &&
!this.readonly
) {
this.hasFocused = true
this.validateOnBlur && this.validate()
}
},
isResetting () {
setTimeout(() => {
this.hasInput = false
this.hasFocused = false
this.isResetting = false
this.validate()
}, 0)
},
hasError (val) {
if (this.shouldValidate) {
this.$emit('update:error', val)
}
},
value (val) {
this.lazyValue = val
}
},
beforeMount () {
this.validate()
},
created () {
this.form && this.form.register(this)
},
beforeDestroy () {
this.form && this.form.unregister(this)
},
methods: {
genInternalMessages (messages: VuetifyMessage): VuetifyRuleValidations {
if (!messages) return []
else if (Array.isArray(messages)) return messages
else return [messages]
},
/** @public */
reset () {
this.isResetting = true
this.internalValue = Array.isArray(this.internalValue)
? []
: undefined
},
/** @public */
resetValidation () {
this.isResetting = true
},
/** @public */
validate (force = false, value?: any): boolean {
const errorBucket = []
value = value || this.internalValue
if (force) this.hasInput = this.hasFocused = true
for (let index = 0; index < this.rules.length; index++) {
const rule = this.rules[index]
const valid = typeof rule === 'function' ? rule(value) : rule
if (typeof valid === 'string') {
errorBucket.push(valid)
} else if (typeof valid !== 'boolean') {
consoleError(`Rules should return a string or boolean, received '${typeof valid}' instead`, this)
}
}
this.errorBucket = errorBucket
this.valid = errorBucket.length === 0
return this.valid
}
}
})