quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
1,790 lines (1,541 loc) • 48.7 kB
JavaScript
import {
h,
ref,
computed,
watch,
Transition,
nextTick,
getCurrentInstance
} from 'vue'
import QBtn from '../btn/QBtn.js'
import useDark, {
useDarkProps
} from '../../composables/private.use-dark/use-dark.js'
import useRenderCache from '../../composables/use-render-cache/use-render-cache.js'
import {
useFormProps,
useFormAttrs,
useFormInject
} from '../../composables/use-form/private.use-form.js'
import useDatetime, {
useDatetimeProps,
useDatetimeEmits,
getDayHash
} from './use-datetime.js'
import { createComponent } from '../../utils/private.create/create.js'
import { hSlot } from '../../utils/private.render/render.js'
import { formatDate, __splitDate, getDateDiff } from '../../utils/date/date.js'
import { pad } from '../../utils/format/format.js'
import {
jalaaliMonthLength,
toGregorian
} from '../../utils/date/private.persian.js'
import { isObject } from '../../utils/is/is.js'
const yearsInterval = 20
const views = ['Calendar', 'Years', 'Months']
const viewIsValid = v => views.includes(v)
const yearMonthValidator = v => /^-?[\d]+\/[0-1]\d$/.test(v)
const lineStr = ' \u2014 '
function getMonthHash(date) {
return date.year + '/' + pad(date.month)
}
export default createComponent({
name: 'QDate',
props: {
...useDatetimeProps,
...useFormProps,
...useDarkProps,
modelValue: {
required: true,
validator: val =>
typeof val === 'string' ||
Array.isArray(val) === true ||
Object(val) === val ||
val === null
},
multiple: Boolean,
range: Boolean,
title: String,
subtitle: String,
mask: {
...useDatetimeProps.mask,
// this mask is forced
// when using persian calendar
default: 'YYYY/MM/DD'
},
defaultYearMonth: {
type: String,
validator: yearMonthValidator
},
yearsInMonthView: Boolean,
events: [Array, Function],
eventColor: [String, Function],
emitImmediately: Boolean,
options: [Array, Function],
navigationMinYearMonth: {
type: String,
validator: yearMonthValidator
},
navigationMaxYearMonth: {
type: String,
validator: yearMonthValidator
},
noUnset: Boolean,
firstDayOfWeek: [String, Number],
todayBtn: Boolean,
minimal: Boolean,
defaultView: {
type: String,
default: 'Calendar',
validator: viewIsValid
}
},
emits: [...useDatetimeEmits, 'rangeStart', 'rangeEnd', 'navigation'],
setup(props, { slots, emit }) {
const { proxy } = getCurrentInstance()
const { $q } = proxy
const isDark = useDark(props, $q)
const { getCache } = useRenderCache()
const { tabindex, headerClass, getLocale, getCurrentDate } = useDatetime(
props,
$q
)
let lastEmitValue
const formAttrs = useFormAttrs(props)
const injectFormInput = useFormInject(formAttrs)
const blurTargetRef = ref(null)
const innerMask = ref(getMask())
const innerLocale = ref(getLocale())
const mask = computed(() => getMask())
const locale = computed(() => getLocale())
const today = computed(() => getCurrentDate())
// model of current calendar view:
const viewModel = ref(getViewModel(innerMask.value, innerLocale.value))
const view = ref(props.defaultView)
const direction = computed(() => ($q.lang.rtl === true ? 'right' : 'left'))
const monthDirection = ref(direction.value)
const yearDirection = ref(direction.value)
const localYear = viewModel.value.year
const startYear = ref(
localYear -
(localYear % yearsInterval) -
(localYear < 0 ? yearsInterval : 0)
)
const editRange = ref(null)
const classes = computed(() => {
const type = props.landscape === true ? 'landscape' : 'portrait'
return (
`q-date q-date--${type} q-date--${type}-${props.minimal === true ? 'minimal' : 'standard'}` +
(isDark.value === true ? ' q-date--dark q-dark' : '') +
(props.bordered === true ? ' q-date--bordered' : '') +
(props.square === true ? ' q-date--square no-border-radius' : '') +
(props.flat === true ? ' q-date--flat no-shadow' : '') +
(props.disable === true
? ' disabled'
: props.readonly === true
? ' q-date--readonly'
: '')
)
})
const computedColor = computed(() => props.color || 'primary')
const computedTextColor = computed(() => props.textColor || 'white')
const isImmediate = computed(
() =>
props.emitImmediately === true &&
props.multiple !== true &&
props.range !== true
)
const normalizedModel = computed(() =>
Array.isArray(props.modelValue) === true
? props.modelValue
: props.modelValue !== null && props.modelValue !== void 0
? [props.modelValue]
: []
)
const daysModel = computed(() =>
normalizedModel.value
.filter(date => typeof date === 'string')
.map(date => decodeString(date, innerMask.value, innerLocale.value))
.filter(
date =>
date.dateHash !== null &&
date.day !== null &&
date.month !== null &&
date.year !== null
)
)
const rangeModel = computed(() => {
const fn = date => decodeString(date, innerMask.value, innerLocale.value)
return normalizedModel.value
.filter(
date =>
isObject(date) === true &&
date.from !== void 0 &&
date.to !== void 0
)
.map(range => ({ from: fn(range.from), to: fn(range.to) }))
.filter(
range =>
range.from.dateHash !== null &&
range.to.dateHash !== null &&
range.from.dateHash < range.to.dateHash
)
})
const getNativeDateFn = computed(() =>
props.calendar !== 'persian'
? model => new Date(model.year, model.month - 1, model.day)
: model => {
const gDate = toGregorian(model.year, model.month, model.day)
return new Date(gDate.gy, gDate.gm - 1, gDate.gd)
}
)
const encodeObjectFn = computed(() =>
props.calendar === 'persian'
? getDayHash
: (date, dateMask, dateLocale) =>
formatDate(
new Date(
date.year,
date.month - 1,
date.day,
date.hour,
date.minute,
date.second,
date.millisecond
),
dateMask === void 0 ? innerMask.value : dateMask,
dateLocale === void 0 ? innerLocale.value : dateLocale,
date.year,
date.timezoneOffset
)
)
const daysInModel = computed(
() =>
daysModel.value.length +
rangeModel.value.reduce(
(acc, range) =>
acc +
1 +
getDateDiff(
getNativeDateFn.value(range.to),
getNativeDateFn.value(range.from)
),
0
)
)
const headerTitle = computed(() => {
if (
props.title !== void 0 &&
props.title !== null &&
props.title.length !== 0
) {
return props.title
}
if (editRange.value !== null) {
const model = editRange.value.init
const date = getNativeDateFn.value(model)
return (
innerLocale.value.daysShort[date.getDay()] +
', ' +
innerLocale.value.monthsShort[model.month - 1] +
' ' +
model.day +
lineStr +
'?'
)
}
if (daysInModel.value === 0) {
return lineStr
}
if (daysInModel.value > 1) {
return `${daysInModel.value} ${innerLocale.value.pluralDay}`
}
const model = daysModel.value[0]
const date = getNativeDateFn.value(model)
if (isNaN(date.valueOf()) === true) {
return lineStr
}
if (innerLocale.value.headerTitle !== void 0) {
return innerLocale.value.headerTitle(date, model)
}
return (
innerLocale.value.daysShort[date.getDay()] +
', ' +
innerLocale.value.monthsShort[model.month - 1] +
' ' +
model.day
)
})
const minSelectedModel = computed(() => {
const model = daysModel.value
.concat(rangeModel.value.map(range => range.from))
.sort((a, b) => a.year - b.year || a.month - b.month)
return model[0]
})
const maxSelectedModel = computed(() => {
const model = daysModel.value
.concat(rangeModel.value.map(range => range.to))
.sort((a, b) => b.year - a.year || b.month - a.month)
return model[0]
})
const headerSubtitle = computed(() => {
if (
props.subtitle !== void 0 &&
props.subtitle !== null &&
props.subtitle.length !== 0
) {
return props.subtitle
}
if (daysInModel.value === 0) {
return lineStr
}
if (daysInModel.value > 1) {
const from = minSelectedModel.value
const to = maxSelectedModel.value
const month = innerLocale.value.monthsShort
return (
month[from.month - 1] +
(from.year !== to.year
? ' ' + from.year + lineStr + month[to.month - 1] + ' '
: from.month !== to.month
? lineStr + month[to.month - 1]
: '') +
' ' +
to.year
)
}
return daysModel.value[0].year
})
const dateArrow = computed(() => {
const val = [
$q.iconSet.datetime.arrowLeft,
$q.iconSet.datetime.arrowRight
]
return $q.lang.rtl === true ? val.reverse() : val
})
const computedFirstDayOfWeek = computed(() =>
props.firstDayOfWeek !== void 0
? Number(props.firstDayOfWeek)
: innerLocale.value.firstDayOfWeek
)
const daysOfWeek = computed(() => {
const days = innerLocale.value.daysShort,
first = computedFirstDayOfWeek.value
return first > 0
? days.slice(first, 7).concat(days.slice(0, first))
: days
})
const daysInMonth = computed(() => {
const date = viewModel.value
return props.calendar !== 'persian'
? new Date(date.year, date.month, 0).getDate()
: jalaaliMonthLength(date.year, date.month)
})
const evtColor = computed(() =>
typeof props.eventColor === 'function'
? props.eventColor
: () => props.eventColor
)
const minNav = computed(() => {
if (props.navigationMinYearMonth === void 0) {
return null
}
const data = props.navigationMinYearMonth.split('/')
return { year: parseInt(data[0], 10), month: parseInt(data[1], 10) }
})
const maxNav = computed(() => {
if (props.navigationMaxYearMonth === void 0) {
return null
}
const data = props.navigationMaxYearMonth.split('/')
return { year: parseInt(data[0], 10), month: parseInt(data[1], 10) }
})
const navBoundaries = computed(() => {
const data = {
month: { prev: true, next: true },
year: { prev: true, next: true }
}
if (minNav.value !== null && minNav.value.year >= viewModel.value.year) {
data.year.prev = false
if (
minNav.value.year === viewModel.value.year &&
minNav.value.month >= viewModel.value.month
) {
data.month.prev = false
}
}
if (maxNav.value !== null && maxNav.value.year <= viewModel.value.year) {
data.year.next = false
if (
maxNav.value.year === viewModel.value.year &&
maxNav.value.month <= viewModel.value.month
) {
data.month.next = false
}
}
return data
})
const daysMap = computed(() => {
const map = {}
daysModel.value.forEach(entry => {
const hash = getMonthHash(entry)
if (map[hash] === void 0) {
map[hash] = []
}
map[hash].push(entry.day)
})
return map
})
const rangeMap = computed(() => {
const map = {}
rangeModel.value.forEach(entry => {
const hashFrom = getMonthHash(entry.from)
const hashTo = getMonthHash(entry.to)
if (map[hashFrom] === void 0) {
map[hashFrom] = []
}
map[hashFrom].push({
from: entry.from.day,
to: hashFrom === hashTo ? entry.to.day : void 0,
range: entry
})
if (hashFrom < hashTo) {
let hash
const { year, month } = entry.from
const cur =
month < 12
? { year, month: month + 1 }
: { year: year + 1, month: 1 }
while ((hash = getMonthHash(cur)) <= hashTo) {
if (map[hash] === void 0) {
map[hash] = []
}
map[hash].push({
from: void 0,
to: hash === hashTo ? entry.to.day : void 0,
range: entry
})
cur.month++
if (cur.month > 12) {
cur.year++
cur.month = 1
}
}
}
})
return map
})
const rangeView = computed(() => {
if (editRange.value === null) return
const { init, initHash, final, finalHash } = editRange.value
const [from, to] = initHash <= finalHash ? [init, final] : [final, init]
const fromHash = getMonthHash(from)
const toHash = getMonthHash(to)
if (fromHash !== viewMonthHash.value && toHash !== viewMonthHash.value) {
return
}
const localView = {}
if (fromHash === viewMonthHash.value) {
localView.from = from.day
localView.includeFrom = true
} else {
localView.from = 1
}
if (toHash === viewMonthHash.value) {
localView.to = to.day
localView.includeTo = true
} else {
localView.to = daysInMonth.value
}
return localView
})
const viewMonthHash = computed(() => getMonthHash(viewModel.value))
const selectionDaysMap = computed(() => {
const map = {}
if (props.options === void 0) {
for (let i = 1; i <= daysInMonth.value; i++) {
map[i] = true
}
return map
}
const fn =
typeof props.options === 'function'
? props.options
: date => props.options.includes(date)
for (let i = 1; i <= daysInMonth.value; i++) {
const dayHash = viewMonthHash.value + '/' + pad(i)
map[i] = fn(dayHash)
}
return map
})
const eventDaysMap = computed(() => {
const map = {}
if (props.events === void 0) {
for (let i = 1; i <= daysInMonth.value; i++) {
map[i] = false
}
} else {
const fn =
typeof props.events === 'function'
? props.events
: date => props.events.includes(date)
for (let i = 1; i <= daysInMonth.value; i++) {
const dayHash = viewMonthHash.value + '/' + pad(i)
map[i] = fn(dayHash) === true && evtColor.value(dayHash)
}
}
return map
})
const viewDays = computed(() => {
let date, endDay
const { year, month } = viewModel.value
if (props.calendar !== 'persian') {
date = new Date(year, month - 1, 1)
endDay = new Date(year, month - 1, 0).getDate()
} else {
const gDate = toGregorian(year, month, 1)
date = new Date(gDate.gy, gDate.gm - 1, gDate.gd)
let prevJM = month - 1
let prevJY = year
if (prevJM === 0) {
prevJM = 12
prevJY--
}
endDay = jalaaliMonthLength(prevJY, prevJM)
}
return {
days: date.getDay() - computedFirstDayOfWeek.value - 1,
endDay
}
})
const days = computed(() => {
const res = []
const { days: localDays, endDay } = viewDays.value
const len = localDays < 0 ? localDays + 7 : localDays
if (len < 6) {
for (let i = endDay - len; i <= endDay; i++) {
res.push({ i, fill: true })
}
}
const index = res.length
for (let i = 1; i <= daysInMonth.value; i++) {
const day = { i, event: eventDaysMap.value[i], classes: [] }
if (selectionDaysMap.value[i] === true) {
day.in = true
day.flat = true
}
res.push(day)
}
// if current view has days in model
if (daysMap.value[viewMonthHash.value] !== void 0) {
daysMap.value[viewMonthHash.value].forEach(day => {
const i = index + day - 1
Object.assign(res[i], {
selected: true,
unelevated: true,
flat: false,
color: computedColor.value,
textColor: computedTextColor.value
})
})
}
// if current view has ranges in model
if (rangeMap.value[viewMonthHash.value] !== void 0) {
rangeMap.value[viewMonthHash.value].forEach(entry => {
if (entry.from !== void 0) {
const from = index + entry.from - 1
const to = index + (entry.to || daysInMonth.value) - 1
for (let day = from; day <= to; day++) {
Object.assign(res[day], {
range: entry.range,
unelevated: true,
color: computedColor.value,
textColor: computedTextColor.value
})
}
Object.assign(res[from], {
rangeFrom: true,
flat: false
})
if (entry.to !== void 0) {
Object.assign(res[to], {
rangeTo: true,
flat: false
})
}
} else if (entry.to !== void 0) {
const to = index + entry.to - 1
for (let day = index; day <= to; day++) {
Object.assign(res[day], {
range: entry.range,
unelevated: true,
color: computedColor.value,
textColor: computedTextColor.value
})
}
Object.assign(res[to], {
flat: false,
rangeTo: true
})
} else {
const to = index + daysInMonth.value - 1
for (let day = index; day <= to; day++) {
Object.assign(res[day], {
range: entry.range,
unelevated: true,
color: computedColor.value,
textColor: computedTextColor.value
})
}
}
})
}
if (rangeView.value !== void 0) {
const from = index + rangeView.value.from - 1
const to = index + rangeView.value.to - 1
for (let day = from; day <= to; day++) {
res[day].color = computedColor.value
res[day].editRange = true
}
if (rangeView.value.includeFrom === true) {
res[from].editRangeFrom = true
}
if (rangeView.value.includeTo === true) {
res[to].editRangeTo = true
}
}
if (
viewModel.value.year === today.value.year &&
viewModel.value.month === today.value.month
) {
res[index + today.value.day - 1].today = true
}
const left = res.length % 7
if (left > 0) {
const afterDays = 7 - left
for (let i = 1; i <= afterDays; i++) {
res.push({ i, fill: true })
}
}
res.forEach(day => {
let cls = 'q-date__calendar-item '
if (day.fill === true) {
cls += 'q-date__calendar-item--fill'
} else {
cls += `q-date__calendar-item--${day.in === true ? 'in' : 'out'}`
if (day.range !== void 0) {
cls += ` q-date__range${day.rangeTo === true ? '-to' : day.rangeFrom === true ? '-from' : ''}`
}
if (day.editRange === true) {
cls += ` q-date__edit-range${day.editRangeFrom === true ? '-from' : ''}${day.editRangeTo === true ? '-to' : ''}`
}
if (day.range !== void 0 || day.editRange === true) {
cls += ` text-${day.color}`
}
}
day.classes = cls
})
return res
})
const attributes = computed(() =>
props.disable === true ? { 'aria-disabled': 'true' } : {}
)
watch(
() => props.modelValue,
v => {
if (lastEmitValue === JSON.stringify(v)) {
lastEmitValue = 0
} else {
const model = getViewModel(innerMask.value, innerLocale.value)
updateViewModel(model.year, model.month, model)
}
}
)
watch(view, () => {
if (
blurTargetRef.value !== null &&
proxy.$el.contains(document.activeElement) === true
) {
blurTargetRef.value.focus()
}
})
watch(
() => viewModel.value.year + '|' + viewModel.value.month,
() => {
emit('navigation', {
year: viewModel.value.year,
month: viewModel.value.month
})
}
)
watch(mask, val => {
updateValue(val, innerLocale.value, 'mask')
innerMask.value = val
})
watch(locale, val => {
updateValue(innerMask.value, val, 'locale')
innerLocale.value = val
})
function setLastValue(v) {
lastEmitValue = JSON.stringify(v)
}
function setToday() {
const { year, month, day } = today.value
const date = {
// contains more props than needed (hour, minute, second, millisecond)
// but those aren't used in the processing of this "date" variable
...viewModel.value,
// overwriting with today's date
year,
month,
day
}
const monthMap = daysMap.value[getMonthHash(date)]
if (monthMap === void 0 || monthMap.includes(date.day) === false) {
addToModel(date)
}
setCalendarTo(date.year, date.month)
}
function setView(viewMode) {
if (viewIsValid(viewMode) === true) {
view.value = viewMode
}
}
function offsetCalendar(type, descending) {
if (['month', 'year'].includes(type)) {
const fn = type === 'month' ? goToMonth : goToYear
fn(descending === true ? -1 : 1)
}
}
function setCalendarTo(year, month) {
view.value = 'Calendar'
updateViewModel(year, month)
}
function setEditingRange(from, to) {
if (props.range === false || !from) {
editRange.value = null
return
}
const init = Object.assign({ ...viewModel.value }, from)
const final =
to !== void 0 ? Object.assign({ ...viewModel.value }, to) : init
editRange.value = {
init,
initHash: getDayHash(init),
final,
finalHash: getDayHash(final)
}
setCalendarTo(init.year, init.month)
}
function getMask() {
return props.calendar === 'persian' ? 'YYYY/MM/DD' : props.mask
}
function decodeString(date, dateMask, dateLocale) {
return __splitDate(date, dateMask, dateLocale, props.calendar, {
hour: 0,
minute: 0,
second: 0,
millisecond: 0
})
}
function getViewModel(dateMask, dateLocale) {
const model =
Array.isArray(props.modelValue) === true
? props.modelValue
: props.modelValue
? [props.modelValue]
: []
if (model.length === 0) {
return getDefaultViewModel()
}
const target = model[model.length - 1]
const decoded = decodeString(
target.from !== void 0 ? target.from : target,
dateMask,
dateLocale
)
return decoded.dateHash === null ? getDefaultViewModel() : decoded
}
function getDefaultViewModel() {
let year, month
if (props.defaultYearMonth !== void 0) {
const d = props.defaultYearMonth.split('/')
year = parseInt(d[0], 10)
month = parseInt(d[1], 10)
} else {
// may come from data() where computed
// props are not yet available
const d = today.value !== void 0 ? today.value : getCurrentDate()
year = d.year
month = d.month
}
return {
year,
month,
day: 1,
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
dateHash: year + '/' + pad(month) + '/01'
}
}
function goToMonth(offset) {
let year = viewModel.value.year
let month = Number(viewModel.value.month) + offset
if (month === 13) {
month = 1
year++
} else if (month === 0) {
month = 12
year--
}
updateViewModel(year, month)
if (isImmediate.value === true) emitImmediately('month')
}
function goToYear(offset) {
const year = Number(viewModel.value.year) + offset
updateViewModel(year, viewModel.value.month)
if (isImmediate.value === true) emitImmediately('year')
}
function setYear(year) {
updateViewModel(year, viewModel.value.month)
view.value = props.defaultView === 'Years' ? 'Months' : 'Calendar'
if (isImmediate.value === true) emitImmediately('year')
}
function setMonth(month) {
updateViewModel(viewModel.value.year, month)
view.value = 'Calendar'
if (isImmediate.value === true) emitImmediately('month')
}
function toggleDate(date, monthHash) {
const month = daysMap.value[monthHash]
const fn =
month?.includes(date.day) === true ? removeFromModel : addToModel
fn(date)
}
function getShortDate(date) {
return { year: date.year, month: date.month, day: date.day }
}
function updateViewModel(year, month, time) {
if (minNav.value !== null && year <= minNav.value.year) {
if (month < minNav.value.month || year < minNav.value.year) {
month = minNav.value.month
}
year = minNav.value.year
}
if (maxNav.value !== null && year >= maxNav.value.year) {
if (month > maxNav.value.month || year > maxNav.value.year) {
month = maxNav.value.month
}
year = maxNav.value.year
}
if (time !== void 0) {
const { hour, minute, second, millisecond, timezoneOffset, timeHash } =
time
Object.assign(viewModel.value, {
hour,
minute,
second,
millisecond,
timezoneOffset,
timeHash
})
}
const newHash = year + '/' + pad(month) + '/01'
if (newHash !== viewModel.value.dateHash) {
monthDirection.value =
viewModel.value.dateHash < newHash === ($q.lang.rtl !== true)
? 'left'
: 'right'
if (year !== viewModel.value.year) {
yearDirection.value = monthDirection.value
}
nextTick(() => {
startYear.value =
year - (year % yearsInterval) - (year < 0 ? yearsInterval : 0)
Object.assign(viewModel.value, {
year,
month,
day: 1,
dateHash: newHash
})
})
}
}
function emitValue(val, action, date) {
const value =
val !== null && val.length === 1 && props.multiple === false
? val[0]
: val
const { reason, details } = getEmitParams(action, date)
setLastValue(value)
emit('update:modelValue', value, reason, details)
}
function emitImmediately(reason) {
const date =
daysModel.value[0] !== void 0 && daysModel.value[0].dateHash !== null
? { ...daysModel.value[0] }
: { ...viewModel.value } // inherit day, hours, minutes, milliseconds...
// nextTick required because of animation delay in viewModel
nextTick(() => {
date.year = viewModel.value.year
date.month = viewModel.value.month
const maxDay =
props.calendar !== 'persian'
? new Date(date.year, date.month, 0).getDate()
: jalaaliMonthLength(date.year, date.month)
date.day = Math.min(Math.max(1, date.day), maxDay)
const value = encodeEntry(date)
const { details } = getEmitParams('', date)
setLastValue(value)
emit('update:modelValue', value, reason, details)
})
}
function getEmitParams(action, date) {
return date.from !== void 0
? {
reason: `${action}-range`,
details: {
...getShortDate(date.target),
from: getShortDate(date.from),
to: getShortDate(date.to)
}
}
: {
reason: `${action}-day`,
details: getShortDate(date)
}
}
function encodeEntry(date, dateMask, dateLocale) {
return date.from !== void 0
? {
from: encodeObjectFn.value(date.from, dateMask, dateLocale),
to: encodeObjectFn.value(date.to, dateMask, dateLocale)
}
: encodeObjectFn.value(date, dateMask, dateLocale)
}
function addToModel(date) {
let value
if (props.multiple === true) {
if (date.from !== void 0) {
// we also need to filter out intersections
const fromHash = getDayHash(date.from)
const toHash = getDayHash(date.to)
const localDays = daysModel.value.filter(
day => day.dateHash < fromHash || day.dateHash > toHash
)
const ranges = rangeModel.value.filter(
({ from, to }) => to.dateHash < fromHash || from.dateHash > toHash
)
value = localDays
.concat(ranges)
.concat(date)
.map(entry => encodeEntry(entry))
} else {
const model = normalizedModel.value.slice()
model.push(encodeEntry(date))
value = model
}
} else {
value = encodeEntry(date)
}
emitValue(value, 'add', date)
}
function removeFromModel(date) {
if (props.noUnset === true) return
let model = null
if (props.multiple === true && Array.isArray(props.modelValue) === true) {
const val = encodeEntry(date)
if (date.from !== void 0) {
model = props.modelValue.filter(item =>
item.from !== void 0
? item.from !== val.from && item.to !== val.to
: true
)
} else {
model = props.modelValue.filter(item => item !== val)
}
if (model.length === 0) {
model = null
}
}
emitValue(model, 'remove', date)
}
function updateValue(dateMask, dateLocale, reason) {
const model = daysModel.value
.concat(rangeModel.value)
.map(entry => encodeEntry(entry, dateMask, dateLocale))
.filter(entry =>
entry.from !== void 0
? entry.from.dateHash !== null && entry.to.dateHash !== null
: entry.dateHash !== null
)
const value = (props.multiple === true ? model : model[0]) || null
setLastValue(value)
emit('update:modelValue', value, reason)
}
function getHeader() {
if (props.minimal === true) return
return h(
'div',
{
class: 'q-date__header ' + headerClass.value
},
[
h(
'div',
{
class: 'relative-position'
},
[
h(
Transition,
{
name: 'q-transition--fade'
},
() =>
h(
'div',
{
key: 'h-yr-' + headerSubtitle.value,
class:
'q-date__header-subtitle q-date__header-link ' +
(view.value === 'Years'
? 'q-date__header-link--active'
: 'cursor-pointer'),
tabindex: tabindex.value,
...getCache('vY', {
onClick() {
view.value = 'Years'
},
onKeyup(e) {
if (e.keyCode === 13) {
view.value = 'Years'
}
}
})
},
[headerSubtitle.value]
)
)
]
),
h(
'div',
{
class: 'q-date__header-title relative-position flex no-wrap'
},
[
h(
'div',
{
class: 'relative-position col'
},
[
h(
Transition,
{
name: 'q-transition--fade'
},
() =>
h(
'div',
{
key: 'h-sub' + headerTitle.value,
class:
'q-date__header-title-label q-date__header-link ' +
(view.value === 'Calendar'
? 'q-date__header-link--active'
: 'cursor-pointer'),
tabindex: tabindex.value,
...getCache('vC', {
onClick() {
view.value = 'Calendar'
},
onKeyup(e) {
if (e.keyCode === 13) {
view.value = 'Calendar'
}
}
})
},
[headerTitle.value]
)
)
]
),
props.todayBtn === true
? h(QBtn, {
class: 'q-date__header-today self-start',
icon: $q.iconSet.datetime.today,
'aria-label': $q.lang.date.today,
flat: true,
size: 'sm',
round: true,
tabindex: tabindex.value,
onClick: setToday
})
: null
]
)
]
)
}
function getNavigation({ label, type, key, dir, goTo, boundaries, cls }) {
return [
h(
'div',
{
class: 'row items-center q-date__arrow'
},
[
h(QBtn, {
round: true,
dense: true,
size: 'sm',
flat: true,
icon: dateArrow.value[0],
'aria-label':
type === 'Years'
? $q.lang.date.prevYear
: $q.lang.date.prevMonth,
tabindex: tabindex.value,
disable: boundaries.prev === false,
...getCache('go-#' + type, {
onClick() {
goTo(-1)
}
})
})
]
),
h(
'div',
{
class: 'relative-position overflow-hidden flex flex-center' + cls
},
[
h(
Transition,
{
name: 'q-transition--jump-' + dir
},
() =>
h('div', { key }, [
h(QBtn, {
flat: true,
dense: true,
noCaps: true,
label,
tabindex: tabindex.value,
...getCache('view#' + type, {
onClick: () => {
view.value = type
}
})
})
])
)
]
),
h(
'div',
{
class: 'row items-center q-date__arrow'
},
[
h(QBtn, {
round: true,
dense: true,
size: 'sm',
flat: true,
icon: dateArrow.value[1],
'aria-label':
type === 'Years'
? $q.lang.date.nextYear
: $q.lang.date.nextMonth,
tabindex: tabindex.value,
disable: boundaries.next === false,
...getCache('go+#' + type, {
onClick() {
goTo(1)
}
})
})
]
)
]
}
const renderViews = {
Calendar: () => [
h(
'div',
{
key: 'calendar-view',
class: 'q-date__view q-date__calendar'
},
[
h(
'div',
{
class: 'q-date__navigation row items-center no-wrap'
},
getNavigation({
label: innerLocale.value.months[viewModel.value.month - 1],
type: 'Months',
key: viewModel.value.month,
dir: monthDirection.value,
goTo: goToMonth,
boundaries: navBoundaries.value.month,
cls: ' col'
}).concat(
getNavigation({
label: viewModel.value.year,
type: 'Years',
key: viewModel.value.year,
dir: yearDirection.value,
goTo: goToYear,
boundaries: navBoundaries.value.year,
cls: ''
})
)
),
h(
'div',
{
class: 'q-date__calendar-weekdays row items-center no-wrap'
},
daysOfWeek.value.map(day =>
h('div', { class: 'q-date__calendar-item' }, [h('div', day)])
)
),
h(
'div',
{
class:
'q-date__calendar-days-container relative-position overflow-hidden'
},
[
h(
Transition,
{
name: 'q-transition--slide-' + monthDirection.value
},
() =>
h(
'div',
{
key: viewMonthHash.value,
class: 'q-date__calendar-days fit'
},
days.value.map(day =>
h('div', { class: day.classes }, [
day.in === true
? h(
QBtn,
{
class:
day.today === true ? 'q-date__today' : '',
dense: true,
flat: day.flat,
unelevated: day.unelevated,
color: day.color,
textColor: day.textColor,
label: day.i,
tabindex: tabindex.value,
...getCache('day#' + day.i, {
onClick: () => {
onDayClick(day.i)
},
onMouseover: () => {
onDayMouseover(day.i)
}
})
},
day.event !== false
? () =>
h('div', {
class: 'q-date__event bg-' + day.event
})
: null
)
: h('div', String(day.i))
])
)
)
)
]
)
]
)
],
Months() {
const currentYear = viewModel.value.year === today.value.year
const isDisabled = month =>
(minNav.value !== null &&
viewModel.value.year === minNav.value.year &&
minNav.value.month > month) ||
(maxNav.value !== null &&
viewModel.value.year === maxNav.value.year &&
maxNav.value.month < month)
const content = innerLocale.value.monthsShort.map((month, i) => {
const active = viewModel.value.month === i + 1
return h(
'div',
{
class: 'q-date__months-item flex flex-center'
},
[
h(QBtn, {
class:
currentYear === true && today.value.month === i + 1
? 'q-date__today'
: null,
flat: active !== true,
label: month,
unelevated: active,
color: active === true ? computedColor.value : null,
textColor: active === true ? computedTextColor.value : null,
tabindex: tabindex.value,
disable: isDisabled(i + 1),
...getCache('month#' + i, {
onClick: () => {
setMonth(i + 1)
}
})
})
]
)
})
if (props.yearsInMonthView === true) {
content.unshift(
h('div', { class: 'row no-wrap full-width' }, [
getNavigation({
label: viewModel.value.year,
type: 'Years',
key: viewModel.value.year,
dir: yearDirection.value,
goTo: goToYear,
boundaries: navBoundaries.value.year,
cls: ' col'
})
])
)
}
return h(
'div',
{
key: 'months-view',
class: 'q-date__view q-date__months flex flex-center'
},
content
)
},
Years() {
const start = startYear.value,
stop = start + yearsInterval,
years = []
const isDisabled = year =>
(minNav.value !== null && minNav.value.year > year) ||
(maxNav.value !== null && maxNav.value.year < year)
for (let i = start; i <= stop; i++) {
const active = viewModel.value.year === i
years.push(
h(
'div',
{
class: 'q-date__years-item flex flex-center'
},
[
h(QBtn, {
key: 'yr' + i,
class: today.value.year === i ? 'q-date__today' : null,
flat: !active,
label: i,
dense: true,
unelevated: active,
color: active === true ? computedColor.value : null,
textColor: active === true ? computedTextColor.value : null,
tabindex: tabindex.value,
disable: isDisabled(i),
...getCache('yr#' + i, {
onClick: () => {
setYear(i)
}
})
})
]
)
)
}
return h(
'div',
{
class: 'q-date__view q-date__years flex flex-center'
},
[
h(
'div',
{
class: 'col-auto'
},
[
h(QBtn, {
round: true,
dense: true,
flat: true,
icon: dateArrow.value[0],
'aria-label': $q.lang.date.prevRangeYears(yearsInterval),
tabindex: tabindex.value,
disable: isDisabled(start),
...getCache('y-', {
onClick: () => {
startYear.value -= yearsInterval
}
})
})
]
),
h(
'div',
{
class: 'q-date__years-content col self-stretch row items-center'
},
years
),
h(
'div',
{
class: 'col-auto'
},
[
h(QBtn, {
round: true,
dense: true,
flat: true,
icon: dateArrow.value[1],
'aria-label': $q.lang.date.nextRangeYears(yearsInterval),
tabindex: tabindex.value,
disable: isDisabled(stop),
...getCache('y+', {
onClick: () => {
startYear.value += yearsInterval
}
})
})
]
)
]
)
}
}
function onDayClick(dayIndex) {
const day = { ...viewModel.value, day: dayIndex }
if (props.range === false) {
toggleDate(day, viewMonthHash.value)
return
}
if (editRange.value === null) {
const dayProps = days.value.find(
item => item.fill !== true && item.i === dayIndex
)
if (props.noUnset !== true && dayProps.range !== void 0) {
removeFromModel({
target: day,
from: dayProps.range.from,
to: dayProps.range.to
})
return
}
if (dayProps.selected === true) {
removeFromModel(day)
return
}
const initHash = getDayHash(day)
editRange.value = {
init: day,
initHash,
final: day,
finalHash: initHash
}
emit('rangeStart', getShortDate(day))
} else {
const initHash = editRange.value.initHash,
finalHash = getDayHash(day),
payload =
initHash <= finalHash
? { from: editRange.value.init, to: day }
: { from: day, to: editRange.value.init }
editRange.value = null
addToModel(initHash === finalHash ? day : { target: day, ...payload })
emit('rangeEnd', {
from: getShortDate(payload.from),
to: getShortDate(payload.to)
})
}
}
function onDayMouseover(dayIndex) {
if (editRange.value !== null) {
const final = { ...viewModel.value, day: dayIndex }
Object.assign(editRange.value, {
final,
finalHash: getDayHash(final)
})
}
}
// expose public methods
Object.assign(proxy, {
setToday,
setView,
offsetCalendar,
setCalendarTo,
setEditingRange
})
return () => {
const content = [
h(
'div',
{
class: 'q-date__content col relative-position'
},
[
h(
Transition,
{
name: 'q-transition--fade'
},
renderViews[view.value]
)
]
)
]
const def = hSlot(slots.default)
if (def !== void 0) {
content.push(h('div', { class: 'q-date__actions' }, def))
}
if (props.name !== void 0 && props.disable !== true) {
injectFormInput(content, 'push')
}
return h(
'div',
{
class: classes.value,
...attributes.value
},
[
getHeader(),
h(
'div',
{
ref: blurTargetRef,
class: 'q-date__main col column',
tabindex: -1
},
content
)
]
)
}
}
})