vuetify eingebunden + erster Versuch Combobox
This commit is contained in:
+79
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,13 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
/* @vue/component */
|
||||
export default Vue.extend({
|
||||
name: 'filterable',
|
||||
|
||||
props: {
|
||||
noDataText: {
|
||||
type: String,
|
||||
default: '$vuetify.noDataText'
|
||||
}
|
||||
}
|
||||
})
|
||||
+72
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'transitionable',
|
||||
|
||||
props: {
|
||||
mode: String,
|
||||
origin: String,
|
||||
transition: String
|
||||
}
|
||||
})
|
||||
+60
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user