vuetify
Version:
Vue Material Component Framework
521 lines (493 loc) • 17.9 kB
text/typescript
// Components
import VDatePickerTitle from './VDatePickerTitle'
import VDatePickerHeader from './VDatePickerHeader'
import VDatePickerDateTable from './VDatePickerDateTable'
import VDatePickerMonthTable from './VDatePickerMonthTable'
import VDatePickerYears from './VDatePickerYears'
// Mixins
import Localable from '../../mixins/localable'
import Picker from '../../mixins/picker'
// Utils
import isDateAllowed from './util/isDateAllowed'
import mixins from '../../util/mixins'
import { wrapInArray } from '../../util/helpers'
import { daysInMonth } from '../VCalendar/util/timestamp'
import { consoleWarn } from '../../util/console'
import {
createItemTypeListeners,
createNativeLocaleFormatter,
pad,
} from './util'
// Types
import {
PropType,
PropValidator,
} from 'vue/types/options'
import { VNode } from 'vue'
import {
DatePickerFormatter,
DatePickerMultipleFormatter,
DatePickerAllowedDatesFunction,
DatePickerEventColors,
DatePickerEvents,
DatePickerType,
} from 'vuetify/types'
type DatePickerValue = string | string[] | undefined
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(
Localable,
Picker,
/* @vue/component */
).extend({
name: 'v-date-picker',
props: {
allowedDates: Function as PropType<DatePickerAllowedDatesFunction | undefined>,
// Function formatting the day in date picker table
dayFormat: Function as PropType<DatePickerAllowedDatesFunction | undefined>,
disabled: Boolean,
events: {
type: [Array, Function, Object],
default: () => null,
} as PropValidator<DatePickerEvents | null>,
eventColor: {
type: [Array, Function, Object, String],
default: () => 'warning',
} as PropValidator<DatePickerEventColors>,
firstDayOfWeek: {
type: [String, Number],
default: 0,
},
// Function formatting the tableDate in the day/month table header
headerDateFormat: Function as PropType<DatePickerFormatter | undefined>,
localeFirstDayOfYear: {
type: [String, Number],
default: 0,
},
max: String,
min: String,
// Function formatting month in the months table
monthFormat: Function as PropType<DatePickerFormatter | undefined>,
multiple: Boolean,
nextIcon: {
type: String,
default: '$next',
},
nextMonthAriaLabel: {
type: String,
default: '$vuetify.datePicker.nextMonthAriaLabel',
},
nextYearAriaLabel: {
type: String,
default: '$vuetify.datePicker.nextYearAriaLabel',
},
pickerDate: String,
prevIcon: {
type: String,
default: '$prev',
},
prevMonthAriaLabel: {
type: String,
default: '$vuetify.datePicker.prevMonthAriaLabel',
},
prevYearAriaLabel: {
type: String,
default: '$vuetify.datePicker.prevYearAriaLabel',
},
range: Boolean,
reactive: Boolean,
readonly: Boolean,
scrollable: Boolean,
showCurrent: {
type: [Boolean, String],
default: true,
},
selectedItemsText: {
type: String,
default: '$vuetify.datePicker.itemsSelected',
},
showWeek: Boolean,
// Function formatting currently selected date in the picker title
titleDateFormat: Function as PropType<DatePickerFormatter | DatePickerMultipleFormatter | undefined>,
type: {
type: String,
default: 'date',
validator: (type: any) => ['date', 'month'].includes(type), // TODO: year
} as PropValidator<DatePickerType>,
value: [Array, String] as PropType<DatePickerValue>,
weekdayFormat: Function as PropType<DatePickerFormatter | undefined>,
// Function formatting the year in table header and pickup title
yearFormat: Function as PropType<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 multipleValue = wrapInArray(this.value)
const date = multipleValue[multipleValue.length - 1] ||
(typeof this.showCurrent === 'string' ? this.showCurrent : `${now.getFullYear()}-${now.getMonth() + 1}`)
return sanitizeDateString(date as string, this.type === 'date' ? 'month' : 'year')
})(),
}
},
computed: {
multipleValue (): string[] {
return wrapInArray(this.value)
},
isMultiple (): boolean {
return this.multiple || this.range
},
lastValue (): string | null {
return this.isMultiple ? this.multipleValue[this.multipleValue.length - 1] : (this.value as string | null)
},
selectedMonths (): string | string[] | undefined {
if (!this.value || this.type === 'month') {
return this.value
} else if (this.isMultiple) {
return this.multipleValue.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.currentLocale, { year: 'numeric', timeZone: 'UTC' }, { length: 4 }),
titleDate: this.titleDateFormat ||
(this.isMultiple ? this.defaultTitleMultipleDateFormatter : this.defaultTitleDateFormatter),
}
},
defaultTitleMultipleDateFormatter (): DatePickerMultipleFormatter {
return dates => {
if (!dates.length) {
return '-'
}
if (dates.length === 1) {
return this.defaultTitleDateFormatter(dates[0])
}
return this.$vuetify.lang.t(this.selectedItemsText, dates.length)
}
},
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.currentLocale, 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:picker-date', 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.isMultiple && this.value && !this.pickerDate) ||
(this.isMultiple && this.multipleValue.length && (!oldValue || !oldValue.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.multipleValue
.map((val: string) => sanitizeDateString(val, type))
.filter(this.isDateAllowed)
this.$emit('input', this.isMultiple ? output : output[0])
}
},
},
created () {
this.checkMultipleProp()
if (this.pickerDate !== this.tableDate) {
this.$emit('update:picker-date', this.tableDate)
}
this.setInputDate()
},
methods: {
emitInput (newInput: string) {
if (this.range) {
if (this.multipleValue.length !== 1) {
this.$emit('input', [newInput])
} else {
const output = [this.multipleValue[0], newInput]
this.$emit('input', output)
this.$emit('change', output)
}
return
}
const output = this.multiple
? (
this.multipleValue.indexOf(newInput) === -1
? this.multipleValue.concat([newInput])
: this.multipleValue.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.isMultiple ? 'Array' : 'String'
if (valueType !== expected) {
consoleWarn(`Value must be ${this.isMultiple ? 'an' : 'a'} ${expected}, got ${valueType}`, this)
}
},
isDateAllowed (value: string): boolean {
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.isMultiple && 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.isMultiple && 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 (): VNode {
return this.$createElement(VDatePickerTitle, {
props: {
date: this.value ? (this.formatters.titleDate as (value: any) => string)(this.isMultiple ? this.multipleValue : this.value) : '',
disabled: this.disabled,
readonly: this.readonly,
selectingYear: this.activePicker === 'YEAR',
year: this.formatters.year(this.multipleValue.length ? `${this.inputYear}` : this.tableDate),
yearIcon: this.yearIcon,
value: this.multipleValue[0],
},
slot: 'title',
on: {
'update:selecting-year': (value: boolean) => this.activePicker = value ? 'YEAR' : this.type.toUpperCase(),
},
})
},
genTableHeader (): VNode {
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,
nextAriaLabel: this.activePicker === 'DATE' ? this.nextMonthAriaLabel : this.nextYearAriaLabel,
prevAriaLabel: this.activePicker === 'DATE' ? this.prevMonthAriaLabel : this.prevYearAriaLabel,
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 (): VNode {
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,
localeFirstDayOfYear: this.localeFirstDayOfYear,
min: this.min,
max: this.max,
range: this.range,
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,
'update:table-date': (value: string) => this.tableDate = value,
...createItemTypeListeners(this, ':date'),
},
})
},
genMonthTable (): VNode {
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,
range: this.range,
readonly: this.readonly && this.type === 'month',
scrollable: this.scrollable,
value: this.selectedMonths,
tableDate: `${pad(this.tableYear, 4)}`,
},
ref: 'table',
on: {
input: this.monthClick,
'update:table-date': (value: string) => this.tableDate = value,
...createItemTypeListeners(this, ':month'),
},
})
},
genYears (): VNode {
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,
...createItemTypeListeners(this, ':year'),
},
})
},
genPickerBody (): VNode {
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')
},
})