vuetify eingebunden + erster Versuch Combobox

This commit is contained in:
2019-04-17 15:58:15 +02:00
parent 24642ca1a8
commit 56f4a3f940
1846 changed files with 219701 additions and 53848 deletions
+107
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VAlert from './VAlert'
export { VAlert }
export default VAlert
+63
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VApp from './VApp'
export { VApp }
export default VApp
+109
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VAutocomplete from './VAutocomplete'
export { VAutocomplete }
export default VAutocomplete
+42
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VAvatar from './VAvatar'
export { VAvatar }
export default VAvatar
+72
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VBadge from './VBadge'
export { VBadge }
export default VBadge
+79
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VBottomNav from './VBottomNav'
export { VBottomNav }
export default VBottomNav
+48
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VBottomSheet from './VBottomSheet'
export { VBottomSheet }
export default VBottomSheet
+113
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VBtn from './VBtn'
export { VBtn }
export default VBtn
+28
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VBtnToggle from './VBtnToggle'
export { VBtnToggle }
export default VBtnToggle
+210
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
})
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,11 @@
import VCarousel from './VCarousel'
import VCarouselItem from './VCarouselItem'
export { VCarousel, VCarouselItem }
export default {
$_vuetify_subcomponents: {
VCarousel,
VCarouselItem
}
}
+90
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VCheckbox from './VCheckbox'
export { VCheckbox }
export default VCheckbox
+90
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VChip from './VChip'
export { VChip }
export default VChip
+218
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VCombobox from './VCombobox'
export { VCombobox }
export default VCombobox
+40
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VCounter from './VCounter'
export { VCounter }
export default VCounter
+110
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VDataIterator from './VDataIterator'
export { VDataIterator }
export default VDataIterator
+121
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
])
}
})
@@ -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
View File
@@ -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 || '&nbsp;' },
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
View File
@@ -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
View File
@@ -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
}
}
@@ -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])
}
}
})
@@ -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
View File
@@ -0,0 +1,9 @@
import createNativeLocaleFormatter from './createNativeLocaleFormatter'
import monthChange from './monthChange'
import pad from './pad'
export {
createNativeLocaleFormatter,
monthChange,
pad
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VDialog from './VDialog'
export { VDialog }
export default VDialog
+30
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VDivider from './VDivider'
export { VDivider }
export default VDivider
+132
View File
@@ -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)
}
})
@@ -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
View File
@@ -0,0 +1,11 @@
import VExpansionPanel from './VExpansionPanel'
import VExpansionPanelContent from './VExpansionPanelContent'
export { VExpansionPanel, VExpansionPanelContent }
export default {
$_vuetify_subcomponents: {
VExpansionPanel,
VExpansionPanelContent
}
}
+100
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VFooter from './VFooter'
export { VFooter }
export default VFooter
+128
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VForm from './VForm'
export { VForm }
export default VForm
+5
View File
@@ -0,0 +1,5 @@
import '../../stylus/components/_grid.styl'
import Grid from './grid'
export default Grid('container')
+50
View File
@@ -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
View File
@@ -0,0 +1,5 @@
import '../../stylus/components/_grid.styl'
import Grid from './grid'
export default Grid('flex')
+5
View File
@@ -0,0 +1,5 @@
import '../../stylus/components/_grid.styl'
import Grid from './grid'
export default Grid('layout')
+51
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VHover from './VHover'
export { VHover }
export default VHover
+166
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VIcon from './VIcon'
export { VIcon }
export default VIcon
+224
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VImg from './VImg'
export { VImg }
export default VImg
+311
View File
@@ -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
View File
@@ -0,0 +1,4 @@
import VInput from './VInput'
export { VInput }
export default VInput
+56
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,14 @@
import VItem from './VItem'
import VItemGroup from './VItemGroup'
export {
VItem,
VItemGroup
}
export default {
$_vuetify_subcomponents: {
VItem,
VItemGroup
}
}
+97
View File
@@ -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
View File
@@ -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