quasar-framework
Version:
Build responsive SPA, SSR, PWA, Hybrid Mobile Apps and Electron apps, all simultaneously using the same codebase
914 lines (852 loc) • 27.2 kB
JavaScript
import { height, width, offset } from '../../utils/dom.js'
import { position, stopAndPrevent, getEventKey } from '../../utils/event.js'
import QBtn from '../btn/QBtn.js'
import { isSameDate, isValid, adjustDate } from '../../utils/date.js'
import DateMixin from './datetime-mixin.js'
import CanRenderMixin from '../../mixins/can-render.js'
import ParentFieldMixin from '../../mixins/parent-field.js'
import Ripple from '../../directives/ripple.js'
function convertToAmPm (hour) {
return hour === 0 ? 12 : (hour >= 13 ? hour - 12 : hour)
}
export default {
name: 'QDatetimePicker',
mixins: [DateMixin, ParentFieldMixin, CanRenderMixin],
props: {
defaultValue: [String, Number, Date],
disable: Boolean,
readonly: Boolean
},
directives: {
Ripple
},
data () {
return {
view: this.__calcView(this.defaultView),
dragging: false,
centerClockPos: 0,
fakeValue: {
year: null,
month: null
}
}
},
watch: {
value (val) {
if (!val) {
this.view = ['date', 'datetime'].includes(this.type) ? 'day' : 'hour'
}
},
view () {
this.__scrollView(true)
},
model () {
if (this.fakeValue.month !== this.month) {
this.fakeValue.month = this.month
this.__scrollView()
}
if (this.fakeValue.year !== this.year) {
this.fakeValue.year = this.year
this.__scrollView()
}
}
},
computed: {
classes () {
const cls = []
this.disable && cls.push('disabled')
this.readonly && cls.push('readonly')
this.dark && cls.push('q-datetime-dark')
this.minimal && cls.push('q-datetime-minimal')
this.color && cls.push(`text-${this.color}`)
return cls
},
dateArrow () {
const val = [ this.$q.icon.datetime.arrowLeft, this.$q.icon.datetime.arrowRight ]
return this.$q.i18n.rtl ? val.reverse() : val
},
computedFormat24h () {
return this.format24h !== 0
? this.format24h
: this.$q.i18n.date.format24h
},
computedFirstDayOfWeek () {
return this.firstDayOfWeek !== void 0
? this.firstDayOfWeek
: this.$q.i18n.date.firstDayOfWeek
},
headerDayNames () {
const
days = this.$q.i18n.date.daysShort,
first = this.computedFirstDayOfWeek
return first > 0
? days.slice(first, 7).concat(days.slice(0, first))
: days
},
fakeModel () {
return new Date(this.fakeYear, this.fakeMonth - 1, 1)
},
fakeYear () {
return this.fakeValue.year || this.year
},
fakeMonth () {
return this.fakeValue.month || this.month
},
daysInMonth () {
return (new Date(this.fakeYear, this.fakeMonth, 0)).getDate()
},
monthString () {
return `${this.$q.i18n.date.monthsShort[this.month - 1]}`
},
monthStamp () {
return `${this.$q.i18n.date.months[this.fakeMonth - 1]} ${this.fakeYear}`
},
weekDayString () {
return this.headerLabel || this.$q.i18n.date.days[this.model.getDay()]
},
fillerDays () {
let days = (this.fakeModel.getDay() - this.computedFirstDayOfWeek)
if (days < 0) {
days += 7
}
return days
},
beforeMinDays () {
if (this.pmin === null) {
return false
}
const
pminYear = this.pmin.getFullYear(),
pminMonth = this.pmin.getMonth() + 1
if (pminYear === this.fakeYear && pminMonth === this.fakeMonth) {
return this.pmin.getDate() - 1
}
if (pminYear > this.fakeYear || (pminYear === this.fakeYear && pminMonth > this.fakeMonth)) {
return this.daysInMonth
}
return false
},
afterMaxDays () {
if (this.pmax === null) {
return false
}
const
pmaxYear = this.pmax.getFullYear(),
pmaxMonth = this.pmax.getMonth() + 1
if (pmaxYear === this.fakeYear && pmaxMonth === this.fakeMonth) {
return this.daysInMonth - this.maxDay
}
if (pmaxYear < this.fakeYear || (pmaxYear === this.fakeYear && pmaxMonth < this.fakeMonth)) {
return this.daysInMonth
}
return false
},
maxDay () {
return this.pmax !== null ? this.pmax.getDate() : this.daysInMonth
},
dateInterval () {
let after = this.pmax === null || this.afterMaxDays === false ? 0 : this.afterMaxDays
if (this.beforeMinDays > 0 || after) {
let min = this.beforeMinDays > 0 ? this.beforeMinDays + 1 : 1
return { min, max: this.daysInMonth - after }
}
return { min: 1, max: this.daysInMonth }
},
hour () {
const h = this.model.getHours()
return this.computedFormat24h
? h
: convertToAmPm(h)
},
minute () {
return this.model.getMinutes()
},
am () {
return this.model.getHours() <= 11
},
clockPointerStyle () {
let
forMinute = this.view === 'minute',
divider = forMinute ? 60 : 12,
degrees = Math.round((forMinute ? this.minute : this.hour) * (360 / divider)) - 180,
transforms = [`rotate(${degrees}deg)`]
if (!forMinute && this.computedFormat24h && !(this.hour > 0 && this.hour < 13)) {
transforms.push('scale(.7, .7)')
}
return { transform: transforms.join(' ') }
},
isValid () {
return isValid(this.value)
},
today () {
const today = new Date()
return isSameDate(today, this.fakeModel, 'month')
? today.getDate()
: -1
}
},
methods: {
/* date */
setYear (value, skipView) {
if (this.editable) {
if (!skipView) {
this.view = 'month'
}
this.model = new Date(new Date(this.model).setFullYear(this.__parseTypeValue('year', value)))
}
},
setMonth (value, skipView) {
if (this.editable) {
if (!skipView) {
this.view = 'day'
}
this.model = adjustDate(this.model, { month: value })
}
},
moveFakeMonth (direction) {
let
month = this.fakeMonth + (direction > 0 ? 1 : -1),
year = this.fakeYear
if (month < 1) {
month = 12
year -= 1
}
else if (month > 12) {
month = 1
year += 1
}
if (this.pmin !== null && direction > 0) {
const
pminYear = this.pmin.getFullYear(),
pminMonth = this.pmin.getMonth() + 1
if (year < pminYear) {
year = pminYear
month = pminMonth
}
else if (year === pminYear && month < pminMonth) {
month = pminMonth
}
}
if (this.pmax !== null && direction < 0) {
const
pmaxYear = this.pmax.getFullYear(),
pmaxMonth = this.pmax.getMonth() + 1
if (year > pmaxYear) {
year = pmaxYear
month = pmaxMonth
}
else if (year === pmaxYear && month > pmaxMonth) {
month = pmaxMonth
}
}
this.fakeValue.year = year
this.fakeValue.month = month
},
setDay (value, skipView, year, month) {
if (this.editable) {
if (year && month) {
const fake = adjustDate(this.model, { month })
fake.setFullYear(this.__parseTypeValue('year', year))
fake.setDate(this.__parseTypeValue('date', value))
this.model = fake
}
else {
this.model = new Date(new Date(this.model).setDate(this.__parseTypeValue('date', value)))
}
if (!skipView && this.type === 'date') {
this.$emit('canClose')
if (this.minimal) {
this.setView(this.defaultView)
}
}
else if (!skipView) {
this.view = 'hour'
}
}
},
setHour (value) {
if (!this.editable) {
return
}
value = this.__parseTypeValue('hour', value)
if (!this.computedFormat24h && value < 12 && !this.am) {
value += 12
}
this.model = new Date(new Date(this.model).setHours(value))
},
setMinute (value) {
if (!this.editable) {
return
}
this.model = new Date(new Date(this.model).setMinutes(this.__parseTypeValue('minute', value)))
},
setView (view) {
const newView = this.__calcView(view)
if (this.view !== newView) {
this.view = newView
}
},
/* helpers */
__calcView (view) {
switch (this.type) {
case 'time':
return ['hour', 'minute'].includes(view) ? view : 'hour'
case 'date':
return ['year', 'month', 'day'].includes(view) ? view : 'day'
default:
return ['year', 'month', 'day', 'hour', 'minute'].includes(view) ? view : 'day'
}
},
__pad (unit, filler) {
return (unit < 10 ? filler || '0' : '') + unit
},
__scrollView (delayed) {
if (this.view !== 'year' && this.view !== 'month') {
return
}
if (delayed) {
setTimeout(() => { this.__scrollView() }, 200) // wait to settle and recenter
}
const
el = this.$refs.selector,
itemInactive = el ? el.querySelector('.q-btn:not(.active)') : null,
itemActive = el ? el.querySelector('.q-btn.active') : null,
viewHeight = el ? el.offsetHeight : 0
this.$nextTick(() => {
const rowsAbove = this.view === 'year' ? this.year - this.yearInterval.min : this.month - this.monthMin - 1
if (viewHeight && itemActive) {
el.scrollTop = rowsAbove * (itemInactive ? itemInactive.offsetHeight : 0) + (itemActive.offsetHeight - viewHeight) / 2
}
})
},
__dragStart (ev, value) {
stopAndPrevent(ev)
const
clock = this.$refs.clock,
clockOffset = offset(clock)
this.centerClockPos = {
top: clockOffset.top + height(clock) / 2,
left: clockOffset.left + width(clock) / 2
}
this.dragging = true
this.__updateClock(ev, value)
},
__dragMove (ev) {
if (!this.dragging) {
return
}
stopAndPrevent(ev)
this.__updateClock(ev)
},
__dragStop (ev, value) {
stopAndPrevent(ev)
this.dragging = false
if (ev !== void 0) {
this.__updateClock(ev, value)
}
if (this.view === 'minute') {
this.$emit('canClose')
if (this.minimal) {
this.setView(this.defaultView)
}
}
else {
this.view = 'minute'
}
},
__updateClock (ev, value) {
if (value !== void 0) {
return this[this.view === 'hour' ? 'setHour' : 'setMinute'](value)
}
let
pos = position(ev),
height = Math.abs(pos.top - this.centerClockPos.top),
distance = Math.sqrt(
Math.pow(Math.abs(pos.top - this.centerClockPos.top), 2) +
Math.pow(Math.abs(pos.left - this.centerClockPos.left), 2)
),
angle = Math.asin(height / distance) * (180 / Math.PI)
if (pos.top < this.centerClockPos.top) {
angle = this.centerClockPos.left < pos.left ? 90 - angle : 270 + angle
}
else {
angle = this.centerClockPos.left < pos.left ? angle + 90 : 270 - angle
}
if (this.view === 'hour') {
let hour = Math.round(angle / 30)
if (this.computedFormat24h) {
if (!hour) {
hour = distance < 85 ? 0 : 12
}
else if (distance < 85) {
hour += 12
}
}
this.setHour(hour)
}
else {
this.setMinute(Math.round(angle / 6))
}
},
__repeatTimeout (count) {
return Math.max(100, 300 - count * count * 10)
},
__getTopSection (h) {
const child = [
this.typeHasDate
? h('div', { staticClass: 'q-datetime-weekdaystring' }, [this.weekDayString])
: void 0,
h('div', { staticClass: 'col' })
]
if (this.typeHasDate) {
const content = [
h('div', { staticClass: 'q-datetime-datestring row justify-center items-end' }, [
h('span', {
staticClass: 'q-datetime-link small col-auto col-md-12',
'class': {active: this.view === 'month'},
attrs: { tabindex: 0 },
on: {
keydown: e => {
const key = getEventKey(e)
if (key === 38 || key === 39) { // up, right
stopAndPrevent(e)
this.setMonth(this.month - 1, true)
}
else if (key === 40 || key === 37) { // down, left
stopAndPrevent(e)
this.setMonth(this.month + 1, true)
}
else if (key === 13 || key === 20) { // enter, space
this.view = 'month'
}
}
}
}, [
h('span', {
attrs: { tabindex: -1 },
on: this.disable ? {} : {
click: () => { this.view = 'month' }
}
}, [ this.monthString ])
]),
h('span', {
staticClass: 'q-datetime-link col-auto col-md-12',
'class': {active: this.view === 'day'},
attrs: { tabindex: 0 },
on: {
keydown: e => {
const key = getEventKey(e)
if (key === 37 || key === 38) { // left, up
stopAndPrevent(e)
this.setDay(this.day - (key === 37 ? 1 : 7), true)
}
else if (key === 39 || key === 40) { // right, down
stopAndPrevent(e)
this.setDay(this.day + (key === 39 ? 1 : 7), true)
}
else if (key === 13 || key === 20) { // enter, space
this.view = 'day'
}
}
}
}, [
h('span', {
attrs: { tabindex: -1 },
on: this.disable ? {} : {
click: () => { this.view = 'day' }
}
}, [ this.day ])
]),
h('span', {
staticClass: 'q-datetime-link small col-auto col-md-12',
'class': {active: this.view === 'year'},
attrs: { tabindex: 0 },
on: {
keydown: e => {
const key = getEventKey(e)
if (key === 38 || key === 39) { // up, right
stopAndPrevent(e)
this.setYear(this.year - 1, true)
}
else if (key === 40 || key === 37) { // down, left
stopAndPrevent(e)
this.setYear(this.year + 1, true)
}
else if (key === 13 || key === 20) { // enter, space
this.view = 'year'
}
}
}
}, [
h('span', {
attrs: { tabindex: -1 },
on: this.disable ? {} : {
click: () => { this.view = 'year' }
}
}, [ this.year ])
])
])
]
child.push(h('div', content))
}
if (this.typeHasTime) {
const ampm = (!this.computedFormat24h && h('span', {
staticClass: 'q-datetime-ampm column',
attrs: { tabindex: 0 },
on: this.__amPmEvents
}, [
h('span', {
staticClass: 'q-datetime-link',
'class': { active: this.am }
}, [
h('span', {
attrs: { tabindex: -1 },
on: { click: this.toggleAmPm }
}, [ 'AM' ])
]),
h('span', {
staticClass: 'q-datetime-link',
'class': { active: !this.am }
}, [
h('span', {
attrs: { tabindex: -1 },
on: { click: this.toggleAmPm }
}, [ 'PM' ])
])
]))
const content = [
h('span', {
staticClass: 'col-auto',
style: { textAlign: 'right' }
}, [
h('span', {
staticClass: 'q-datetime-link',
style: { textAlign: 'right' },
'class': {active: this.view === 'hour'},
attrs: { tabindex: 0 },
on: {
keydown: e => {
const key = getEventKey(e)
if (key === 40 || key === 37) { // down, left
stopAndPrevent(e)
this.setHour(this.hour - 1, true)
}
else if (key === 38 || key === 39) { // up, right
stopAndPrevent(e)
this.setHour(this.hour + 1, true)
}
else if (key === 13 || key === 20) { // enter, space
this.view = 'hour'
}
}
}
}, [
h('span', {
attrs: { tabindex: -1 },
on: this.disable ? {} : {
click: () => { this.view = 'hour' }
}
}, [ this.computedFormat24h ? this.__pad(this.hour) : this.hour ])
])
]),
h('span', { style: 'opacity:0.6;' }, [ ':' ]),
h('span', {
staticClass: 'col-auto row no-wrap items-center',
style: { textAlign: 'left' }
}, [
h('span', {
staticClass: 'q-datetime-link',
style: { textAlign: 'left' },
'class': {active: this.view === 'minute'},
attrs: { tabindex: 0 },
on: {
keydown: e => {
const key = getEventKey(e)
if (key === 40 || key === 37) { // down, left
stopAndPrevent(e)
this.setMinute(this.minute - 1, true)
}
else if (key === 38 || key === 39) { // up, right
stopAndPrevent(e)
this.setMinute(this.minute + 1, true)
}
else if (key === 13 || key === 20) { // enter, space
this.view = 'minute'
}
}
}
}, [
h('span', {
attrs: { tabindex: -1 },
on: this.disable ? {} : {
click: () => { this.view = 'minute' }
}
}, [ this.__pad(this.minute) ])
]),
ampm
])
]
child.push(h('div', {
staticClass: 'q-datetime-time row scroll flex-center'
}, [
h('div', {
staticClass: 'q-datetime-clockstring col row justify-center items-start'
}, content)
]))
}
child.push(h('div', { staticClass: 'col' }))
return h('div', {
staticClass: 'q-datetime-header column no-wrap items-center'
}, child)
},
__getYearView (h) {
const content = [h('div', { staticClass: 'col-grow' })] // vertical align when there are limits
for (let i = this.yearInterval.min; i <= this.yearInterval.max; i++) {
content.push(h(QBtn, {
staticClass: 'q-datetime-btn no-border-radius',
'class': {active: i === this.year},
attrs: { tabindex: -1 },
props: {
flat: true,
disable: !this.editable
},
on: {
click: () => {
this.setYear(i)
}
}
}, [ i ]))
}
content.push(h('div', { staticClass: 'col-grow' })) // vertical align when there are limits
return h('div', {
staticClass: `q-datetime-view-year fit column no-wrap`
}, content)
},
__getMonthView (h) {
const content = [h('div', { staticClass: 'col-grow' })] // vertical align when there are limits
for (let i = this.monthInterval.min; i <= this.monthInterval.max; i++) {
content.push(h(QBtn, {
staticClass: 'q-datetime-btn no-border-radius',
'class': {active: i + 1 === this.month},
attrs: { tabindex: -1 },
props: {
flat: true,
disable: !this.editable
},
on: {
click: () => {
this.setMonth(i + 1)
}
}
}, [ this.$q.i18n.date.months[i] ]))
}
content.push(h('div', { staticClass: 'col-grow' })) // vertical align when there are limits
return h('div', {
staticClass: `q-datetime-view-month fit column no-wrap`
}, content)
},
__getDayView (h) {
const
days = [],
day = this.fakeMonth === this.month && this.fakeYear === this.year ? this.day : -1
for (let i = 1; i <= this.fillerDays; i++) {
days.push(h('div', {
staticClass: 'q-datetime-fillerday'
}))
}
if (this.min) {
for (let i = 1; i <= this.beforeMinDays; i++) {
days.push(h('div', {
staticClass: 'row items-center content-center justify-center disabled',
'class': {
'q-datetime-day-active': this.isValid && i === day
}
}, [
h('span', [i])
]))
}
}
const { min, max } = this.dateInterval
for (let i = min; i <= max; i++) {
days.push(h('div', {
staticClass: 'row items-center content-center justify-center cursor-pointer',
'class': [this.color && i === day ? `text-${this.color}` : null, {
'q-datetime-day-active': this.isValid && i === day,
'q-datetime-day-today': i === this.today,
'disabled': !this.editable
}],
on: {
click: () => { this.setDay(i, false, this.fakeYear, this.fakeMonth) }
}
}, [
h('span', [ i ])
]))
}
if (this.max) {
for (let i = 1; i <= this.afterMaxDays; i++) {
days.push(h('div', {
staticClass: 'row items-center content-center justify-center disabled',
'class': {
'q-datetime-day-active': this.isValid && i + this.maxDay === day
}
}, [
h('span', [(i + this.maxDay)])
]))
}
}
return h('div', { staticClass: 'q-datetime-view-day' }, [
h('div', { staticClass: 'row items-center content-center' }, [
h(QBtn, {
staticClass: 'q-datetime-arrow',
attrs: { tabindex: -1 },
props: {
round: true,
dense: true,
flat: true,
icon: this.dateArrow[0],
repeatTimeout: this.__repeatTimeout,
disable: this.beforeMinDays > 0 || this.disable || this.readonly
},
on: {
click: () => { this.moveFakeMonth(-1) }
}
}),
h('div', { staticClass: 'col q-datetime-month-stamp' }, [
this.monthStamp
]),
h(QBtn, {
staticClass: 'q-datetime-arrow',
attrs: { tabindex: -1 },
props: {
round: true,
dense: true,
flat: true,
icon: this.dateArrow[1],
repeatTimeout: this.__repeatTimeout,
disable: this.afterMaxDays > 0 || this.disable || this.readonly
},
on: {
click: () => { this.moveFakeMonth(1) }
}
})
]),
h('div', {
staticClass: 'q-datetime-weekdays row no-wrap items-center justify-start'
}, this.headerDayNames.map(day => h('div', [ day ]))),
h('div', {
staticClass: 'q-datetime-days row wrap items-center justify-start content-center'
}, days)
])
},
__getClockView (h) {
let content = []
if (this.view === 'hour') {
let init, max, cls = ''
if (this.computedFormat24h) {
init = 0
max = 24
cls = ' fmt24'
}
else {
init = 1
max = 13
}
for (let i = init; i < max; i++) {
content.push(h('div', {
staticClass: `q-datetime-clock-position${cls}`,
'class': [`q-datetime-clock-pos-${i}`, i === this.hour ? 'active' : ''],
on: {
'!mousedown': ev => this.__dragStart(ev, i),
'!mouseup': ev => this.__dragStop(ev, i)
}
}, [ h('span', [ i || '00' ]) ]))
}
}
else {
for (let i = 0; i < 12; i++) {
const five = i * 5
content.push(h('div', {
staticClass: 'q-datetime-clock-position',
'class': [`q-datetime-clock-pos-${i}`, five === this.minute ? 'active' : '']
}, [
h('span', [ five ])
]))
}
}
return h('div', {
ref: 'clock',
key: 'clock' + this.view,
staticClass: 'column items-center content-center justify-center'
}, [
h('div', {
staticClass: 'q-datetime-clock cursor-pointer',
on: {
mousedown: this.__dragStart,
mousemove: this.__dragMove,
mouseup: this.__dragStop,
touchstart: this.__dragStart,
touchmove: this.__dragMove,
touchend: this.__dragStop
}
}, [
h('div', { staticClass: 'q-datetime-clock-circle full-width full-height' }, [
h('div', { staticClass: 'q-datetime-clock-center' }),
h('div', {
staticClass: 'q-datetime-clock-pointer',
style: this.clockPointerStyle
}, [ h('span') ]),
content
])
])
])
},
__getViewSection (h) {
switch (this.view) {
case 'year':
return this.__getYearView(h)
case 'month':
return this.__getMonthView(h)
case 'day':
return this.__getDayView(h)
case 'hour':
case 'minute':
return this.__getClockView(h)
}
}
},
created () {
this.__amPmEvents = {
keydown: e => {
const key = getEventKey(e)
if ([13, 32, 37, 38, 39, 40].includes(key)) { // enter, space, arrows
stopAndPrevent(e)
this.toggleAmPm()
}
}
}
},
mounted () {
this.__scrollView(true)
},
render (h) {
if (!this.canRender) { return }
return h('div', {
staticClass: 'q-datetime row',
'class': this.classes
}, [
(!this.minimal && this.__getTopSection(h)) || void 0,
h('div', {
staticClass: 'q-datetime-content scroll'
}, [
h('div', {
ref: 'selector',
staticClass: 'q-datetime-selector row items-center'
}, [
h('div', { 'class': 'col' }),
this.__getViewSection(h),
h('div', { 'class': 'col' })
])
].concat(this.$slots.default))
])
}
}