UNPKG

quasar-framework

Version:

Build responsive SPA, SSR, PWA, Hybrid Mobile Apps and Electron apps, all simultaneously using the same codebase

760 lines (701 loc) 21.8 kB
import { height, width, offset, cssTransform } 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, __amPmEvents: { keyup: e => { const key = getEventKey(e) if (key === 13 || key === 32) { // enter, space stopAndPrevent(e) this.toggleAmPm() } } } } }, watch: { value (val) { if (!val) { this.view = ['date', 'datetime'].includes(this.type) ? 'day' : 'hour' } }, view () { 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 }, contentClasses () { if (!this.minimal) { return 'col-md-8' } }, 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 }, monthString () { return `${this.$q.i18n.date.monthsShort[this.month - 1]}` }, monthStamp () { return `${this.$q.i18n.date.months[this.month - 1]} ${this.year}` }, weekDayString () { return this.$q.i18n.date.days[this.model.getDay()] }, fillerDays () { let days = (new Date(this.model.getFullYear(), this.model.getMonth(), 1).getDay() - this.computedFirstDayOfWeek) if (days < 0) { days += 7 } return days }, beforeMinDays () { if (this.pmin === null || !isSameDate(this.pmin, this.model, 'month')) { return false } return this.pmin.getDate() - 1 }, afterMaxDays () { if (this.pmax === null || !isSameDate(this.pmax, this.model, 'month')) { return false } return this.daysInMonth - this.maxDay }, maxDay () { return this.pmax !== null ? this.pmax.getDate() : this.daysInMonth }, daysInterval () { 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 + 1 } } 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 divider = this.view === 'minute' ? 60 : (this.computedFormat24h ? 24 : 12), degrees = Math.round((this.view === 'minute' ? this.minute : this.hour) * (360 / divider)) - 180 return cssTransform(`rotate(${degrees}deg)`) }, isValid () { return isValid(this.value) }, today () { const today = new Date() return isSameDate(today, this.model, 'month') ? today.getDate() : -1 } }, methods: { /* date */ setYear (value, skipView) { if (this.editable) { if (!skipView) { this.view = 'day' } this.model = 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}) } }, setDay (value, skipView) { if (this.editable) { this.model = new Date(this.model.setDate(this.__parseTypeValue('date', value))) if (!skipView && this.type === 'date') { this.$emit('canClose') } 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(this.model.setHours(value)) }, setMinute (value) { if (!this.editable) { return } this.model = 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 view ? (['hour', 'minute'].includes(view) ? view : 'hour') : 'hour' case 'date': return view ? (['year', 'month', 'day'].includes(view) ? view : 'day') : 'day' default: return view ? (['year', 'month', 'day', 'hour', 'minute'].includes(view) ? view : 'day') : 'day' } }, __pad (unit, filler) { return (unit < 10 ? filler || '0' : '') + unit }, __scrollView () { if (this.view !== 'year' && this.view !== 'month') { return } const el = this.$refs.selector, rows = this.view === 'year' ? this.year - this.yearMin : this.month - this.monthMin this.$nextTick(() => { if (el) { el.scrollTop = rows * height(el.children[0].children[0]) - height(el) / 2.5 } }) }, __dragStart (ev) { 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) }, __dragMove (ev) { if (!this.dragging) { return } stopAndPrevent(ev) this.__updateClock(ev) }, __dragStop (ev) { stopAndPrevent(ev) this.dragging = false if (this.view === 'minute') { this.$emit('canClose') } else { this.view = 'minute' } }, __updateClock (ev) { 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') { this.setHour(Math.round(angle / (this.computedFormat24h ? 15 : 30))) } else { this.setMinute(Math.round(angle / 6)) } }, __repeatTimeout (count) { return Math.max(100, 300 - count * count * 10) }, __getTopSection (h) { const child = [] if (this.typeHasDate) { const content = [ h('div', { staticClass: 'q-datetime-weekdaystring col-12' }, [ this.weekDayString ]), h('div', { staticClass: 'q-datetime-datestring row flex-center' }, [ 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 === 40 || key === 37) { // down, left stopAndPrevent(e) this.setMonth(this.month - 1, true) } else if (key === 38 || key === 39) { // up, right stopAndPrevent(e) this.setMonth(this.month + 1, true) } } } }, [ 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) } } } }, [ 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 === 40 || key === 37) { // down, left stopAndPrevent(e) this.setYear(this.year - 1, true) } else if (key === 38 || key === 39) { // up, right stopAndPrevent(e) this.setYear(this.year + 1, true) } } } }, [ h('span', { attrs: { tabindex: -1 }, on: this.disable ? {} : { click: () => { this.view = 'year' } } }, [ this.year ]) ]) ]) ] child.push(h('div', content)) } if (this.typeHasTime) { const content = [ h('span', { staticClass: 'q-datetime-link col-md text-right q-pr-sm', '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) } } } }, [ h('span', { attrs: { tabindex: -1 }, on: this.disable ? {} : { click: () => { this.view = 'hour' } } }, [ this.hour ]) ]), h('span', { style: 'opacity:0.6;' }, [ ':' ]), h('span', { staticClass: 'q-datetime-link col-md text-left q-pl-sm', '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.setHour(this.minute + 1, true) } } } }, [ h('span', { attrs: { tabindex: -1 }, on: this.disable ? {} : { click: () => { this.view = 'minute' } } }, [ this.__pad(this.minute) ]) ]) ] child.push(h('div', { staticClass: 'q-datetime-time row flex-center' }, [ h('div', { staticClass: 'q-datetime-clockstring col-auto col-md-12 row no-wrap flex-center' }, content), (!this.computedFormat24h && h('div', { staticClass: 'q-datetime-ampm column col-auto col-md-12 justify-around' }, [ h('div', { staticClass: 'q-datetime-link', 'class': { active: this.am }, attrs: { tabindex: 0 }, on: this.__amPmEvents }, [ h('span', { attrs: { tabindex: -1 }, on: { click: this.toggleAmPm } }, [ 'AM' ]) ]), h('div', { staticClass: 'q-datetime-link', 'class': { active: !this.am }, attrs: { tabindex: 0 }, on: this.__amPmEvents }, [ h('span', { attrs: { tabindex: -1 }, on: { click: this.toggleAmPm } }, [ 'PM' ]) ]) ])) ])) } return h('div', { staticClass: 'q-datetime-header column col-xs-12 col-md-4 justify-center' }, child) }, __getYearView (h) { const content = [], max = this.yearInterval + this.yearMin for (let i = this.yearMin; i <= max; i++) { content.push(h(QBtn, { key: `yi${i}`, staticClass: 'q-datetime-btn full-width', 'class': {active: i === this.year}, attrs: { tabindex: -1 }, props: { flat: true, disable: !this.editable }, on: { click: () => { this.setYear(i) } } }, [ i ])) } return h('div', { staticClass: `q-datetime-view-year full-width full-height` }, content) }, __getMonthView (h) { const content = [] for (let i = this.monthMin; i <= this.monthInterval; i++) { content.push(h(QBtn, { key: `mi${i}`, staticClass: 'q-datetime-btn full-width', 'class': {active: i + 1 === this.month}, attrs: { tabindex: -1 }, props: { flat: true, disable: !this.editable }, on: { click: () => { this.setMonth(i + 1, true) } } }, [ this.$q.i18n.date.months[i] ])) } return h('div', { staticClass: `q-datetime-view-month full-width full-height` }, content) }, __getDayView (h) { const days = [] for (let i = 1; i <= this.fillerDays; i++) { days.push(h('div', { key: `fd${i}`, staticClass: 'q-datetime-fillerday' })) } if (this.min) { for (let i = 1; i <= this.beforeMinDays; i++) { days.push(h('div', { key: `fb${i}`, staticClass: 'row items-center content-center justify-center disabled' }, [ i ])) } } const { min, max } = this.daysInterval for (let i = min; i <= max; i++) { days.push(h('div', { key: `md${i}`, staticClass: 'row items-center content-center justify-center cursor-pointer', 'class': [this.color && i === this.day ? `text-${this.color}` : null, { 'q-datetime-day-active': this.isValid && i === this.day, 'q-datetime-day-today': i === this.today, 'disabled': !this.editable }], on: { click: () => { this.setDay(i) } } }, [ h('span', [ i ]) ])) } if (this.max) { for (let i = 1; i <= this.afterMaxDays; i++) { days.push(h('div', { key: `fa${i}`, staticClass: 'row items-center content-center justify-center disabled' }, [ (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.setMonth(this.month - 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.setMonth(this.month + 1) } } }) ]), h('div', { staticClass: 'q-datetime-weekdays row items-center justify-start' }, this.headerDayNames.map(day => h('div', { key: `dh${day}` }, [ 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', { key: `hi${i}`, staticClass: `q-datetime-clock-position${cls}`, 'class': [`q-datetime-clock-pos-${i}`, i === this.hour ? 'active' : ''] }, [ h('span', [ i ]) ])) } } else { for (let i = 0; i < 12; i++) { const five = i * 5 content.push(h('div', { key: `mi${i}`, staticClass: 'q-datetime-clock-position', 'class': ['q-datetime-clock-pos-' + i, five === this.minute ? 'active' : ''] }, [ h('span', [ five ]) ])) } } return h('div', { ref: 'clock', 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) } } }, mounted () { this.$nextTick(() => { this.__scrollView() }) }, 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 col-xs-12 column', 'class': this.contentClasses }, [ h('div', { ref: 'selector', staticClass: 'q-datetime-selector auto row flex-center' }, [ this.__getViewSection(h) ]) ].concat(this.$slots.default)) ]) } }