quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
781 lines (685 loc) • 22 kB
JavaScript
import Vue from 'vue'
import QBtn from '../btn/QBtn.js'
import DateTimeMixin from './datetime-mixin.js'
import { formatDate, __splitDate } from '../../utils/date.js'
import { pad } from '../../utils/format.js'
import { jalaaliMonthLength, toGregorian } from '../../utils/date-persian.js'
const yearsInterval = 20
const viewIsValid = v => ['Calendar', 'Years', 'Months'].includes(v)
export default Vue.extend({
name: 'QDate',
mixins: [ DateTimeMixin ],
props: {
title: String,
subtitle: String,
emitImmediately: Boolean,
mask: {
// this mask is forced
// when using persian calendar
default: 'YYYY/MM/DD'
},
defaultYearMonth: {
type: String,
validator: v => /^-?[\d]+\/[0-1]\d$/.test(v)
},
events: [Array, Function],
eventColor: [String, Function],
options: [Array, Function],
firstDayOfWeek: [String, Number],
todayBtn: Boolean,
minimal: Boolean,
defaultView: {
type: String,
default: 'Calendar',
validator: viewIsValid
}
},
data () {
const { inner, external } = this.__getModels(this.value, this.mask, this.__getComputedLocale())
return {
view: this.defaultView,
monthDirection: 'left',
yearDirection: 'left',
startYear: inner.year - inner.year % yearsInterval,
innerModel: inner,
extModel: external
}
},
watch: {
value (v) {
const { inner, external } = this.__getModels(v, this.mask, this.__getComputedLocale())
if (
this.extModel.dateHash !== external.dateHash ||
this.extModel.timeHash !== external.timeHash
) {
this.extModel = external
}
if (inner.dateHash !== this.innerModel.dateHash) {
this.monthDirection = this.innerModel.dateHash < inner.dateHash ? 'left' : 'right'
if (inner.year !== this.innerModel.year) {
this.yearDirection = this.monthDirection
}
this.$nextTick(() => {
this.startYear = inner.year - inner.year % yearsInterval
this.innerModel = inner
})
}
},
view () {
this.$refs.blurTarget !== void 0 && this.$refs.blurTarget.focus()
}
},
computed: {
classes () {
const type = this.landscape === true ? 'landscape' : 'portrait'
return `q-date--${type} q-date--${type}-${this.minimal === true ? 'minimal' : 'standard'}` +
(this.dark === true ? ' q-date--dark' : '') +
(this.bordered === true ? ` q-date--bordered` : '') +
(this.square === true ? ` q-date--square no-border-radius` : '') +
(this.flat === true ? ` q-date--flat no-shadow` : '') +
(this.readonly === true && this.disable !== true ? ' q-date--readonly' : '') +
(this.disable === true ? ' disabled' : '')
},
headerTitle () {
if (this.title !== void 0 && this.title !== null && this.title.length > 0) {
return this.title
}
const model = this.extModel
if (model.dateHash === null) { return ' --- ' }
let date
if (this.calendar !== 'persian') {
date = new Date(model.year, model.month - 1, model.day)
}
else {
const gDate = toGregorian(model.year, model.month, model.day)
date = new Date(gDate.gy, gDate.gm - 1, gDate.gd)
}
if (isNaN(date.valueOf()) === true) { return ' --- ' }
if (this.computedLocale.headerTitle !== void 0) {
return this.computedLocale.headerTitle(date, model)
}
return this.computedLocale.daysShort[ date.getDay() ] + ', ' +
this.computedLocale.monthsShort[ model.month - 1 ] + ' ' +
model.day
},
headerSubtitle () {
return this.subtitle !== void 0 && this.subtitle !== null && this.subtitle.length > 0
? this.subtitle
: (
this.extModel.year !== null
? this.extModel.year
: ' --- '
)
},
dateArrow () {
const val = [ this.$q.iconSet.datetime.arrowLeft, this.$q.iconSet.datetime.arrowRight ]
return this.$q.lang.rtl ? val.reverse() : val
},
computedFirstDayOfWeek () {
return this.firstDayOfWeek !== void 0
? Number(this.firstDayOfWeek)
: this.computedLocale.firstDayOfWeek
},
daysOfWeek () {
const
days = this.computedLocale.daysShort,
first = this.computedFirstDayOfWeek
return first > 0
? days.slice(first, 7).concat(days.slice(0, first))
: days
},
daysInMonth () {
return this.__getDaysInMonth(this.innerModel)
},
today () {
return this.__getCurrentDate()
},
evtFn () {
return typeof this.events === 'function'
? this.events
: date => this.events.includes(date)
},
evtColor () {
return typeof this.eventColor === 'function'
? this.eventColor
: date => this.eventColor
},
isInSelection () {
return typeof this.options === 'function'
? this.options
: date => this.options.includes(date)
},
days () {
let date, endDay
const res = []
if (this.calendar !== 'persian') {
date = new Date(this.innerModel.year, this.innerModel.month - 1, 1)
endDay = (new Date(this.innerModel.year, this.innerModel.month - 1, 0)).getDate()
}
else {
const gDate = toGregorian(this.innerModel.year, this.innerModel.month, 1)
date = new Date(gDate.gy, gDate.gm - 1, gDate.gd)
let prevJM = this.innerModel.month - 1
let prevJY = this.innerModel.year
if (prevJM === 0) {
prevJM = 12
prevJY--
}
endDay = jalaaliMonthLength(prevJY, prevJM)
}
const days = (date.getDay() - this.computedFirstDayOfWeek - 1)
const len = days < 0 ? days + 7 : days
if (len < 6) {
for (let i = endDay - len; i <= endDay; i++) {
res.push({ i, fill: true })
}
}
const
index = res.length,
prefix = this.innerModel.year + '/' + pad(this.innerModel.month) + '/'
for (let i = 1; i <= this.daysInMonth; i++) {
const day = prefix + pad(i)
if (this.options !== void 0 && this.isInSelection(day) !== true) {
res.push({ i })
}
else {
const event = this.events !== void 0 && this.evtFn(day) === true
? this.evtColor(day)
: false
res.push({ i, in: true, flat: true, event })
}
}
if (this.innerModel.year === this.extModel.year && this.innerModel.month === this.extModel.month) {
const i = index + this.innerModel.day - 1
res[i] !== void 0 && Object.assign(res[i], {
unelevated: true,
flat: false,
color: this.computedColor,
textColor: this.computedTextColor
})
}
if (this.innerModel.year === this.today.year && this.innerModel.month === this.today.month) {
res[index + this.today.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 })
}
}
return res
}
},
methods: {
setToday () {
this.__updateValue({ ...this.today }, 'today')
this.view = 'Calendar'
},
setView (view) {
if (viewIsValid(view) === true) {
this.view = view
}
},
offsetCalendar (type, descending) {
if (['month', 'year'].includes(type)) {
this[`__goTo${type === 'month' ? 'Month' : 'Year'}`](
descending === true ? -1 : 1
)
}
},
__getModels (val, mask, locale) {
const external = __splitDate(
val,
this.calendar === 'persian' ? 'YYYY/MM/DD' : mask,
locale,
this.calendar
)
return {
external,
inner: external.dateHash === null
? this.__getDefaultModel()
: { ...external }
}
},
__getDefaultModel () {
let year, month
if (this.defaultYearMonth !== void 0) {
const d = this.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 = this.today !== void 0
? this.today
: this.__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'
}
},
__getHeader (h) {
if (this.minimal === true) { return }
return h('div', {
staticClass: 'q-date__header',
class: this.headerClass
}, [
h('div', {
staticClass: 'relative-position'
}, [
h('transition', {
props: {
name: 'q-transition--fade'
}
}, [
h('div', {
key: 'h-yr-' + this.headerSubtitle,
staticClass: 'q-date__header-subtitle q-date__header-link',
class: this.view === 'Years' ? 'q-date__header-link--active' : 'cursor-pointer',
attrs: { tabindex: this.computedTabindex },
on: {
click: () => { this.view = 'Years' },
keyup: e => { e.keyCode === 13 && (this.view = 'Years') }
}
}, [ this.headerSubtitle ])
])
]),
h('div', {
staticClass: 'q-date__header-title relative-position flex no-wrap'
}, [
h('div', {
staticClass: 'relative-position col'
}, [
h('transition', {
props: {
name: 'q-transition--fade'
}
}, [
h('div', {
key: 'h-sub' + this.headerTitle,
staticClass: 'q-date__header-title-label q-date__header-link',
class: this.view === 'Calendar' ? 'q-date__header-link--active' : 'cursor-pointer',
attrs: { tabindex: this.computedTabindex },
on: {
click: () => { this.view = 'Calendar' },
keyup: e => { e.keyCode === 13 && (this.view = 'Calendar') }
}
}, [ this.headerTitle ])
])
]),
this.todayBtn === true ? h(QBtn, {
staticClass: 'q-date__header-today',
props: {
icon: this.$q.iconSet.datetime.today,
flat: true,
size: 'sm',
round: true,
tabindex: this.computedTabindex
},
on: {
click: this.setToday
}
}) : null
])
])
},
__getNavigation (h, { label, view, key, dir, goTo, cls }) {
return [
h('div', {
staticClass: 'row items-center q-date__arrow'
}, [
h(QBtn, {
props: {
round: true,
dense: true,
size: 'sm',
flat: true,
icon: this.dateArrow[0],
tabindex: this.computedTabindex
},
on: {
click () { goTo(-1) }
}
})
]),
h('div', {
staticClass: 'relative-position overflow-hidden flex flex-center' + cls
}, [
h('transition', {
props: {
name: 'q-transition--jump-' + dir
}
}, [
h('div', { key }, [
h(QBtn, {
props: {
flat: true,
dense: true,
noCaps: true,
label,
tabindex: this.computedTabindex
},
on: {
click: () => { this.view = view }
}
})
])
])
]),
h('div', {
staticClass: 'row items-center q-date__arrow'
}, [
h(QBtn, {
props: {
round: true,
dense: true,
size: 'sm',
flat: true,
icon: this.dateArrow[1],
tabindex: this.computedTabindex
},
on: {
click () { goTo(1) }
}
})
])
]
},
__getCalendarView (h) {
return [
h('div', {
key: 'calendar-view',
staticClass: 'q-date__view q-date__calendar'
}, [
h('div', {
staticClass: 'q-date__navigation row items-center no-wrap'
}, this.__getNavigation(h, {
label: this.computedLocale.months[ this.innerModel.month - 1 ],
view: 'Months',
key: this.innerModel.month,
dir: this.monthDirection,
goTo: this.__goToMonth,
cls: ' col'
}).concat(this.__getNavigation(h, {
label: this.innerModel.year,
view: 'Years',
key: this.innerModel.year,
dir: this.yearDirection,
goTo: this.__goToYear,
cls: ''
}))),
h('div', {
staticClass: 'q-date__calendar-weekdays row items-center no-wrap'
}, this.daysOfWeek.map(day => h('div', { staticClass: 'q-date__calendar-item' }, [ h('div', [ day ]) ]))),
h('div', {
staticClass: 'q-date__calendar-days-container relative-position overflow-hidden'
}, [
h('transition', {
props: {
name: 'q-transition--slide-' + this.monthDirection
}
}, [
h('div', {
key: this.innerModel.year + '/' + this.innerModel.month,
staticClass: 'q-date__calendar-days fit'
}, this.days.map(day => h('div', {
staticClass: `q-date__calendar-item q-date__calendar-item--${day.fill === true ? 'fill' : (day.in === true ? 'in' : 'out')}`
}, [
day.in === true
? h(QBtn, {
staticClass: day.today === true ? 'q-date__today' : null,
props: {
dense: true,
flat: day.flat,
unelevated: day.unelevated,
color: day.color,
textColor: day.textColor,
label: day.i,
tabindex: this.computedTabindex
},
on: {
click: () => { this.__setDay(day.i) }
}
}, day.event !== false ? [
h('div', { staticClass: 'q-date__event bg-' + day.event })
] : null)
: h('div', [ day.i ])
])))
])
])
])
]
},
__getMonthsView (h) {
const currentYear = this.innerModel.year === this.today.year
const content = this.computedLocale.monthsShort.map((month, i) => {
const active = this.innerModel.month === i + 1
return h('div', {
staticClass: 'q-date__months-item flex flex-center'
}, [
h(QBtn, {
staticClass: currentYear === true && this.today.month === i + 1 ? 'q-date__today' : null,
props: {
flat: !active,
label: month,
unelevated: active,
color: active ? this.computedColor : null,
textColor: active ? this.computedTextColor : null,
tabindex: this.computedTabindex
},
on: {
click: () => { this.__setMonth(i + 1) }
}
})
])
})
return h('div', {
key: 'months-view',
staticClass: 'q-date__view q-date__months column flex-center'
}, [
h('div', {
staticClass: 'q-date__months-content row'
}, content)
])
},
__getYearsView (h) {
const
start = this.startYear,
stop = start + yearsInterval,
years = []
for (let i = start; i <= stop; i++) {
const active = this.innerModel.year === i
years.push(
h('div', {
staticClass: 'q-date__years-item flex flex-center'
}, [
h(QBtn, {
staticClass: this.today.year === i ? 'q-date__today' : null,
props: {
flat: !active,
label: i,
dense: true,
unelevated: active,
color: active ? this.computedColor : null,
textColor: active ? this.computedTextColor : null,
tabindex: this.computedTabindex
},
on: {
click: () => { this.__setYear(i) }
}
})
])
)
}
return h('div', {
staticClass: 'q-date__view q-date__years flex flex-center full-height'
}, [
h('div', {
staticClass: 'col-auto'
}, [
h(QBtn, {
props: {
round: true,
dense: true,
flat: true,
icon: this.dateArrow[0],
tabindex: this.computedTabindex
},
on: {
click: () => { this.startYear -= yearsInterval }
}
})
]),
h('div', {
staticClass: 'q-date__years-content col full-height row items-center'
}, years),
h('div', {
staticClass: 'col-auto'
}, [
h(QBtn, {
props: {
round: true,
dense: true,
flat: true,
icon: this.dateArrow[1],
tabindex: this.computedTabindex
},
on: {
click: () => { this.startYear += yearsInterval }
}
})
])
])
},
__getDaysInMonth (obj) {
return this.calendar !== 'persian'
? (new Date(obj.year, obj.month, 0)).getDate()
: jalaaliMonthLength(obj.year, obj.month)
},
__goToMonth (offset) {
let
month = Number(this.innerModel.month) + offset,
yearDir = this.yearDirection
if (month === 13) {
month = 1
this.innerModel.year++
yearDir = 'left'
}
else if (month === 0) {
month = 12
this.innerModel.year--
yearDir = 'right'
}
this.monthDirection = offset > 0 ? 'left' : 'right'
this.yearDirection = yearDir
this.innerModel.month = month
this.emitImmediately === true && this.__updateValue({}, 'month')
},
__goToYear (offset) {
this.monthDirection = this.yearDirection = offset > 0 ? 'left' : 'right'
this.innerModel.year = Number(this.innerModel.year) + offset
this.emitImmediately === true && this.__updateValue({}, 'year')
},
__setYear (year) {
this.innerModel.year = year
this.emitImmediately === true && this.__updateValue({ year }, 'year')
this.view = this.extModel.month === null || this.defaultView === 'Years' ? 'Months' : 'Calendar'
},
__setMonth (month) {
this.innerModel.month = month
this.emitImmediately === true && this.__updateValue({ month }, 'month')
this.view = 'Calendar'
},
__setDay (day) {
this.__updateValue({ day }, 'day')
},
__updateValue (date, reason) {
if (date.year === void 0) {
date.year = this.innerModel.year
}
if (date.month === void 0) {
date.month = this.innerModel.month
}
if (
date.day === void 0 ||
(this.emitImmediately === true && (reason === 'year' || reason === 'month'))
) {
date.day = this.innerModel.day
const maxDay = this.emitImmediately === true
? this.__getDaysInMonth(date)
: this.daysInMonth
date.day = Math.min(date.day, maxDay)
}
const val = this.calendar === 'persian'
? date.year + '/' + pad(date.month) + '/' + pad(date.day)
: formatDate(
new Date(
date.year,
date.month - 1,
date.day,
this.extModel.hour,
this.extModel.minute,
this.extModel.second,
this.extModel.millisecond
),
this.mask,
this.computedLocale,
date.year
)
date.changed = val !== this.value
this.$emit('input', val, reason, date)
if (val === this.value && reason === 'today') {
const newHash = date.year + '/' + pad(date.month) + '/' + pad(date.day)
const curHash = this.innerModel.year + '/' + pad(this.innerModel.month) + '/' + pad(this.innerModel.day)
if (newHash !== curHash) {
this.monthDirection = curHash < newHash ? 'left' : 'right'
if (date.year !== this.innerModel.year) {
this.yearDirection = this.monthDirection
}
this.$nextTick(() => {
this.startYear = date.year - date.year % yearsInterval
Object.assign(this.innerModel, {
year: date.year,
month: date.month,
day: date.day,
dateHash: newHash
})
})
}
}
}
},
render (h) {
return h('div', {
staticClass: 'q-date',
class: this.classes,
on: this.$listeners
}, [
this.__getHeader(h),
h('div', {
staticClass: 'q-date__content relative-position overflow-auto',
attrs: { tabindex: -1 },
ref: 'blurTarget'
}, [
h('transition', {
props: {
name: 'q-transition--fade'
}
}, [
this[`__get${this.view}View`](h)
])
])
])
}
})