vuetify eingebunden + erster Versuch Combobox
This commit is contained in:
+107
@@ -0,0 +1,107 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_alerts.styl'
|
||||
|
||||
// Components
|
||||
import VIcon from '../VIcon'
|
||||
|
||||
// Mixins
|
||||
import Colorable from '../../mixins/colorable'
|
||||
import Toggleable from '../../mixins/toggleable'
|
||||
import Transitionable from '../../mixins/transitionable'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue/types'
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
/* @vue/component */
|
||||
export default mixins(Colorable, Toggleable, Transitionable).extend({
|
||||
name: 'v-alert',
|
||||
|
||||
props: {
|
||||
dismissible: Boolean,
|
||||
icon: String,
|
||||
outline: Boolean,
|
||||
type: {
|
||||
type: String,
|
||||
validator (val: string) {
|
||||
return [
|
||||
'info',
|
||||
'error',
|
||||
'success',
|
||||
'warning'
|
||||
].includes(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
computedColor (): string {
|
||||
return (this.type && !this.color) ? this.type : (this.color || 'error')
|
||||
},
|
||||
computedIcon (): string | void {
|
||||
if (this.icon || !this.type) return this.icon
|
||||
|
||||
switch (this.type) {
|
||||
case 'info': return '$vuetify.icons.info'
|
||||
case 'error': return '$vuetify.icons.error'
|
||||
case 'success': return '$vuetify.icons.success'
|
||||
case 'warning': return '$vuetify.icons.warning'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
genIcon (): VNode | null {
|
||||
if (!this.computedIcon) return null
|
||||
|
||||
return this.$createElement(VIcon, {
|
||||
'class': 'v-alert__icon'
|
||||
}, this.computedIcon)
|
||||
},
|
||||
|
||||
genDismissible (): VNode | null {
|
||||
if (!this.dismissible) return null
|
||||
|
||||
return this.$createElement('a', {
|
||||
'class': 'v-alert__dismissible',
|
||||
on: { click: () => { this.isActive = false } }
|
||||
}, [
|
||||
this.$createElement(VIcon, {
|
||||
props: {
|
||||
right: true
|
||||
}
|
||||
}, '$vuetify.icons.cancel')
|
||||
])
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
const children = [
|
||||
this.genIcon(),
|
||||
h('div', this.$slots.default),
|
||||
this.genDismissible()
|
||||
] as any
|
||||
const setColor = this.outline ? this.setTextColor : this.setBackgroundColor
|
||||
const alert = h('div', setColor(this.computedColor, {
|
||||
staticClass: 'v-alert',
|
||||
'class': {
|
||||
'v-alert--outline': this.outline
|
||||
},
|
||||
directives: [{
|
||||
name: 'show',
|
||||
value: this.isActive
|
||||
}],
|
||||
on: this.$listeners
|
||||
}), children)
|
||||
|
||||
if (!this.transition) return alert
|
||||
|
||||
return h('transition', {
|
||||
props: {
|
||||
name: this.transition,
|
||||
origin: this.origin,
|
||||
mode: this.mode
|
||||
}
|
||||
}, [alert])
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VAlert from './VAlert'
|
||||
|
||||
export { VAlert }
|
||||
export default VAlert
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import '../../stylus/components/_app.styl'
|
||||
|
||||
// Component level mixins
|
||||
import AppTheme from './mixins/app-theme'
|
||||
|
||||
import Themeable from '../../mixins/themeable'
|
||||
|
||||
// Directives
|
||||
import Resize from '../../directives/resize'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
name: 'v-app',
|
||||
|
||||
directives: {
|
||||
Resize
|
||||
},
|
||||
|
||||
mixins: [
|
||||
AppTheme,
|
||||
Themeable
|
||||
],
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: 'app'
|
||||
},
|
||||
dark: Boolean
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes () {
|
||||
return {
|
||||
'application--is-rtl': this.$vuetify.rtl,
|
||||
...this.themeClasses
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
dark () {
|
||||
this.$vuetify.dark = this.dark
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.$vuetify.dark = this.dark
|
||||
},
|
||||
|
||||
render (h) {
|
||||
const data = {
|
||||
staticClass: 'application',
|
||||
'class': this.classes,
|
||||
attrs: { 'data-app': true },
|
||||
domProps: { id: this.id }
|
||||
}
|
||||
|
||||
const wrapper = h('div', { staticClass: 'application--wrap' }, this.$slots.default)
|
||||
|
||||
return h('div', data, [wrapper])
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VApp from './VApp'
|
||||
|
||||
export { VApp }
|
||||
export default VApp
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import * as Theme from '../../../util/theme'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
style: null
|
||||
}),
|
||||
|
||||
computed: {
|
||||
parsedTheme () {
|
||||
return Theme.parse(this.$vuetify.theme)
|
||||
},
|
||||
/** @return string */
|
||||
generatedStyles () {
|
||||
const theme = this.parsedTheme
|
||||
let css
|
||||
|
||||
if (this.$vuetify.options.themeCache != null) {
|
||||
css = this.$vuetify.options.themeCache.get(theme)
|
||||
if (css != null) return css
|
||||
}
|
||||
|
||||
css = Theme.genStyles(theme, this.$vuetify.options.customProperties)
|
||||
|
||||
if (this.$vuetify.options.minifyTheme != null) {
|
||||
css = this.$vuetify.options.minifyTheme(css)
|
||||
}
|
||||
|
||||
if (this.$vuetify.options.themeCache != null) {
|
||||
this.$vuetify.options.themeCache.set(theme, css)
|
||||
}
|
||||
|
||||
return css
|
||||
},
|
||||
vueMeta () {
|
||||
if (this.$vuetify.theme === false) return {}
|
||||
|
||||
const options = {
|
||||
cssText: this.generatedStyles,
|
||||
id: 'vuetify-theme-stylesheet',
|
||||
type: 'text/css'
|
||||
}
|
||||
|
||||
if (this.$vuetify.options.cspNonce) {
|
||||
options.nonce = this.$vuetify.options.cspNonce
|
||||
}
|
||||
|
||||
return {
|
||||
style: [options]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Regular vue-meta
|
||||
metaInfo () {
|
||||
return this.vueMeta
|
||||
},
|
||||
|
||||
// Nuxt
|
||||
head () {
|
||||
return this.vueMeta
|
||||
},
|
||||
|
||||
watch: {
|
||||
generatedStyles () {
|
||||
!this.meta && this.applyTheme()
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
if (this.$vuetify.theme === false) return
|
||||
|
||||
if (this.$meta) {
|
||||
// Vue-meta
|
||||
// Handled by metaInfo()/nuxt()
|
||||
} else if (typeof document === 'undefined' && this.$ssrContext) {
|
||||
// SSR
|
||||
const nonce = this.$vuetify.options.cspNonce
|
||||
? ` nonce="${this.$vuetify.options.cspNonce}"`
|
||||
: ''
|
||||
this.$ssrContext.head = this.$ssrContext.head || ''
|
||||
this.$ssrContext.head += `<style type="text/css" id="vuetify-theme-stylesheet"${nonce}>${this.generatedStyles}</style>`
|
||||
} else if (typeof document !== 'undefined') {
|
||||
// Client-side
|
||||
this.genStyle()
|
||||
this.applyTheme()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
applyTheme () {
|
||||
if (this.style) this.style.innerHTML = this.generatedStyles
|
||||
},
|
||||
genStyle () {
|
||||
let style = document.getElementById('vuetify-theme-stylesheet')
|
||||
|
||||
if (!style) {
|
||||
style = document.createElement('style')
|
||||
style.type = 'text/css'
|
||||
style.id = 'vuetify-theme-stylesheet'
|
||||
if (this.$vuetify.options.cspNonce) {
|
||||
style.setAttribute('nonce', this.$vuetify.options.cspNonce)
|
||||
}
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
|
||||
this.style = style
|
||||
}
|
||||
}
|
||||
}
|
||||
+377
@@ -0,0 +1,377 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_autocompletes.styl'
|
||||
|
||||
// Extensions
|
||||
import VSelect, { defaultMenuProps as VSelectMenuProps } from '../VSelect/VSelect'
|
||||
import VTextField from '../VTextField/VTextField'
|
||||
|
||||
// Utils
|
||||
import { keyCodes } from '../../util/helpers'
|
||||
|
||||
const defaultMenuProps = {
|
||||
...VSelectMenuProps,
|
||||
offsetY: true,
|
||||
offsetOverflow: true,
|
||||
transition: false
|
||||
}
|
||||
|
||||
/* @vue/component */
|
||||
export default VSelect.extend({
|
||||
name: 'v-autocomplete',
|
||||
|
||||
props: {
|
||||
allowOverflow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
browserAutocomplete: {
|
||||
type: String,
|
||||
default: 'off'
|
||||
},
|
||||
filter: {
|
||||
type: Function,
|
||||
default: (item, queryText, itemText) => {
|
||||
return itemText.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
|
||||
}
|
||||
},
|
||||
hideNoData: Boolean,
|
||||
noFilter: Boolean,
|
||||
searchInput: {
|
||||
default: undefined
|
||||
},
|
||||
menuProps: {
|
||||
type: VSelect.options.props.menuProps.type,
|
||||
default: () => defaultMenuProps
|
||||
},
|
||||
autoSelectFirst: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data: vm => ({
|
||||
attrsInput: null,
|
||||
lazySearch: vm.searchInput
|
||||
}),
|
||||
|
||||
computed: {
|
||||
classes () {
|
||||
return Object.assign({}, VSelect.options.computed.classes.call(this), {
|
||||
'v-autocomplete': true,
|
||||
'v-autocomplete--is-selecting-index': this.selectedIndex > -1
|
||||
})
|
||||
},
|
||||
computedItems () {
|
||||
return this.filteredItems
|
||||
},
|
||||
selectedValues () {
|
||||
return this.selectedItems.map(item => this.getValue(item))
|
||||
},
|
||||
hasDisplayedItems () {
|
||||
return this.hideSelected
|
||||
? this.filteredItems.some(item => !this.hasItem(item))
|
||||
: this.filteredItems.length > 0
|
||||
},
|
||||
/**
|
||||
* The range of the current input text
|
||||
*
|
||||
* @return {Number}
|
||||
*/
|
||||
currentRange () {
|
||||
if (this.selectedItem == null) return 0
|
||||
|
||||
return this.getText(this.selectedItem).toString().length
|
||||
},
|
||||
filteredItems () {
|
||||
if (!this.isSearching || this.noFilter || this.internalSearch == null) return this.allItems
|
||||
|
||||
return this.allItems.filter(item => this.filter(item, this.internalSearch.toString(), this.getText(item).toString()))
|
||||
},
|
||||
internalSearch: {
|
||||
get () {
|
||||
return this.lazySearch
|
||||
},
|
||||
set (val) {
|
||||
this.lazySearch = val
|
||||
|
||||
this.$emit('update:searchInput', val)
|
||||
}
|
||||
},
|
||||
isAnyValueAllowed () {
|
||||
return false
|
||||
},
|
||||
isDirty () {
|
||||
return this.searchIsDirty || this.selectedItems.length > 0
|
||||
},
|
||||
isSearching () {
|
||||
if (this.multiple) return this.searchIsDirty
|
||||
|
||||
return (
|
||||
this.searchIsDirty &&
|
||||
this.internalSearch !== this.getText(this.selectedItem)
|
||||
)
|
||||
},
|
||||
menuCanShow () {
|
||||
if (!this.isFocused) return false
|
||||
|
||||
return this.hasDisplayedItems || !this.hideNoData
|
||||
},
|
||||
$_menuProps () {
|
||||
const props = VSelect.options.computed.$_menuProps.call(this)
|
||||
props.contentClass = `v-autocomplete__content ${props.contentClass || ''}`.trim()
|
||||
return {
|
||||
...defaultMenuProps,
|
||||
...props
|
||||
}
|
||||
},
|
||||
searchIsDirty () {
|
||||
return this.internalSearch != null &&
|
||||
this.internalSearch !== ''
|
||||
},
|
||||
selectedItem () {
|
||||
if (this.multiple) return null
|
||||
|
||||
return this.selectedItems.find(i => {
|
||||
return this.valueComparator(this.getValue(i), this.getValue(this.internalValue))
|
||||
})
|
||||
},
|
||||
listData () {
|
||||
const data = VSelect.options.computed.listData.call(this)
|
||||
|
||||
Object.assign(data.props, {
|
||||
items: this.virtualizedItems,
|
||||
noFilter: (
|
||||
this.noFilter ||
|
||||
!this.isSearching ||
|
||||
!this.filteredItems.length
|
||||
),
|
||||
searchInput: this.internalSearch
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
filteredItems (val) {
|
||||
this.onFilteredItemsChanged(val)
|
||||
},
|
||||
internalValue () {
|
||||
this.setSearch()
|
||||
},
|
||||
isFocused (val) {
|
||||
if (val) {
|
||||
this.$refs.input &&
|
||||
this.$refs.input.select()
|
||||
} else {
|
||||
this.updateSelf()
|
||||
}
|
||||
},
|
||||
isMenuActive (val) {
|
||||
if (val || !this.hasSlot) return
|
||||
|
||||
this.lazySearch = null
|
||||
},
|
||||
items (val, oldVal) {
|
||||
// If we are focused, the menu
|
||||
// is not active, hide no data is enabled,
|
||||
// and items change
|
||||
// User is probably async loading
|
||||
// items, try to activate the menu
|
||||
if (
|
||||
!(oldVal && oldVal.length) &&
|
||||
this.hideNoData &&
|
||||
this.isFocused &&
|
||||
!this.isMenuActive &&
|
||||
val.length
|
||||
) this.activateMenu()
|
||||
},
|
||||
searchInput (val) {
|
||||
this.lazySearch = val
|
||||
},
|
||||
internalSearch (val) {
|
||||
this.onInternalSearchChanged(val)
|
||||
},
|
||||
itemText () {
|
||||
this.updateSelf()
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.setSearch()
|
||||
},
|
||||
|
||||
methods: {
|
||||
onFilteredItemsChanged (val) {
|
||||
this.setMenuIndex(-1)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.setMenuIndex(val.length > 0 && (val.length === 1 || this.autoSelectFirst) ? 0 : -1)
|
||||
})
|
||||
},
|
||||
onInternalSearchChanged (val) {
|
||||
this.updateMenuDimensions()
|
||||
},
|
||||
updateMenuDimensions () {
|
||||
if (this.isMenuActive &&
|
||||
this.$refs.menu
|
||||
) {
|
||||
this.$refs.menu.updateDimensions()
|
||||
}
|
||||
},
|
||||
changeSelectedIndex (keyCode) {
|
||||
// Do not allow changing of selectedIndex
|
||||
// when search is dirty
|
||||
if (this.searchIsDirty) return
|
||||
|
||||
if (![
|
||||
keyCodes.backspace,
|
||||
keyCodes.left,
|
||||
keyCodes.right,
|
||||
keyCodes.delete
|
||||
].includes(keyCode)) return
|
||||
|
||||
const indexes = this.selectedItems.length - 1
|
||||
|
||||
if (keyCode === keyCodes.left) {
|
||||
this.selectedIndex = this.selectedIndex === -1
|
||||
? indexes
|
||||
: this.selectedIndex - 1
|
||||
} else if (keyCode === keyCodes.right) {
|
||||
this.selectedIndex = this.selectedIndex >= indexes
|
||||
? -1
|
||||
: this.selectedIndex + 1
|
||||
} else if (this.selectedIndex === -1) {
|
||||
this.selectedIndex = indexes
|
||||
return
|
||||
}
|
||||
|
||||
const currentItem = this.selectedItems[this.selectedIndex]
|
||||
|
||||
if ([
|
||||
keyCodes.backspace,
|
||||
keyCodes.delete
|
||||
].includes(keyCode) &&
|
||||
!this.getDisabled(currentItem)
|
||||
) {
|
||||
const newIndex = this.selectedIndex === indexes
|
||||
? this.selectedIndex - 1
|
||||
: this.selectedItems[this.selectedIndex + 1]
|
||||
? this.selectedIndex
|
||||
: -1
|
||||
|
||||
if (newIndex === -1) {
|
||||
this.setValue(this.multiple ? [] : undefined)
|
||||
} else {
|
||||
this.selectItem(currentItem)
|
||||
}
|
||||
|
||||
this.selectedIndex = newIndex
|
||||
}
|
||||
},
|
||||
clearableCallback () {
|
||||
this.internalSearch = undefined
|
||||
|
||||
VSelect.options.methods.clearableCallback.call(this)
|
||||
},
|
||||
genInput () {
|
||||
const input = VTextField.options.methods.genInput.call(this)
|
||||
|
||||
input.data.attrs.role = 'combobox'
|
||||
input.data.domProps.value = this.internalSearch
|
||||
|
||||
return input
|
||||
},
|
||||
genSelections () {
|
||||
return this.hasSlot || this.multiple
|
||||
? VSelect.options.methods.genSelections.call(this)
|
||||
: []
|
||||
},
|
||||
onClick () {
|
||||
if (this.isDisabled) return
|
||||
|
||||
this.selectedIndex > -1
|
||||
? (this.selectedIndex = -1)
|
||||
: this.onFocus()
|
||||
|
||||
this.activateMenu()
|
||||
},
|
||||
onEnterDown () {
|
||||
// Avoid invoking this method
|
||||
// will cause updateSelf to
|
||||
// be called emptying search
|
||||
},
|
||||
onInput (e) {
|
||||
if (this.selectedIndex > -1) return
|
||||
|
||||
// If typing and menu is not currently active
|
||||
if (e.target.value) {
|
||||
this.activateMenu()
|
||||
if (!this.isAnyValueAllowed) this.setMenuIndex(0)
|
||||
}
|
||||
|
||||
this.mask && this.resetSelections(e.target)
|
||||
this.internalSearch = e.target.value
|
||||
this.badInput = e.target.validity && e.target.validity.badInput
|
||||
},
|
||||
onKeyDown (e) {
|
||||
const keyCode = e.keyCode
|
||||
|
||||
VSelect.options.methods.onKeyDown.call(this, e)
|
||||
|
||||
// The ordering is important here
|
||||
// allows new value to be updated
|
||||
// and then moves the index to the
|
||||
// proper location
|
||||
this.changeSelectedIndex(keyCode)
|
||||
},
|
||||
onTabDown (e) {
|
||||
VSelect.options.methods.onTabDown.call(this, e)
|
||||
this.updateSelf()
|
||||
},
|
||||
setSelectedItems () {
|
||||
VSelect.options.methods.setSelectedItems.call(this)
|
||||
|
||||
// #4273 Don't replace if searching
|
||||
// #4403 Don't replace if focused
|
||||
if (!this.isFocused) this.setSearch()
|
||||
},
|
||||
setSearch () {
|
||||
// Wait for nextTick so selectedItem
|
||||
// has had time to update
|
||||
this.$nextTick(() => {
|
||||
this.internalSearch = (
|
||||
this.multiple &&
|
||||
this.internalSearch &&
|
||||
this.isMenuActive
|
||||
)
|
||||
? this.internalSearch
|
||||
: (
|
||||
!this.selectedItems.length ||
|
||||
this.multiple ||
|
||||
this.hasSlot
|
||||
)
|
||||
? null
|
||||
: this.getText(this.selectedItem)
|
||||
})
|
||||
},
|
||||
updateSelf () {
|
||||
this.updateAutocomplete()
|
||||
},
|
||||
updateAutocomplete () {
|
||||
if (!this.searchIsDirty &&
|
||||
!this.internalValue
|
||||
) return
|
||||
|
||||
if (!this.valueComparator(
|
||||
this.internalSearch,
|
||||
this.getValue(this.internalValue)
|
||||
)) {
|
||||
this.setSearch()
|
||||
}
|
||||
},
|
||||
hasItem (item) {
|
||||
return this.selectedValues.indexOf(this.getValue(item)) > -1
|
||||
}
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VAutocomplete from './VAutocomplete'
|
||||
|
||||
export { VAutocomplete }
|
||||
export default VAutocomplete
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import '../../stylus/components/_avatars.styl'
|
||||
|
||||
// Mixins
|
||||
import Colorable from '../../mixins/colorable'
|
||||
import { convertToUnit } from '../../util/helpers'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
/* @vue/component */
|
||||
export default mixins(Colorable).extend({
|
||||
name: 'v-avatar',
|
||||
|
||||
functional: true,
|
||||
|
||||
props: {
|
||||
// TODO: inherit these
|
||||
color: String,
|
||||
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 48
|
||||
},
|
||||
tile: Boolean
|
||||
},
|
||||
|
||||
render (h, { data, props, children }): VNode {
|
||||
data.staticClass = (`v-avatar ${data.staticClass || ''}`).trim()
|
||||
|
||||
if (props.tile) data.staticClass += ' v-avatar--tile'
|
||||
|
||||
const size = convertToUnit(props.size)
|
||||
data.style = {
|
||||
height: size,
|
||||
width: size,
|
||||
...data.style as object
|
||||
}
|
||||
|
||||
return h('div', Colorable.options.methods.setBackgroundColor(props.color, data), children)
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VAvatar from './VAvatar'
|
||||
|
||||
export { VAvatar }
|
||||
export default VAvatar
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_badges.styl'
|
||||
|
||||
// Mixins
|
||||
import Colorable from '../../mixins/colorable'
|
||||
import Toggleable from '../../mixins/toggleable'
|
||||
import { factory as PositionableFactory } from '../../mixins/positionable'
|
||||
import Transitionable from '../../mixins/transitionable'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
export default mixins(
|
||||
Colorable,
|
||||
Toggleable,
|
||||
PositionableFactory(['left', 'bottom']),
|
||||
Transitionable
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-badge',
|
||||
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: 'primary'
|
||||
},
|
||||
overlap: Boolean,
|
||||
transition: {
|
||||
type: String,
|
||||
default: 'fab-transition'
|
||||
},
|
||||
value: {
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes (): object {
|
||||
return {
|
||||
'v-badge--bottom': this.bottom,
|
||||
'v-badge--left': this.left,
|
||||
'v-badge--overlap': this.overlap
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
const badge = this.$slots.badge && [h('span', this.setBackgroundColor(this.color, {
|
||||
staticClass: 'v-badge__badge',
|
||||
attrs: this.$attrs,
|
||||
directives: [{
|
||||
name: 'show',
|
||||
value: this.isActive
|
||||
}]
|
||||
}), this.$slots.badge)]
|
||||
|
||||
return h('span', {
|
||||
staticClass: 'v-badge',
|
||||
'class': this.classes
|
||||
}, [
|
||||
this.$slots.default,
|
||||
h('transition', {
|
||||
props: {
|
||||
name: this.transition,
|
||||
origin: this.origin,
|
||||
mode: this.mode
|
||||
}
|
||||
}, badge)
|
||||
])
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VBadge from './VBadge'
|
||||
|
||||
export { VBadge }
|
||||
export default VBadge
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_bottom-navs.styl'
|
||||
|
||||
// Mixins
|
||||
import Applicationable from '../../mixins/applicationable'
|
||||
import ButtonGroup from '../../mixins/button-group'
|
||||
import Colorable from '../../mixins/colorable'
|
||||
import Themeable from '../../mixins/themeable'
|
||||
|
||||
// Util
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
import { PropValidator } from 'vue/types/options'
|
||||
|
||||
export default mixins(
|
||||
Applicationable('bottom', [
|
||||
'height',
|
||||
'value'
|
||||
]),
|
||||
Colorable,
|
||||
Themeable
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-bottom-nav',
|
||||
|
||||
props: {
|
||||
active: [Number, String],
|
||||
mandatory: Boolean,
|
||||
height: {
|
||||
default: 56,
|
||||
type: [Number, String],
|
||||
validator: (v: string | number): boolean => !isNaN(parseInt(v))
|
||||
},
|
||||
shift: Boolean,
|
||||
value: null as any as PropValidator<any>
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes (): object {
|
||||
return {
|
||||
'v-bottom-nav--absolute': this.absolute,
|
||||
'v-bottom-nav--fixed': !this.absolute && (this.app || this.fixed),
|
||||
'v-bottom-nav--shift': this.shift,
|
||||
'v-bottom-nav--active': this.value
|
||||
}
|
||||
},
|
||||
computedHeight (): number {
|
||||
return parseInt(this.height)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateApplication (): number {
|
||||
return !this.value
|
||||
? 0
|
||||
: this.computedHeight
|
||||
},
|
||||
updateValue (val: any) {
|
||||
this.$emit('update:active', val)
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
return h(ButtonGroup, this.setBackgroundColor(this.color, {
|
||||
staticClass: 'v-bottom-nav',
|
||||
class: this.classes,
|
||||
style: {
|
||||
height: `${parseInt(this.computedHeight)}px`
|
||||
},
|
||||
props: {
|
||||
mandatory: Boolean(this.mandatory || this.active !== undefined),
|
||||
value: this.active
|
||||
},
|
||||
on: { change: this.updateValue }
|
||||
}), this.$slots.default)
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VBottomNav from './VBottomNav'
|
||||
|
||||
export { VBottomNav }
|
||||
export default VBottomNav
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import '../../stylus/components/_bottom-sheets.styl'
|
||||
|
||||
import VDialog from '../VDialog/VDialog'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
name: 'v-bottom-sheet',
|
||||
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
fullWidth: Boolean,
|
||||
hideOverlay: Boolean,
|
||||
inset: Boolean,
|
||||
lazy: Boolean,
|
||||
maxWidth: {
|
||||
type: [String, Number],
|
||||
default: 'auto'
|
||||
},
|
||||
persistent: Boolean,
|
||||
value: null
|
||||
},
|
||||
|
||||
render (h) {
|
||||
const activator = h('template', {
|
||||
slot: 'activator'
|
||||
}, this.$slots.activator)
|
||||
|
||||
const contentClass = [
|
||||
'v-bottom-sheet',
|
||||
this.inset ? 'v-bottom-sheet--inset' : ''
|
||||
].join(' ')
|
||||
|
||||
return h(VDialog, {
|
||||
attrs: {
|
||||
...this.$props
|
||||
},
|
||||
on: {
|
||||
...this.$listeners
|
||||
},
|
||||
props: {
|
||||
contentClass,
|
||||
noClickAnimation: true,
|
||||
transition: 'bottom-sheet-transition',
|
||||
value: this.value
|
||||
}
|
||||
}, [activator, this.$slots.default])
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VBottomSheet from './VBottomSheet'
|
||||
|
||||
export { VBottomSheet }
|
||||
export default VBottomSheet
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_breadcrumbs.styl'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
import { PropValidator } from 'vue/types/options'
|
||||
|
||||
// Components
|
||||
import { VBreadcrumbsDivider, VBreadcrumbsItem } from '.'
|
||||
|
||||
// Mixins
|
||||
import Themeable from '../../mixins/themeable'
|
||||
|
||||
// Utils
|
||||
import { deprecate } from '../../util/console'
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
export default mixins(
|
||||
Themeable
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-breadcrumbs',
|
||||
|
||||
props: {
|
||||
divider: {
|
||||
type: String,
|
||||
default: '/'
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
} as PropValidator<any[]>,
|
||||
large: Boolean,
|
||||
justifyCenter: Boolean,
|
||||
justifyEnd: Boolean
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes (): object {
|
||||
return {
|
||||
'v-breadcrumbs--large': this.large,
|
||||
'justify-center': this.justifyCenter,
|
||||
'justify-end': this.justifyEnd,
|
||||
...this.themeClasses
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.justifyCenter) deprecate('justify-center', 'class="justify-center"', this)
|
||||
if (this.justifyEnd) deprecate('justify-end', 'class="justify-end"', this)
|
||||
if (this.$slots.default) deprecate('default slot', ':items and scoped slot "item"', this)
|
||||
},
|
||||
|
||||
methods: {
|
||||
/* @deprecated */
|
||||
genChildren /* istanbul ignore next */ () {
|
||||
if (!this.$slots.default) return undefined
|
||||
|
||||
const children = []
|
||||
|
||||
let createDividers = false
|
||||
for (let i = 0; i < this.$slots.default.length; i++) {
|
||||
const elm = this.$slots.default[i]
|
||||
|
||||
if (
|
||||
!elm.componentOptions ||
|
||||
elm.componentOptions.Ctor.options.name !== 'v-breadcrumbs-item'
|
||||
) {
|
||||
children.push(elm)
|
||||
} else {
|
||||
if (createDividers) {
|
||||
children.push(this.genDivider())
|
||||
}
|
||||
children.push(elm)
|
||||
createDividers = true
|
||||
}
|
||||
}
|
||||
|
||||
return children
|
||||
},
|
||||
genDivider () {
|
||||
return this.$createElement(VBreadcrumbsDivider, this.$slots.divider ? this.$slots.divider : this.divider)
|
||||
},
|
||||
genItems () {
|
||||
const items = []
|
||||
const hasSlot = !!this.$scopedSlots.item
|
||||
const keys = []
|
||||
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
const item = this.items[i]
|
||||
|
||||
keys.push(item.text)
|
||||
|
||||
if (hasSlot) items.push(this.$scopedSlots.item!({ item }))
|
||||
else items.push(this.$createElement(VBreadcrumbsItem, { key: keys.join('.'), props: item }, [item.text]))
|
||||
|
||||
if (i < this.items.length - 1) items.push(this.genDivider())
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
const children = this.$slots.default ? this.genChildren() : this.genItems()
|
||||
|
||||
return h('ul', {
|
||||
staticClass: 'v-breadcrumbs',
|
||||
'class': this.classes
|
||||
}, children)
|
||||
}
|
||||
})
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import Routable from '../../mixins/routable'
|
||||
|
||||
import mixins from '../../util/mixins'
|
||||
import { VNode } from 'vue'
|
||||
|
||||
/* @vue/component */
|
||||
export default mixins(Routable).extend({
|
||||
name: 'v-breadcrumbs-item',
|
||||
|
||||
props: {
|
||||
// In a breadcrumb, the currently
|
||||
// active item should be dimmed
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: 'v-breadcrumbs__item--disabled'
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes (): object {
|
||||
return {
|
||||
'v-breadcrumbs__item': true,
|
||||
[this.activeClass]: this.disabled
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
const { tag, data } = this.generateRouteLink(this.classes)
|
||||
|
||||
return h('li', [
|
||||
h(tag, data, this.$slots.default)
|
||||
])
|
||||
}
|
||||
})
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import VBreadcrumbs from './VBreadcrumbs'
|
||||
import VBreadcrumbsItem from './VBreadcrumbsItem'
|
||||
import { createSimpleFunctional } from '../../util/helpers'
|
||||
|
||||
const VBreadcrumbsDivider = createSimpleFunctional('v-breadcrumbs__divider', 'li')
|
||||
|
||||
export { VBreadcrumbs, VBreadcrumbsItem, VBreadcrumbsDivider }
|
||||
|
||||
export default {
|
||||
$_vuetify_subcomponents: {
|
||||
VBreadcrumbs,
|
||||
VBreadcrumbsItem,
|
||||
VBreadcrumbsDivider
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_buttons.styl'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
import { PropValidator } from 'vue/types/options'
|
||||
import mixins, { ExtractVue } from '../../util/mixins'
|
||||
import { RippleOptions } from '../../directives/ripple'
|
||||
|
||||
// Components
|
||||
import VProgressCircular from '../VProgressCircular'
|
||||
|
||||
// Mixins
|
||||
import Colorable from '../../mixins/colorable'
|
||||
import { factory as GroupableFactory } from '../../mixins/groupable'
|
||||
import Positionable from '../../mixins/positionable'
|
||||
import Routable from '../../mixins/routable'
|
||||
import Themeable from '../../mixins/themeable'
|
||||
import { factory as ToggleableFactory } from '../../mixins/toggleable'
|
||||
|
||||
// Utilities
|
||||
import { getObjectValueByPath } from '../../util/helpers'
|
||||
|
||||
const baseMixins = mixins(
|
||||
Colorable,
|
||||
Routable,
|
||||
Positionable,
|
||||
Themeable,
|
||||
GroupableFactory('btnToggle'),
|
||||
ToggleableFactory('inputValue')
|
||||
/* @vue/component */
|
||||
)
|
||||
interface options extends ExtractVue<typeof baseMixins> {
|
||||
$el: HTMLElement
|
||||
}
|
||||
|
||||
export default baseMixins.extend<options>().extend({
|
||||
name: 'v-btn',
|
||||
|
||||
props: {
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: 'v-btn--active'
|
||||
},
|
||||
block: Boolean,
|
||||
depressed: Boolean,
|
||||
fab: Boolean,
|
||||
flat: Boolean,
|
||||
icon: Boolean,
|
||||
large: Boolean,
|
||||
loading: Boolean,
|
||||
outline: Boolean,
|
||||
ripple: {
|
||||
type: [Boolean, Object],
|
||||
default: null
|
||||
},
|
||||
round: Boolean,
|
||||
small: Boolean,
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
},
|
||||
value: null as any as PropValidator<any>
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes (): any {
|
||||
return {
|
||||
'v-btn': true,
|
||||
[this.activeClass]: this.isActive,
|
||||
'v-btn--absolute': this.absolute,
|
||||
'v-btn--block': this.block,
|
||||
'v-btn--bottom': this.bottom,
|
||||
'v-btn--disabled': this.disabled,
|
||||
'v-btn--flat': this.flat,
|
||||
'v-btn--floating': this.fab,
|
||||
'v-btn--fixed': this.fixed,
|
||||
'v-btn--icon': this.icon,
|
||||
'v-btn--large': this.large,
|
||||
'v-btn--left': this.left,
|
||||
'v-btn--loader': this.loading,
|
||||
'v-btn--outline': this.outline,
|
||||
'v-btn--depressed': (this.depressed && !this.flat) || this.outline,
|
||||
'v-btn--right': this.right,
|
||||
'v-btn--round': this.round,
|
||||
'v-btn--router': this.to,
|
||||
'v-btn--small': this.small,
|
||||
'v-btn--top': this.top,
|
||||
...this.themeClasses
|
||||
}
|
||||
},
|
||||
computedRipple (): RippleOptions | boolean {
|
||||
const defaultRipple = this.icon || this.fab ? { circle: true } : true
|
||||
if (this.disabled) return false
|
||||
else return this.ripple !== null ? this.ripple : defaultRipple
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route': 'onRouteChange'
|
||||
},
|
||||
|
||||
methods: {
|
||||
// Prevent focus to match md spec
|
||||
click (e: MouseEvent): void {
|
||||
!this.fab &&
|
||||
e.detail &&
|
||||
this.$el.blur()
|
||||
|
||||
this.$emit('click', e)
|
||||
|
||||
this.btnToggle && this.toggle()
|
||||
},
|
||||
genContent (): VNode {
|
||||
return this.$createElement(
|
||||
'div',
|
||||
{ 'class': 'v-btn__content' },
|
||||
this.$slots.default
|
||||
)
|
||||
},
|
||||
genLoader (): VNode {
|
||||
return this.$createElement('span', {
|
||||
class: 'v-btn__loading'
|
||||
}, this.$slots.loader || [this.$createElement(VProgressCircular, {
|
||||
props: {
|
||||
indeterminate: true,
|
||||
size: 23,
|
||||
width: 2
|
||||
}
|
||||
})])
|
||||
},
|
||||
onRouteChange () {
|
||||
if (!this.to || !this.$refs.link) return
|
||||
|
||||
const path = `_vnode.data.class.${this.activeClass}`
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (getObjectValueByPath(this.$refs.link, path)) {
|
||||
this.toggle()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
const setColor = (!this.outline && !this.flat && !this.disabled) ? this.setBackgroundColor : this.setTextColor
|
||||
const { tag, data } = this.generateRouteLink(this.classes)
|
||||
const children = [
|
||||
this.genContent(),
|
||||
this.loading && this.genLoader()
|
||||
]
|
||||
|
||||
if (tag === 'button') data.attrs!.type = this.type
|
||||
|
||||
data.attrs!.value = ['string', 'number'].includes(typeof this.value)
|
||||
? this.value
|
||||
: JSON.stringify(this.value)
|
||||
|
||||
if (this.btnToggle) {
|
||||
data.ref = 'link'
|
||||
}
|
||||
|
||||
return h(tag, setColor(this.color, data), children)
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VBtn from './VBtn'
|
||||
|
||||
export { VBtn }
|
||||
export default VBtn
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_button-toggle.styl'
|
||||
|
||||
// Mixins
|
||||
import ButtonGroup from '../../mixins/button-group'
|
||||
|
||||
/* @vue/component */
|
||||
export default ButtonGroup.extend({
|
||||
name: 'v-btn-toggle',
|
||||
|
||||
props: {
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: 'v-btn--active'
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes (): object {
|
||||
return {
|
||||
...ButtonGroup.options.computed.classes.call(this),
|
||||
'v-btn-toggle': true,
|
||||
'v-btn-toggle--only-child': this.selectedItems.length === 1,
|
||||
'v-btn-toggle--selected': this.selectedItems.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VBtnToggle from './VBtnToggle'
|
||||
|
||||
export { VBtnToggle }
|
||||
export default VBtnToggle
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
// Styles
|
||||
// import '../../stylus/components/_calendar-daily.styl'
|
||||
|
||||
// Types
|
||||
import { VNode, Component } from 'vue'
|
||||
|
||||
// Mixins
|
||||
import CalendarBase from './mixins/calendar-base'
|
||||
|
||||
// Util
|
||||
import props from './util/props'
|
||||
import {
|
||||
DAYS_IN_MONTH_MAX,
|
||||
DAY_MIN,
|
||||
DAYS_IN_WEEK,
|
||||
VTimestamp,
|
||||
VTime,
|
||||
parseTimestamp,
|
||||
relativeDays,
|
||||
nextDay,
|
||||
prevDay,
|
||||
copyTimestamp,
|
||||
updateFormatted,
|
||||
updateWeekday,
|
||||
updateRelative,
|
||||
getStartOfMonth,
|
||||
getEndOfMonth
|
||||
} from './util/timestamp'
|
||||
|
||||
// Calendars
|
||||
import VCalendarMonthly from './VCalendarMonthly'
|
||||
import VCalendarDaily from './VCalendarDaily'
|
||||
import VCalendarWeekly from './VCalendarWeekly'
|
||||
|
||||
// Types
|
||||
interface VCalendarRenderProps {
|
||||
start: VTimestamp
|
||||
end: VTimestamp
|
||||
component: string | Component
|
||||
maxDays: number
|
||||
}
|
||||
|
||||
/* @vue/component */
|
||||
export default CalendarBase.extend({
|
||||
name: 'v-calendar',
|
||||
|
||||
props: {
|
||||
...props.calendar,
|
||||
...props.weeks,
|
||||
...props.intervals
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
lastStart: null as VTimestamp | null,
|
||||
lastEnd: null as VTimestamp | null
|
||||
}),
|
||||
|
||||
computed: {
|
||||
parsedValue (): VTimestamp {
|
||||
return parseTimestamp(this.value) ||
|
||||
this.parsedStart ||
|
||||
this.times.today
|
||||
},
|
||||
renderProps (): VCalendarRenderProps {
|
||||
const around = this.parsedValue
|
||||
let component: any = 'div'
|
||||
let maxDays = this.maxDays
|
||||
let start = around
|
||||
let end = around
|
||||
switch (this.type) {
|
||||
case 'month':
|
||||
component = VCalendarMonthly
|
||||
start = getStartOfMonth(around)
|
||||
end = getEndOfMonth(around)
|
||||
break
|
||||
case 'week':
|
||||
component = VCalendarDaily
|
||||
start = this.getStartOfWeek(around)
|
||||
end = this.getEndOfWeek(around)
|
||||
maxDays = 7
|
||||
break
|
||||
case 'day':
|
||||
component = VCalendarDaily
|
||||
maxDays = 1
|
||||
break
|
||||
case '4day':
|
||||
component = VCalendarDaily
|
||||
end = relativeDays(copyTimestamp(end), nextDay, 4)
|
||||
updateFormatted(end)
|
||||
maxDays = 4
|
||||
break
|
||||
case 'custom-weekly':
|
||||
component = VCalendarWeekly
|
||||
start = this.parsedStart || around
|
||||
end = this.parsedEnd
|
||||
break
|
||||
case 'custom-daily':
|
||||
component = VCalendarDaily
|
||||
start = this.parsedStart || around
|
||||
end = this.parsedEnd
|
||||
break
|
||||
}
|
||||
|
||||
return { component, start, end, maxDays }
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
renderProps: 'checkChange'
|
||||
},
|
||||
|
||||
methods: {
|
||||
checkChange (): void {
|
||||
const { start, end } = this.renderProps
|
||||
if (start !== this.lastStart || end !== this.lastEnd) {
|
||||
this.lastStart = start
|
||||
this.lastEnd = end
|
||||
this.$emit('change', { start, end })
|
||||
}
|
||||
},
|
||||
move (amount = 1): void {
|
||||
const moved = copyTimestamp(this.parsedValue)
|
||||
const forward = amount > 0
|
||||
const mover = forward ? nextDay : prevDay
|
||||
const limit = forward ? DAYS_IN_MONTH_MAX : DAY_MIN
|
||||
let times = forward ? amount : -amount
|
||||
|
||||
while (--times >= 0) {
|
||||
switch (this.type) {
|
||||
case 'month':
|
||||
moved.day = limit
|
||||
mover(moved)
|
||||
break
|
||||
case 'week':
|
||||
relativeDays(moved, mover, DAYS_IN_WEEK)
|
||||
break
|
||||
case 'day':
|
||||
mover(moved)
|
||||
break
|
||||
case '4day':
|
||||
relativeDays(moved, mover, 4)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
updateWeekday(moved)
|
||||
updateFormatted(moved)
|
||||
updateRelative(moved, this.times.now)
|
||||
|
||||
this.$emit('input', moved.date)
|
||||
this.$emit('moved', moved)
|
||||
},
|
||||
next (amount = 1): void {
|
||||
this.move(amount)
|
||||
},
|
||||
prev (amount = 1): void {
|
||||
this.move(-amount)
|
||||
},
|
||||
timeToY (time: VTime, clamp = true): number | false {
|
||||
const c = this.$children[0] as any
|
||||
if (c && c.timeToY) {
|
||||
return c.timeToY(time, clamp)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
minutesToPixels (minutes: number): number {
|
||||
const c = this.$children[0] as any
|
||||
if (c && c.minutesToPixels) {
|
||||
return c.minutesToPixels(minutes)
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
},
|
||||
scrollToTime (time: VTime): boolean {
|
||||
const c = this.$children[0] as any
|
||||
if (c && c.scrollToTime) {
|
||||
return c.scrollToTime(time)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
const { start, end, maxDays, component } = this.renderProps
|
||||
|
||||
return h(component, {
|
||||
staticClass: 'v-calendar',
|
||||
props: {
|
||||
...this.$props,
|
||||
start: start.date,
|
||||
end: end.date,
|
||||
maxDays
|
||||
},
|
||||
on: {
|
||||
...this.$listeners,
|
||||
'click:date': (day: VTimestamp) => {
|
||||
if (this.$listeners['input']) {
|
||||
this.$emit('input', day.date)
|
||||
}
|
||||
if (this.$listeners['click:date']) {
|
||||
this.$emit('click:date', day)
|
||||
}
|
||||
}
|
||||
},
|
||||
scopedSlots: this.$scopedSlots
|
||||
})
|
||||
}
|
||||
})
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_calendar-daily.styl'
|
||||
|
||||
// Types
|
||||
import { VNode, VNodeChildren } from 'vue'
|
||||
|
||||
// Directives
|
||||
import Resize from '../../directives/resize'
|
||||
|
||||
// Mixins
|
||||
import CalendarWithIntervals from './mixins/calendar-with-intervals'
|
||||
|
||||
// Util
|
||||
import { convertToUnit } from '../../util/helpers'
|
||||
import { VTimestamp } from './util/timestamp'
|
||||
|
||||
/* @vue/component */
|
||||
export default CalendarWithIntervals.extend({
|
||||
name: 'v-calendar-daily',
|
||||
|
||||
directives: { Resize },
|
||||
|
||||
data: () => ({
|
||||
scrollPush: 0
|
||||
}),
|
||||
|
||||
computed: {
|
||||
classes (): object {
|
||||
return {
|
||||
'v-calendar-daily': true,
|
||||
...this.themeClasses
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.init()
|
||||
},
|
||||
|
||||
methods: {
|
||||
init () {
|
||||
this.$nextTick(this.onResize)
|
||||
},
|
||||
onResize () {
|
||||
this.scrollPush = this.getScrollPush()
|
||||
},
|
||||
getScrollPush (): number {
|
||||
const area = this.$refs.scrollArea as HTMLElement
|
||||
const pane = this.$refs.pane as HTMLElement
|
||||
|
||||
return area && pane ? (area.offsetWidth - pane.offsetWidth) : 0
|
||||
},
|
||||
genHead (): VNode {
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-calendar-daily__head',
|
||||
style: {
|
||||
marginRight: this.scrollPush + 'px'
|
||||
}
|
||||
}, [
|
||||
this.genHeadIntervals(),
|
||||
...this.genHeadDays()
|
||||
])
|
||||
},
|
||||
genHeadIntervals (): VNode {
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-calendar-daily__intervals-head'
|
||||
})
|
||||
},
|
||||
genHeadDays (): VNode[] {
|
||||
return this.days.map(this.genHeadDay)
|
||||
},
|
||||
genHeadDay (day: VTimestamp): VNode {
|
||||
const slot = this.$scopedSlots.dayHeader
|
||||
|
||||
return this.$createElement('div', {
|
||||
key: day.date,
|
||||
staticClass: 'v-calendar-daily_head-day',
|
||||
class: this.getRelativeClasses(day),
|
||||
on: this.getDefaultMouseEventHandlers(':day', _e => {
|
||||
return this.getSlotScope(day)
|
||||
})
|
||||
}, [
|
||||
this.genHeadWeekday(day),
|
||||
this.genHeadDayLabel(day),
|
||||
slot ? slot(day) : ''
|
||||
])
|
||||
},
|
||||
genHeadWeekday (day: VTimestamp): VNode {
|
||||
const color = day.present ? this.color : undefined
|
||||
|
||||
return this.$createElement('div', this.setTextColor(color, {
|
||||
staticClass: 'v-calendar-daily_head-weekday'
|
||||
}), this.weekdayFormatter(day, this.shortWeekdays))
|
||||
},
|
||||
genHeadDayLabel (day: VTimestamp): VNode {
|
||||
const color = day.present ? this.color : undefined
|
||||
|
||||
return this.$createElement('div', this.setTextColor(color, {
|
||||
staticClass: 'v-calendar-daily_head-day-label',
|
||||
on: this.getMouseEventHandlers({
|
||||
'click:date': { event: 'click', stop: true },
|
||||
'contextmenu:date': { event: 'contextmenu', stop: true, prevent: true, result: false }
|
||||
}, _e => {
|
||||
return day
|
||||
})
|
||||
}), this.dayFormatter(day, false))
|
||||
},
|
||||
genBody (): VNode {
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-calendar-daily__body'
|
||||
}, [
|
||||
this.genScrollArea()
|
||||
])
|
||||
},
|
||||
genScrollArea (): VNode {
|
||||
return this.$createElement('div', {
|
||||
ref: 'scrollArea',
|
||||
staticClass: 'v-calendar-daily__scroll-area'
|
||||
}, [
|
||||
this.genPane()
|
||||
])
|
||||
},
|
||||
genPane (): VNode {
|
||||
return this.$createElement('div', {
|
||||
ref: 'pane',
|
||||
staticClass: 'v-calendar-daily__pane',
|
||||
style: {
|
||||
height: convertToUnit(this.bodyHeight)
|
||||
}
|
||||
}, [
|
||||
this.genDayContainer()
|
||||
])
|
||||
},
|
||||
genDayContainer (): VNode {
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-calendar-daily__day-container'
|
||||
}, [
|
||||
this.genBodyIntervals(),
|
||||
...this.genDays()
|
||||
])
|
||||
},
|
||||
genDays (): VNode[] {
|
||||
return this.days.map(this.genDay)
|
||||
},
|
||||
genDay (day: VTimestamp, index: number): VNode {
|
||||
const slot = this.$scopedSlots.dayBody
|
||||
const scope = this.getSlotScope(day)
|
||||
|
||||
return this.$createElement('div', {
|
||||
key: day.date,
|
||||
staticClass: 'v-calendar-daily__day',
|
||||
class: this.getRelativeClasses(day),
|
||||
on: this.getDefaultMouseEventHandlers(':time', e => {
|
||||
return this.getSlotScope(this.getTimestampAtEvent(e, day))
|
||||
})
|
||||
}, [
|
||||
...this.genDayIntervals(index),
|
||||
slot ? slot(scope) : ''
|
||||
])
|
||||
},
|
||||
genDayIntervals (index: number): VNode[] {
|
||||
return this.intervals[index].map(this.genDayInterval)
|
||||
},
|
||||
genDayInterval (interval: VTimestamp): VNode {
|
||||
const height: string | undefined = convertToUnit(this.intervalHeight)
|
||||
const styler = this.intervalStyle || this.intervalStyleDefault
|
||||
const slot = this.$scopedSlots.interval
|
||||
const scope = this.getSlotScope(interval)
|
||||
|
||||
const data = {
|
||||
key: interval.time,
|
||||
staticClass: 'v-calendar-daily__day-interval',
|
||||
style: {
|
||||
height,
|
||||
...styler(interval)
|
||||
}
|
||||
}
|
||||
|
||||
const children = slot ? slot(scope) as VNodeChildren : undefined
|
||||
|
||||
return this.$createElement('div', data, children)
|
||||
},
|
||||
genBodyIntervals (): VNode {
|
||||
const data = {
|
||||
staticClass: 'v-calendar-daily__intervals-body',
|
||||
on: this.getDefaultMouseEventHandlers(':interval', e => {
|
||||
return this.getTimestampAtEvent(e, this.parsedStart)
|
||||
})
|
||||
}
|
||||
|
||||
return this.$createElement('div', data, this.genIntervalLabels())
|
||||
},
|
||||
genIntervalLabels (): VNode[] {
|
||||
return this.intervals[0].map(this.genIntervalLabel)
|
||||
},
|
||||
genIntervalLabel (interval: VTimestamp): VNode {
|
||||
const height: string | undefined = convertToUnit(this.intervalHeight)
|
||||
const short: boolean = this.shortIntervals
|
||||
const shower = this.showIntervalLabel || this.showIntervalLabelDefault
|
||||
const show = shower(interval)
|
||||
const label = show ? this.intervalFormatter(interval, short) : undefined
|
||||
|
||||
return this.$createElement('div', {
|
||||
key: interval.time,
|
||||
staticClass: 'v-calendar-daily__interval',
|
||||
style: {
|
||||
height
|
||||
}
|
||||
}, [
|
||||
this.$createElement('div', {
|
||||
staticClass: 'v-calendar-daily__interval-text'
|
||||
}, label)
|
||||
])
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
return h('div', {
|
||||
class: this.classes,
|
||||
nativeOn: {
|
||||
dragstart: (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
directives: [{
|
||||
modifiers: { quiet: true },
|
||||
name: 'resize',
|
||||
value: this.onResize
|
||||
}]
|
||||
}, [
|
||||
!this.hideHeader ? this.genHead() : '',
|
||||
this.genBody()
|
||||
])
|
||||
}
|
||||
})
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_calendar-weekly.styl'
|
||||
|
||||
// Mixins
|
||||
import VCalendarWeekly from './VCalendarWeekly'
|
||||
|
||||
// Util
|
||||
import { VTimestamp, parseTimestamp, getStartOfMonth, getEndOfMonth } from './util/timestamp'
|
||||
|
||||
/* @vue/component */
|
||||
export default VCalendarWeekly.extend({
|
||||
name: 'v-calendar-monthly',
|
||||
|
||||
computed: {
|
||||
staticClass (): string {
|
||||
return 'v-calendar-monthly v-calendar-weekly'
|
||||
},
|
||||
parsedStart (): VTimestamp {
|
||||
return getStartOfMonth(parseTimestamp(this.start) as VTimestamp)
|
||||
},
|
||||
parsedEnd (): VTimestamp {
|
||||
return getEndOfMonth(parseTimestamp(this.end) as VTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_calendar-weekly.styl'
|
||||
|
||||
// Types
|
||||
import { VNode, VNodeChildren } from 'vue'
|
||||
|
||||
// Mixins
|
||||
import CalendarBase from './mixins/calendar-base'
|
||||
|
||||
// Util
|
||||
import props from './util/props'
|
||||
import {
|
||||
VTimestamp,
|
||||
VTimestampFormatter,
|
||||
createDayList,
|
||||
getDayIdentifier,
|
||||
createNativeLocaleFormatter
|
||||
} from './util/timestamp'
|
||||
|
||||
/* @vue/component */
|
||||
export default CalendarBase.extend({
|
||||
name: 'v-calendar-weekly',
|
||||
|
||||
props: props.weeks,
|
||||
|
||||
computed: {
|
||||
staticClass (): string {
|
||||
return 'v-calendar-weekly'
|
||||
},
|
||||
classes (): object {
|
||||
return this.themeClasses
|
||||
},
|
||||
parsedMinWeeks (): number {
|
||||
return parseInt(this.minWeeks)
|
||||
},
|
||||
days (): VTimestamp[] {
|
||||
const minDays = this.parsedMinWeeks * this.weekdays.length
|
||||
const start = this.getStartOfWeek(this.parsedStart)
|
||||
const end = this.getEndOfWeek(this.parsedEnd)
|
||||
|
||||
return createDayList(
|
||||
start,
|
||||
end,
|
||||
this.times.today,
|
||||
this.weekdaySkips,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
minDays
|
||||
)
|
||||
},
|
||||
todayWeek (): VTimestamp[] {
|
||||
const today = this.times.today
|
||||
const start = this.getStartOfWeek(today)
|
||||
const end = this.getEndOfWeek(today)
|
||||
|
||||
return createDayList(
|
||||
start,
|
||||
end,
|
||||
today,
|
||||
this.weekdaySkips,
|
||||
this.weekdays.length,
|
||||
this.weekdays.length
|
||||
)
|
||||
},
|
||||
monthFormatter (): VTimestampFormatter {
|
||||
if (this.monthFormat) {
|
||||
return this.monthFormat as VTimestampFormatter
|
||||
}
|
||||
|
||||
const longOptions = { timeZone: 'UTC', month: 'long' }
|
||||
const shortOptions = { timeZone: 'UTC', month: 'short' }
|
||||
|
||||
return createNativeLocaleFormatter(
|
||||
this.locale,
|
||||
(_tms, short) => short ? shortOptions : longOptions
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
isOutside (day: VTimestamp): boolean {
|
||||
const dayIdentifier = getDayIdentifier(day)
|
||||
|
||||
return dayIdentifier < getDayIdentifier(this.parsedStart) ||
|
||||
dayIdentifier > getDayIdentifier(this.parsedEnd)
|
||||
},
|
||||
genHead (): VNode {
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-calendar-weekly__head'
|
||||
}, this.genHeadDays())
|
||||
},
|
||||
genHeadDays (): VNode[] {
|
||||
return this.todayWeek.map(this.genHeadDay)
|
||||
},
|
||||
genHeadDay (day: VTimestamp, index: number): VNode {
|
||||
const outside = this.isOutside(this.days[index])
|
||||
const color = day.present ? this.color : undefined
|
||||
|
||||
return this.$createElement('div', this.setTextColor(color, {
|
||||
key: day.date,
|
||||
staticClass: 'v-calendar-weekly__head-weekday',
|
||||
class: this.getRelativeClasses(day, outside)
|
||||
}), this.weekdayFormatter(day, this.shortWeekdays))
|
||||
},
|
||||
genWeeks (): VNode[] {
|
||||
const days = this.days
|
||||
const weekDays = this.weekdays.length
|
||||
const weeks: VNode[] = []
|
||||
for (let i = 0; i < days.length; i += weekDays) {
|
||||
weeks.push(this.genWeek(days.slice(i, i + weekDays)))
|
||||
}
|
||||
|
||||
return weeks
|
||||
},
|
||||
genWeek (week: VTimestamp[]): VNode {
|
||||
return this.$createElement('div', {
|
||||
key: week[0].date,
|
||||
staticClass: 'v-calendar-weekly__week'
|
||||
}, week.map(this.genDay))
|
||||
},
|
||||
genDay (day: VTimestamp): VNode {
|
||||
const outside = this.isOutside(day)
|
||||
const slot = this.$scopedSlots.day
|
||||
const slotData = { outside, ...day }
|
||||
const hasMonth = day.day === 1 && this.showMonthOnFirst
|
||||
|
||||
return this.$createElement('div', {
|
||||
key: day.date,
|
||||
staticClass: 'v-calendar-weekly__day',
|
||||
class: this.getRelativeClasses(day, outside),
|
||||
on: this.getDefaultMouseEventHandlers(':day', _e => day)
|
||||
}, [
|
||||
this.genDayLabel(day),
|
||||
hasMonth ? this.genDayMonth(day) : '',
|
||||
slot ? slot(slotData) : ''
|
||||
])
|
||||
},
|
||||
genDayLabel (day: VTimestamp): VNode {
|
||||
const color = day.present ? this.color : undefined
|
||||
const slot = this.$scopedSlots.dayLabel
|
||||
|
||||
return this.$createElement('div', this.setTextColor(color, {
|
||||
staticClass: 'v-calendar-weekly__day-label',
|
||||
on: this.getMouseEventHandlers({
|
||||
'click:date': { event: 'click', stop: true },
|
||||
'contextmenu:date': { event: 'contextmenu', stop: true, prevent: true, result: false }
|
||||
}, _e => day)
|
||||
}), slot ? slot(day) as VNodeChildren : this.dayFormatter(day, false))
|
||||
},
|
||||
genDayMonth (day: VTimestamp): VNode | string {
|
||||
const color = day.present ? this.color : undefined
|
||||
const slot = this.$scopedSlots.dayMonth
|
||||
|
||||
return this.$createElement('div', this.setTextColor(color, {
|
||||
staticClass: 'v-calendar-weekly__day-month'
|
||||
}), slot ? slot(day) as VNodeChildren : this.monthFormatter(day, this.shortMonths))
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
return h('div', {
|
||||
staticClass: this.staticClass,
|
||||
class: this.classes,
|
||||
nativeOn: {
|
||||
dragstart: (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}, [
|
||||
!this.hideHeader ? this.genHead() : '',
|
||||
...this.genWeeks()
|
||||
])
|
||||
}
|
||||
})
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import VCalendar from './VCalendar'
|
||||
import VCalendarDaily from './VCalendarDaily'
|
||||
import VCalendarWeekly from './VCalendarWeekly'
|
||||
import VCalendarMonthly from './VCalendarMonthly'
|
||||
|
||||
export { VCalendar, VCalendarDaily, VCalendarWeekly, VCalendarMonthly }
|
||||
|
||||
export default {
|
||||
$_vuetify_subcomponents: {
|
||||
VCalendar,
|
||||
VCalendarDaily,
|
||||
VCalendarWeekly,
|
||||
VCalendarMonthly
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
|
||||
// Mixins
|
||||
import mixins from '../../../util/mixins'
|
||||
import Themeable from '../../../mixins/themeable'
|
||||
import Colorable from '../../../mixins/colorable'
|
||||
import Times from './times'
|
||||
import Mouse from './mouse'
|
||||
|
||||
// Util
|
||||
import props from '../util/props'
|
||||
import {
|
||||
VTimestamp,
|
||||
VTimestampFormatter,
|
||||
parseTimestamp,
|
||||
getWeekdaySkips,
|
||||
createDayList,
|
||||
createNativeLocaleFormatter,
|
||||
getStartOfWeek,
|
||||
getEndOfWeek
|
||||
} from '../util/timestamp'
|
||||
|
||||
/* @vue/component */
|
||||
export default mixins(Colorable, Themeable, Times, Mouse).extend({
|
||||
name: 'calendar-base',
|
||||
|
||||
props: props.base,
|
||||
|
||||
computed: {
|
||||
weekdaySkips (): number[] {
|
||||
return getWeekdaySkips(this.weekdays)
|
||||
},
|
||||
parsedStart (): VTimestamp {
|
||||
return parseTimestamp(this.start) as VTimestamp
|
||||
},
|
||||
parsedEnd (): VTimestamp {
|
||||
return parseTimestamp(this.end) as VTimestamp
|
||||
},
|
||||
days (): VTimestamp[] {
|
||||
return createDayList(
|
||||
this.parsedStart,
|
||||
this.parsedEnd,
|
||||
this.times.today,
|
||||
this.weekdaySkips
|
||||
)
|
||||
},
|
||||
dayFormatter (): VTimestampFormatter {
|
||||
if (this.dayFormat) {
|
||||
return this.dayFormat as VTimestampFormatter
|
||||
}
|
||||
|
||||
const options = { timeZone: 'UTC', day: 'numeric' }
|
||||
|
||||
return createNativeLocaleFormatter(
|
||||
this.locale,
|
||||
(_tms, _short) => options
|
||||
)
|
||||
},
|
||||
weekdayFormatter (): VTimestampFormatter {
|
||||
if (this.weekdayFormat) {
|
||||
return this.weekdayFormat as VTimestampFormatter
|
||||
}
|
||||
|
||||
const longOptions = { timeZone: 'UTC', weekday: 'long' }
|
||||
const shortOptions = { timeZone: 'UTC', weekday: 'short' }
|
||||
|
||||
return createNativeLocaleFormatter(
|
||||
this.locale,
|
||||
(_tms, short) => short ? shortOptions : longOptions
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getRelativeClasses (timestamp: VTimestamp, outside = false): object {
|
||||
return {
|
||||
'v-present': timestamp.present,
|
||||
'v-past': timestamp.past,
|
||||
'v-future': timestamp.future,
|
||||
'v-outside': outside
|
||||
}
|
||||
},
|
||||
getStartOfWeek (timestamp: VTimestamp): VTimestamp {
|
||||
return getStartOfWeek(timestamp, this.weekdays, this.times.today)
|
||||
},
|
||||
getEndOfWeek (timestamp: VTimestamp): VTimestamp {
|
||||
return getEndOfWeek(timestamp, this.weekdays, this.times.today)
|
||||
}
|
||||
}
|
||||
})
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
|
||||
// Mixins
|
||||
import CalendarBase from './calendar-base'
|
||||
|
||||
// Util
|
||||
import props from '../util/props'
|
||||
import {
|
||||
VTimestamp,
|
||||
VTime,
|
||||
VTimestampFormatter,
|
||||
parseTime,
|
||||
copyTimestamp,
|
||||
updateMinutes,
|
||||
createDayList,
|
||||
createIntervalList,
|
||||
createNativeLocaleFormatter
|
||||
} from '../util/timestamp'
|
||||
|
||||
/* @vue/component */
|
||||
export default CalendarBase.extend({
|
||||
name: 'calendar-with-intervals',
|
||||
|
||||
props: props.intervals,
|
||||
|
||||
computed: {
|
||||
parsedFirstInterval (): number {
|
||||
return parseInt(this.firstInterval)
|
||||
},
|
||||
parsedIntervalMinutes (): number {
|
||||
return parseInt(this.intervalMinutes)
|
||||
},
|
||||
parsedIntervalCount (): number {
|
||||
return parseInt(this.intervalCount)
|
||||
},
|
||||
parsedIntervalHeight (): number {
|
||||
return parseFloat(this.intervalHeight)
|
||||
},
|
||||
firstMinute (): number {
|
||||
return this.parsedFirstInterval * this.parsedIntervalMinutes
|
||||
},
|
||||
bodyHeight (): number {
|
||||
return this.parsedIntervalCount * this.parsedIntervalHeight
|
||||
},
|
||||
days (): VTimestamp[] {
|
||||
return createDayList(
|
||||
this.parsedStart,
|
||||
this.parsedEnd,
|
||||
this.times.today,
|
||||
this.weekdaySkips,
|
||||
this.maxDays
|
||||
)
|
||||
},
|
||||
intervals (): VTimestamp[][] {
|
||||
const days: VTimestamp[] = this.days
|
||||
const first: number = this.parsedFirstInterval
|
||||
const minutes: number = this.parsedIntervalMinutes
|
||||
const count: number = this.parsedIntervalCount
|
||||
const now: VTimestamp = this.times.now
|
||||
|
||||
return days.map(d => createIntervalList(d, first, minutes, count, now))
|
||||
},
|
||||
intervalFormatter (): VTimestampFormatter {
|
||||
if (this.intervalFormat) {
|
||||
return this.intervalFormat as VTimestampFormatter
|
||||
}
|
||||
|
||||
const longOptions = { timeZone: 'UTC', hour12: true, hour: '2-digit', minute: '2-digit' }
|
||||
const shortOptions = { timeZone: 'UTC', hour12: true, hour: 'numeric', minute: '2-digit' }
|
||||
const shortHourOptions = { timeZone: 'UTC', hour12: true, hour: 'numeric' }
|
||||
|
||||
return createNativeLocaleFormatter(
|
||||
this.locale,
|
||||
(tms, short) => short ? (tms.minute === 0 ? shortHourOptions : shortOptions) : longOptions
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
showIntervalLabelDefault (interval: VTimestamp): boolean {
|
||||
const first: VTimestamp = this.intervals[0][0]
|
||||
const isFirst: boolean = first.hour === interval.hour && first.minute === interval.minute
|
||||
return !isFirst && interval.minute === 0
|
||||
},
|
||||
intervalStyleDefault (_interval: VTimestamp): object | undefined {
|
||||
return undefined
|
||||
},
|
||||
getTimestampAtEvent (e: MouseEvent | TouchEvent, day: VTimestamp): VTimestamp {
|
||||
const timestamp: VTimestamp = copyTimestamp(day)
|
||||
const bounds = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
const baseMinutes: number = this.firstMinute
|
||||
const touchEvent: TouchEvent = e as TouchEvent
|
||||
const mouseEvent: MouseEvent = e as MouseEvent
|
||||
const touches: TouchList = touchEvent.changedTouches || touchEvent.touches
|
||||
const clientY: number = touches && touches[0] ? touches[0].clientY : mouseEvent.clientY
|
||||
const addIntervals: number = (clientY - bounds.top) / this.parsedIntervalHeight
|
||||
const addMinutes: number = Math.floor(addIntervals * this.parsedIntervalMinutes)
|
||||
const minutes: number = baseMinutes + addMinutes
|
||||
|
||||
return updateMinutes(timestamp, minutes, this.times.now)
|
||||
},
|
||||
getSlotScope (timestamp: VTimestamp): any {
|
||||
const scope = copyTimestamp(timestamp) as any
|
||||
scope.timeToY = this.timeToY
|
||||
scope.minutesToPixels = this.minutesToPixels
|
||||
return scope
|
||||
},
|
||||
scrollToTime (time: VTime): boolean {
|
||||
const y = this.timeToY(time)
|
||||
const pane = this.$refs.scrollArea as HTMLElement
|
||||
|
||||
if (y === false || !pane) {
|
||||
return false
|
||||
}
|
||||
|
||||
pane.scrollTop = y
|
||||
|
||||
return true
|
||||
},
|
||||
minutesToPixels (minutes: number): number {
|
||||
return minutes / this.parsedIntervalMinutes * this.parsedIntervalHeight
|
||||
},
|
||||
timeToY (time: VTime, clamp = true): number | false {
|
||||
const minutes = parseTime(time)
|
||||
|
||||
if (minutes === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
const min: number = this.firstMinute
|
||||
const gap: number = this.parsedIntervalCount * this.parsedIntervalMinutes
|
||||
const delta: number = (minutes - min) / gap
|
||||
let y: number = delta * this.bodyHeight
|
||||
|
||||
if (clamp) {
|
||||
if (y < 0) {
|
||||
y = 0
|
||||
}
|
||||
if (y > this.bodyHeight) {
|
||||
y = this.bodyHeight
|
||||
}
|
||||
}
|
||||
|
||||
return y
|
||||
}
|
||||
}
|
||||
})
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
export type MouseHandler = (e: MouseEvent | TouchEvent) => any
|
||||
|
||||
export type MouseEvents = {
|
||||
[event: string]: {
|
||||
event: string
|
||||
passive?: boolean
|
||||
capture?: boolean
|
||||
once?: boolean
|
||||
stop?: boolean
|
||||
prevent?: boolean
|
||||
button?: number
|
||||
result?: any
|
||||
}
|
||||
}
|
||||
|
||||
export type MouseEventsMap = {
|
||||
[event: string]: MouseHandler | MouseHandler[]
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'mouse',
|
||||
|
||||
methods: {
|
||||
getDefaultMouseEventHandlers (suffix: string, getEvent: MouseHandler): MouseEventsMap {
|
||||
return this.getMouseEventHandlers({
|
||||
['click' + suffix]: { event: 'click' },
|
||||
['contextmenu' + suffix]: { event: 'contextmenu', prevent: true, result: false },
|
||||
['mousedown' + suffix]: { event: 'mousedown' },
|
||||
['mousemove' + suffix]: { event: 'mousemove' },
|
||||
['mouseup' + suffix]: { event: 'mouseup' },
|
||||
['mouseenter' + suffix]: { event: 'mouseenter' },
|
||||
['mouseleave' + suffix]: { event: 'mouseleave' },
|
||||
['touchstart' + suffix]: { event: 'touchstart' },
|
||||
['touchmove' + suffix]: { event: 'touchmove' },
|
||||
['touchend' + suffix]: { event: 'touchend' }
|
||||
}, getEvent)
|
||||
},
|
||||
getMouseEventHandlers (events: MouseEvents, getEvent: MouseHandler): MouseEventsMap {
|
||||
const on: MouseEventsMap = {}
|
||||
|
||||
for (const event in events) {
|
||||
const eventOptions = events[event]
|
||||
|
||||
if (!this.$listeners[event]) continue
|
||||
|
||||
// TODO somehow pull in modifiers
|
||||
|
||||
const prefix = eventOptions.passive ? '&' : ((eventOptions.once ? '~' : '') + (eventOptions.capture ? '!' : ''))
|
||||
const key = prefix + eventOptions.event
|
||||
|
||||
const handler: MouseHandler = e => {
|
||||
const mouseEvent: MouseEvent = e as MouseEvent
|
||||
if (eventOptions.button === undefined || (mouseEvent.buttons > 0 && mouseEvent.button === eventOptions.button)) {
|
||||
if (eventOptions.prevent) {
|
||||
e.preventDefault()
|
||||
}
|
||||
if (eventOptions.stop) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
this.$emit(event, getEvent(e))
|
||||
}
|
||||
|
||||
return eventOptions.result
|
||||
}
|
||||
|
||||
if (key in on) {
|
||||
if (Array.isArray(on[key])) {
|
||||
(on[key] as MouseHandler[]).push(handler)
|
||||
} else {
|
||||
on[key] = [on[key], handler] as MouseHandler[]
|
||||
}
|
||||
} else {
|
||||
on[key] = handler
|
||||
}
|
||||
}
|
||||
|
||||
return on
|
||||
}
|
||||
}
|
||||
})
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import {
|
||||
VTimestamp,
|
||||
validateTimestamp,
|
||||
parseTimestamp,
|
||||
parseDate
|
||||
} from '../util/timestamp'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'times',
|
||||
|
||||
props: {
|
||||
now: {
|
||||
type: String,
|
||||
validator: validateTimestamp
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
times: {
|
||||
now: parseTimestamp('0000-00-00 00:00') as VTimestamp,
|
||||
today: parseTimestamp('0000-00-00') as VTimestamp
|
||||
}
|
||||
}),
|
||||
|
||||
computed: {
|
||||
parsedNow (): VTimestamp | null {
|
||||
return this.now ? parseTimestamp(this.now) : null
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
parsedNow: 'updateTimes'
|
||||
},
|
||||
|
||||
created () {
|
||||
this.updateTimes()
|
||||
this.setPresent()
|
||||
},
|
||||
|
||||
methods: {
|
||||
setPresent (): void {
|
||||
this.times.now.present = this.times.today.present = true
|
||||
this.times.now.past = this.times.today.past = false
|
||||
this.times.now.future = this.times.today.future = false
|
||||
},
|
||||
updateTimes (): void {
|
||||
const now: VTimestamp = this.parsedNow || this.getNow()
|
||||
this.updateDay(now, this.times.now)
|
||||
this.updateTime(now, this.times.now)
|
||||
this.updateDay(now, this.times.today)
|
||||
},
|
||||
getNow (): VTimestamp {
|
||||
return parseDate(new Date())
|
||||
},
|
||||
updateDay (now: VTimestamp, target: VTimestamp): void {
|
||||
if (now.date !== target.date) {
|
||||
target.year = now.year
|
||||
target.month = now.month
|
||||
target.day = now.day
|
||||
target.weekday = now.weekday
|
||||
target.date = now.date
|
||||
}
|
||||
},
|
||||
updateTime (now: VTimestamp, target: VTimestamp): void {
|
||||
if (now.time !== target.time) {
|
||||
target.hour = now.hour
|
||||
target.minute = now.minute
|
||||
target.time = now.time
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
|
||||
import { validateTimestamp, parseDate } from './timestamp'
|
||||
|
||||
export default {
|
||||
base: {
|
||||
start: {
|
||||
type: String,
|
||||
validate: validateTimestamp,
|
||||
default: () => parseDate(new Date()).date
|
||||
},
|
||||
end: {
|
||||
type: String,
|
||||
validate: validateTimestamp,
|
||||
default: '0000-00-00'
|
||||
},
|
||||
weekdays: {
|
||||
type: Array as () => number[],
|
||||
default: () => [0, 1, 2, 3, 4, 5, 6]
|
||||
},
|
||||
hideHeader: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
shortWeekdays: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
weekdayFormat: {
|
||||
type: Function, // VTimestampFormatter,
|
||||
default: null
|
||||
},
|
||||
dayFormat: {
|
||||
type: Function, // VTimestampFormatter,
|
||||
default: null
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
default: 'en-us'
|
||||
}
|
||||
},
|
||||
intervals: {
|
||||
maxDays: {
|
||||
type: Number,
|
||||
default: 7
|
||||
},
|
||||
shortIntervals: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
intervalHeight: {
|
||||
type: [Number, String],
|
||||
default: 40,
|
||||
validate: validateNumber
|
||||
},
|
||||
intervalMinutes: {
|
||||
type: [Number, String],
|
||||
default: 60,
|
||||
validate: validateNumber
|
||||
},
|
||||
firstInterval: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
validate: validateNumber
|
||||
},
|
||||
intervalCount: {
|
||||
type: [Number, String],
|
||||
default: 24,
|
||||
validate: validateNumber
|
||||
},
|
||||
intervalFormat: {
|
||||
type: Function, // VTimestampFormatter,
|
||||
default: null
|
||||
},
|
||||
intervalStyle: {
|
||||
type: Function, // (interval: VTimestamp): object
|
||||
default: null
|
||||
},
|
||||
showIntervalLabel: {
|
||||
type: Function, // (interval: VTimestamp): boolean
|
||||
default: null
|
||||
}
|
||||
},
|
||||
weeks: {
|
||||
minWeeks: {
|
||||
validate: validateNumber,
|
||||
default: 1
|
||||
},
|
||||
shortMonths: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showMonthOnFirst: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
monthFormat: {
|
||||
type: Function, // VTimestampFormatter,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
calendar: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'month'
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
validate: validateTimestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateNumber (input: any): boolean {
|
||||
return isFinite(parseInt(input))
|
||||
}
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
|
||||
export const PARSE_REGEX: RegExp = /^(\d{4})-(\d{1,2})(-(\d{1,2}))?([^\d]+(\d{1,2}))?(:(\d{1,2}))?(:(\d{1,2}))?$/
|
||||
export const PARSE_TIME: RegExp = /(\d\d?)(:(\d\d?)|)(:(\d\d?)|)/
|
||||
|
||||
export const DAYS_IN_MONTH: number[] = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
export const DAYS_IN_MONTH_LEAP: number[] = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
export const DAYS_IN_MONTH_MIN = 28
|
||||
export const DAYS_IN_MONTH_MAX = 31
|
||||
export const MONTH_MAX = 12
|
||||
export const MONTH_MIN = 1
|
||||
export const DAY_MIN = 1
|
||||
export const DAYS_IN_WEEK = 7
|
||||
export const MINUTES_IN_HOUR = 60
|
||||
export const HOURS_IN_DAY = 24
|
||||
export const FIRST_HOUR = 0
|
||||
|
||||
export interface VTimestamp {
|
||||
date: string
|
||||
time: string
|
||||
year: number
|
||||
month: number
|
||||
day: number
|
||||
weekday: number
|
||||
hour: number
|
||||
minute: number
|
||||
hasDay: boolean
|
||||
hasTime: boolean
|
||||
past: boolean
|
||||
present: boolean
|
||||
future: boolean
|
||||
}
|
||||
|
||||
export interface VTimeObject {
|
||||
hour: number
|
||||
minute: number
|
||||
}
|
||||
|
||||
export type VTime = number | string | VTimeObject
|
||||
|
||||
export type VTimestampFormatter = (timestamp: VTimestamp, short: boolean) => string
|
||||
|
||||
export type VTimestampFormatOptions = (timestamp: VTimestamp, short: boolean) => object
|
||||
|
||||
export type VTimestampOperation = (timestamp: VTimestamp) => VTimestamp
|
||||
|
||||
export function getStartOfWeek (timestamp: VTimestamp, weekdays: number[], today?: VTimestamp): VTimestamp {
|
||||
const start = copyTimestamp(timestamp)
|
||||
findWeekday(start, weekdays[0], prevDay)
|
||||
updateFormatted(start)
|
||||
if (today) {
|
||||
updateRelative(start, today, start.hasTime)
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
export function getEndOfWeek (timestamp: VTimestamp, weekdays: number[], today?: VTimestamp): VTimestamp {
|
||||
const end = copyTimestamp(timestamp)
|
||||
findWeekday(end, weekdays[weekdays.length - 1])
|
||||
updateFormatted(end)
|
||||
if (today) {
|
||||
updateRelative(end, today, end.hasTime)
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
export function getStartOfMonth (timestamp: VTimestamp): VTimestamp {
|
||||
const start = copyTimestamp(timestamp)
|
||||
start.day = DAY_MIN
|
||||
updateWeekday(start)
|
||||
updateFormatted(start)
|
||||
return start
|
||||
}
|
||||
|
||||
export function getEndOfMonth (timestamp: VTimestamp): VTimestamp {
|
||||
const end = copyTimestamp(timestamp)
|
||||
end.day = daysInMonth(end.year, end.month)
|
||||
updateWeekday(end)
|
||||
updateFormatted(end)
|
||||
return end
|
||||
}
|
||||
|
||||
export function parseTime (input: any): number | false {
|
||||
if (typeof input === 'number') {
|
||||
// when a number is given, it's minutes since 12:00am
|
||||
return input
|
||||
} else if (typeof input === 'string') {
|
||||
// when a string is given, it's a hh:mm:ss format where seconds are optional
|
||||
const parts = PARSE_TIME.exec(input)
|
||||
if (!parts) {
|
||||
return false
|
||||
}
|
||||
return parseInt(parts[1]) * 60 + parseInt(parts[3] || 0)
|
||||
} else if (typeof input === 'object') {
|
||||
// when an object is given, it must have hour and minute
|
||||
if (typeof input.hour !== 'number' || typeof input.minute !== 'number') {
|
||||
return false
|
||||
}
|
||||
return input.hour * 60 + input.minute
|
||||
} else {
|
||||
// unsupported type
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function validateTimestamp (input: any): boolean {
|
||||
return !!PARSE_REGEX.exec(input)
|
||||
}
|
||||
|
||||
export function parseTimestamp (input: string, now?: VTimestamp): VTimestamp | null {
|
||||
// YYYY-MM-DD hh:mm:ss
|
||||
const parts = PARSE_REGEX.exec(input)
|
||||
|
||||
if (!parts) return null
|
||||
|
||||
const timestamp: VTimestamp = {
|
||||
date: input,
|
||||
time: '',
|
||||
year: parseInt(parts[1]),
|
||||
month: parseInt(parts[2]),
|
||||
day: parseInt(parts[4]) || 1,
|
||||
hour: parseInt(parts[6]) || 0,
|
||||
minute: parseInt(parts[8]) || 0,
|
||||
weekday: 0,
|
||||
hasDay: !!parts[4],
|
||||
hasTime: !!(parts[6] && parts[8]),
|
||||
past: false,
|
||||
present: false,
|
||||
future: false
|
||||
}
|
||||
|
||||
updateWeekday(timestamp)
|
||||
updateFormatted(timestamp)
|
||||
|
||||
if (now) {
|
||||
updateRelative(timestamp, now, timestamp.hasTime)
|
||||
}
|
||||
|
||||
return timestamp
|
||||
}
|
||||
|
||||
export function parseDate (date: Date): VTimestamp {
|
||||
return updateFormatted({
|
||||
date: '',
|
||||
time: '',
|
||||
year: date.getFullYear(),
|
||||
month: date.getMonth() + 1,
|
||||
day: date.getDate(),
|
||||
weekday: date.getDay(),
|
||||
hour: date.getHours(),
|
||||
minute: date.getMinutes(),
|
||||
hasDay: true,
|
||||
hasTime: true,
|
||||
past: false,
|
||||
present: true,
|
||||
future: false
|
||||
})
|
||||
}
|
||||
|
||||
export function getDayIdentifier (timestamp: VTimestamp): number {
|
||||
return timestamp.year * 10000 + timestamp.month * 100 + timestamp.day
|
||||
}
|
||||
|
||||
export function getTimeIdentifier (timestamp: VTimestamp): number {
|
||||
return timestamp.hour * 100 + timestamp.minute
|
||||
}
|
||||
|
||||
export function updateRelative (timestamp: VTimestamp, now: VTimestamp, time = false): VTimestamp {
|
||||
let a = getDayIdentifier(now)
|
||||
let b = getDayIdentifier(timestamp)
|
||||
let present = a === b
|
||||
|
||||
if (timestamp.hasTime && time && present) {
|
||||
a = getTimeIdentifier(now)
|
||||
b = getTimeIdentifier(timestamp)
|
||||
present = a === b
|
||||
}
|
||||
|
||||
timestamp.past = b < a
|
||||
timestamp.present = present
|
||||
timestamp.future = b > a
|
||||
|
||||
return timestamp
|
||||
}
|
||||
|
||||
export function updateMinutes (timestamp: VTimestamp, minutes: number, now?: VTimestamp): VTimestamp {
|
||||
timestamp.hasTime = true
|
||||
timestamp.hour = Math.floor(minutes / MINUTES_IN_HOUR)
|
||||
timestamp.minute = minutes % MINUTES_IN_HOUR
|
||||
timestamp.time = getTime(timestamp)
|
||||
if (now) {
|
||||
updateRelative(timestamp, now, true)
|
||||
}
|
||||
|
||||
return timestamp
|
||||
}
|
||||
|
||||
export function updateWeekday (timestamp: VTimestamp): VTimestamp {
|
||||
timestamp.weekday = getWeekday(timestamp)
|
||||
|
||||
return timestamp
|
||||
}
|
||||
|
||||
export function updateFormatted (timestamp: VTimestamp): VTimestamp {
|
||||
timestamp.time = getTime(timestamp)
|
||||
timestamp.date = getDate(timestamp)
|
||||
|
||||
return timestamp
|
||||
}
|
||||
|
||||
export function getWeekday (timestamp: VTimestamp): number {
|
||||
if (timestamp.hasDay) {
|
||||
const _ = Math.floor
|
||||
const k = timestamp.day
|
||||
const m = ((timestamp.month + 9) % MONTH_MAX) + 1
|
||||
const C = _(timestamp.year / 100)
|
||||
const Y = (timestamp.year % 100) - (timestamp.month <= 2 ? 1 : 0)
|
||||
|
||||
return (((k + _(2.6 * m - 0.2) - 2 * C + Y + _(Y / 4) + _(C / 4)) % 7) + 7) % 7
|
||||
}
|
||||
|
||||
return timestamp.weekday
|
||||
}
|
||||
|
||||
export function isLeapYear (year: number): boolean {
|
||||
return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)
|
||||
}
|
||||
|
||||
export function daysInMonth (year: number, month: number) {
|
||||
return isLeapYear(year) ? DAYS_IN_MONTH_LEAP[month] : DAYS_IN_MONTH[month]
|
||||
}
|
||||
|
||||
export function copyTimestamp (timestamp: VTimestamp): VTimestamp {
|
||||
const { date, time, year, month, day, weekday, hour, minute, hasDay, hasTime, past, present, future } = timestamp
|
||||
|
||||
return { date, time, year, month, day, weekday, hour, minute, hasDay, hasTime, past, present, future }
|
||||
}
|
||||
|
||||
export function padNumber (x: number, length: number): string {
|
||||
let padded = String(x)
|
||||
while (padded.length < length) {
|
||||
padded = '0' + padded
|
||||
}
|
||||
|
||||
return padded
|
||||
}
|
||||
|
||||
export function getDate (timestamp: VTimestamp): string {
|
||||
let str = `${padNumber(timestamp.year, 4)}-${padNumber(timestamp.month, 2)}`
|
||||
|
||||
if (timestamp.hasDay) str += `-${padNumber(timestamp.day, 2)}`
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
export function getTime (timestamp: VTimestamp): string {
|
||||
if (!timestamp.hasTime) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return `${padNumber(timestamp.hour, 2)}:${padNumber(timestamp.minute, 2)}`
|
||||
}
|
||||
|
||||
export function nextMinutes (timestamp: VTimestamp, minutes: number): VTimestamp {
|
||||
timestamp.minute += minutes
|
||||
while (timestamp.minute > MINUTES_IN_HOUR) {
|
||||
timestamp.minute -= MINUTES_IN_HOUR
|
||||
timestamp.hour++
|
||||
if (timestamp.hour >= HOURS_IN_DAY) {
|
||||
nextDay(timestamp)
|
||||
timestamp.hour = FIRST_HOUR
|
||||
}
|
||||
}
|
||||
|
||||
return timestamp
|
||||
}
|
||||
|
||||
export function nextDay (timestamp: VTimestamp): VTimestamp {
|
||||
timestamp.day++
|
||||
timestamp.weekday = (timestamp.weekday + 1) % DAYS_IN_WEEK
|
||||
if (timestamp.day > DAYS_IN_MONTH_MIN && timestamp.day > daysInMonth(timestamp.year, timestamp.month)) {
|
||||
timestamp.day = DAY_MIN
|
||||
timestamp.month++
|
||||
if (timestamp.month > MONTH_MAX) {
|
||||
timestamp.month = MONTH_MIN
|
||||
timestamp.year++
|
||||
}
|
||||
}
|
||||
|
||||
return timestamp
|
||||
}
|
||||
|
||||
export function prevDay (timestamp: VTimestamp): VTimestamp {
|
||||
timestamp.day--
|
||||
timestamp.weekday = (timestamp.weekday + 6) % DAYS_IN_WEEK
|
||||
if (timestamp.day < DAY_MIN) {
|
||||
timestamp.month--
|
||||
if (timestamp.month < MONTH_MIN) {
|
||||
timestamp.year--
|
||||
timestamp.month = MONTH_MAX
|
||||
}
|
||||
timestamp.day = daysInMonth(timestamp.year, timestamp.month)
|
||||
}
|
||||
|
||||
return timestamp
|
||||
}
|
||||
|
||||
export function relativeDays (timestamp: VTimestamp,
|
||||
mover: VTimestampOperation = nextDay, days = 1): VTimestamp {
|
||||
while (--days >= 0) mover(timestamp)
|
||||
|
||||
return timestamp
|
||||
}
|
||||
|
||||
export function findWeekday (timestamp: VTimestamp, weekday: number,
|
||||
mover: VTimestampOperation = nextDay, maxDays = 6): VTimestamp {
|
||||
while (timestamp.weekday !== weekday && --maxDays >= 0) mover(timestamp)
|
||||
|
||||
return timestamp
|
||||
}
|
||||
|
||||
export function getWeekdaySkips (weekdays: number[]): number[] {
|
||||
const skips: number[] = [1, 1, 1, 1, 1, 1, 1]
|
||||
const filled: number[] = [0, 0, 0, 0, 0, 0, 0]
|
||||
for (let i = 0; i < weekdays.length; i++) {
|
||||
filled[weekdays[i]] = 1
|
||||
}
|
||||
for (let k = 0; k < DAYS_IN_WEEK; k++) {
|
||||
let skip = 1
|
||||
for (let j = 1; j < DAYS_IN_WEEK; j++) {
|
||||
const next = (k + j) % DAYS_IN_WEEK
|
||||
if (filled[next]) {
|
||||
break
|
||||
}
|
||||
skip++
|
||||
}
|
||||
skips[k] = filled[k] * skip
|
||||
}
|
||||
|
||||
return skips
|
||||
}
|
||||
|
||||
export function createDayList (start: VTimestamp, end: VTimestamp, now: VTimestamp,
|
||||
weekdaySkips: number[], max = 42, min = 0): VTimestamp[] {
|
||||
const stop = getDayIdentifier(end)
|
||||
const days: VTimestamp[] = []
|
||||
let current = copyTimestamp(start)
|
||||
let currentIdentifier = 0
|
||||
let stopped = currentIdentifier === stop
|
||||
|
||||
if (stop < getDayIdentifier(start)) {
|
||||
return days
|
||||
}
|
||||
|
||||
while ((!stopped || days.length < min) && days.length < max) {
|
||||
currentIdentifier = getDayIdentifier(current)
|
||||
stopped = stopped || currentIdentifier === stop
|
||||
if (weekdaySkips[current.weekday] === 0) {
|
||||
current = nextDay(current)
|
||||
continue
|
||||
}
|
||||
const day = copyTimestamp(current)
|
||||
updateFormatted(day)
|
||||
updateRelative(day, now)
|
||||
days.push(day)
|
||||
current = relativeDays(current, nextDay, weekdaySkips[current.weekday])
|
||||
}
|
||||
|
||||
return days
|
||||
}
|
||||
|
||||
export function createIntervalList (timestamp: VTimestamp, first: number,
|
||||
minutes: number, count: number, now?: VTimestamp): VTimestamp[] {
|
||||
const intervals: VTimestamp[] = []
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const mins = (first + i) * minutes
|
||||
const int = copyTimestamp(timestamp)
|
||||
intervals.push(updateMinutes(int, mins, now))
|
||||
}
|
||||
|
||||
return intervals
|
||||
}
|
||||
|
||||
export function createNativeLocaleFormatter (locale: string, getOptions: VTimestampFormatOptions): VTimestampFormatter {
|
||||
const emptyFormatter: VTimestampFormatter = (_t, _s) => ''
|
||||
|
||||
if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat === 'undefined') {
|
||||
return emptyFormatter
|
||||
}
|
||||
|
||||
return (timestamp, short) => {
|
||||
try {
|
||||
const intlFormatter = new Intl.DateTimeFormat(locale || undefined, getOptions(timestamp, short))
|
||||
const time = `${padNumber(timestamp.hour, 2)}:${padNumber(timestamp.minute, 2)}`
|
||||
const date = timestamp.date
|
||||
return intlFormatter.format(new Date(`${date}T${time}:00+00:00`))
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_cards.styl'
|
||||
|
||||
// Extensions
|
||||
import VSheet from '../VSheet'
|
||||
|
||||
// Mixins
|
||||
import Routable from '../../mixins/routable'
|
||||
|
||||
// Helpers
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
|
||||
/* @vue/component */
|
||||
export default mixins(
|
||||
Routable,
|
||||
VSheet
|
||||
).extend({
|
||||
name: 'v-card',
|
||||
|
||||
props: {
|
||||
flat: Boolean,
|
||||
hover: Boolean,
|
||||
img: String,
|
||||
raised: Boolean
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes (): object {
|
||||
return {
|
||||
'v-card': true,
|
||||
'v-card--flat': this.flat,
|
||||
'v-card--hover': this.hover,
|
||||
...VSheet.options.computed.classes.call(this)
|
||||
}
|
||||
},
|
||||
styles (): object {
|
||||
const style: Dictionary<string> = {
|
||||
...VSheet.options.computed.styles.call(this)
|
||||
}
|
||||
|
||||
if (this.img) {
|
||||
style.background = `url("${this.img}") center center / cover no-repeat`
|
||||
}
|
||||
|
||||
return style
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
const { tag, data } = this.generateRouteLink(this.classes)
|
||||
|
||||
data.style = this.styles
|
||||
|
||||
return h(tag, this.setBackgroundColor(this.color, data), this.$slots.default)
|
||||
}
|
||||
})
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Components
|
||||
import VImg from '../VImg/VImg'
|
||||
|
||||
// Utils
|
||||
import { deprecate } from '../../util/console'
|
||||
|
||||
/* istanbul ignore next */
|
||||
/* @vue/component */
|
||||
export default VImg.extend({
|
||||
name: 'v-card-media',
|
||||
|
||||
mounted () {
|
||||
deprecate('v-card-media', this.src ? 'v-img' : 'v-responsive', this)
|
||||
}
|
||||
})
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// Types
|
||||
import Vue, { VNode } from 'vue'
|
||||
|
||||
/* @vue/component */
|
||||
export default Vue.extend({
|
||||
name: 'v-card-title',
|
||||
|
||||
functional: true,
|
||||
|
||||
props: {
|
||||
primaryTitle: Boolean
|
||||
},
|
||||
|
||||
render (h, { data, props, children }): VNode {
|
||||
data.staticClass = (`v-card__title ${data.staticClass || ''}`).trim()
|
||||
|
||||
if (props.primaryTitle) data.staticClass += ' v-card__title--primary'
|
||||
|
||||
return h('div', data, children)
|
||||
}
|
||||
})
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { createSimpleFunctional } from '../../util/helpers'
|
||||
import VCard from './VCard'
|
||||
import VCardMedia from './VCardMedia'
|
||||
import VCardTitle from './VCardTitle'
|
||||
|
||||
const VCardActions = createSimpleFunctional('v-card__actions')
|
||||
const VCardText = createSimpleFunctional('v-card__text')
|
||||
|
||||
export { VCard, VCardMedia, VCardTitle, VCardActions, VCardText }
|
||||
|
||||
export default {
|
||||
$_vuetify_subcomponents: {
|
||||
VCard,
|
||||
VCardMedia,
|
||||
VCardTitle,
|
||||
VCardActions,
|
||||
VCardText
|
||||
}
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_carousel.styl'
|
||||
|
||||
// Extensions
|
||||
import VWindow from '../VWindow/VWindow'
|
||||
|
||||
// Components
|
||||
import VBtn from '../VBtn'
|
||||
import VIcon from '../VIcon'
|
||||
|
||||
// Mixins
|
||||
// TODO: Move this into core components v2.0
|
||||
import ButtonGroup from '../../mixins/button-group'
|
||||
|
||||
// Utilities
|
||||
import { convertToUnit } from '../../util/helpers'
|
||||
import { deprecate } from '../../util/console'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
import { VNodeDirective } from 'vue/types/vnode'
|
||||
|
||||
export default VWindow.extend({
|
||||
name: 'v-carousel',
|
||||
|
||||
props: {
|
||||
cycle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
delimiterIcon: {
|
||||
type: String,
|
||||
default: '$vuetify.icons.delimiter'
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 500
|
||||
},
|
||||
hideControls: Boolean,
|
||||
hideDelimiters: Boolean,
|
||||
interval: {
|
||||
type: [Number, String],
|
||||
default: 6000,
|
||||
validator: (value: string | number) => value > 0
|
||||
},
|
||||
mandatory: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
nextIcon: {
|
||||
type: [Boolean, String],
|
||||
default: '$vuetify.icons.next'
|
||||
},
|
||||
prevIcon: {
|
||||
type: [Boolean, String],
|
||||
default: '$vuetify.icons.prev'
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
changedByDelimiters: false,
|
||||
internalHeight: this.height,
|
||||
slideTimeout: undefined as number | undefined
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isDark (): boolean {
|
||||
return this.dark || !this.light
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
internalValue (val) {
|
||||
this.restartTimeout()
|
||||
/* @deprecate */
|
||||
/* istanbul ignore else */
|
||||
if (!this.$listeners['input']) return
|
||||
|
||||
this.$emit('input', val)
|
||||
},
|
||||
interval: 'restartTimeout',
|
||||
height (val, oldVal) {
|
||||
if (val === oldVal || !val) return
|
||||
this.internalHeight = val
|
||||
},
|
||||
cycle (val) {
|
||||
if (val) {
|
||||
this.restartTimeout()
|
||||
} else {
|
||||
clearTimeout(this.slideTimeout)
|
||||
this.slideTimeout = undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
/* @deprecate */
|
||||
/* istanbul ignore next */
|
||||
if (this.$listeners['input']) {
|
||||
deprecate('@input', '@change', this)
|
||||
}
|
||||
this.startTimeout()
|
||||
},
|
||||
|
||||
methods: {
|
||||
genDelimiters (): VNode {
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-carousel__controls'
|
||||
}, [this.genItems()])
|
||||
},
|
||||
genIcon (
|
||||
direction: 'prev' | 'next',
|
||||
icon: string,
|
||||
fn: () => void
|
||||
): VNode {
|
||||
return this.$createElement('div', {
|
||||
staticClass: `v-carousel__${direction}`
|
||||
}, [
|
||||
this.$createElement(VBtn, {
|
||||
props: {
|
||||
icon: true
|
||||
},
|
||||
attrs: {
|
||||
'aria-label': this.$vuetify.t(`$vuetify.carousel.${direction}`)
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.changedByDelimiters = true
|
||||
fn()
|
||||
}
|
||||
}
|
||||
}, [
|
||||
this.$createElement(VIcon, {
|
||||
props: { 'size': '46px' }
|
||||
}, icon)
|
||||
])
|
||||
])
|
||||
},
|
||||
genIcons (): VNode[] {
|
||||
const icons = []
|
||||
|
||||
const prevIcon = this.$vuetify.rtl
|
||||
? this.nextIcon
|
||||
: this.prevIcon
|
||||
|
||||
if (prevIcon && typeof prevIcon === 'string') {
|
||||
icons.push(this.genIcon('prev', prevIcon, this.prev))
|
||||
}
|
||||
|
||||
const nextIcon = this.$vuetify.rtl
|
||||
? this.prevIcon
|
||||
: this.nextIcon
|
||||
|
||||
if (nextIcon && typeof nextIcon === 'string') {
|
||||
icons.push(this.genIcon('next', nextIcon, this.next))
|
||||
}
|
||||
|
||||
return icons
|
||||
},
|
||||
genItems (): VNode {
|
||||
const length = this.items.length
|
||||
const children = []
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const child = this.$createElement(VBtn, {
|
||||
class: {
|
||||
'v-carousel__controls__item': true
|
||||
},
|
||||
props: {
|
||||
icon: true,
|
||||
small: true,
|
||||
value: this.getValue(this.items[i], i)
|
||||
}
|
||||
}, [
|
||||
this.$createElement(VIcon, {
|
||||
props: { size: 18 }
|
||||
}, this.delimiterIcon)
|
||||
])
|
||||
|
||||
children.push(child)
|
||||
}
|
||||
|
||||
return this.$createElement(ButtonGroup, {
|
||||
props: {
|
||||
value: this.internalValue
|
||||
},
|
||||
on: {
|
||||
change: (val: any) => {
|
||||
this.internalValue = val
|
||||
}
|
||||
}
|
||||
}, children)
|
||||
},
|
||||
restartTimeout () {
|
||||
this.slideTimeout && clearTimeout(this.slideTimeout)
|
||||
this.slideTimeout = undefined
|
||||
|
||||
const raf = requestAnimationFrame || setTimeout
|
||||
raf(this.startTimeout)
|
||||
},
|
||||
startTimeout () {
|
||||
if (!this.cycle) return
|
||||
|
||||
this.slideTimeout = window.setTimeout(this.next, +this.interval > 0 ? +this.interval : 6000)
|
||||
},
|
||||
updateReverse (val: number, oldVal: number) {
|
||||
if (this.changedByDelimiters) {
|
||||
this.changedByDelimiters = false
|
||||
return
|
||||
}
|
||||
|
||||
VWindow.options.methods.updateReverse.call(this, val, oldVal)
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
const children = []
|
||||
const data = {
|
||||
staticClass: 'v-window v-carousel',
|
||||
style: {
|
||||
height: convertToUnit(this.height)
|
||||
},
|
||||
directives: [] as VNodeDirective[]
|
||||
}
|
||||
|
||||
if (!this.touchless) {
|
||||
data.directives.push({
|
||||
name: 'touch',
|
||||
value: {
|
||||
left: this.next,
|
||||
right: this.prev
|
||||
}
|
||||
} as VNodeDirective)
|
||||
}
|
||||
|
||||
if (!this.hideControls) {
|
||||
children.push(this.genIcons())
|
||||
}
|
||||
|
||||
if (!this.hideDelimiters) {
|
||||
children.push(this.genDelimiters())
|
||||
}
|
||||
|
||||
return h('div', data, [this.genContainer(), children])
|
||||
}
|
||||
})
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// Extensions
|
||||
import VWindowItem from '../VWindow/VWindowItem'
|
||||
|
||||
// Components
|
||||
import { VImg } from '../VImg'
|
||||
|
||||
/* @vue/component */
|
||||
export default VWindowItem.extend({
|
||||
name: 'v-carousel-item',
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
methods: {
|
||||
genDefaultSlot () {
|
||||
return [
|
||||
this.$createElement(VImg, {
|
||||
staticClass: 'v-carousel__item',
|
||||
props: {
|
||||
...this.$attrs,
|
||||
height: this.windowGroup.internalHeight
|
||||
},
|
||||
on: this.$listeners
|
||||
}, this.$slots.default)
|
||||
]
|
||||
},
|
||||
onBeforeEnter () { /* noop */ },
|
||||
onEnter () { /* noop */ },
|
||||
onAfterEnter () { /* noop */ },
|
||||
onBeforeLeave () { /* noop */ },
|
||||
onEnterCancelled () { /* noop */ }
|
||||
}
|
||||
})
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import VCarousel from './VCarousel'
|
||||
import VCarouselItem from './VCarouselItem'
|
||||
|
||||
export { VCarousel, VCarouselItem }
|
||||
|
||||
export default {
|
||||
$_vuetify_subcomponents: {
|
||||
VCarousel,
|
||||
VCarouselItem
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_selection-controls.styl'
|
||||
|
||||
// Components
|
||||
import VIcon from '../VIcon'
|
||||
// import { VFadeTransition } from '../transitions'
|
||||
|
||||
// Mixins
|
||||
import Selectable from '../../mixins/selectable'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
name: 'v-checkbox',
|
||||
|
||||
mixins: [
|
||||
Selectable
|
||||
],
|
||||
|
||||
props: {
|
||||
indeterminate: Boolean,
|
||||
indeterminateIcon: {
|
||||
type: String,
|
||||
default: '$vuetify.icons.checkboxIndeterminate'
|
||||
},
|
||||
onIcon: {
|
||||
type: String,
|
||||
default: '$vuetify.icons.checkboxOn'
|
||||
},
|
||||
offIcon: {
|
||||
type: String,
|
||||
default: '$vuetify.icons.checkboxOff'
|
||||
}
|
||||
},
|
||||
|
||||
data: vm => ({
|
||||
inputIndeterminate: vm.indeterminate
|
||||
}),
|
||||
|
||||
computed: {
|
||||
classes () {
|
||||
return {
|
||||
'v-input--selection-controls': true,
|
||||
'v-input--checkbox': true
|
||||
}
|
||||
},
|
||||
computedIcon () {
|
||||
if (this.inputIndeterminate) {
|
||||
return this.indeterminateIcon
|
||||
} else if (this.isActive) {
|
||||
return this.onIcon
|
||||
} else {
|
||||
return this.offIcon
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
indeterminate (val) {
|
||||
this.inputIndeterminate = val
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
genCheckbox () {
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-input--selection-controls__input'
|
||||
}, [
|
||||
this.genInput('checkbox', {
|
||||
...this.$attrs,
|
||||
'aria-checked': this.inputIndeterminate
|
||||
? 'mixed'
|
||||
: this.isActive.toString()
|
||||
}),
|
||||
this.genRipple(this.setTextColor(this.computedColor)),
|
||||
this.$createElement(VIcon, this.setTextColor(this.computedColor, {
|
||||
props: {
|
||||
dark: this.dark,
|
||||
light: this.light
|
||||
}
|
||||
}), this.computedIcon)
|
||||
])
|
||||
},
|
||||
genDefaultSlot () {
|
||||
return [
|
||||
this.genCheckbox(),
|
||||
this.genLabel()
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VCheckbox from './VCheckbox'
|
||||
|
||||
export { VCheckbox }
|
||||
export default VCheckbox
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import '../../stylus/components/_chips.styl'
|
||||
|
||||
// Types
|
||||
import { CreateElement, VNode } from 'vue'
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
// Components
|
||||
import VIcon from '../VIcon'
|
||||
|
||||
// Mixins
|
||||
import Colorable from '../../mixins/colorable'
|
||||
import Themeable from '../../mixins/themeable'
|
||||
import Toggleable from '../../mixins/toggleable'
|
||||
|
||||
/* @vue/component */
|
||||
export default mixins(Colorable, Themeable, Toggleable).extend({
|
||||
name: 'v-chip',
|
||||
|
||||
props: {
|
||||
close: Boolean,
|
||||
disabled: Boolean,
|
||||
label: Boolean,
|
||||
outline: Boolean,
|
||||
// Used for selects/tagging
|
||||
selected: Boolean,
|
||||
small: Boolean,
|
||||
textColor: String,
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes (): object {
|
||||
return {
|
||||
'v-chip--disabled': this.disabled,
|
||||
'v-chip--selected': this.selected && !this.disabled,
|
||||
'v-chip--label': this.label,
|
||||
'v-chip--outline': this.outline,
|
||||
'v-chip--small': this.small,
|
||||
'v-chip--removable': this.close,
|
||||
...this.themeClasses
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
genClose (h: CreateElement): VNode {
|
||||
const data = {
|
||||
staticClass: 'v-chip__close',
|
||||
on: {
|
||||
click: (e: Event) => {
|
||||
e.stopPropagation()
|
||||
|
||||
this.$emit('input', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return h('div', data, [
|
||||
h(VIcon, '$vuetify.icons.delete')
|
||||
])
|
||||
},
|
||||
genContent (h: CreateElement): VNode {
|
||||
return h('span', {
|
||||
staticClass: 'v-chip__content'
|
||||
}, [
|
||||
this.$slots.default,
|
||||
this.close && this.genClose(h)
|
||||
])
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
const data = this.setBackgroundColor(this.color, {
|
||||
staticClass: 'v-chip',
|
||||
'class': this.classes,
|
||||
attrs: { tabindex: this.disabled ? -1 : 0 },
|
||||
directives: [{
|
||||
name: 'show',
|
||||
value: this.isActive
|
||||
}],
|
||||
on: this.$listeners
|
||||
})
|
||||
|
||||
const color = this.textColor || (this.outline && this.color)
|
||||
return h('span', this.setTextColor(color, data), [this.genContent(h)])
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VChip from './VChip'
|
||||
|
||||
export { VChip }
|
||||
export default VChip
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_autocompletes.styl'
|
||||
|
||||
// Extensions
|
||||
import VSelect from '../VSelect/VSelect'
|
||||
import VAutocomplete from '../VAutocomplete/VAutocomplete'
|
||||
|
||||
// Utils
|
||||
import { keyCodes } from '../../util/helpers'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
name: 'v-combobox',
|
||||
|
||||
extends: VAutocomplete,
|
||||
|
||||
props: {
|
||||
delimiters: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
},
|
||||
returnObject: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
editingIndex: -1
|
||||
}),
|
||||
|
||||
computed: {
|
||||
counterValue () {
|
||||
return this.multiple
|
||||
? this.selectedItems.length
|
||||
: (this.internalSearch || '').toString().length
|
||||
},
|
||||
hasSlot () {
|
||||
return VSelect.options.computed.hasSlot.call(this) || this.multiple
|
||||
},
|
||||
isAnyValueAllowed () {
|
||||
return true
|
||||
},
|
||||
menuCanShow () {
|
||||
if (!this.isFocused) return false
|
||||
|
||||
return this.hasDisplayedItems ||
|
||||
(!!this.$slots['no-data'] && !this.hideNoData)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onFilteredItemsChanged () {
|
||||
// nop
|
||||
},
|
||||
onInternalSearchChanged (val) {
|
||||
if (
|
||||
val &&
|
||||
this.multiple &&
|
||||
this.delimiters.length
|
||||
) {
|
||||
const delimiter = this.delimiters.find(d => val.endsWith(d))
|
||||
if (delimiter != null) {
|
||||
this.internalSearch = val.slice(0, val.length - delimiter.length)
|
||||
this.updateTags()
|
||||
}
|
||||
}
|
||||
|
||||
this.updateMenuDimensions()
|
||||
},
|
||||
genChipSelection (item, index) {
|
||||
const chip = VSelect.options.methods.genChipSelection.call(this, item, index)
|
||||
|
||||
// Allow user to update an existing value
|
||||
if (this.multiple) {
|
||||
chip.componentOptions.listeners.dblclick = () => {
|
||||
this.editingIndex = index
|
||||
this.internalSearch = this.getText(item)
|
||||
this.selectedIndex = -1
|
||||
}
|
||||
}
|
||||
|
||||
return chip
|
||||
},
|
||||
onChipInput (item) {
|
||||
VSelect.options.methods.onChipInput.call(this, item)
|
||||
|
||||
this.editingIndex = -1
|
||||
},
|
||||
// Requires a manual definition
|
||||
// to overwrite removal in v-autocomplete
|
||||
onEnterDown (e) {
|
||||
e.preventDefault()
|
||||
|
||||
VSelect.options.methods.onEnterDown.call(this)
|
||||
|
||||
// If has menu index, let v-select-list handle
|
||||
if (this.getMenuIndex() > -1) return
|
||||
|
||||
this.updateSelf()
|
||||
},
|
||||
onKeyDown (e) {
|
||||
const keyCode = e.keyCode
|
||||
|
||||
VSelect.options.methods.onKeyDown.call(this, e)
|
||||
|
||||
// If user is at selection index of 0
|
||||
// create a new tag
|
||||
if (this.multiple &&
|
||||
keyCode === keyCodes.left &&
|
||||
this.$refs.input.selectionStart === 0
|
||||
) {
|
||||
this.updateSelf()
|
||||
}
|
||||
|
||||
// The ordering is important here
|
||||
// allows new value to be updated
|
||||
// and then moves the index to the
|
||||
// proper location
|
||||
this.changeSelectedIndex(keyCode)
|
||||
},
|
||||
onTabDown (e) {
|
||||
// When adding tags, if searching and
|
||||
// there is not a filtered options,
|
||||
// add the value to the tags list
|
||||
if (this.multiple &&
|
||||
this.internalSearch &&
|
||||
this.getMenuIndex() === -1
|
||||
) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
return this.updateTags()
|
||||
}
|
||||
|
||||
VAutocomplete.options.methods.onTabDown.call(this, e)
|
||||
},
|
||||
selectItem (item) {
|
||||
// Currently only supports items:<string[]>
|
||||
if (this.editingIndex > -1) {
|
||||
this.updateEditing()
|
||||
} else {
|
||||
VSelect.options.methods.selectItem.call(this, item)
|
||||
}
|
||||
},
|
||||
setSelectedItems () {
|
||||
if (this.internalValue == null ||
|
||||
this.internalValue === ''
|
||||
) {
|
||||
this.selectedItems = []
|
||||
} else {
|
||||
this.selectedItems = this.multiple ? this.internalValue : [this.internalValue]
|
||||
}
|
||||
},
|
||||
setValue (value = this.internalSearch) {
|
||||
VSelect.options.methods.setValue.call(this, value)
|
||||
},
|
||||
updateEditing () {
|
||||
const value = this.internalValue.slice()
|
||||
value[this.editingIndex] = this.internalSearch
|
||||
|
||||
this.setValue(value)
|
||||
|
||||
this.editingIndex = -1
|
||||
},
|
||||
updateCombobox () {
|
||||
const isUsingSlot = Boolean(this.$scopedSlots.selection) || this.hasChips
|
||||
|
||||
// If search is not dirty and is
|
||||
// using slot, do nothing
|
||||
if (isUsingSlot && !this.searchIsDirty) return
|
||||
|
||||
// The internal search is not matching
|
||||
// the internal value, update the input
|
||||
if (this.internalSearch !== this.getText(this.internalValue)) this.setValue()
|
||||
|
||||
// Reset search if using slot
|
||||
// to avoid a double input
|
||||
if (isUsingSlot) this.internalSearch = undefined
|
||||
},
|
||||
updateSelf () {
|
||||
this.multiple ? this.updateTags() : this.updateCombobox()
|
||||
},
|
||||
updateTags () {
|
||||
const menuIndex = this.getMenuIndex()
|
||||
|
||||
// If the user is not searching
|
||||
// and no menu item is selected
|
||||
// do nothing
|
||||
if (menuIndex < 0 &&
|
||||
!this.searchIsDirty
|
||||
) return
|
||||
|
||||
if (this.editingIndex > -1) {
|
||||
return this.updateEditing()
|
||||
}
|
||||
|
||||
const index = this.selectedItems.indexOf(this.internalSearch)
|
||||
// If it already exists, do nothing
|
||||
// this might need to change to bring
|
||||
// the duplicated item to the last entered
|
||||
if (index > -1) {
|
||||
const internalValue = this.internalValue.slice()
|
||||
internalValue.splice(index, 1)
|
||||
|
||||
this.setValue(internalValue)
|
||||
}
|
||||
|
||||
// If menu index is greater than 1
|
||||
// the selection is handled elsewhere
|
||||
// TODO: find out where
|
||||
if (menuIndex > -1) return (this.internalSearch = null)
|
||||
|
||||
this.selectItem(this.internalSearch)
|
||||
this.internalSearch = null
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VCombobox from './VCombobox'
|
||||
|
||||
export { VCombobox }
|
||||
export default VCombobox
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_counters.styl'
|
||||
|
||||
// Mixins
|
||||
import Themeable, { functionalThemeClasses } from '../../mixins/themeable'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
/* @vue/component */
|
||||
export default mixins(Themeable).extend({
|
||||
name: 'v-counter',
|
||||
|
||||
functional: true,
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
max: [Number, String]
|
||||
},
|
||||
|
||||
render (h, ctx): VNode {
|
||||
const { props } = ctx
|
||||
const max = parseInt(props.max, 10)
|
||||
const value = parseInt(props.value, 10)
|
||||
const content = max ? `${value} / ${max}` : String(props.value)
|
||||
const isGreater = max && (value > max)
|
||||
|
||||
return h('div', {
|
||||
staticClass: 'v-counter',
|
||||
class: {
|
||||
'error--text': isGreater,
|
||||
...functionalThemeClasses(ctx)
|
||||
}
|
||||
}, content)
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VCounter from './VCounter'
|
||||
|
||||
export { VCounter }
|
||||
export default VCounter
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import '../../stylus/components/_data-iterator.styl'
|
||||
|
||||
import DataIterable from '../../mixins/data-iterable'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
name: 'v-data-iterator',
|
||||
|
||||
mixins: [DataIterable],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
contentTag: {
|
||||
type: String,
|
||||
default: 'div'
|
||||
},
|
||||
contentProps: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
contentClass: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes () {
|
||||
return {
|
||||
'v-data-iterator': true,
|
||||
'v-data-iterator--select-all': this.selectAll !== false,
|
||||
...this.themeClasses
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.initPagination()
|
||||
},
|
||||
|
||||
methods: {
|
||||
genContent () {
|
||||
const children = this.genItems()
|
||||
|
||||
const data = {
|
||||
'class': this.contentClass,
|
||||
attrs: this.$attrs,
|
||||
on: this.$listeners,
|
||||
props: this.contentProps
|
||||
}
|
||||
|
||||
return this.$createElement(this.contentTag, data, children)
|
||||
},
|
||||
genEmptyItems (content) {
|
||||
return [this.$createElement('div', {
|
||||
'class': 'text-xs-center',
|
||||
style: 'width: 100%'
|
||||
}, content)]
|
||||
},
|
||||
genFilteredItems () {
|
||||
if (!this.$scopedSlots.item) {
|
||||
return null
|
||||
}
|
||||
|
||||
const items = []
|
||||
for (let index = 0, len = this.filteredItems.length; index < len; ++index) {
|
||||
const item = this.filteredItems[index]
|
||||
const props = this.createProps(item, index)
|
||||
items.push(this.$scopedSlots.item(props))
|
||||
}
|
||||
|
||||
return items
|
||||
},
|
||||
genFooter () {
|
||||
const children = []
|
||||
|
||||
if (this.$slots.footer) {
|
||||
children.push(this.$slots.footer)
|
||||
}
|
||||
|
||||
if (!this.hideActions) {
|
||||
children.push(this.genActions())
|
||||
}
|
||||
|
||||
if (!children.length) return null
|
||||
return this.$createElement('div', children)
|
||||
},
|
||||
genHeader () {
|
||||
const children = []
|
||||
|
||||
if (this.$slots.header) {
|
||||
children.push(this.$slots.header)
|
||||
}
|
||||
|
||||
if (!children.length) return null
|
||||
return this.$createElement('div', children)
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
return h('div', {
|
||||
'class': this.classes
|
||||
}, [
|
||||
this.genHeader(),
|
||||
this.genContent(),
|
||||
this.genFooter()
|
||||
])
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VDataIterator from './VDataIterator'
|
||||
|
||||
export { VDataIterator }
|
||||
export default VDataIterator
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import '../../stylus/components/_tables.styl'
|
||||
import '../../stylus/components/_data-table.styl'
|
||||
|
||||
import DataIterable from '../../mixins/data-iterable'
|
||||
|
||||
import Head from './mixins/head'
|
||||
import Body from './mixins/body'
|
||||
import Foot from './mixins/foot'
|
||||
import Progress from './mixins/progress'
|
||||
|
||||
import {
|
||||
createSimpleFunctional,
|
||||
getObjectValueByPath
|
||||
} from '../../util/helpers'
|
||||
|
||||
// Importing does not work properly
|
||||
const VTableOverflow = createSimpleFunctional('v-table__overflow')
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
name: 'v-data-table',
|
||||
|
||||
mixins: [DataIterable, Head, Body, Foot, Progress],
|
||||
|
||||
props: {
|
||||
headers: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
headersLength: {
|
||||
type: Number
|
||||
},
|
||||
headerText: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
headerKey: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
hideHeaders: Boolean,
|
||||
rowsPerPageText: {
|
||||
type: String,
|
||||
default: '$vuetify.dataTable.rowsPerPageText'
|
||||
},
|
||||
customFilter: {
|
||||
type: Function,
|
||||
default: (items, search, filter, headers) => {
|
||||
search = search.toString().toLowerCase()
|
||||
if (search.trim() === '') return items
|
||||
|
||||
const props = headers.map(h => h.value)
|
||||
|
||||
return items.filter(item => props.some(prop => filter(getObjectValueByPath(item, prop, item[prop]), search)))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
actionsClasses: 'v-datatable__actions',
|
||||
actionsRangeControlsClasses: 'v-datatable__actions__range-controls',
|
||||
actionsSelectClasses: 'v-datatable__actions__select',
|
||||
actionsPaginationClasses: 'v-datatable__actions__pagination'
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes () {
|
||||
return {
|
||||
'v-datatable v-table': true,
|
||||
'v-datatable--select-all': this.selectAll !== false,
|
||||
...this.themeClasses
|
||||
}
|
||||
},
|
||||
filteredItems () {
|
||||
return this.filteredItemsImpl(this.headers)
|
||||
},
|
||||
headerColumns () {
|
||||
return this.headersLength || this.headers.length + (this.selectAll !== false)
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
const firstSortable = this.headers.find(h => (
|
||||
!('sortable' in h) || h.sortable)
|
||||
)
|
||||
|
||||
this.defaultPagination.sortBy = !this.disableInitialSort && firstSortable
|
||||
? firstSortable.value
|
||||
: null
|
||||
|
||||
this.initPagination()
|
||||
},
|
||||
|
||||
methods: {
|
||||
hasTag (elements, tag) {
|
||||
return Array.isArray(elements) && elements.find(e => e.tag === tag)
|
||||
},
|
||||
genTR (children, data = {}) {
|
||||
return this.$createElement('tr', data, children)
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
const tableOverflow = h(VTableOverflow, {}, [
|
||||
h('table', {
|
||||
'class': this.classes
|
||||
}, [
|
||||
this.genTHead(),
|
||||
this.genTBody(),
|
||||
this.genTFoot()
|
||||
])
|
||||
])
|
||||
|
||||
return h('div', [
|
||||
tableOverflow,
|
||||
this.genActionsFooter()
|
||||
])
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import '../../stylus/components/_small-dialog.styl'
|
||||
|
||||
// Mixins
|
||||
import Returnable from '../../mixins/returnable'
|
||||
import Themeable from '../../mixins/themeable'
|
||||
|
||||
// Utils
|
||||
import { keyCodes } from '../../util/helpers'
|
||||
|
||||
import VBtn from '../VBtn'
|
||||
import VMenu from '../VMenu'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
name: 'v-edit-dialog',
|
||||
|
||||
mixins: [Returnable, Themeable],
|
||||
|
||||
props: {
|
||||
cancelText: {
|
||||
default: 'Cancel'
|
||||
},
|
||||
large: Boolean,
|
||||
lazy: Boolean,
|
||||
persistent: Boolean,
|
||||
saveText: {
|
||||
default: 'Save'
|
||||
},
|
||||
transition: {
|
||||
type: String,
|
||||
default: 'slide-x-reverse-transition'
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
isActive: false
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
isActive (val) {
|
||||
if (val) {
|
||||
this.$emit('open')
|
||||
setTimeout(this.focus, 50) // Give DOM time to paint
|
||||
} else {
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
cancel () {
|
||||
this.isActive = false
|
||||
this.$emit('cancel')
|
||||
},
|
||||
focus () {
|
||||
const input = this.$refs.content.querySelector('input')
|
||||
input && input.focus()
|
||||
},
|
||||
genButton (fn, text) {
|
||||
return this.$createElement(VBtn, {
|
||||
props: {
|
||||
flat: true,
|
||||
color: 'primary',
|
||||
light: true
|
||||
},
|
||||
on: { click: fn }
|
||||
}, text)
|
||||
},
|
||||
genActions () {
|
||||
return this.$createElement('div', {
|
||||
'class': 'v-small-dialog__actions'
|
||||
}, [
|
||||
this.genButton(this.cancel, this.cancelText),
|
||||
this.genButton(() => {
|
||||
this.save(this.returnValue)
|
||||
this.$emit('save')
|
||||
}, this.saveText)
|
||||
])
|
||||
},
|
||||
genContent () {
|
||||
return this.$createElement('div', {
|
||||
on: {
|
||||
keydown: e => {
|
||||
const input = this.$refs.content.querySelector('input')
|
||||
e.keyCode === keyCodes.esc && this.cancel()
|
||||
if (e.keyCode === keyCodes.enter && input) {
|
||||
this.save(input.value)
|
||||
this.$emit('save')
|
||||
}
|
||||
}
|
||||
},
|
||||
ref: 'content'
|
||||
}, [this.$slots.input])
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
return h(VMenu, {
|
||||
staticClass: 'v-small-dialog',
|
||||
class: this.themeClasses,
|
||||
props: {
|
||||
contentClass: 'v-small-dialog__content',
|
||||
transition: this.transition,
|
||||
origin: 'top right',
|
||||
right: true,
|
||||
value: this.isActive,
|
||||
closeOnClick: !this.persistent,
|
||||
closeOnContentClick: false,
|
||||
lazy: this.lazy,
|
||||
light: this.light,
|
||||
dark: this.dark
|
||||
},
|
||||
on: {
|
||||
input: val => (this.isActive = val)
|
||||
}
|
||||
}, [
|
||||
h('a', {
|
||||
slot: 'activator'
|
||||
}, this.$slots.default),
|
||||
this.genContent(),
|
||||
this.large ? this.genActions() : null
|
||||
])
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { createSimpleFunctional } from '../../util/helpers'
|
||||
|
||||
import VDataTable from './VDataTable'
|
||||
import VEditDialog from './VEditDialog'
|
||||
|
||||
const VTableOverflow = createSimpleFunctional('v-table__overflow')
|
||||
|
||||
export { VDataTable, VEditDialog, VTableOverflow }
|
||||
|
||||
export default {
|
||||
$_vuetify_subcomponents: {
|
||||
VDataTable,
|
||||
VEditDialog,
|
||||
VTableOverflow
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import ExpandTransitionGenerator from '../../transitions/expand-transition'
|
||||
|
||||
import { getObjectValueByPath } from '../../../util/helpers'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
methods: {
|
||||
genTBody () {
|
||||
const children = this.genItems()
|
||||
|
||||
return this.$createElement('tbody', children)
|
||||
},
|
||||
genExpandedRow (props) {
|
||||
const children = []
|
||||
|
||||
if (this.isExpanded(props.item)) {
|
||||
const expand = this.$createElement('div', {
|
||||
class: 'v-datatable__expand-content',
|
||||
key: getObjectValueByPath(props.item, this.itemKey)
|
||||
}, [this.$scopedSlots.expand(props)])
|
||||
|
||||
children.push(expand)
|
||||
}
|
||||
|
||||
const transition = this.$createElement('transition-group', {
|
||||
class: 'v-datatable__expand-col',
|
||||
attrs: { colspan: this.headerColumns },
|
||||
props: {
|
||||
tag: 'td'
|
||||
},
|
||||
on: ExpandTransitionGenerator('v-datatable__expand-col--expanded')
|
||||
}, children)
|
||||
|
||||
return this.genTR([transition], { class: 'v-datatable__expand-row' })
|
||||
},
|
||||
genFilteredItems () {
|
||||
if (!this.$scopedSlots.items) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rows = []
|
||||
for (let index = 0, len = this.filteredItems.length; index < len; ++index) {
|
||||
const item = this.filteredItems[index]
|
||||
const props = this.createProps(item, index)
|
||||
const row = this.$scopedSlots.items(props)
|
||||
|
||||
rows.push(this.hasTag(row, 'td')
|
||||
? this.genTR(row, {
|
||||
key: this.itemKey ? getObjectValueByPath(props.item, this.itemKey) : index,
|
||||
attrs: { active: this.isSelected(item) }
|
||||
})
|
||||
: row)
|
||||
|
||||
if (this.$scopedSlots.expand) {
|
||||
const expandRow = this.genExpandedRow(props)
|
||||
rows.push(expandRow)
|
||||
}
|
||||
}
|
||||
|
||||
return rows
|
||||
},
|
||||
genEmptyItems (content) {
|
||||
if (this.hasTag(content, 'tr')) {
|
||||
return content
|
||||
} else if (this.hasTag(content, 'td')) {
|
||||
return this.genTR(content)
|
||||
} else {
|
||||
return this.genTR([this.$createElement('td', {
|
||||
class: {
|
||||
'text-xs-center': typeof content === 'string'
|
||||
},
|
||||
attrs: { colspan: this.headerColumns }
|
||||
}, content)])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/* @vue/component */
|
||||
export default {
|
||||
methods: {
|
||||
genTFoot () {
|
||||
if (!this.$slots.footer) {
|
||||
return null
|
||||
}
|
||||
|
||||
const footer = this.$slots.footer
|
||||
const row = this.hasTag(footer, 'td') ? this.genTR(footer) : footer
|
||||
|
||||
return this.$createElement('tfoot', [row])
|
||||
},
|
||||
genActionsFooter () {
|
||||
if (this.hideActions) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.$createElement('div', {
|
||||
'class': this.classes
|
||||
}, this.genActions())
|
||||
}
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { consoleWarn } from '../../../util/console'
|
||||
|
||||
import VCheckbox from '../../VCheckbox'
|
||||
import VIcon from '../../VIcon'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
props: {
|
||||
sortIcon: {
|
||||
type: String,
|
||||
default: '$vuetify.icons.sort'
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
genTHead () {
|
||||
if (this.hideHeaders) return // Exit Early since no headers are needed.
|
||||
|
||||
let children = []
|
||||
|
||||
if (this.$scopedSlots.headers) {
|
||||
const row = this.$scopedSlots.headers({
|
||||
headers: this.headers,
|
||||
indeterminate: this.indeterminate,
|
||||
all: this.everyItem
|
||||
})
|
||||
|
||||
children = [this.hasTag(row, 'th') ? this.genTR(row) : row, this.genTProgress()]
|
||||
} else {
|
||||
const row = this.headers.map((o, i) => this.genHeader(o, this.headerKey ? o[this.headerKey] : i))
|
||||
const checkbox = this.$createElement(VCheckbox, {
|
||||
props: {
|
||||
dark: this.dark,
|
||||
light: this.light,
|
||||
color: this.selectAll === true ? '' : this.selectAll,
|
||||
hideDetails: true,
|
||||
inputValue: this.everyItem,
|
||||
indeterminate: this.indeterminate
|
||||
},
|
||||
on: { change: this.toggle }
|
||||
})
|
||||
|
||||
this.hasSelectAll && row.unshift(this.$createElement('th', [checkbox]))
|
||||
|
||||
children = [this.genTR(row), this.genTProgress()]
|
||||
}
|
||||
|
||||
return this.$createElement('thead', [children])
|
||||
},
|
||||
genHeader (header, key) {
|
||||
const array = [
|
||||
this.$scopedSlots.headerCell
|
||||
? this.$scopedSlots.headerCell({ header })
|
||||
: header[this.headerText]
|
||||
]
|
||||
|
||||
return this.$createElement('th', ...this.genHeaderData(header, array, key))
|
||||
},
|
||||
genHeaderData (header, children, key) {
|
||||
const classes = ['column']
|
||||
const data = {
|
||||
key,
|
||||
attrs: {
|
||||
role: 'columnheader',
|
||||
scope: 'col',
|
||||
width: header.width || null,
|
||||
'aria-label': header[this.headerText] || '',
|
||||
'aria-sort': 'none'
|
||||
}
|
||||
}
|
||||
|
||||
if (header.sortable == null || header.sortable) {
|
||||
this.genHeaderSortingData(header, children, data, classes)
|
||||
} else {
|
||||
data.attrs['aria-label'] += ': Not sorted.' // TODO: Localization
|
||||
}
|
||||
|
||||
classes.push(`text-xs-${header.align || 'left'}`)
|
||||
if (Array.isArray(header.class)) {
|
||||
classes.push(...header.class)
|
||||
} else if (header.class) {
|
||||
classes.push(header.class)
|
||||
}
|
||||
data.class = classes
|
||||
|
||||
return [data, children]
|
||||
},
|
||||
genHeaderSortingData (header, children, data, classes) {
|
||||
if (!('value' in header)) {
|
||||
consoleWarn('Headers must have a value property that corresponds to a value in the v-model array', this)
|
||||
}
|
||||
|
||||
data.attrs.tabIndex = 0
|
||||
data.on = {
|
||||
click: () => {
|
||||
this.expanded = {}
|
||||
this.sort(header.value)
|
||||
},
|
||||
keydown: e => {
|
||||
// check for space
|
||||
if (e.keyCode === 32) {
|
||||
e.preventDefault()
|
||||
this.sort(header.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
classes.push('sortable')
|
||||
const icon = this.$createElement(VIcon, {
|
||||
props: {
|
||||
small: true
|
||||
}
|
||||
}, this.sortIcon)
|
||||
if (!header.align || header.align === 'left') {
|
||||
children.push(icon)
|
||||
} else {
|
||||
children.unshift(icon)
|
||||
}
|
||||
|
||||
const pagination = this.computedPagination
|
||||
const beingSorted = pagination.sortBy === header.value
|
||||
if (beingSorted) {
|
||||
classes.push('active')
|
||||
if (pagination.descending) {
|
||||
classes.push('desc')
|
||||
data.attrs['aria-sort'] = 'descending'
|
||||
data.attrs['aria-label'] += ': Sorted descending. Activate to remove sorting.' // TODO: Localization
|
||||
} else {
|
||||
classes.push('asc')
|
||||
data.attrs['aria-sort'] = 'ascending'
|
||||
data.attrs['aria-label'] += ': Sorted ascending. Activate to sort descending.' // TODO: Localization
|
||||
}
|
||||
} else {
|
||||
data.attrs['aria-label'] += ': Not sorted. Activate to sort ascending.' // TODO: Localization
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/* @vue/component */
|
||||
export default {
|
||||
methods: {
|
||||
genTProgress () {
|
||||
const col = this.$createElement('th', {
|
||||
staticClass: 'column',
|
||||
attrs: {
|
||||
colspan: this.headerColumns
|
||||
}
|
||||
}, [this.genProgress()])
|
||||
|
||||
return this.genTR([col], {
|
||||
staticClass: 'v-datatable__progress'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
// Components
|
||||
import VDatePickerTitle from './VDatePickerTitle'
|
||||
import VDatePickerHeader from './VDatePickerHeader'
|
||||
import VDatePickerDateTable from './VDatePickerDateTable'
|
||||
import VDatePickerMonthTable from './VDatePickerMonthTable'
|
||||
import VDatePickerYears from './VDatePickerYears'
|
||||
|
||||
// Mixins
|
||||
import Picker from '../../mixins/picker'
|
||||
|
||||
// Utils
|
||||
import { pad, createNativeLocaleFormatter } from './util'
|
||||
import isDateAllowed, { AllowedDateFunction } from './util/isDateAllowed'
|
||||
import { consoleWarn } from '../../util/console'
|
||||
import { daysInMonth } from '../VCalendar/util/timestamp'
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
// Types
|
||||
import { PropValidator } from 'vue/types/options'
|
||||
import { DatePickerFormatter } from './util/createNativeLocaleFormatter'
|
||||
import { VNode } from 'vue'
|
||||
|
||||
export type DateEventColorValue = string | string[]
|
||||
export type DateEvents = string[] | ((date: string) => boolean | DateEventColorValue) | Record<string, DateEventColorValue>
|
||||
export type DateEventColors = DateEventColorValue | Record<string, DateEventColorValue> | ((date: string) => DateEventColorValue)
|
||||
type DatePickerValue = string | string[] | undefined
|
||||
type DatePickerType = 'date' | 'month'
|
||||
type DatePickerMultipleFormatter = (date: string[]) => string
|
||||
interface Formatters {
|
||||
year: DatePickerFormatter
|
||||
titleDate: DatePickerFormatter | DatePickerMultipleFormatter
|
||||
}
|
||||
|
||||
// Adds leading zero to month/day if necessary, returns 'YYYY' if type = 'year',
|
||||
// 'YYYY-MM' if 'month' and 'YYYY-MM-DD' if 'date'
|
||||
function sanitizeDateString (dateString: string, type: 'date' | 'month' | 'year'): string {
|
||||
const [year, month = 1, date = 1] = dateString.split('-')
|
||||
return `${year}-${pad(month)}-${pad(date)}`.substr(0, { date: 10, month: 7, year: 4 }[type])
|
||||
}
|
||||
|
||||
export default mixins(
|
||||
Picker
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-date-picker',
|
||||
|
||||
props: {
|
||||
allowedDates: Function as PropValidator<AllowedDateFunction | undefined>,
|
||||
// Function formatting the day in date picker table
|
||||
dayFormat: Function as PropValidator<AllowedDateFunction | undefined>,
|
||||
disabled: Boolean,
|
||||
events: {
|
||||
type: [Array, Function, Object],
|
||||
default: () => null
|
||||
} as any as PropValidator<DateEvents>,
|
||||
eventColor: {
|
||||
type: [Array, Function, Object, String],
|
||||
default: () => 'warning'
|
||||
} as any as PropValidator<DateEventColors>,
|
||||
firstDayOfWeek: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
// Function formatting the tableDate in the day/month table header
|
||||
headerDateFormat: Function as PropValidator<DatePickerFormatter | undefined>,
|
||||
locale: {
|
||||
type: String,
|
||||
default: 'en-us'
|
||||
},
|
||||
max: String,
|
||||
min: String,
|
||||
// Function formatting month in the months table
|
||||
monthFormat: Function as PropValidator<DatePickerFormatter | undefined>,
|
||||
multiple: Boolean,
|
||||
nextIcon: {
|
||||
type: String,
|
||||
default: '$vuetify.icons.next'
|
||||
},
|
||||
pickerDate: String,
|
||||
prevIcon: {
|
||||
type: String,
|
||||
default: '$vuetify.icons.prev'
|
||||
},
|
||||
reactive: Boolean,
|
||||
readonly: Boolean,
|
||||
scrollable: Boolean,
|
||||
showCurrent: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
showWeek: Boolean,
|
||||
// Function formatting currently selected date in the picker title
|
||||
titleDateFormat: Function as PropValidator<DatePickerFormatter | DatePickerMultipleFormatter | undefined>,
|
||||
type: {
|
||||
type: String,
|
||||
default: 'date',
|
||||
validator: (type: any) => ['date', 'month'].includes(type) // TODO: year
|
||||
} as any as PropValidator<DatePickerType>,
|
||||
value: [Array, String] as PropValidator<DatePickerValue>,
|
||||
weekdayFormat: Function as PropValidator<DatePickerFormatter | undefined>,
|
||||
// Function formatting the year in table header and pickup title
|
||||
yearFormat: Function as PropValidator<DatePickerFormatter | undefined>,
|
||||
yearIcon: String
|
||||
},
|
||||
|
||||
data () {
|
||||
const now = new Date()
|
||||
return {
|
||||
activePicker: this.type.toUpperCase(),
|
||||
inputDay: null as number | null,
|
||||
inputMonth: null as number | null,
|
||||
inputYear: null as number | null,
|
||||
isReversing: false,
|
||||
now,
|
||||
// tableDate is a string in 'YYYY' / 'YYYY-M' format (leading zero for month is not required)
|
||||
tableDate: (() => {
|
||||
if (this.pickerDate) {
|
||||
return this.pickerDate
|
||||
}
|
||||
|
||||
const date = (this.multiple ? (this.value as string[])[(this.value as string[]).length - 1] : this.value) ||
|
||||
`${now.getFullYear()}-${now.getMonth() + 1}`
|
||||
return sanitizeDateString(date as string, this.type === 'date' ? 'month' : 'year')
|
||||
})()
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
lastValue (): string | null {
|
||||
return this.multiple ? (this.value as string[])[(this.value as string[]).length - 1] : (this.value as string | null)
|
||||
},
|
||||
selectedMonths (): string | string[] | undefined {
|
||||
if (!this.value || !this.value.length || this.type === 'month') {
|
||||
return this.value
|
||||
} else if (this.multiple) {
|
||||
return (this.value as string[]).map(val => val.substr(0, 7))
|
||||
} else {
|
||||
return (this.value as string).substr(0, 7)
|
||||
}
|
||||
},
|
||||
current (): string | null {
|
||||
if (this.showCurrent === true) {
|
||||
return sanitizeDateString(`${this.now.getFullYear()}-${this.now.getMonth() + 1}-${this.now.getDate()}`, this.type)
|
||||
}
|
||||
|
||||
return this.showCurrent || null
|
||||
},
|
||||
inputDate (): string {
|
||||
return this.type === 'date'
|
||||
? `${this.inputYear}-${pad(this.inputMonth! + 1)}-${pad(this.inputDay!)}`
|
||||
: `${this.inputYear}-${pad(this.inputMonth! + 1)}`
|
||||
},
|
||||
tableMonth (): number {
|
||||
return Number((this.pickerDate || this.tableDate).split('-')[1]) - 1
|
||||
},
|
||||
tableYear (): number {
|
||||
return Number((this.pickerDate || this.tableDate).split('-')[0])
|
||||
},
|
||||
minMonth (): string | null {
|
||||
return this.min ? sanitizeDateString(this.min, 'month') : null
|
||||
},
|
||||
maxMonth (): string | null {
|
||||
return this.max ? sanitizeDateString(this.max, 'month') : null
|
||||
},
|
||||
minYear (): string | null {
|
||||
return this.min ? sanitizeDateString(this.min, 'year') : null
|
||||
},
|
||||
maxYear (): string | null {
|
||||
return this.max ? sanitizeDateString(this.max, 'year') : null
|
||||
},
|
||||
formatters (): Formatters {
|
||||
return {
|
||||
year: this.yearFormat || createNativeLocaleFormatter(this.locale, { year: 'numeric', timeZone: 'UTC' }, { length: 4 }),
|
||||
titleDate: this.titleDateFormat || (this.multiple ? this.defaultTitleMultipleDateFormatter : this.defaultTitleDateFormatter)
|
||||
}
|
||||
},
|
||||
defaultTitleMultipleDateFormatter (): DatePickerMultipleFormatter {
|
||||
if ((this.value as string[]).length < 2) {
|
||||
return dates => dates.length ? this.defaultTitleDateFormatter(dates[0]) : '0 selected'
|
||||
}
|
||||
|
||||
return dates => `${dates.length} selected`
|
||||
},
|
||||
defaultTitleDateFormatter (): DatePickerFormatter {
|
||||
const titleFormats = {
|
||||
year: { year: 'numeric', timeZone: 'UTC' },
|
||||
month: { month: 'long', timeZone: 'UTC' },
|
||||
date: { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' }
|
||||
}
|
||||
|
||||
const titleDateFormatter = createNativeLocaleFormatter(this.locale, titleFormats[this.type], {
|
||||
start: 0,
|
||||
length: { date: 10, month: 7, year: 4 }[this.type]
|
||||
})
|
||||
|
||||
const landscapeFormatter = (date: string) => titleDateFormatter(date)
|
||||
.replace(/([^\d\s])([\d])/g, (match, nonDigit, digit) => `${nonDigit} ${digit}`)
|
||||
.replace(', ', ',<br>')
|
||||
|
||||
return this.landscape ? landscapeFormatter : titleDateFormatter
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
tableDate (val: string, prev: string) {
|
||||
// Make a ISO 8601 strings from val and prev for comparision, otherwise it will incorrectly
|
||||
// compare for example '2000-9' and '2000-10'
|
||||
const sanitizeType = this.type === 'month' ? 'year' : 'month'
|
||||
this.isReversing = sanitizeDateString(val, sanitizeType) < sanitizeDateString(prev, sanitizeType)
|
||||
this.$emit('update:pickerDate', val)
|
||||
},
|
||||
pickerDate (val: string | null) {
|
||||
if (val) {
|
||||
this.tableDate = val
|
||||
} else if (this.lastValue && this.type === 'date') {
|
||||
this.tableDate = sanitizeDateString(this.lastValue, 'month')
|
||||
} else if (this.lastValue && this.type === 'month') {
|
||||
this.tableDate = sanitizeDateString(this.lastValue, 'year')
|
||||
}
|
||||
},
|
||||
value (newValue: DatePickerValue, oldValue: DatePickerValue) {
|
||||
this.checkMultipleProp()
|
||||
this.setInputDate()
|
||||
|
||||
if (!this.multiple && this.value && !this.pickerDate) {
|
||||
this.tableDate = sanitizeDateString(this.inputDate, this.type === 'month' ? 'year' : 'month')
|
||||
} else if (this.multiple && (this.value as string[]).length && !(oldValue as string[]).length && !this.pickerDate) {
|
||||
this.tableDate = sanitizeDateString(this.inputDate, this.type === 'month' ? 'year' : 'month')
|
||||
}
|
||||
},
|
||||
type (type: DatePickerType) {
|
||||
this.activePicker = type.toUpperCase()
|
||||
|
||||
if (this.value && this.value.length) {
|
||||
const output = (this.multiple ? (this.value as string[]) : [this.value as string])
|
||||
.map((val: string) => sanitizeDateString(val, type))
|
||||
.filter(this.isDateAllowed)
|
||||
this.$emit('input', this.multiple ? output : output[0])
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.checkMultipleProp()
|
||||
|
||||
if (this.pickerDate !== this.tableDate) {
|
||||
this.$emit('update:pickerDate', this.tableDate)
|
||||
}
|
||||
this.setInputDate()
|
||||
},
|
||||
|
||||
methods: {
|
||||
emitInput (newInput: string) {
|
||||
const output = this.multiple
|
||||
? (
|
||||
(this.value as string[]).indexOf(newInput) === -1
|
||||
? (this.value as string[]).concat([newInput])
|
||||
: (this.value as string[]).filter(x => x !== newInput)
|
||||
)
|
||||
: newInput
|
||||
|
||||
this.$emit('input', output)
|
||||
this.multiple || this.$emit('change', newInput)
|
||||
},
|
||||
checkMultipleProp () {
|
||||
if (this.value == null) return
|
||||
const valueType = this.value.constructor.name
|
||||
const expected = this.multiple ? 'Array' : 'String'
|
||||
if (valueType !== expected) {
|
||||
consoleWarn(`Value must be ${this.multiple ? 'an' : 'a'} ${expected}, got ${valueType}`, this)
|
||||
}
|
||||
},
|
||||
isDateAllowed (value: string) {
|
||||
return isDateAllowed(value, this.min, this.max, this.allowedDates)
|
||||
},
|
||||
yearClick (value: number) {
|
||||
this.inputYear = value
|
||||
if (this.type === 'month') {
|
||||
this.tableDate = `${value}`
|
||||
} else {
|
||||
this.tableDate = `${value}-${pad((this.tableMonth || 0) + 1)}`
|
||||
}
|
||||
this.activePicker = 'MONTH'
|
||||
if (this.reactive && !this.readonly && !this.multiple && this.isDateAllowed(this.inputDate)) {
|
||||
this.$emit('input', this.inputDate)
|
||||
}
|
||||
},
|
||||
monthClick (value: string) {
|
||||
this.inputYear = parseInt(value.split('-')[0], 10)
|
||||
this.inputMonth = parseInt(value.split('-')[1], 10) - 1
|
||||
if (this.type === 'date') {
|
||||
if (this.inputDay) {
|
||||
this.inputDay = Math.min(this.inputDay, daysInMonth(this.inputYear, this.inputMonth + 1))
|
||||
}
|
||||
|
||||
this.tableDate = value
|
||||
this.activePicker = 'DATE'
|
||||
if (this.reactive && !this.readonly && !this.multiple && this.isDateAllowed(this.inputDate)) {
|
||||
this.$emit('input', this.inputDate)
|
||||
}
|
||||
} else {
|
||||
this.emitInput(this.inputDate)
|
||||
}
|
||||
},
|
||||
dateClick (value: string) {
|
||||
this.inputYear = parseInt(value.split('-')[0], 10)
|
||||
this.inputMonth = parseInt(value.split('-')[1], 10) - 1
|
||||
this.inputDay = parseInt(value.split('-')[2], 10)
|
||||
this.emitInput(this.inputDate)
|
||||
},
|
||||
genPickerTitle () {
|
||||
return this.$createElement(VDatePickerTitle, {
|
||||
props: {
|
||||
date: this.value ? (this.formatters.titleDate as (value: any) => string)(this.value) : '',
|
||||
disabled: this.disabled,
|
||||
readonly: this.readonly,
|
||||
selectingYear: this.activePicker === 'YEAR',
|
||||
year: this.formatters.year(this.value ? `${this.inputYear}` : this.tableDate),
|
||||
yearIcon: this.yearIcon,
|
||||
value: this.multiple ? (this.value as string[])[0] : this.value
|
||||
},
|
||||
slot: 'title',
|
||||
on: {
|
||||
'update:selectingYear': (value: boolean) => this.activePicker = value ? 'YEAR' : this.type.toUpperCase()
|
||||
}
|
||||
})
|
||||
},
|
||||
genTableHeader () {
|
||||
return this.$createElement(VDatePickerHeader, {
|
||||
props: {
|
||||
nextIcon: this.nextIcon,
|
||||
color: this.color,
|
||||
dark: this.dark,
|
||||
disabled: this.disabled,
|
||||
format: this.headerDateFormat,
|
||||
light: this.light,
|
||||
locale: this.locale,
|
||||
min: this.activePicker === 'DATE' ? this.minMonth : this.minYear,
|
||||
max: this.activePicker === 'DATE' ? this.maxMonth : this.maxYear,
|
||||
prevIcon: this.prevIcon,
|
||||
readonly: this.readonly,
|
||||
value: this.activePicker === 'DATE' ? `${pad(this.tableYear, 4)}-${pad(this.tableMonth + 1)}` : `${pad(this.tableYear, 4)}`
|
||||
},
|
||||
on: {
|
||||
toggle: () => this.activePicker = (this.activePicker === 'DATE' ? 'MONTH' : 'YEAR'),
|
||||
input: (value: string) => this.tableDate = value
|
||||
}
|
||||
})
|
||||
},
|
||||
genDateTable () {
|
||||
return this.$createElement(VDatePickerDateTable, {
|
||||
props: {
|
||||
allowedDates: this.allowedDates,
|
||||
color: this.color,
|
||||
current: this.current,
|
||||
dark: this.dark,
|
||||
disabled: this.disabled,
|
||||
events: this.events,
|
||||
eventColor: this.eventColor,
|
||||
firstDayOfWeek: this.firstDayOfWeek,
|
||||
format: this.dayFormat,
|
||||
light: this.light,
|
||||
locale: this.locale,
|
||||
min: this.min,
|
||||
max: this.max,
|
||||
readonly: this.readonly,
|
||||
scrollable: this.scrollable,
|
||||
showWeek: this.showWeek,
|
||||
tableDate: `${pad(this.tableYear, 4)}-${pad(this.tableMonth + 1)}`,
|
||||
value: this.value,
|
||||
weekdayFormat: this.weekdayFormat
|
||||
},
|
||||
ref: 'table',
|
||||
on: {
|
||||
input: this.dateClick,
|
||||
tableDate: (value: string) => this.tableDate = value,
|
||||
'click:date': (value: string) => this.$emit('click:date', value),
|
||||
'dblclick:date': (value: string) => this.$emit('dblclick:date', value)
|
||||
}
|
||||
})
|
||||
},
|
||||
genMonthTable () {
|
||||
return this.$createElement(VDatePickerMonthTable, {
|
||||
props: {
|
||||
allowedDates: this.type === 'month' ? this.allowedDates : null,
|
||||
color: this.color,
|
||||
current: this.current ? sanitizeDateString(this.current, 'month') : null,
|
||||
dark: this.dark,
|
||||
disabled: this.disabled,
|
||||
events: this.type === 'month' ? this.events : null,
|
||||
eventColor: this.type === 'month' ? this.eventColor : null,
|
||||
format: this.monthFormat,
|
||||
light: this.light,
|
||||
locale: this.locale,
|
||||
min: this.minMonth,
|
||||
max: this.maxMonth,
|
||||
readonly: this.readonly && this.type === 'month',
|
||||
scrollable: this.scrollable,
|
||||
value: this.selectedMonths,
|
||||
tableDate: `${pad(this.tableYear, 4)}`
|
||||
},
|
||||
ref: 'table',
|
||||
on: {
|
||||
input: this.monthClick,
|
||||
tableDate: (value: string) => this.tableDate = value,
|
||||
'click:month': (value: string) => this.$emit('click:month', value),
|
||||
'dblclick:month': (value: string) => this.$emit('dblclick:month', value)
|
||||
}
|
||||
})
|
||||
},
|
||||
genYears () {
|
||||
return this.$createElement(VDatePickerYears, {
|
||||
props: {
|
||||
color: this.color,
|
||||
format: this.yearFormat,
|
||||
locale: this.locale,
|
||||
min: this.minYear,
|
||||
max: this.maxYear,
|
||||
value: this.tableYear
|
||||
},
|
||||
on: {
|
||||
input: this.yearClick
|
||||
}
|
||||
})
|
||||
},
|
||||
genPickerBody () {
|
||||
const children = this.activePicker === 'YEAR' ? [
|
||||
this.genYears()
|
||||
] : [
|
||||
this.genTableHeader(),
|
||||
this.activePicker === 'DATE' ? this.genDateTable() : this.genMonthTable()
|
||||
]
|
||||
|
||||
return this.$createElement('div', {
|
||||
key: this.activePicker
|
||||
}, children)
|
||||
},
|
||||
setInputDate () {
|
||||
if (this.lastValue) {
|
||||
const array = this.lastValue.split('-')
|
||||
this.inputYear = parseInt(array[0], 10)
|
||||
this.inputMonth = parseInt(array[1], 10) - 1
|
||||
if (this.type === 'date') {
|
||||
this.inputDay = parseInt(array[2], 10)
|
||||
}
|
||||
} else {
|
||||
this.inputYear = this.inputYear || this.now.getFullYear()
|
||||
this.inputMonth = this.inputMonth == null ? this.inputMonth : this.now.getMonth()
|
||||
this.inputDay = this.inputDay || this.now.getDate()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render (): VNode {
|
||||
return this.genPicker('v-picker--date')
|
||||
}
|
||||
})
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
// Mixins
|
||||
import DatePickerTable from './mixins/date-picker-table'
|
||||
|
||||
// Utils
|
||||
import { pad, createNativeLocaleFormatter, monthChange } from './util'
|
||||
import { createRange } from '../../util/helpers'
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
// Types
|
||||
import { DatePickerFormatter } from './util/createNativeLocaleFormatter'
|
||||
import { PropValidator } from 'vue/types/options'
|
||||
import { VNode, VNodeChildren } from 'vue'
|
||||
|
||||
export default mixins(
|
||||
DatePickerTable
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-date-picker-date-table',
|
||||
|
||||
props: {
|
||||
firstDayOfWeek: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
showWeek: Boolean,
|
||||
weekdayFormat: Function as PropValidator<DatePickerFormatter | undefined>
|
||||
},
|
||||
|
||||
computed: {
|
||||
formatter (): DatePickerFormatter {
|
||||
return this.format || createNativeLocaleFormatter(this.locale, { day: 'numeric', timeZone: 'UTC' }, { start: 8, length: 2 })
|
||||
},
|
||||
weekdayFormatter (): DatePickerFormatter | undefined {
|
||||
return this.weekdayFormat || createNativeLocaleFormatter(this.locale, { weekday: 'narrow', timeZone: 'UTC' })
|
||||
},
|
||||
weekDays (): string[] {
|
||||
const first = parseInt(this.firstDayOfWeek, 10)
|
||||
|
||||
return this.weekdayFormatter
|
||||
? createRange(7).map(i => this.weekdayFormatter!(`2017-01-${first + i + 15}`)) // 2017-01-15 is Sunday
|
||||
: createRange(7).map(i => ['S', 'M', 'T', 'W', 'T', 'F', 'S'][(i + first) % 7])
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
calculateTableDate (delta: number) {
|
||||
return monthChange(this.tableDate, Math.sign(delta || 1))
|
||||
},
|
||||
genTHead () {
|
||||
const days = this.weekDays.map(day => this.$createElement('th', day))
|
||||
this.showWeek && days.unshift(this.$createElement('th'))
|
||||
return this.$createElement('thead', this.genTR(days))
|
||||
},
|
||||
// Returns number of the days from the firstDayOfWeek to the first day of the current month
|
||||
weekDaysBeforeFirstDayOfTheMonth () {
|
||||
const firstDayOfTheMonth = new Date(`${this.displayedYear}-${pad(this.displayedMonth + 1)}-01T00:00:00+00:00`)
|
||||
const weekDay = firstDayOfTheMonth.getUTCDay()
|
||||
return (weekDay - parseInt(this.firstDayOfWeek) + 7) % 7
|
||||
},
|
||||
getWeekNumber () {
|
||||
let dayOfYear = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334][this.displayedMonth]
|
||||
if (this.displayedMonth > 1 &&
|
||||
(((this.displayedYear % 4 === 0) && (this.displayedYear % 100 !== 0)) || (this.displayedYear % 400 === 0))
|
||||
) {
|
||||
dayOfYear++
|
||||
}
|
||||
const offset = (
|
||||
this.displayedYear +
|
||||
((this.displayedYear - 1) >> 2) -
|
||||
Math.floor((this.displayedYear - 1) / 100) +
|
||||
Math.floor((this.displayedYear - 1) / 400) -
|
||||
Number(this.firstDayOfWeek)
|
||||
) % 7 // https://en.wikipedia.org/wiki/Zeller%27s_congruence
|
||||
return Math.floor((dayOfYear + offset) / 7) + 1
|
||||
},
|
||||
genWeekNumber (weekNumber: number) {
|
||||
return this.$createElement('td', [
|
||||
this.$createElement('small', {
|
||||
staticClass: 'v-date-picker-table--date__week'
|
||||
}, String(weekNumber).padStart(2, '0'))
|
||||
])
|
||||
},
|
||||
genTBody () {
|
||||
const children = []
|
||||
const daysInMonth = new Date(this.displayedYear, this.displayedMonth + 1, 0).getDate()
|
||||
let rows = []
|
||||
let day = this.weekDaysBeforeFirstDayOfTheMonth()
|
||||
let weekNumber = this.getWeekNumber()
|
||||
|
||||
this.showWeek && rows.push(this.genWeekNumber(weekNumber++))
|
||||
|
||||
while (day--) rows.push(this.$createElement('td'))
|
||||
for (day = 1; day <= daysInMonth; day++) {
|
||||
const date = `${this.displayedYear}-${pad(this.displayedMonth + 1)}-${pad(day)}`
|
||||
|
||||
rows.push(this.$createElement('td', [
|
||||
this.genButton(date, true, 'date', this.formatter)
|
||||
]))
|
||||
|
||||
if (rows.length % (this.showWeek ? 8 : 7) === 0) {
|
||||
children.push(this.genTR(rows))
|
||||
rows = []
|
||||
day < daysInMonth && this.showWeek && rows.push(this.genWeekNumber(weekNumber++))
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.length) {
|
||||
children.push(this.genTR(rows))
|
||||
}
|
||||
|
||||
return this.$createElement('tbody', children)
|
||||
},
|
||||
genTR (children: VNodeChildren) {
|
||||
return [this.$createElement('tr', children)]
|
||||
}
|
||||
},
|
||||
|
||||
render (): VNode {
|
||||
return this.genTable('v-date-picker-table v-date-picker-table--date', [
|
||||
this.genTHead(),
|
||||
this.genTBody()
|
||||
], this.calculateTableDate)
|
||||
}
|
||||
})
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import '../../stylus/components/_date-picker-header.styl'
|
||||
|
||||
// Components
|
||||
import VBtn from '../VBtn'
|
||||
import VIcon from '../VIcon'
|
||||
|
||||
// Mixins
|
||||
import Colorable from '../../mixins/colorable'
|
||||
import Themeable from '../../mixins/themeable'
|
||||
|
||||
// Utils
|
||||
import { createNativeLocaleFormatter, monthChange } from './util'
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
import { DatePickerFormatter } from './util/createNativeLocaleFormatter'
|
||||
import { PropValidator } from 'vue/types/options'
|
||||
|
||||
export default mixins(
|
||||
Colorable,
|
||||
Themeable
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-date-picker-header',
|
||||
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
format: Function as PropValidator<DatePickerFormatter | undefined>,
|
||||
locale: {
|
||||
type: String,
|
||||
default: 'en-us'
|
||||
},
|
||||
min: String,
|
||||
max: String,
|
||||
nextIcon: {
|
||||
type: String,
|
||||
default: '$vuetify.icons.next'
|
||||
},
|
||||
prevIcon: {
|
||||
type: String,
|
||||
default: '$vuetify.icons.prev'
|
||||
},
|
||||
readonly: Boolean,
|
||||
value: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
isReversing: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
formatter (): DatePickerFormatter {
|
||||
if (this.format) {
|
||||
return this.format
|
||||
} else if (String(this.value).split('-')[1]) {
|
||||
return createNativeLocaleFormatter(this.locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }, { length: 7 })
|
||||
} else {
|
||||
return createNativeLocaleFormatter(this.locale, { year: 'numeric', timeZone: 'UTC' }, { length: 4 })
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value (newVal, oldVal) {
|
||||
this.isReversing = newVal < oldVal
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
genBtn (change: number) {
|
||||
const disabled = this.disabled ||
|
||||
(change < 0 && this.min && this.calculateChange(change) < this.min) ||
|
||||
(change > 0 && this.max && this.calculateChange(change) > this.max)
|
||||
|
||||
return this.$createElement(VBtn, {
|
||||
props: {
|
||||
dark: this.dark,
|
||||
disabled,
|
||||
icon: true,
|
||||
light: this.light
|
||||
},
|
||||
nativeOn: {
|
||||
click: (e: Event) => {
|
||||
e.stopPropagation()
|
||||
this.$emit('input', this.calculateChange(change))
|
||||
}
|
||||
}
|
||||
}, [
|
||||
this.$createElement(VIcon, ((change < 0) === !this.$vuetify.rtl) ? this.prevIcon : this.nextIcon)
|
||||
])
|
||||
},
|
||||
calculateChange (sign: number) {
|
||||
const [year, month] = String(this.value).split('-').map(Number)
|
||||
|
||||
if (month == null) {
|
||||
return `${year + sign}`
|
||||
} else {
|
||||
return monthChange(String(this.value), sign)
|
||||
}
|
||||
},
|
||||
genHeader () {
|
||||
const color = !this.disabled && (this.color || 'accent')
|
||||
const header = this.$createElement('div', this.setTextColor(color, {
|
||||
key: String(this.value)
|
||||
}), [this.$createElement('button', {
|
||||
attrs: {
|
||||
type: 'button'
|
||||
},
|
||||
on: {
|
||||
click: () => this.$emit('toggle')
|
||||
}
|
||||
}, [this.$slots.default || this.formatter(String(this.value))])])
|
||||
|
||||
const transition = this.$createElement('transition', {
|
||||
props: {
|
||||
name: (this.isReversing === !this.$vuetify.rtl) ? 'tab-reverse-transition' : 'tab-transition'
|
||||
}
|
||||
}, [header])
|
||||
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-date-picker-header__value',
|
||||
class: {
|
||||
'v-date-picker-header__value--disabled': this.disabled
|
||||
}
|
||||
}, [transition])
|
||||
}
|
||||
},
|
||||
|
||||
render (): VNode {
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-date-picker-header',
|
||||
class: {
|
||||
'v-date-picker-header--disabled': this.disabled,
|
||||
...this.themeClasses
|
||||
}
|
||||
}, [
|
||||
this.genBtn(-1),
|
||||
this.genHeader(),
|
||||
this.genBtn(+1)
|
||||
])
|
||||
}
|
||||
})
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// Mixins
|
||||
import DatePickerTable from './mixins/date-picker-table'
|
||||
|
||||
// Utils
|
||||
import { pad, createNativeLocaleFormatter } from './util'
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
import { DatePickerFormatter } from './util/createNativeLocaleFormatter'
|
||||
|
||||
export default mixins(
|
||||
DatePickerTable
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-date-picker-month-table',
|
||||
|
||||
computed: {
|
||||
formatter (): DatePickerFormatter {
|
||||
return this.format || createNativeLocaleFormatter(this.locale, { month: 'short', timeZone: 'UTC' }, { start: 5, length: 2 })
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
calculateTableDate (delta: number) {
|
||||
return `${parseInt(this.tableDate, 10) + Math.sign(delta || 1)}`
|
||||
},
|
||||
genTBody () {
|
||||
const children = []
|
||||
const cols = Array(3).fill(null)
|
||||
const rows = 12 / cols.length
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
const tds = cols.map((_, col) => {
|
||||
const month = row * cols.length + col
|
||||
const date = `${this.displayedYear}-${pad(month + 1)}`
|
||||
return this.$createElement('td', {
|
||||
key: month
|
||||
}, [
|
||||
this.genButton(date, false, 'month', this.formatter)
|
||||
])
|
||||
})
|
||||
|
||||
children.push(this.$createElement('tr', {
|
||||
key: row
|
||||
}, tds))
|
||||
}
|
||||
|
||||
return this.$createElement('tbody', children)
|
||||
}
|
||||
},
|
||||
|
||||
render (): VNode {
|
||||
return this.genTable('v-date-picker-table v-date-picker-table--month', [
|
||||
this.genTBody()
|
||||
], this.calculateTableDate)
|
||||
}
|
||||
})
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import '../../stylus/components/_date-picker-title.styl'
|
||||
|
||||
// Components
|
||||
import VIcon from '../VIcon'
|
||||
|
||||
// Mixins
|
||||
import PickerButton from '../../mixins/picker-button'
|
||||
|
||||
// Utils
|
||||
import mixins from '../../util/mixins'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
|
||||
export default mixins(
|
||||
PickerButton
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-date-picker-title',
|
||||
|
||||
props: {
|
||||
date: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: Boolean,
|
||||
readonly: Boolean,
|
||||
selectingYear: Boolean,
|
||||
value: {
|
||||
type: String
|
||||
},
|
||||
year: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
yearIcon: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
isReversing: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
computedTransition (): string {
|
||||
return this.isReversing ? 'picker-reverse-transition' : 'picker-transition'
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value (val: string, prev: string) {
|
||||
this.isReversing = val < prev
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
genYearIcon (): VNode {
|
||||
return this.$createElement(VIcon, {
|
||||
props: {
|
||||
dark: true
|
||||
}
|
||||
}, this.yearIcon)
|
||||
},
|
||||
getYearBtn (): VNode {
|
||||
return this.genPickerButton('selectingYear', true, [
|
||||
String(this.year),
|
||||
this.yearIcon ? this.genYearIcon() : null
|
||||
], false, 'v-date-picker-title__year')
|
||||
},
|
||||
genTitleText (): VNode {
|
||||
return this.$createElement('transition', {
|
||||
props: {
|
||||
name: this.computedTransition
|
||||
}
|
||||
}, [
|
||||
this.$createElement('div', {
|
||||
domProps: { innerHTML: this.date || ' ' },
|
||||
key: this.value
|
||||
})
|
||||
])
|
||||
},
|
||||
genTitleDate (): VNode {
|
||||
return this.genPickerButton('selectingYear', false, [this.genTitleText()], false, 'v-date-picker-title__date')
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
return h('div', {
|
||||
staticClass: 'v-date-picker-title',
|
||||
'class': {
|
||||
'v-date-picker-title--disabled': this.disabled
|
||||
}
|
||||
}, [
|
||||
this.getYearBtn(),
|
||||
this.genTitleDate()
|
||||
])
|
||||
}
|
||||
})
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import '../../stylus/components/_date-picker-years.styl'
|
||||
|
||||
// Mixins
|
||||
import Colorable from '../../mixins/colorable'
|
||||
|
||||
// Utils
|
||||
import { createNativeLocaleFormatter } from './util'
|
||||
import mixins, { ExtractVue } from '../../util/mixins'
|
||||
|
||||
// Types
|
||||
import Vue, { VNode } from 'vue'
|
||||
import { DatePickerFormatter } from './util/createNativeLocaleFormatter'
|
||||
import { PropValidator } from 'vue/types/options'
|
||||
|
||||
interface options extends Vue {
|
||||
$el: HTMLElement
|
||||
}
|
||||
|
||||
export default mixins<options &
|
||||
/* eslint-disable indent */
|
||||
ExtractVue<[
|
||||
typeof Colorable
|
||||
]>
|
||||
/* eslint-enable indent */
|
||||
>(
|
||||
Colorable
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-date-picker-years',
|
||||
|
||||
props: {
|
||||
format: Function as PropValidator<DatePickerFormatter | undefined>,
|
||||
locale: {
|
||||
type: String,
|
||||
default: 'en-us'
|
||||
},
|
||||
min: [Number, String],
|
||||
max: [Number, String],
|
||||
readonly: Boolean,
|
||||
value: [Number, String]
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
defaultColor: 'primary'
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
formatter (): DatePickerFormatter {
|
||||
return this.format || createNativeLocaleFormatter(this.locale, { year: 'numeric', timeZone: 'UTC' }, { length: 4 })
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
setTimeout(() => {
|
||||
const activeItem = this.$el.getElementsByClassName('active')[0]
|
||||
if (activeItem) {
|
||||
this.$el.scrollTop = activeItem.offsetTop - this.$el.offsetHeight / 2 + activeItem.offsetHeight / 2
|
||||
} else {
|
||||
this.$el.scrollTop = this.$el.scrollHeight / 2 - this.$el.offsetHeight / 2
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
genYearItem (year: number): VNode {
|
||||
const formatted = this.formatter(`${year}`)
|
||||
const active = parseInt(this.value, 10) === year
|
||||
const color = active && (this.color || 'primary')
|
||||
|
||||
return this.$createElement('li', this.setTextColor(color, {
|
||||
key: year,
|
||||
'class': { active },
|
||||
on: {
|
||||
click: () => this.$emit('input', year)
|
||||
}
|
||||
}), formatted)
|
||||
},
|
||||
|
||||
genYearItems (): VNode[] {
|
||||
const children = []
|
||||
const selectedYear = this.value ? parseInt(this.value, 10) : new Date().getFullYear()
|
||||
const maxYear = this.max ? parseInt(this.max, 10) : (selectedYear + 100)
|
||||
const minYear = Math.min(maxYear, this.min ? parseInt(this.min, 10) : (selectedYear - 100))
|
||||
|
||||
for (let year = maxYear; year >= minYear; year--) {
|
||||
children.push(this.genYearItem(year))
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
},
|
||||
|
||||
render (): VNode {
|
||||
return this.$createElement('ul', {
|
||||
staticClass: 'v-date-picker-years',
|
||||
ref: 'years'
|
||||
}, this.genYearItems())
|
||||
}
|
||||
})
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import VDatePicker from './VDatePicker'
|
||||
import VDatePickerTitle from './VDatePickerTitle'
|
||||
import VDatePickerHeader from './VDatePickerHeader'
|
||||
import VDatePickerDateTable from './VDatePickerDateTable'
|
||||
import VDatePickerMonthTable from './VDatePickerMonthTable'
|
||||
import VDatePickerYears from './VDatePickerYears'
|
||||
|
||||
export {
|
||||
VDatePicker,
|
||||
VDatePickerTitle,
|
||||
VDatePickerHeader,
|
||||
VDatePickerDateTable,
|
||||
VDatePickerMonthTable,
|
||||
VDatePickerYears
|
||||
}
|
||||
|
||||
export default {
|
||||
$_vuetify_subcomponents: {
|
||||
VDatePicker,
|
||||
VDatePickerTitle,
|
||||
VDatePickerHeader,
|
||||
VDatePickerDateTable,
|
||||
VDatePickerMonthTable,
|
||||
VDatePickerYears
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
import '../../../stylus/components/_date-picker-table.styl'
|
||||
|
||||
// Directives
|
||||
import Touch, { TouchWrapper } from '../../../directives/touch'
|
||||
|
||||
// Mixins
|
||||
import Colorable from '../../../mixins/colorable'
|
||||
import Themeable from '../../../mixins/themeable'
|
||||
|
||||
// Utils
|
||||
import isDateAllowed, { AllowedDateFunction } from '../util/isDateAllowed'
|
||||
import mixins from '../../../util/mixins'
|
||||
|
||||
// Types
|
||||
import { VNodeChildren } from 'vue'
|
||||
import { PropValidator } from 'vue/types/options'
|
||||
import { DatePickerFormatter } from '../util/createNativeLocaleFormatter'
|
||||
import { DateEvents, DateEventColors, DateEventColorValue } from '../VDatePicker'
|
||||
|
||||
type CalculateTableDateFunction = (v: number) => string
|
||||
|
||||
export default mixins(
|
||||
Colorable,
|
||||
Themeable
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
directives: { Touch },
|
||||
|
||||
props: {
|
||||
allowedDates: Function as PropValidator<AllowedDateFunction | undefined>,
|
||||
current: String,
|
||||
disabled: Boolean,
|
||||
format: Function as PropValidator<DatePickerFormatter | undefined>,
|
||||
events: {
|
||||
type: [Array, Function, Object],
|
||||
default: () => null
|
||||
} as any as PropValidator<DateEvents>,
|
||||
eventColor: {
|
||||
type: [Array, Function, Object, String],
|
||||
default: () => 'warning'
|
||||
} as any as PropValidator<DateEventColors>,
|
||||
locale: {
|
||||
type: String,
|
||||
default: 'en-us'
|
||||
},
|
||||
min: String,
|
||||
max: String,
|
||||
readonly: Boolean,
|
||||
scrollable: Boolean,
|
||||
tableDate: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
value: [String, Array]
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
isReversing: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
computedTransition (): string {
|
||||
return (this.isReversing === !this.$vuetify.rtl) ? 'tab-reverse-transition' : 'tab-transition'
|
||||
},
|
||||
displayedMonth (): number {
|
||||
return Number(this.tableDate.split('-')[1]) - 1
|
||||
},
|
||||
displayedYear (): number {
|
||||
return Number(this.tableDate.split('-')[0])
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
tableDate (newVal: string, oldVal: string) {
|
||||
this.isReversing = newVal < oldVal
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
genButtonClasses (isAllowed: boolean, isFloating: boolean, isSelected: boolean, isCurrent: boolean) {
|
||||
return {
|
||||
'v-btn--active': isSelected,
|
||||
'v-btn--flat': !isSelected,
|
||||
'v-btn--icon': isSelected && isAllowed && isFloating,
|
||||
'v-btn--floating': isFloating,
|
||||
'v-btn--depressed': !isFloating && isSelected,
|
||||
'v-btn--disabled': !isAllowed || (this.disabled && isSelected),
|
||||
'v-btn--outline': isCurrent && !isSelected,
|
||||
...this.themeClasses
|
||||
}
|
||||
},
|
||||
genButtonEvents (value: string, isAllowed: boolean, mouseEventType: string) {
|
||||
if (this.disabled) return undefined
|
||||
|
||||
return {
|
||||
click: () => {
|
||||
isAllowed && !this.readonly && this.$emit('input', value)
|
||||
this.$emit(`click:${mouseEventType}`, value)
|
||||
},
|
||||
dblclick: () => this.$emit(`dblclick:${mouseEventType}`, value)
|
||||
}
|
||||
},
|
||||
genButton (value: string, isFloating: boolean, mouseEventType: string, formatter: DatePickerFormatter) {
|
||||
const isAllowed = isDateAllowed(value, this.min, this.max, this.allowedDates)
|
||||
const isSelected = value === this.value || (Array.isArray(this.value) && this.value.indexOf(value) !== -1)
|
||||
const isCurrent = value === this.current
|
||||
const setColor = isSelected ? this.setBackgroundColor : this.setTextColor
|
||||
const color = (isSelected || isCurrent) && (this.color || 'accent')
|
||||
|
||||
return this.$createElement('button', setColor(color, {
|
||||
staticClass: 'v-btn',
|
||||
'class': this.genButtonClasses(isAllowed, isFloating, isSelected, isCurrent),
|
||||
attrs: {
|
||||
type: 'button'
|
||||
},
|
||||
domProps: {
|
||||
disabled: this.disabled || !isAllowed
|
||||
},
|
||||
on: this.genButtonEvents(value, isAllowed, mouseEventType)
|
||||
}), [
|
||||
this.$createElement('div', {
|
||||
staticClass: 'v-btn__content'
|
||||
}, [formatter(value)]),
|
||||
this.genEvents(value)
|
||||
])
|
||||
},
|
||||
getEventColors (date: string) {
|
||||
const arrayize = (v: string | string[]) => Array.isArray(v) ? v : [v]
|
||||
let eventData: boolean | DateEventColorValue
|
||||
let eventColors: string[] = []
|
||||
|
||||
if (Array.isArray(this.events)) {
|
||||
eventData = this.events.includes(date)
|
||||
} else if (this.events instanceof Function) {
|
||||
eventData = this.events(date) || false
|
||||
} else if (this.events) {
|
||||
eventData = this.events[date] || false
|
||||
} else {
|
||||
eventData = false
|
||||
}
|
||||
|
||||
if (!eventData) {
|
||||
return []
|
||||
} else if (eventData !== true) {
|
||||
eventColors = arrayize(eventData)
|
||||
} else if (typeof this.eventColor === 'string') {
|
||||
eventColors = [this.eventColor]
|
||||
} else if (typeof this.eventColor === 'function') {
|
||||
eventColors = arrayize(this.eventColor(date))
|
||||
} else if (Array.isArray(this.eventColor)) {
|
||||
eventColors = this.eventColor
|
||||
} else {
|
||||
eventColors = arrayize(this.eventColor[date])
|
||||
}
|
||||
|
||||
return eventColors.filter(v => v)
|
||||
},
|
||||
genEvents (date: string) {
|
||||
const eventColors = this.getEventColors(date)
|
||||
|
||||
return eventColors.length ? this.$createElement('div', {
|
||||
staticClass: 'v-date-picker-table__events'
|
||||
}, eventColors.map(color => this.$createElement('div', this.setBackgroundColor(color)))) : null
|
||||
},
|
||||
wheel (e: WheelEvent, calculateTableDate: CalculateTableDateFunction) {
|
||||
e.preventDefault()
|
||||
this.$emit('tableDate', calculateTableDate(e.deltaY))
|
||||
},
|
||||
touch (value: number, calculateTableDate: CalculateTableDateFunction) {
|
||||
this.$emit('tableDate', calculateTableDate(value))
|
||||
},
|
||||
genTable (staticClass: string, children: VNodeChildren, calculateTableDate: CalculateTableDateFunction) {
|
||||
const transition = this.$createElement('transition', {
|
||||
props: { name: this.computedTransition }
|
||||
}, [this.$createElement('table', { key: this.tableDate }, children)])
|
||||
|
||||
const touchDirective = {
|
||||
name: 'touch',
|
||||
value: {
|
||||
left: (e: TouchWrapper) => (e.offsetX < -15) && this.touch(1, calculateTableDate),
|
||||
right: (e: TouchWrapper) => (e.offsetX > 15) && this.touch(-1, calculateTableDate)
|
||||
}
|
||||
}
|
||||
|
||||
return this.$createElement('div', {
|
||||
staticClass,
|
||||
class: {
|
||||
'v-date-picker-table--disabled': this.disabled,
|
||||
...this.themeClasses
|
||||
},
|
||||
on: (!this.disabled && this.scrollable) ? {
|
||||
wheel: (e: WheelEvent) => this.wheel(e, calculateTableDate)
|
||||
} : undefined,
|
||||
directives: [touchDirective]
|
||||
}, [transition])
|
||||
}
|
||||
}
|
||||
})
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
import pad from './pad'
|
||||
|
||||
interface SubstrOptions {
|
||||
start?: number
|
||||
length: number
|
||||
}
|
||||
|
||||
export type DatePickerFormatter = (date: string) => string
|
||||
|
||||
function createNativeLocaleFormatter (
|
||||
local: string | undefined,
|
||||
options: Intl.DateTimeFormatOptions
|
||||
): DatePickerFormatter | undefined
|
||||
|
||||
function createNativeLocaleFormatter (
|
||||
local: string | undefined,
|
||||
options: Intl.DateTimeFormatOptions,
|
||||
substrOptions: SubstrOptions
|
||||
): DatePickerFormatter
|
||||
|
||||
function createNativeLocaleFormatter (
|
||||
locale: string | undefined,
|
||||
options: Intl.DateTimeFormatOptions,
|
||||
substrOptions: SubstrOptions = { start: 0, length: 0 }
|
||||
): DatePickerFormatter | undefined {
|
||||
const makeIsoString = (dateString: string) => {
|
||||
const [year, month, date] = dateString.trim().split(' ')[0].split('-')
|
||||
return [pad(year, 4), pad(month || 1), pad(date || 1)].join('-')
|
||||
}
|
||||
|
||||
try {
|
||||
const intlFormatter = new Intl.DateTimeFormat(locale || undefined, options)
|
||||
return (dateString: string) => intlFormatter.format(new Date(`${makeIsoString(dateString)}T00:00:00+00:00`))
|
||||
} catch (e) {
|
||||
return (substrOptions.start || substrOptions.length)
|
||||
? (dateString: string) => makeIsoString(dateString).substr(substrOptions.start || 0, substrOptions.length)
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
export default createNativeLocaleFormatter
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import createNativeLocaleFormatter from './createNativeLocaleFormatter'
|
||||
import monthChange from './monthChange'
|
||||
import pad from './pad'
|
||||
|
||||
export {
|
||||
createNativeLocaleFormatter,
|
||||
monthChange,
|
||||
pad
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export type AllowedDateFunction = (date: string) => boolean
|
||||
|
||||
export default function isDateAllowed (date: string, min: string, max: string, allowedFn: AllowedDateFunction | undefined) {
|
||||
return (!allowedFn || allowedFn(date)) &&
|
||||
(!min || date >= min) &&
|
||||
(!max || date <= max)
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import pad from './pad'
|
||||
|
||||
/**
|
||||
* @param {String} value YYYY-MM format
|
||||
* @param {Number} sign -1 or +1
|
||||
*/
|
||||
export default (value: string, sign: number) => {
|
||||
const [year, month] = value.split('-').map(Number)
|
||||
|
||||
if (month + sign === 0) {
|
||||
return `${year - 1}-12`
|
||||
} else if (month + sign === 13) {
|
||||
return `${year + 1}-01`
|
||||
} else {
|
||||
return `${year}-${pad(month + sign)}`
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
const padStart = (string: number | string, targetLength: number, padString: string) => {
|
||||
targetLength = targetLength >> 0
|
||||
string = String(string)
|
||||
padString = String(padString)
|
||||
if (string.length > targetLength) {
|
||||
return String(string)
|
||||
}
|
||||
|
||||
targetLength = targetLength - string.length
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length)
|
||||
}
|
||||
return padString.slice(0, targetLength) + String(string)
|
||||
}
|
||||
|
||||
export default (n: string | number, length = 2) => padStart(n, length, '0')
|
||||
+335
@@ -0,0 +1,335 @@
|
||||
import '../../stylus/components/_dialogs.styl'
|
||||
|
||||
// Mixins
|
||||
import Dependent from '../../mixins/dependent'
|
||||
import Detachable from '../../mixins/detachable'
|
||||
import Overlayable from '../../mixins/overlayable'
|
||||
import Returnable from '../../mixins/returnable'
|
||||
import Stackable from '../../mixins/stackable'
|
||||
import Toggleable from '../../mixins/toggleable'
|
||||
|
||||
// Directives
|
||||
import ClickOutside from '../../directives/click-outside'
|
||||
|
||||
// Helpers
|
||||
import { convertToUnit, keyCodes, getSlotType } from '../../util/helpers'
|
||||
import ThemeProvider from '../../util/ThemeProvider'
|
||||
import { consoleError } from '../../util/console'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
name: 'v-dialog',
|
||||
|
||||
directives: {
|
||||
ClickOutside
|
||||
},
|
||||
|
||||
mixins: [
|
||||
Dependent,
|
||||
Detachable,
|
||||
Overlayable,
|
||||
Returnable,
|
||||
Stackable,
|
||||
Toggleable
|
||||
],
|
||||
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
persistent: Boolean,
|
||||
fullscreen: Boolean,
|
||||
fullWidth: Boolean,
|
||||
noClickAnimation: Boolean,
|
||||
light: Boolean,
|
||||
dark: Boolean,
|
||||
maxWidth: {
|
||||
type: [String, Number],
|
||||
default: 'none'
|
||||
},
|
||||
origin: {
|
||||
type: String,
|
||||
default: 'center center'
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: 'auto'
|
||||
},
|
||||
scrollable: Boolean,
|
||||
transition: {
|
||||
type: [String, Boolean],
|
||||
default: 'dialog-transition'
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
animate: false,
|
||||
animateTimeout: null,
|
||||
stackClass: 'v-dialog__content--active',
|
||||
stackMinZIndex: 200
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes () {
|
||||
return {
|
||||
[(`v-dialog ${this.contentClass}`).trim()]: true,
|
||||
'v-dialog--active': this.isActive,
|
||||
'v-dialog--persistent': this.persistent,
|
||||
'v-dialog--fullscreen': this.fullscreen,
|
||||
'v-dialog--scrollable': this.scrollable,
|
||||
'v-dialog--animated': this.animate
|
||||
}
|
||||
},
|
||||
contentClasses () {
|
||||
return {
|
||||
'v-dialog__content': true,
|
||||
'v-dialog__content--active': this.isActive
|
||||
}
|
||||
},
|
||||
hasActivator () {
|
||||
return Boolean(
|
||||
!!this.$slots.activator ||
|
||||
!!this.$scopedSlots.activator
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
isActive (val) {
|
||||
if (val) {
|
||||
this.show()
|
||||
this.hideScroll()
|
||||
} else {
|
||||
this.removeOverlay()
|
||||
this.unbind()
|
||||
}
|
||||
},
|
||||
fullscreen (val) {
|
||||
if (!this.isActive) return
|
||||
|
||||
if (val) {
|
||||
this.hideScroll()
|
||||
this.removeOverlay(false)
|
||||
} else {
|
||||
this.showScroll()
|
||||
this.genOverlay()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
beforeMount () {
|
||||
this.$nextTick(() => {
|
||||
this.isBooted = this.isActive
|
||||
this.isActive && this.show()
|
||||
})
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (getSlotType(this, 'activator', true) === 'v-slot') {
|
||||
consoleError(`v-dialog's activator slot must be bound, try '<template #activator="data"><v-btn v-on="data.on>'`, this)
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
if (typeof window !== 'undefined') this.unbind()
|
||||
},
|
||||
|
||||
methods: {
|
||||
animateClick () {
|
||||
this.animate = false
|
||||
// Needed for when clicking very fast
|
||||
// outside of the dialog
|
||||
this.$nextTick(() => {
|
||||
this.animate = true
|
||||
clearTimeout(this.animateTimeout)
|
||||
this.animateTimeout = setTimeout(() => (this.animate = false), 150)
|
||||
})
|
||||
},
|
||||
closeConditional (e) {
|
||||
// If the dialog content contains
|
||||
// the click event, or if the
|
||||
// dialog is not active
|
||||
if (!this.isActive || this.$refs.content.contains(e.target)) return false
|
||||
|
||||
// If we made it here, the click is outside
|
||||
// and is active. If persistent, and the
|
||||
// click is on the overlay, animate
|
||||
if (this.persistent) {
|
||||
if (!this.noClickAnimation &&
|
||||
this.overlay === e.target
|
||||
) this.animateClick()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// close dialog if !persistent, clicked outside and we're the topmost dialog.
|
||||
// Since this should only be called in a capture event (bottom up), we shouldn't need to stop propagation
|
||||
return this.activeZIndex >= this.getMaxZIndex()
|
||||
},
|
||||
hideScroll () {
|
||||
if (this.fullscreen) {
|
||||
document.documentElement.classList.add('overflow-y-hidden')
|
||||
} else {
|
||||
Overlayable.options.methods.hideScroll.call(this)
|
||||
}
|
||||
},
|
||||
show () {
|
||||
!this.fullscreen && !this.hideOverlay && this.genOverlay()
|
||||
this.$refs.content.focus()
|
||||
this.bind()
|
||||
},
|
||||
bind () {
|
||||
window.addEventListener('focusin', this.onFocusin)
|
||||
},
|
||||
unbind () {
|
||||
window.removeEventListener('focusin', this.onFocusin)
|
||||
},
|
||||
onKeydown (e) {
|
||||
if (e.keyCode === keyCodes.esc && !this.getOpenDependents().length) {
|
||||
if (!this.persistent) {
|
||||
this.isActive = false
|
||||
const activator = this.getActivator()
|
||||
this.$nextTick(() => activator && activator.focus())
|
||||
} else if (!this.noClickAnimation) {
|
||||
this.animateClick()
|
||||
}
|
||||
}
|
||||
this.$emit('keydown', e)
|
||||
},
|
||||
onFocusin (e) {
|
||||
const { target } = event
|
||||
|
||||
if (
|
||||
// It isn't the document or the dialog body
|
||||
![document, this.$refs.content].includes(target) &&
|
||||
// It isn't inside the dialog body
|
||||
!this.$refs.content.contains(target) &&
|
||||
// We're the topmost dialog
|
||||
this.activeZIndex >= this.getMaxZIndex() &&
|
||||
// It isn't inside a dependent element (like a menu)
|
||||
!this.getOpenDependentElements().some(el => el.contains(target))
|
||||
// So we must have focused something outside the dialog and its children
|
||||
) {
|
||||
// Find and focus the first available element inside the dialog
|
||||
const focusable = this.$refs.content.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
||||
focusable.length && focusable[0].focus()
|
||||
}
|
||||
},
|
||||
getActivator (e) {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
consoleError('No activator found')
|
||||
},
|
||||
genActivator () {
|
||||
if (!this.hasActivator) return null
|
||||
|
||||
const listeners = this.disabled ? {} : {
|
||||
click: e => {
|
||||
e.stopPropagation()
|
||||
this.getActivator(e)
|
||||
if (!this.disabled) this.isActive = !this.isActive
|
||||
}
|
||||
}
|
||||
|
||||
if (getSlotType(this, 'activator') === 'scoped') {
|
||||
const activator = this.$scopedSlots.activator({ on: listeners })
|
||||
this.activatorNode = activator
|
||||
return activator
|
||||
}
|
||||
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-dialog__activator',
|
||||
class: {
|
||||
'v-dialog__activator--disabled': this.disabled
|
||||
},
|
||||
ref: 'activator',
|
||||
on: listeners
|
||||
}, this.$slots.activator)
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
const children = []
|
||||
const data = {
|
||||
'class': this.classes,
|
||||
ref: 'dialog',
|
||||
directives: [
|
||||
{
|
||||
name: 'click-outside',
|
||||
value: () => { this.isActive = false },
|
||||
args: {
|
||||
closeConditional: this.closeConditional,
|
||||
include: this.getOpenDependentElements
|
||||
}
|
||||
},
|
||||
{ name: 'show', value: this.isActive }
|
||||
],
|
||||
on: {
|
||||
click: e => { e.stopPropagation() }
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.fullscreen) {
|
||||
data.style = {
|
||||
maxWidth: this.maxWidth === 'none' ? undefined : convertToUnit(this.maxWidth),
|
||||
width: this.width === 'auto' ? undefined : convertToUnit(this.width)
|
||||
}
|
||||
}
|
||||
|
||||
children.push(this.genActivator())
|
||||
|
||||
let dialog = h('div', data, this.showLazyContent(this.$slots.default))
|
||||
if (this.transition) {
|
||||
dialog = h('transition', {
|
||||
props: {
|
||||
name: this.transition,
|
||||
origin: this.origin
|
||||
}
|
||||
}, [dialog])
|
||||
}
|
||||
|
||||
children.push(h('div', {
|
||||
'class': this.contentClasses,
|
||||
attrs: {
|
||||
tabIndex: '-1',
|
||||
...this.getScopeIdAttrs()
|
||||
},
|
||||
on: {
|
||||
keydown: this.onKeydown
|
||||
},
|
||||
style: { zIndex: this.activeZIndex },
|
||||
ref: 'content'
|
||||
}, [
|
||||
this.$createElement(ThemeProvider, {
|
||||
props: {
|
||||
root: true,
|
||||
light: this.light,
|
||||
dark: this.dark
|
||||
}
|
||||
}, [dialog])
|
||||
]))
|
||||
|
||||
return h('div', {
|
||||
staticClass: 'v-dialog__container',
|
||||
style: {
|
||||
display: (!this.hasActivator || this.fullWidth) ? 'block' : 'inline-block'
|
||||
}
|
||||
}, children)
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VDialog from './VDialog'
|
||||
|
||||
export { VDialog }
|
||||
export default VDialog
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_dividers.styl'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
|
||||
// Mixins
|
||||
import Themeable from '../../mixins/themeable'
|
||||
|
||||
export default Themeable.extend({
|
||||
name: 'v-divider',
|
||||
|
||||
props: {
|
||||
inset: Boolean,
|
||||
vertical: Boolean
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
return h('hr', {
|
||||
class: {
|
||||
'v-divider': true,
|
||||
'v-divider--inset': this.inset,
|
||||
'v-divider--vertical': this.vertical,
|
||||
...this.themeClasses
|
||||
},
|
||||
attrs: this.$attrs,
|
||||
on: this.$listeners
|
||||
})
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VDivider from './VDivider'
|
||||
|
||||
export { VDivider }
|
||||
export default VDivider
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import '../../stylus/components/_expansion-panel.styl'
|
||||
|
||||
import { VExpansionPanelContent } from '.'
|
||||
|
||||
import Themeable from '../../mixins/themeable'
|
||||
import { provide as RegistrableProvide } from '../../mixins/registrable'
|
||||
|
||||
import mixins from '../../util/mixins'
|
||||
import { VNode } from 'vue'
|
||||
import { PropValidator } from 'vue/types/options'
|
||||
|
||||
type VExpansionPanelContentInstance = InstanceType<typeof VExpansionPanelContent>
|
||||
|
||||
/* @vue/component */
|
||||
export default mixins(Themeable, RegistrableProvide('expansionPanel')).extend({
|
||||
name: 'v-expansion-panel',
|
||||
|
||||
provide (): object {
|
||||
return {
|
||||
expansionPanel: this
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
readonly: Boolean,
|
||||
expand: Boolean,
|
||||
focusable: Boolean,
|
||||
inset: Boolean,
|
||||
popout: Boolean,
|
||||
value: {
|
||||
type: [Number, Array],
|
||||
default: () => null
|
||||
} as any as PropValidator<number | number[]>
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
items: [] as VExpansionPanelContentInstance[],
|
||||
open: [] as boolean[]
|
||||
}),
|
||||
|
||||
computed: {
|
||||
classes (): object {
|
||||
return {
|
||||
'v-expansion-panel--focusable': this.focusable,
|
||||
'v-expansion-panel--popout': this.popout,
|
||||
'v-expansion-panel--inset': this.inset,
|
||||
...this.themeClasses
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
expand (v: boolean) {
|
||||
let openIndex = -1
|
||||
if (!v) {
|
||||
// Close all panels unless only one is open
|
||||
const openCount = this.open.reduce((acc, val) => val ? acc + 1 : acc, 0)
|
||||
const open = Array(this.items.length).fill(false)
|
||||
|
||||
if (openCount === 1) {
|
||||
openIndex = this.open.indexOf(true)
|
||||
}
|
||||
|
||||
if (openIndex > -1) {
|
||||
open[openIndex] = true
|
||||
}
|
||||
|
||||
this.open = open
|
||||
}
|
||||
|
||||
this.$emit('input', v ? this.open : (openIndex > -1 ? openIndex : null))
|
||||
},
|
||||
value (v: number | number[]) {
|
||||
this.updateFromValue(v)
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.value !== null && this.updateFromValue(this.value)
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateFromValue (v: number | number[]) {
|
||||
if (Array.isArray(v) && !this.expand) return
|
||||
|
||||
let open = Array(this.items.length).fill(false)
|
||||
if (typeof v === 'number') {
|
||||
open[v] = true
|
||||
} else if (v !== null) {
|
||||
open = v
|
||||
}
|
||||
|
||||
this.updatePanels(open)
|
||||
},
|
||||
updatePanels (open: boolean[]) {
|
||||
this.open = open
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
this.items[i].toggle(open && open[i])
|
||||
}
|
||||
},
|
||||
panelClick (uid: number) {
|
||||
const open = this.expand ? this.open.slice() : Array(this.items.length).fill(false)
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
if (this.items[i]._uid === uid) {
|
||||
open[i] = !this.open[i]
|
||||
!this.expand && this.$emit('input', open[i] ? i : null)
|
||||
}
|
||||
}
|
||||
|
||||
this.updatePanels(open)
|
||||
if (this.expand) this.$emit('input', open)
|
||||
},
|
||||
register (content: VExpansionPanelContentInstance) {
|
||||
const i = this.items.push(content) - 1
|
||||
this.value !== null && this.updateFromValue(this.value)
|
||||
content.toggle(!!this.open[i])
|
||||
},
|
||||
unregister (content: VExpansionPanelContentInstance) {
|
||||
const index = this.items.findIndex(i => i._uid === content._uid)
|
||||
this.items.splice(index, 1)
|
||||
this.open.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
return h('ul', {
|
||||
staticClass: 'v-expansion-panel',
|
||||
class: this.classes
|
||||
}, this.$slots.default)
|
||||
}
|
||||
})
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { VExpandTransition } from '../transitions'
|
||||
|
||||
import Bootable from '../../mixins/bootable'
|
||||
import Toggleable from '../../mixins/toggleable'
|
||||
import Rippleable from '../../mixins/rippleable'
|
||||
import { Registrable, inject as RegistrableInject } from '../../mixins/registrable'
|
||||
|
||||
import VIcon from '../VIcon'
|
||||
import VExpansionPanel from './VExpansionPanel'
|
||||
|
||||
import mixins, { ExtractVue } from '../../util/mixins'
|
||||
import Vue, { VNode } from 'vue'
|
||||
|
||||
import { consoleWarn } from '../../util/console'
|
||||
|
||||
type VExpansionPanelInstance = InstanceType<typeof VExpansionPanel>
|
||||
|
||||
interface options extends Vue {
|
||||
expansionPanel: VExpansionPanelInstance
|
||||
}
|
||||
|
||||
export default mixins<options &
|
||||
/* eslint-disable indent */
|
||||
ExtractVue<[
|
||||
typeof Bootable,
|
||||
typeof Toggleable,
|
||||
typeof Rippleable,
|
||||
Registrable<'expansionPanel'>
|
||||
]>
|
||||
/* eslint-enable indent */
|
||||
>(
|
||||
Bootable,
|
||||
Toggleable,
|
||||
Rippleable,
|
||||
RegistrableInject('expansionPanel', 'v-expansion-panel-content', 'v-expansion-panel')
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-expansion-panel-content',
|
||||
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
readonly: Boolean,
|
||||
expandIcon: {
|
||||
type: String,
|
||||
default: '$vuetify.icons.expand'
|
||||
},
|
||||
hideActions: Boolean,
|
||||
ripple: {
|
||||
type: [Boolean, Object],
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
height: 'auto'
|
||||
}),
|
||||
|
||||
computed: {
|
||||
containerClasses (): object {
|
||||
return {
|
||||
'v-expansion-panel__container--active': this.isActive,
|
||||
'v-expansion-panel__container--disabled': this.isDisabled
|
||||
}
|
||||
},
|
||||
isDisabled (): boolean {
|
||||
return this.expansionPanel.disabled || this.disabled
|
||||
},
|
||||
isReadonly (): boolean {
|
||||
return this.expansionPanel.readonly || this.readonly
|
||||
}
|
||||
},
|
||||
|
||||
beforeMount () {
|
||||
this.expansionPanel.register(this)
|
||||
|
||||
// Can be removed once fully deprecated
|
||||
if (typeof this.value !== 'undefined') consoleWarn('v-model has been deprecated', this)
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
this.expansionPanel.unregister(this)
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeydown (e: KeyboardEvent) {
|
||||
// Ensure element is the activeElement
|
||||
if (
|
||||
e.keyCode === 13 &&
|
||||
this.$el === document.activeElement
|
||||
) this.expansionPanel.panelClick(this._uid)
|
||||
},
|
||||
onHeaderClick () {
|
||||
this.isReadonly || this.expansionPanel.panelClick(this._uid)
|
||||
},
|
||||
genBody () {
|
||||
return this.$createElement('div', {
|
||||
ref: 'body',
|
||||
class: 'v-expansion-panel__body',
|
||||
directives: [{
|
||||
name: 'show',
|
||||
value: this.isActive
|
||||
}]
|
||||
}, this.showLazyContent(this.$slots.default))
|
||||
},
|
||||
genHeader () {
|
||||
const children = [...(this.$slots.header || [])]
|
||||
|
||||
if (!this.hideActions) children.push(this.genIcon())
|
||||
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-expansion-panel__header',
|
||||
directives: [{
|
||||
name: 'ripple',
|
||||
value: this.ripple
|
||||
}],
|
||||
on: {
|
||||
click: this.onHeaderClick
|
||||
}
|
||||
}, children)
|
||||
},
|
||||
genIcon () {
|
||||
const icon = this.$slots.actions ||
|
||||
[this.$createElement(VIcon, this.expandIcon)]
|
||||
|
||||
return this.$createElement('transition', {
|
||||
attrs: { name: 'fade-transition' }
|
||||
}, [
|
||||
this.$createElement('div', {
|
||||
staticClass: 'v-expansion-panel__header__icon',
|
||||
directives: [{
|
||||
name: 'show',
|
||||
value: !this.isDisabled
|
||||
}]
|
||||
}, icon)
|
||||
])
|
||||
},
|
||||
toggle (active: boolean) {
|
||||
if (active) this.isBooted = true
|
||||
|
||||
this.$nextTick(() => (this.isActive = active))
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
return h('li', {
|
||||
staticClass: 'v-expansion-panel__container',
|
||||
class: this.containerClasses,
|
||||
attrs: {
|
||||
tabindex: this.isReadonly || this.isDisabled ? null : 0,
|
||||
'aria-expanded': Boolean(this.isActive)
|
||||
},
|
||||
on: {
|
||||
keydown: this.onKeydown
|
||||
}
|
||||
}, [
|
||||
this.$slots.header && this.genHeader(),
|
||||
h(VExpandTransition, [this.genBody()])
|
||||
])
|
||||
}
|
||||
})
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import VExpansionPanel from './VExpansionPanel'
|
||||
import VExpansionPanelContent from './VExpansionPanelContent'
|
||||
|
||||
export { VExpansionPanel, VExpansionPanelContent }
|
||||
|
||||
export default {
|
||||
$_vuetify_subcomponents: {
|
||||
VExpansionPanel,
|
||||
VExpansionPanelContent
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_footer.styl'
|
||||
|
||||
// Mixins
|
||||
import Applicationable from '../../mixins/applicationable'
|
||||
import Colorable from '../../mixins/colorable'
|
||||
import Themeable from '../../mixins/themeable'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
name: 'v-footer',
|
||||
|
||||
mixins: [
|
||||
Applicationable(null, [
|
||||
'height',
|
||||
'inset'
|
||||
]),
|
||||
Colorable,
|
||||
Themeable
|
||||
],
|
||||
|
||||
props: {
|
||||
height: {
|
||||
default: 32,
|
||||
type: [Number, String]
|
||||
},
|
||||
inset: Boolean
|
||||
},
|
||||
|
||||
computed: {
|
||||
applicationProperty () {
|
||||
return this.inset ? 'insetFooter' : 'footer'
|
||||
},
|
||||
computedMarginBottom () {
|
||||
if (!this.app) return
|
||||
|
||||
return this.$vuetify.application.bottom
|
||||
},
|
||||
computedPaddingLeft () {
|
||||
return !this.app || !this.inset
|
||||
? 0
|
||||
: this.$vuetify.application.left
|
||||
},
|
||||
computedPaddingRight () {
|
||||
return !this.app || !this.inset
|
||||
? 0
|
||||
: this.$vuetify.application.right
|
||||
},
|
||||
styles () {
|
||||
const styles = {
|
||||
height: isNaN(this.height) ? this.height : `${this.height}px`
|
||||
}
|
||||
|
||||
if (this.computedPaddingLeft) {
|
||||
styles.paddingLeft = `${this.computedPaddingLeft}px`
|
||||
}
|
||||
|
||||
if (this.computedPaddingRight) {
|
||||
styles.paddingRight = `${this.computedPaddingRight}px`
|
||||
}
|
||||
|
||||
if (this.computedMarginBottom) {
|
||||
styles.marginBottom = `${this.computedMarginBottom}px`
|
||||
}
|
||||
|
||||
return styles
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the application layout
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
updateApplication () {
|
||||
const height = parseInt(this.height)
|
||||
|
||||
return isNaN(height)
|
||||
? this.$el ? this.$el.clientHeight : 0
|
||||
: height
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
const data = this.setBackgroundColor(this.color, {
|
||||
staticClass: 'v-footer',
|
||||
'class': {
|
||||
'v-footer--absolute': this.absolute,
|
||||
'v-footer--fixed': !this.absolute && (this.app || this.fixed),
|
||||
'v-footer--inset': this.inset,
|
||||
...this.themeClasses
|
||||
},
|
||||
style: this.styles,
|
||||
ref: 'content'
|
||||
})
|
||||
|
||||
return h('footer', data, this.$slots.default)
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VFooter from './VFooter'
|
||||
|
||||
export { VFooter }
|
||||
export default VFooter
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_forms.styl'
|
||||
|
||||
import { provide as RegistrableProvide } from '../../mixins/registrable'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
name: 'v-form',
|
||||
|
||||
mixins: [RegistrableProvide('form')],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
value: Boolean,
|
||||
lazyValidation: Boolean
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
inputs: [],
|
||||
watchers: [],
|
||||
errorBag: {}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
errorBag: {
|
||||
handler () {
|
||||
const errors = Object.values(this.errorBag).includes(true)
|
||||
this.$emit('input', !errors)
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
watchInput (input) {
|
||||
const watcher = input => {
|
||||
return input.$watch('hasError', val => {
|
||||
this.$set(this.errorBag, input._uid, val)
|
||||
}, { immediate: true })
|
||||
}
|
||||
|
||||
const watchers = {
|
||||
_uid: input._uid,
|
||||
valid: undefined,
|
||||
shouldValidate: undefined
|
||||
}
|
||||
|
||||
if (this.lazyValidation) {
|
||||
// Only start watching inputs if we need to
|
||||
watchers.shouldValidate = input.$watch('shouldValidate', val => {
|
||||
if (!val) return
|
||||
|
||||
// Only watch if we're not already doing it
|
||||
if (this.errorBag.hasOwnProperty(input._uid)) return
|
||||
|
||||
watchers.valid = watcher(input)
|
||||
})
|
||||
} else {
|
||||
watchers.valid = watcher(input)
|
||||
}
|
||||
|
||||
return watchers
|
||||
},
|
||||
/** @public */
|
||||
validate () {
|
||||
const errors = this.inputs.filter(input => !input.validate(true)).length
|
||||
return !errors
|
||||
},
|
||||
/** @public */
|
||||
reset () {
|
||||
for (let i = this.inputs.length; i--;) {
|
||||
this.inputs[i].reset()
|
||||
}
|
||||
if (this.lazyValidation) {
|
||||
// Account for timeout in validatable
|
||||
setTimeout(() => {
|
||||
this.errorBag = {}
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
/** @public */
|
||||
resetValidation () {
|
||||
for (let i = this.inputs.length; i--;) {
|
||||
this.inputs[i].resetValidation()
|
||||
}
|
||||
if (this.lazyValidation) {
|
||||
// Account for timeout in validatable
|
||||
setTimeout(() => {
|
||||
this.errorBag = {}
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
register (input) {
|
||||
const unwatch = this.watchInput(input)
|
||||
this.inputs.push(input)
|
||||
this.watchers.push(unwatch)
|
||||
},
|
||||
unregister (input) {
|
||||
const found = this.inputs.find(i => i._uid === input._uid)
|
||||
|
||||
if (!found) return
|
||||
|
||||
const unwatch = this.watchers.find(i => i._uid === found._uid)
|
||||
unwatch.valid && unwatch.valid()
|
||||
unwatch.shouldValidate && unwatch.shouldValidate()
|
||||
|
||||
this.watchers = this.watchers.filter(i => i._uid !== found._uid)
|
||||
this.inputs = this.inputs.filter(i => i._uid !== found._uid)
|
||||
this.$delete(this.errorBag, found._uid)
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
return h('form', {
|
||||
staticClass: 'v-form',
|
||||
attrs: Object.assign({
|
||||
novalidate: true
|
||||
}, this.$attrs),
|
||||
on: {
|
||||
submit: e => this.$emit('submit', e)
|
||||
}
|
||||
}, this.$slots.default)
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VForm from './VForm'
|
||||
|
||||
export { VForm }
|
||||
export default VForm
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import '../../stylus/components/_grid.styl'
|
||||
|
||||
import Grid from './grid'
|
||||
|
||||
export default Grid('container')
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_content.styl'
|
||||
|
||||
// Mixins
|
||||
import SSRBootable from '../../mixins/ssr-bootable'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
name: 'v-content',
|
||||
|
||||
mixins: [SSRBootable],
|
||||
|
||||
props: {
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'main'
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
styles () {
|
||||
const {
|
||||
bar, top, right, footer, insetFooter, bottom, left
|
||||
} = this.$vuetify.application
|
||||
|
||||
return {
|
||||
paddingTop: `${top + bar}px`,
|
||||
paddingRight: `${right}px`,
|
||||
paddingBottom: `${footer + insetFooter + bottom}px`,
|
||||
paddingLeft: `${left}px`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
const data = {
|
||||
staticClass: 'v-content',
|
||||
style: this.styles,
|
||||
ref: 'content'
|
||||
}
|
||||
|
||||
return h(this.tag, data, [
|
||||
h(
|
||||
'div',
|
||||
{ staticClass: 'v-content__wrap' },
|
||||
this.$slots.default
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import '../../stylus/components/_grid.styl'
|
||||
|
||||
import Grid from './grid'
|
||||
|
||||
export default Grid('flex')
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import '../../stylus/components/_grid.styl'
|
||||
|
||||
import Grid from './grid'
|
||||
|
||||
export default Grid('layout')
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
export default function Grid (name) {
|
||||
/* @vue/component */
|
||||
return {
|
||||
name: `v-${name}`,
|
||||
|
||||
functional: true,
|
||||
|
||||
props: {
|
||||
id: String,
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'div'
|
||||
}
|
||||
},
|
||||
|
||||
render: (h, { props, data, children }) => {
|
||||
data.staticClass = (`${name} ${data.staticClass || ''}`).trim()
|
||||
|
||||
const { attrs } = data
|
||||
if (attrs) {
|
||||
// reset attrs to extract utility clases like pa-3
|
||||
data.attrs = {}
|
||||
const classes = Object.keys(attrs).filter(key => {
|
||||
// TODO: Remove once resolved
|
||||
// https://github.com/vuejs/vue/issues/7841
|
||||
if (key === 'slot') return false
|
||||
|
||||
const value = attrs[key]
|
||||
|
||||
// add back data attributes like data-test="foo" but do not
|
||||
// add them as classes
|
||||
if (key.startsWith('data-')) {
|
||||
data.attrs[key] = value
|
||||
return false
|
||||
}
|
||||
|
||||
return value || typeof value === 'string'
|
||||
})
|
||||
|
||||
if (classes.length) data.staticClass += ` ${classes.join(' ')}`
|
||||
}
|
||||
|
||||
if (props.id) {
|
||||
data.domProps = data.domProps || {}
|
||||
data.domProps.id = props.id
|
||||
}
|
||||
|
||||
return h(props.tag, data, children)
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
createSimpleFunctional
|
||||
} from '../../util/helpers'
|
||||
import VContainer from './VContainer'
|
||||
import VContent from './VContent'
|
||||
import VFlex from './VFlex'
|
||||
import VLayout from './VLayout'
|
||||
|
||||
const VSpacer = createSimpleFunctional('spacer', 'div', 'v-spacer')
|
||||
|
||||
export {
|
||||
VContainer,
|
||||
VContent,
|
||||
VFlex,
|
||||
VLayout,
|
||||
VSpacer
|
||||
}
|
||||
|
||||
export default {
|
||||
$_vuetify_subcomponents: {
|
||||
VContainer,
|
||||
VContent,
|
||||
VFlex,
|
||||
VLayout,
|
||||
VSpacer
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
// Mixins
|
||||
import Delayable from '../../mixins/delayable'
|
||||
import Toggleable from '../../mixins/toggleable'
|
||||
|
||||
// Utilities
|
||||
import mixins from '../../util/mixins'
|
||||
import { consoleWarn } from '../../util/console'
|
||||
|
||||
// Types
|
||||
import { VNode, ScopedSlotChildren } from 'vue/types/vnode'
|
||||
|
||||
export default mixins(
|
||||
Delayable,
|
||||
Toggleable
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-hover',
|
||||
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onMouseEnter () {
|
||||
this.runDelay('open')
|
||||
},
|
||||
onMouseLeave () {
|
||||
this.runDelay('close')
|
||||
}
|
||||
},
|
||||
|
||||
render (): VNode {
|
||||
if (!this.$scopedSlots.default && this.value === undefined) {
|
||||
consoleWarn('v-hover is missing a default scopedSlot or bound value', this)
|
||||
|
||||
return null as any
|
||||
}
|
||||
|
||||
let element: VNode | ScopedSlotChildren
|
||||
|
||||
if (this.$scopedSlots.default) {
|
||||
element = this.$scopedSlots.default({ hover: this.isActive })
|
||||
} else if (this.$slots.default && this.$slots.default.length === 1) {
|
||||
element = this.$slots.default[0]
|
||||
}
|
||||
|
||||
if (Array.isArray(element) && element.length === 1) {
|
||||
element = element[0]
|
||||
}
|
||||
|
||||
if (!element || Array.isArray(element) || !element.tag) {
|
||||
consoleWarn('v-hover should only contain a single element', this)
|
||||
|
||||
return element as any
|
||||
}
|
||||
|
||||
if (!this.disabled) {
|
||||
element.data = element.data || {}
|
||||
this._g(element.data, {
|
||||
mouseenter: this.onMouseEnter,
|
||||
mouseleave: this.onMouseLeave
|
||||
})
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VHover from './VHover'
|
||||
|
||||
export { VHover }
|
||||
export default VHover
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
import '../../stylus/components/_icons.styl'
|
||||
// Mixins
|
||||
import Colorable from '../../mixins/colorable'
|
||||
import Sizeable from '../../mixins/sizeable'
|
||||
import Themeable from '../../mixins/themeable'
|
||||
// Util
|
||||
import { convertToUnit, keys, remapInternalIcon } from '../../util/helpers'
|
||||
// Types
|
||||
import Vue, { CreateElement, VNode, VNodeChildren, VNodeData } from 'vue'
|
||||
import mixins from '../../util/mixins'
|
||||
import { VuetifyIcon, VuetifyIconComponent } from 'vuetify'
|
||||
|
||||
enum SIZE_MAP {
|
||||
small = '16px',
|
||||
default = '24px',
|
||||
medium = '28px',
|
||||
large = '36px',
|
||||
xLarge = '40px'
|
||||
}
|
||||
|
||||
function isFontAwesome5 (iconType: string): boolean {
|
||||
return ['fas', 'far', 'fal', 'fab'].some(val => iconType.includes(val))
|
||||
}
|
||||
|
||||
const VIcon = mixins(
|
||||
Colorable,
|
||||
Sizeable,
|
||||
Themeable
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-icon',
|
||||
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
left: Boolean,
|
||||
right: Boolean
|
||||
},
|
||||
|
||||
methods: {
|
||||
getIcon (): VuetifyIcon {
|
||||
let iconName = ''
|
||||
if (this.$slots.default) iconName = this.$slots.default[0].text!.trim()
|
||||
|
||||
return remapInternalIcon(this, iconName)
|
||||
},
|
||||
getSize (): string | undefined {
|
||||
const sizes = {
|
||||
small: this.small,
|
||||
medium: this.medium,
|
||||
large: this.large,
|
||||
xLarge: this.xLarge
|
||||
}
|
||||
|
||||
const explicitSize = keys(sizes).find(key => sizes[key])
|
||||
|
||||
return (explicitSize && SIZE_MAP[explicitSize]) || convertToUnit(this.size)
|
||||
},
|
||||
// Component data for both font and svg icon.
|
||||
getDefaultData (): VNodeData {
|
||||
const data: VNodeData = {
|
||||
staticClass: 'v-icon',
|
||||
class: {
|
||||
'v-icon--disabled': this.disabled,
|
||||
'v-icon--left': this.left,
|
||||
'v-icon--link': this.$listeners.click || this.$listeners['!click'],
|
||||
'v-icon--right': this.right
|
||||
},
|
||||
attrs: {
|
||||
'aria-hidden': true,
|
||||
...this.$attrs
|
||||
},
|
||||
on: this.$listeners
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
applyColors (data: VNodeData): void {
|
||||
data.class = { ...data.class, ...this.themeClasses }
|
||||
this.setTextColor(this.color, data)
|
||||
},
|
||||
renderFontIcon (icon: string, h: CreateElement): VNode {
|
||||
const newChildren: VNodeChildren = []
|
||||
const data = this.getDefaultData()
|
||||
|
||||
let iconType = 'material-icons'
|
||||
// Material Icon delimiter is _
|
||||
// https://material.io/icons/
|
||||
const delimiterIndex = icon.indexOf('-')
|
||||
const isMaterialIcon = delimiterIndex <= -1
|
||||
|
||||
if (isMaterialIcon) {
|
||||
// Material icon uses ligatures.
|
||||
newChildren.push(icon)
|
||||
} else {
|
||||
iconType = icon.slice(0, delimiterIndex)
|
||||
if (isFontAwesome5(iconType)) iconType = ''
|
||||
}
|
||||
|
||||
data.class[iconType] = true
|
||||
data.class[icon] = !isMaterialIcon
|
||||
|
||||
const fontSize = this.getSize()
|
||||
if (fontSize) data.style = { fontSize }
|
||||
|
||||
this.applyColors(data)
|
||||
|
||||
return h('i', data, newChildren)
|
||||
},
|
||||
renderSvgIcon (icon: VuetifyIconComponent, h: CreateElement): VNode {
|
||||
const data = this.getDefaultData()
|
||||
data.class['v-icon--is-component'] = true
|
||||
|
||||
const size = this.getSize()
|
||||
if (size) {
|
||||
data.style = {
|
||||
fontSize: size,
|
||||
height: size
|
||||
}
|
||||
}
|
||||
|
||||
this.applyColors(data)
|
||||
|
||||
const component = icon.component
|
||||
data.props = icon.props
|
||||
data.nativeOn = data.on
|
||||
|
||||
return h(component, data)
|
||||
}
|
||||
},
|
||||
|
||||
render (h: CreateElement): VNode {
|
||||
const icon = this.getIcon()
|
||||
|
||||
if (typeof icon === 'string') {
|
||||
return this.renderFontIcon(icon, h)
|
||||
}
|
||||
|
||||
return this.renderSvgIcon(icon, h)
|
||||
}
|
||||
})
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'v-icon',
|
||||
|
||||
$_wrapperFor: VIcon,
|
||||
|
||||
functional: true,
|
||||
|
||||
render (h, { data, children }): VNode {
|
||||
let iconName = ''
|
||||
|
||||
// Support usage of v-text and v-html
|
||||
if (data.domProps) {
|
||||
iconName = data.domProps.textContent ||
|
||||
data.domProps.innerHTML ||
|
||||
iconName
|
||||
|
||||
// Remove nodes so it doesn't
|
||||
// overwrite our changes
|
||||
delete data.domProps.textContent
|
||||
delete data.domProps.innerHTML
|
||||
}
|
||||
|
||||
return h(VIcon, data, iconName ? [iconName] : children)
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VIcon from './VIcon'
|
||||
|
||||
export { VIcon }
|
||||
export default VIcon
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
import '../../stylus/components/_images.styl'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue'
|
||||
import { PropValidator } from 'vue/types/options'
|
||||
|
||||
// Components
|
||||
import VResponsive from '../VResponsive'
|
||||
|
||||
// Utils
|
||||
import { consoleError, consoleWarn } from '../../util/console'
|
||||
|
||||
// not intended for public use, this is passed in by vuetify-loader
|
||||
export interface srcObject {
|
||||
src: string
|
||||
srcset?: string
|
||||
lazySrc: string
|
||||
aspect: number
|
||||
}
|
||||
|
||||
/* @vue/component */
|
||||
export default VResponsive.extend({
|
||||
name: 'v-img',
|
||||
|
||||
props: {
|
||||
alt: String,
|
||||
contain: Boolean,
|
||||
src: {
|
||||
type: [String, Object],
|
||||
default: ''
|
||||
} as PropValidator<string | srcObject>,
|
||||
gradient: String,
|
||||
lazySrc: String,
|
||||
srcset: String,
|
||||
sizes: String,
|
||||
position: {
|
||||
type: String,
|
||||
default: 'center center'
|
||||
},
|
||||
transition: {
|
||||
type: [Boolean, String],
|
||||
default: 'fade-transition'
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
currentSrc: '', // Set from srcset
|
||||
image: null as HTMLImageElement | null,
|
||||
isLoading: true,
|
||||
calculatedAspectRatio: undefined as number | undefined
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
computedAspectRatio (): number {
|
||||
return this.normalisedSrc.aspect
|
||||
},
|
||||
normalisedSrc (): srcObject {
|
||||
return typeof this.src === 'string'
|
||||
? {
|
||||
src: this.src,
|
||||
srcset: this.srcset,
|
||||
lazySrc: this.lazySrc,
|
||||
aspect: Number(this.aspectRatio || this.calculatedAspectRatio)
|
||||
}
|
||||
: {
|
||||
src: this.src.src,
|
||||
srcset: this.srcset || this.src.srcset,
|
||||
lazySrc: this.lazySrc || this.src.lazySrc,
|
||||
aspect: Number(this.aspectRatio || this.src.aspect || this.calculatedAspectRatio)
|
||||
}
|
||||
},
|
||||
__cachedImage (): VNode | [] {
|
||||
if (!(this.normalisedSrc.src || this.normalisedSrc.lazySrc)) return []
|
||||
|
||||
const backgroundImage: string[] = []
|
||||
const src = this.isLoading ? this.normalisedSrc.lazySrc : this.currentSrc
|
||||
|
||||
if (this.gradient) backgroundImage.push(`linear-gradient(${this.gradient})`)
|
||||
if (src) backgroundImage.push(`url("${src}")`)
|
||||
|
||||
const image = this.$createElement('div', {
|
||||
staticClass: 'v-image__image',
|
||||
class: {
|
||||
'v-image__image--preload': this.isLoading,
|
||||
'v-image__image--contain': this.contain,
|
||||
'v-image__image--cover': !this.contain
|
||||
},
|
||||
style: {
|
||||
backgroundImage: backgroundImage.join(', '),
|
||||
backgroundPosition: this.position
|
||||
},
|
||||
key: +this.isLoading
|
||||
})
|
||||
|
||||
if (!this.transition) return image
|
||||
|
||||
return this.$createElement('transition', {
|
||||
attrs: {
|
||||
name: this.transition,
|
||||
mode: 'in-out'
|
||||
}
|
||||
}, [image])
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
src () {
|
||||
if (!this.isLoading) this.init()
|
||||
else this.loadImage()
|
||||
},
|
||||
'$vuetify.breakpoint.width': 'getSrc'
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.init()
|
||||
},
|
||||
|
||||
methods: {
|
||||
init () {
|
||||
if (this.normalisedSrc.lazySrc) {
|
||||
const lazyImg = new Image()
|
||||
lazyImg.src = this.normalisedSrc.lazySrc
|
||||
this.pollForSize(lazyImg, null)
|
||||
}
|
||||
/* istanbul ignore else */
|
||||
if (this.normalisedSrc.src) this.loadImage()
|
||||
},
|
||||
onLoad () {
|
||||
this.getSrc()
|
||||
this.isLoading = false
|
||||
this.$emit('load', this.src)
|
||||
},
|
||||
onError () {
|
||||
consoleError(
|
||||
`Image load failed\n\n` +
|
||||
`src: ${this.normalisedSrc.src}`,
|
||||
this
|
||||
)
|
||||
this.$emit('error', this.src)
|
||||
},
|
||||
getSrc () {
|
||||
/* istanbul ignore else */
|
||||
if (this.image) this.currentSrc = this.image.currentSrc || this.image.src
|
||||
},
|
||||
loadImage () {
|
||||
const image = new Image()
|
||||
this.image = image
|
||||
|
||||
image.onload = () => {
|
||||
/* istanbul ignore if */
|
||||
if (image.decode) {
|
||||
image.decode().catch((err: DOMException) => {
|
||||
consoleWarn(
|
||||
`Failed to decode image, trying to render anyway\n\n` +
|
||||
`src: ${this.normalisedSrc.src}` +
|
||||
(err.message ? `\nOriginal error: ${err.message}` : ''),
|
||||
this
|
||||
)
|
||||
}).then(this.onLoad)
|
||||
} else {
|
||||
this.onLoad()
|
||||
}
|
||||
}
|
||||
image.onerror = this.onError
|
||||
|
||||
image.src = this.normalisedSrc.src
|
||||
this.sizes && (image.sizes = this.sizes)
|
||||
this.normalisedSrc.srcset && (image.srcset = this.normalisedSrc.srcset)
|
||||
|
||||
this.aspectRatio || this.pollForSize(image)
|
||||
this.getSrc()
|
||||
},
|
||||
pollForSize (img: HTMLImageElement, timeout: number | null = 100) {
|
||||
const poll = () => {
|
||||
const { naturalHeight, naturalWidth } = img
|
||||
|
||||
if (naturalHeight || naturalWidth) {
|
||||
this.calculatedAspectRatio = naturalWidth / naturalHeight
|
||||
} else {
|
||||
timeout != null && setTimeout(poll, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
poll()
|
||||
},
|
||||
__genPlaceholder (): VNode | void {
|
||||
if (this.$slots.placeholder) {
|
||||
const placeholder = this.isLoading
|
||||
? [this.$createElement('div', {
|
||||
staticClass: 'v-image__placeholder'
|
||||
}, this.$slots.placeholder)]
|
||||
: []
|
||||
|
||||
if (!this.transition) return placeholder[0]
|
||||
|
||||
return this.$createElement('transition', {
|
||||
attrs: { name: this.transition }
|
||||
}, placeholder)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
const node = VResponsive.options.render.call(this, h)
|
||||
|
||||
node.data!.staticClass += ' v-image'
|
||||
|
||||
node.data!.attrs = {
|
||||
role: this.alt ? 'img' : undefined,
|
||||
'aria-label': this.alt
|
||||
}
|
||||
|
||||
node.children = [
|
||||
this.__cachedSizer,
|
||||
this.__cachedImage,
|
||||
this.__genPlaceholder(),
|
||||
this.genContent()
|
||||
] as VNode[]
|
||||
|
||||
return h(node.tag, node.data, node.children)
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VImg from './VImg'
|
||||
|
||||
export { VImg }
|
||||
export default VImg
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_inputs.styl'
|
||||
|
||||
// Components
|
||||
import VIcon from '../VIcon'
|
||||
import VLabel from '../VLabel'
|
||||
import VMessages from '../VMessages'
|
||||
|
||||
// Mixins
|
||||
import Colorable from '../../mixins/colorable'
|
||||
import Themeable from '../../mixins/themeable'
|
||||
import Validatable from '../../mixins/validatable'
|
||||
|
||||
// Utilities
|
||||
import {
|
||||
convertToUnit,
|
||||
kebabCase
|
||||
} from '../../util/helpers'
|
||||
import { deprecate } from '../../util/console'
|
||||
import mixins, { ExtractVue } from '../../util/mixins'
|
||||
|
||||
// Types
|
||||
import Vue, { VNode, VNodeData } from 'vue'
|
||||
interface options extends Vue {
|
||||
/* eslint-disable-next-line camelcase */
|
||||
$_modelEvent: string
|
||||
}
|
||||
|
||||
export default mixins<options &
|
||||
/* eslint-disable indent */
|
||||
ExtractVue<[
|
||||
typeof Colorable,
|
||||
typeof Themeable,
|
||||
typeof Validatable
|
||||
]>
|
||||
/* eslint-enable indent */
|
||||
>(
|
||||
Colorable,
|
||||
Themeable,
|
||||
Validatable
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-input',
|
||||
|
||||
props: {
|
||||
appendIcon: String,
|
||||
/** @deprecated */
|
||||
appendIconCb: Function,
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
height: [Number, String],
|
||||
hideDetails: Boolean,
|
||||
hint: String,
|
||||
label: String,
|
||||
loading: Boolean,
|
||||
persistentHint: Boolean,
|
||||
prependIcon: String,
|
||||
/** @deprecated */
|
||||
prependIconCb: Function,
|
||||
value: { required: false }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
attrsInput: {},
|
||||
lazyValue: this.value as any,
|
||||
hasMouseDown: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes: () => ({}),
|
||||
classesInput (): object {
|
||||
return {
|
||||
...this.classes,
|
||||
'v-input--has-state': this.hasState,
|
||||
'v-input--hide-details': this.hideDetails,
|
||||
'v-input--is-label-active': this.isLabelActive,
|
||||
'v-input--is-dirty': this.isDirty,
|
||||
'v-input--is-disabled': this.disabled,
|
||||
'v-input--is-focused': this.isFocused,
|
||||
'v-input--is-loading': this.loading !== false && this.loading !== undefined,
|
||||
'v-input--is-readonly': this.readonly,
|
||||
...this.themeClasses
|
||||
}
|
||||
},
|
||||
directivesInput () {
|
||||
return []
|
||||
},
|
||||
hasHint () {
|
||||
return !this.hasMessages &&
|
||||
this.hint &&
|
||||
(this.persistentHint || this.isFocused)
|
||||
},
|
||||
hasLabel () {
|
||||
return Boolean(this.$slots.label || this.label)
|
||||
},
|
||||
// Proxy for `lazyValue`
|
||||
// This allows an input
|
||||
// to function without
|
||||
// a provided model
|
||||
internalValue: {
|
||||
get () {
|
||||
return this.lazyValue
|
||||
},
|
||||
set (val: any) {
|
||||
this.lazyValue = val
|
||||
this.$emit(this.$_modelEvent, val)
|
||||
}
|
||||
},
|
||||
isDirty () {
|
||||
return !!this.lazyValue
|
||||
},
|
||||
isDisabled () {
|
||||
return Boolean(this.disabled || this.readonly)
|
||||
},
|
||||
isLabelActive () {
|
||||
return this.isDirty
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value (val) {
|
||||
this.lazyValue = val
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
// v-radio-group needs to emit a different event
|
||||
// https://github.com/vuetifyjs/vuetify/issues/4752
|
||||
this.$_modelEvent = (this.$options.model && this.$options.model.event) || 'input'
|
||||
},
|
||||
|
||||
methods: {
|
||||
genContent () {
|
||||
return [
|
||||
this.genPrependSlot(),
|
||||
this.genControl(),
|
||||
this.genAppendSlot()
|
||||
]
|
||||
},
|
||||
genControl () {
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-input__control'
|
||||
}, [
|
||||
this.genInputSlot(),
|
||||
this.genMessages()
|
||||
])
|
||||
},
|
||||
genDefaultSlot () {
|
||||
return [
|
||||
this.genLabel(),
|
||||
this.$slots.default
|
||||
]
|
||||
},
|
||||
// TODO: remove shouldDeprecate (2.0), used for clearIcon
|
||||
genIcon (
|
||||
type: string,
|
||||
cb?: (e: Event) => void,
|
||||
shouldDeprecate = true
|
||||
) {
|
||||
const icon = (this as any)[`${type}Icon`]
|
||||
const eventName = `click:${kebabCase(type)}`
|
||||
cb = cb || (this as any)[`${type}IconCb`]
|
||||
|
||||
if (shouldDeprecate && type && cb) {
|
||||
deprecate(`:${type}-icon-cb`, `@${eventName}`, this)
|
||||
}
|
||||
|
||||
const data: VNodeData = {
|
||||
props: {
|
||||
color: this.validationState,
|
||||
dark: this.dark,
|
||||
disabled: this.disabled,
|
||||
light: this.light
|
||||
},
|
||||
on: !(this.$listeners[eventName] || cb)
|
||||
? undefined
|
||||
: {
|
||||
click: (e: Event) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
this.$emit(eventName, e)
|
||||
cb && cb(e)
|
||||
},
|
||||
// Container has mouseup event that will
|
||||
// trigger menu open if enclosed
|
||||
mouseup: (e: Event) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.$createElement('div', {
|
||||
staticClass: `v-input__icon v-input__icon--${kebabCase(type)}`,
|
||||
key: `${type}${icon}`
|
||||
}, [
|
||||
this.$createElement(
|
||||
VIcon,
|
||||
data,
|
||||
icon
|
||||
)
|
||||
])
|
||||
},
|
||||
genInputSlot () {
|
||||
return this.$createElement('div', this.setBackgroundColor(this.backgroundColor, {
|
||||
staticClass: 'v-input__slot',
|
||||
style: { height: convertToUnit(this.height) },
|
||||
directives: this.directivesInput,
|
||||
on: {
|
||||
click: this.onClick,
|
||||
mousedown: this.onMouseDown,
|
||||
mouseup: this.onMouseUp
|
||||
},
|
||||
ref: 'input-slot'
|
||||
}), [this.genDefaultSlot()])
|
||||
},
|
||||
genLabel () {
|
||||
if (!this.hasLabel) return null
|
||||
|
||||
return this.$createElement(VLabel, {
|
||||
props: {
|
||||
color: this.validationState,
|
||||
dark: this.dark,
|
||||
focused: this.hasState,
|
||||
for: this.$attrs.id,
|
||||
light: this.light
|
||||
}
|
||||
}, this.$slots.label || this.label)
|
||||
},
|
||||
genMessages () {
|
||||
if (this.hideDetails) return null
|
||||
|
||||
const messages = this.hasHint
|
||||
? [this.hint]
|
||||
: this.validations
|
||||
|
||||
return this.$createElement(VMessages, {
|
||||
props: {
|
||||
color: this.hasHint ? '' : this.validationState,
|
||||
dark: this.dark,
|
||||
light: this.light,
|
||||
value: (this.hasMessages || this.hasHint) ? messages : []
|
||||
}
|
||||
})
|
||||
},
|
||||
genSlot (
|
||||
type: string,
|
||||
location: string,
|
||||
slot: (VNode | VNode[])[]
|
||||
) {
|
||||
if (!slot.length) return null
|
||||
|
||||
const ref = `${type}-${location}`
|
||||
|
||||
return this.$createElement('div', {
|
||||
staticClass: `v-input__${ref}`,
|
||||
ref
|
||||
}, slot)
|
||||
},
|
||||
genPrependSlot () {
|
||||
const slot = []
|
||||
|
||||
if (this.$slots.prepend) {
|
||||
slot.push(this.$slots.prepend)
|
||||
} else if (this.prependIcon) {
|
||||
slot.push(this.genIcon('prepend'))
|
||||
}
|
||||
|
||||
return this.genSlot('prepend', 'outer', slot)
|
||||
},
|
||||
genAppendSlot () {
|
||||
const slot = []
|
||||
|
||||
// Append icon for text field was really
|
||||
// an appended inner icon, v-text-field
|
||||
// will overwrite this method in order to obtain
|
||||
// backwards compat
|
||||
if (this.$slots.append) {
|
||||
slot.push(this.$slots.append)
|
||||
} else if (this.appendIcon) {
|
||||
slot.push(this.genIcon('append'))
|
||||
}
|
||||
|
||||
return this.genSlot('append', 'outer', slot)
|
||||
},
|
||||
onClick (e: Event) {
|
||||
this.$emit('click', e)
|
||||
},
|
||||
onMouseDown (e: Event) {
|
||||
this.hasMouseDown = true
|
||||
this.$emit('mousedown', e)
|
||||
},
|
||||
onMouseUp (e: Event) {
|
||||
this.hasMouseDown = false
|
||||
this.$emit('mouseup', e)
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
return h('div', this.setTextColor(this.validationState, {
|
||||
staticClass: 'v-input',
|
||||
attrs: this.attrsInput,
|
||||
'class': this.classesInput
|
||||
}), this.genContent())
|
||||
}
|
||||
})
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VInput from './VInput'
|
||||
|
||||
export { VInput }
|
||||
export default VInput
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// Mixins
|
||||
import { factory as GroupableFactory } from '../../mixins/groupable'
|
||||
|
||||
// Utilities
|
||||
import mixins from '../../util/mixins'
|
||||
import { consoleWarn } from '../../util/console'
|
||||
|
||||
// Types
|
||||
import { VNode, ScopedSlotChildren } from 'vue/types/vnode'
|
||||
|
||||
export default mixins(
|
||||
GroupableFactory('itemGroup', 'v-item', 'v-item-group')
|
||||
/* @vue/component */
|
||||
).extend({
|
||||
name: 'v-item',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
render (): VNode {
|
||||
if (!this.$scopedSlots.default) {
|
||||
consoleWarn('v-item is missing a default scopedSlot', this)
|
||||
|
||||
return null as any
|
||||
}
|
||||
|
||||
let element: VNode | ScopedSlotChildren
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (this.$scopedSlots.default) {
|
||||
element = this.$scopedSlots.default({
|
||||
active: this.isActive,
|
||||
toggle: this.toggle
|
||||
})
|
||||
}
|
||||
|
||||
if (Array.isArray(element) && element.length === 1) {
|
||||
element = element[0]
|
||||
}
|
||||
|
||||
if (!element || Array.isArray(element) || !element.tag) {
|
||||
consoleWarn('v-item should only contain a single element', this)
|
||||
|
||||
return element as any
|
||||
}
|
||||
|
||||
element.data = this._b(element.data || {}, element.tag!, {
|
||||
class: { [this.activeClass]: this.isActive }
|
||||
})
|
||||
|
||||
return element
|
||||
}
|
||||
})
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
// Styles
|
||||
import '../../stylus/components/_item-group.styl'
|
||||
|
||||
// Mixins
|
||||
import Groupable from '../../mixins/groupable'
|
||||
import Proxyable from '../../mixins/proxyable'
|
||||
import Themeable from '../../mixins/themeable'
|
||||
|
||||
// Utilities
|
||||
import mixins from '../../util/mixins'
|
||||
import { consoleWarn } from '../../util/console'
|
||||
|
||||
// Types
|
||||
import { VNode } from 'vue/types'
|
||||
|
||||
type GroupableInstance = InstanceType<typeof Groupable> & { value?: any }
|
||||
|
||||
export const BaseItemGroup = mixins(
|
||||
Proxyable,
|
||||
Themeable
|
||||
).extend({
|
||||
name: 'base-item-group',
|
||||
|
||||
props: {
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: 'v-item--active'
|
||||
},
|
||||
mandatory: Boolean,
|
||||
max: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
},
|
||||
multiple: Boolean
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
// As long as a value is defined, show it
|
||||
// Otherwise, check if multiple
|
||||
// to determine which default to provide
|
||||
internalLazyValue: this.value !== undefined
|
||||
? this.value
|
||||
: this.multiple ? [] : undefined,
|
||||
items: [] as GroupableInstance[]
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes (): Record<string, boolean> {
|
||||
return {
|
||||
...this.themeClasses
|
||||
}
|
||||
},
|
||||
selectedItems (): GroupableInstance[] {
|
||||
return this.items.filter((item, index) => {
|
||||
return this.toggleMethod(this.getValue(item, index))
|
||||
})
|
||||
},
|
||||
selectedValues (): any[] {
|
||||
return Array.isArray(this.internalValue)
|
||||
? this.internalValue
|
||||
: [this.internalValue]
|
||||
},
|
||||
toggleMethod (): (v: any) => boolean {
|
||||
if (!this.multiple) {
|
||||
return (v: any) => this.internalValue === v
|
||||
}
|
||||
|
||||
const internalValue = this.internalValue
|
||||
if (Array.isArray(internalValue)) {
|
||||
return (v: any) => internalValue.includes(v)
|
||||
}
|
||||
|
||||
return () => false
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
internalValue () {
|
||||
// https://github.com/vuetifyjs/vuetify/issues/5352
|
||||
this.$nextTick(this.updateItemsState)
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
if (this.multiple && !Array.isArray(this.internalValue)) {
|
||||
consoleWarn('Model must be bound to an array if the multiple property is true.', this)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getValue (item: GroupableInstance, i: number): unknown {
|
||||
return item.value == null || item.value === ''
|
||||
? i
|
||||
: item.value
|
||||
},
|
||||
onClick (item: GroupableInstance, index: number) {
|
||||
this.updateInternalValue(
|
||||
this.getValue(item, index)
|
||||
)
|
||||
},
|
||||
register (item: GroupableInstance) {
|
||||
const index = this.items.push(item) - 1
|
||||
|
||||
item.$on('change', () => this.onClick(item, index))
|
||||
|
||||
// If no value provided and mandatory,
|
||||
// assign first registered item
|
||||
if (this.mandatory && this.internalLazyValue == null) {
|
||||
this.updateMandatory()
|
||||
}
|
||||
|
||||
this.updateItem(item, index)
|
||||
},
|
||||
unregister (item: GroupableInstance) {
|
||||
if (this._isDestroyed) return
|
||||
|
||||
const index = this.items.indexOf(item)
|
||||
const value = this.getValue(item, index)
|
||||
|
||||
this.items.splice(index, 1)
|
||||
|
||||
const valueIndex = this.selectedValues.indexOf(value)
|
||||
|
||||
// Items is not selected, do nothing
|
||||
if (valueIndex < 0) return
|
||||
|
||||
// If not mandatory, use regular update process
|
||||
if (!this.mandatory) {
|
||||
return this.updateInternalValue(value)
|
||||
}
|
||||
|
||||
// Remove the value
|
||||
if (this.multiple && Array.isArray(this.internalValue)) {
|
||||
this.internalValue = this.internalValue.filter(v => v !== value)
|
||||
} else {
|
||||
this.internalValue = undefined
|
||||
}
|
||||
|
||||
// If mandatory and we have no selection
|
||||
// add the last item as value
|
||||
/* istanbul ignore else */
|
||||
if (!this.selectedItems.length) {
|
||||
this.updateMandatory(true)
|
||||
}
|
||||
},
|
||||
updateItem (item: GroupableInstance, index: number) {
|
||||
const value = this.getValue(item, index)
|
||||
|
||||
item.isActive = this.toggleMethod(value)
|
||||
},
|
||||
updateItemsState () {
|
||||
if (this.mandatory &&
|
||||
!this.selectedItems.length
|
||||
) {
|
||||
return this.updateMandatory()
|
||||
}
|
||||
|
||||
// TODO: Make this smarter so it
|
||||
// doesn't have to iterate every
|
||||
// child in an update
|
||||
this.items.forEach(this.updateItem)
|
||||
},
|
||||
updateInternalValue (value: any) {
|
||||
this.multiple
|
||||
? this.updateMultiple(value)
|
||||
: this.updateSingle(value)
|
||||
},
|
||||
updateMandatory (last?: boolean) {
|
||||
if (!this.items.length) return
|
||||
|
||||
const index = last ? this.items.length - 1 : 0
|
||||
|
||||
this.updateInternalValue(
|
||||
this.getValue(this.items[index], index)
|
||||
)
|
||||
},
|
||||
updateMultiple (value: any) {
|
||||
const defaultValue = Array.isArray(this.internalValue)
|
||||
? this.internalValue
|
||||
: []
|
||||
const internalValue = defaultValue.slice()
|
||||
const index = internalValue.findIndex(val => val === value)
|
||||
|
||||
if (
|
||||
this.mandatory &&
|
||||
// Item already exists
|
||||
index > -1 &&
|
||||
// value would be reduced below min
|
||||
internalValue.length - 1 < 1
|
||||
) return
|
||||
|
||||
if (
|
||||
// Max is set
|
||||
this.max != null &&
|
||||
// Item doesn't exist
|
||||
index < 0 &&
|
||||
// value would be increased above max
|
||||
internalValue.length + 1 > this.max
|
||||
) return
|
||||
|
||||
index > -1
|
||||
? internalValue.splice(index, 1)
|
||||
: internalValue.push(value)
|
||||
|
||||
this.internalValue = internalValue
|
||||
},
|
||||
updateSingle (value: any) {
|
||||
const isSame = value === this.internalValue
|
||||
|
||||
if (this.mandatory && isSame) return
|
||||
|
||||
this.internalValue = isSame ? undefined : value
|
||||
}
|
||||
},
|
||||
|
||||
render (h): VNode {
|
||||
return h('div', {
|
||||
staticClass: 'v-item-group',
|
||||
class: this.classes
|
||||
}, this.$slots.default)
|
||||
}
|
||||
})
|
||||
|
||||
export default BaseItemGroup.extend({
|
||||
name: 'v-item-group',
|
||||
|
||||
provide (): object {
|
||||
return {
|
||||
itemGroup: this
|
||||
}
|
||||
}
|
||||
})
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import VItem from './VItem'
|
||||
import VItemGroup from './VItemGroup'
|
||||
|
||||
export {
|
||||
VItem,
|
||||
VItemGroup
|
||||
}
|
||||
|
||||
export default {
|
||||
$_vuetify_subcomponents: {
|
||||
VItem,
|
||||
VItemGroup
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import '../../stylus/components/_jumbotrons.styl'
|
||||
|
||||
// Mixins
|
||||
import Colorable from '../../mixins/colorable'
|
||||
import Routable from '../../mixins/routable'
|
||||
import Themeable from '../../mixins/themeable'
|
||||
|
||||
// Utils
|
||||
import { deprecate } from '../../util/console'
|
||||
|
||||
/* @vue/component */
|
||||
export default {
|
||||
name: 'v-jumbotron',
|
||||
|
||||
mixins: [
|
||||
Colorable,
|
||||
Routable,
|
||||
Themeable
|
||||
],
|
||||
|
||||
props: {
|
||||
gradient: String,
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: '400px'
|
||||
},
|
||||
src: String,
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'div'
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
backgroundStyles () {
|
||||
const styles = {}
|
||||
|
||||
if (this.gradient) {
|
||||
styles.background = `linear-gradient(${this.gradient})`
|
||||
}
|
||||
|
||||
return styles
|
||||
},
|
||||
classes () {
|
||||
return this.themeClasses
|
||||
},
|
||||
styles () {
|
||||
return {
|
||||
height: this.height
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
deprecate('v-jumbotron', this.src ? 'v-img' : 'v-responsive', this)
|
||||
},
|
||||
|
||||
methods: {
|
||||
genBackground () {
|
||||
return this.$createElement('div', this.setBackgroundColor(this.color, {
|
||||
staticClass: 'v-jumbotron__background',
|
||||
style: this.backgroundStyles
|
||||
}))
|
||||
},
|
||||
genContent () {
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-jumbotron__content'
|
||||
}, this.$slots.default)
|
||||
},
|
||||
genImage () {
|
||||
if (!this.src) return null
|
||||
if (this.$slots.img) return this.$slots.img({ src: this.src })
|
||||
|
||||
return this.$createElement('img', {
|
||||
staticClass: 'v-jumbotron__image',
|
||||
attrs: { src: this.src }
|
||||
})
|
||||
},
|
||||
genWrapper () {
|
||||
return this.$createElement('div', {
|
||||
staticClass: 'v-jumbotron__wrapper'
|
||||
}, [
|
||||
this.genImage(),
|
||||
this.genBackground(),
|
||||
this.genContent()
|
||||
])
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
const { tag, data } = this.generateRouteLink(this.classes)
|
||||
data.staticClass = 'v-jumbotron'
|
||||
data.style = this.styles
|
||||
|
||||
return h(tag, data, [this.genWrapper()])
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import VJumbotron from './VJumbotron'
|
||||
|
||||
export { VJumbotron }
|
||||
export default VJumbotron
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user