From fb3df332351f65abaa3c945b2ffb5413f304ebb6 Mon Sep 17 00:00:00 2001 From: Dan Popescu Date: Sat, 20 Feb 2021 09:35:31 +0200 Subject: [PATCH] feat(QTime): add change event; sync view and focused header; fixes for kbd navigation; allow parsing partial date/time strings #8487 #8487 --- ui/dev/src/pages/form/time.vue | 78 +++++++++++++++++ ui/src/components/time/QTime.js | 79 +++++++++++------ ui/src/components/time/QTime.json | 71 ++++++++++++++++ ui/src/utils/date.js | 137 ++++++++++++++++-------------- 4 files changed, 275 insertions(+), 90 deletions(-) diff --git a/ui/dev/src/pages/form/time.vue b/ui/dev/src/pages/form/time.vue index 568caca6ecb4..e24dfa021cd0 100644 --- a/ui/dev/src/pages/form/time.vue +++ b/ui/dev/src/pages/form/time.vue @@ -163,6 +163,67 @@ + +
+ Close on change +
+ + + + + + + + + + + +
+ + + + +
@@ -180,6 +241,8 @@ export default { nowBtn: false, time: '10:56', + timeWithSeconds: '10:50:30', + timePartial: '12:10:', nullTime: null, input: null, input2: '12:35 PM', @@ -220,6 +283,21 @@ export default { if (min !== null && (min <= 25 || min >= 58)) { return false } if (sec !== null && sec % 10 !== 0) { return false } return true + }, + + onChangeTime (...ev) { + console.log('time', ...ev) + this.$refs.timeProxy.hide() + }, + + onChangeTimeWithSeconds (...ev) { + console.log('timeWithSeconds', ...ev) + this.$refs.timeWithSecondsProxy.hide() + }, + + onChangeTimePartial (...ev) { + console.log('timePartial', ...ev) + this.$refs.timePartialProxy.hide() } } } diff --git a/ui/src/components/time/QTime.js b/ui/src/components/time/QTime.js index 95f11bd9ebc9..a2efb81180f2 100644 --- a/ui/src/components/time/QTime.js +++ b/ui/src/components/time/QTime.js @@ -5,11 +5,15 @@ import TouchPan from '../../directives/TouchPan.js' import { slot } from '../../utils/slot.js' import { formatDate, __splitDate } from '../../utils/date.js' -import { position } from '../../utils/event.js' +import { position, prevent } from '../../utils/event.js' import { pad } from '../../utils/format.js' import cache from '../../utils/cache.js' import DateTimeMixin from '../../mixins/datetime.js' +function preventKbdArrows (ev) { + [ 37, 38, 39, 40 ].includes(ev.keyCode) === true && prevent(ev) +} + export default Vue.extend({ name: 'QTime', @@ -87,10 +91,14 @@ export default Vue.extend({ this.innerModel = model if (model.hour === null) { - this.view = 'Hour' + this.__goToView('Hour') } else { this.isAM = model.hour < 12 + + if (model.minute === null && this.view === 'Second') { + this.__goToView('Minute') + } } } }, @@ -291,7 +299,7 @@ export default Vue.extend({ ...this.__getCurrentDate(), ...this.__getCurrentTime() }) - this.view = 'Hour' + this.__goToView('Hour') }, __getValidValues (start, count, testFn) { @@ -403,12 +411,24 @@ export default Vue.extend({ } }, + __goToView (view) { + if (this.view !== view) { + this.view = view + } + if (this.$refs[view] !== void 0 && this.$refs[view] !== document.activeElement) { + this.$refs[view].focus() + } + }, + __goToNextView () { if (this.view === 'Hour') { - this.view = 'Minute' + this.__goToView('Minute') } else if (this.withSeconds && this.view === 'Minute') { - this.view = 'Second' + this.__goToView('Second') + } + else { + this.__updateValue(void 0, true) } }, @@ -514,10 +534,10 @@ export default Vue.extend({ __onKeyupHour (e) { if (e.keyCode === 13) { // ENTER - this.view = 'Hour' + this.view === 'Hour' && this.__goToNextView() } - else if ([ 37, 39 ].includes(e.keyCode)) { - const payload = e.keyCode === 37 ? -1 : 1 + else if ([ 37, 38, 39, 40 ].includes(e.keyCode)) { + const payload = e.keyCode === 37 || e.keyCode === 40 ? -1 : 1 if (this.validHours !== void 0) { const values = this.computedFormat24h === true @@ -552,10 +572,10 @@ export default Vue.extend({ __onKeyupMinute (e) { if (e.keyCode === 13) { // ENTER - this.view = 'Minute' + this.view === 'Minute' && this.__goToNextView() } - else if ([ 37, 39 ].includes(e.keyCode)) { - const payload = e.keyCode === 37 ? -1 : 1 + else if ([ 37, 38, 39, 40 ].includes(e.keyCode)) { + const payload = e.keyCode === 37 || e.keyCode === 40 ? -1 : 1 if (this.validMinutes !== void 0) { const values = this.validMinutes.values @@ -584,10 +604,10 @@ export default Vue.extend({ __onKeyupSecond (e) { if (e.keyCode === 13) { // ENTER - this.view = 'Second' + this.view === 'Second' && this.__goToNextView() } - else if ([ 37, 39 ].includes(e.keyCode)) { - const payload = e.keyCode === 37 ? -1 : 1 + else if ([ 37, 38, 39, 40 ].includes(e.keyCode)) { + const payload = e.keyCode === 37 || e.keyCode === 40 ? -1 : 1 if (this.validSeconds !== void 0) { const values = this.validSeconds.values @@ -619,9 +639,10 @@ export default Vue.extend({ h('div', { staticClass: 'q-time__link', class: this.view === 'Hour' ? 'q-time__link--active' : 'cursor-pointer', - attrs: { tabindex: this.computedTabindex }, + attrs: { tabindex: this.computedTabindex, 'data-autofocus': this.view === 'Hour' }, on: cache(this, 'vH', { - click: () => { this.view = 'Hour' }, + focus: () => { this.view = 'Hour' }, + keydown: preventKbdArrows, keyup: this.__onKeyupHour }) }, [ this.stringModel.hour ]), @@ -632,11 +653,13 @@ export default Vue.extend({ 'div', this.minLink === true ? { + ref: 'Minute', staticClass: 'q-time__link', class: this.view === 'Minute' ? 'q-time__link--active' : 'cursor-pointer', - attrs: { tabindex: this.computedTabindex }, + attrs: { tabindex: this.computedTabindex, 'data-autofocus': this.view === 'Minute' }, on: cache(this, 'vM', { - click: () => { this.view = 'Minute' }, + focus: () => { this.view = 'Minute' }, + keydown: preventKbdArrows, keyup: this.__onKeyupMinute }) } @@ -653,11 +676,13 @@ export default Vue.extend({ 'div', this.secLink === true ? { + ref: 'Second', staticClass: 'q-time__link', class: this.view === 'Second' ? 'q-time__link--active' : 'cursor-pointer', - attrs: { tabindex: this.computedTabindex }, + attrs: { tabindex: this.computedTabindex, 'data-autofocus': this.view === 'Second' }, on: cache(this, 'vS', { - click: () => { this.view = 'Second' }, + focus: () => { this.view = 'Second' }, + keydown: preventKbdArrows, keyup: this.__onKeyupSecond }) } @@ -819,20 +844,20 @@ export default Vue.extend({ __verifyAndUpdate () { if (this.hourInSelection !== void 0 && this.hourInSelection(this.innerModel.hour) !== true) { this.innerModel = __splitDate() - this.view = 'Hour' + this.__goToView('Hour') return } if (this.minuteInSelection !== void 0 && this.minuteInSelection(this.innerModel.minute) !== true) { this.innerModel.minute = null this.innerModel.second = null - this.view = 'Minute' + this.__goToView('Minute') return } if (this.withSeconds === true && this.secondInSelection !== void 0 && this.secondInSelection(this.innerModel.second) !== true) { this.innerModel.second = null - this.view = 'Second' + this.__goToView('Second') return } @@ -843,7 +868,7 @@ export default Vue.extend({ this.__updateValue() }, - __updateValue (obj) { + __updateValue (obj, final) { const date = Object.assign({ ...this.innerModel }, obj) const val = this.calendar === 'persian' @@ -867,7 +892,11 @@ export default Vue.extend({ ) date.changed = val !== this.value - this.$emit('input', val, date) + + if (final !== true || date.changed === true) { + this.$emit('input', val, date) + } + final === true && this.$emit('change', val, date) } }, diff --git a/ui/src/components/time/QTime.json b/ui/src/components/time/QTime.json index 107cb641a0b0..d70ec97ae901 100644 --- a/ui/src/components/time/QTime.json +++ b/ui/src/components/time/QTime.json @@ -171,6 +171,77 @@ } } } + }, + + "change": { + "extends": "input", + "desc": "Fires at the end of time adjustment (click or drag end) on the minutes screen (or seconds one if withSeconds is set)", + "params": { + "value": { + "type": "String" + }, + "details": { + "type": "Object", + "desc": "Object of properties on the new model", + "definition": { + "year": { + "type": "Number", + "desc": "The year", + "__exemption": [ + "examples" + ] + }, + "month": { + "type": "Number", + "desc": "The month", + "__exemption": [ + "examples" + ] + }, + "day": { + "type": "Number", + "desc": "The day of the month", + "__exemption": [ + "examples" + ] + }, + "hour": { + "type": "Number", + "desc": "The hour", + "__exemption": [ + "examples" + ] + }, + "minute": { + "type": "Number", + "desc": "The minute", + "__exemption": [ + "examples" + ] + }, + "second": { + "type": "Number", + "desc": "The second", + "__exemption": [ + "examples" + ] + }, + "millisecond": { + "type": "Number", + "desc": "The millisecond", + "__exemption": [ + "examples" + ] + }, + "changed": { + "type": "Boolean", + "desc": "Did the model change?", + "addedIn": "v1.1.1" + } + } + } + }, + "addedIn": "v1.15.5" } }, diff --git a/ui/src/utils/date.js b/ui/src/utils/date.js index bcd4852712ca..48ee2c815010 100644 --- a/ui/src/utils/date.js +++ b/ui/src/utils/date.js @@ -16,7 +16,7 @@ const function getRegexData (mask, dateLocale) { const - days = '(' + dateLocale.days.join('|') + ')', + days = '(' + dateLocale.days.join('|') + '|)', key = mask + days if (regexStore[key] !== void 0) { @@ -24,9 +24,9 @@ function getRegexData (mask, dateLocale) { } const - daysShort = '(' + dateLocale.daysShort.join('|') + ')', - months = '(' + dateLocale.months.join('|') + ')', - monthsShort = '(' + dateLocale.monthsShort.join('|') + ')' + daysShort = '(' + dateLocale.daysShort.join('|') + '|)', + months = '(' + dateLocale.months.join('|') + '|)', + monthsShort = '(' + dateLocale.monthsShort.join('|') + '|)' const map = {} let index = 0 @@ -36,16 +36,16 @@ function getRegexData (mask, dateLocale) { switch (match) { case 'YY': map.YY = index - return '(-?\\d{1,2})' + return '(-?\\d{1,2}|)' case 'YYYY': map.YYYY = index - return '(-?\\d{1,4})' + return '(-?\\d{1,4}|)' case 'M': map.M = index - return '(\\d{1,2})' + return '(\\d{1,2}|)' case 'MM': map.M = index // bumping to M - return '(\\d{2})' + return '(\\d{2}|)' case 'MMM': map.MMM = index return monthsShort @@ -54,55 +54,55 @@ function getRegexData (mask, dateLocale) { return months case 'D': map.D = index - return '(\\d{1,2})' + return '(\\d{1,2}|)' case 'Do': - map.D = index++ // bumping to D - return '(\\d{1,2}(st|nd|rd|th))' + map.D = index // bumping to D + return '(\\d{1,2}(?:st|nd|rd|th)|)' case 'DD': map.D = index // bumping to D - return '(\\d{2})' + return '(\\d{2}|)' case 'H': map.H = index - return '(\\d{1,2})' + return '(\\d{1,2}|)' case 'HH': map.H = index // bumping to H - return '(\\d{2})' + return '(\\d{2}|)' case 'h': map.h = index - return '(\\d{1,2})' + return '(\\d{1,2}|)' case 'hh': map.h = index // bumping to h - return '(\\d{2})' + return '(\\d{2}|)' case 'm': map.m = index - return '(\\d{1,2})' + return '(\\d{1,2}|)' case 'mm': map.m = index // bumping to m - return '(\\d{2})' + return '(\\d{2}|)' case 's': map.s = index - return '(\\d{1,2})' + return '(\\d{1,2}|)' case 'ss': map.s = index // bumping to s - return '(\\d{2})' + return '(\\d{2}|)' case 'S': map.S = index - return '(\\d{1})' + return '(\\d{1}|)' case 'SS': map.S = index // bump to S - return '(\\d{2})' + return '(\\d{2}|)' case 'SSS': map.S = index // bump to S - return '(\\d{3})' + return '(\\d{3}|)' case 'A': map.A = index - return '(AM|PM)' + return '(AM|PM|)' case 'a': map.a = index - return '(am|pm)' + return '(am|pm|)' case 'aa': map.aa = index - return '(a\\.m\\.|p\\.m\\.)' + return '(a\\.m\\.|p\\.m\\.|)' case 'ddd': return daysShort @@ -111,30 +111,30 @@ function getRegexData (mask, dateLocale) { case 'Q': case 'd': case 'E': - return '(\\d{1})' + return '(\\d{1}|)' case 'Qo': - return '(1st|2nd|3rd|4th)' + return '(1st|2nd|3rd|4th|)' case 'DDD': case 'DDDD': - return '(\\d{1,3})' + return '(\\d{1,3}|)' case 'w': - return '(\\d{1,2})' + return '(\\d{1,2}|)' case 'ww': - return '(\\d{2})' + return '(\\d{2}|)' case 'Z': // to split: (?:(Z)()()|([+-])?(\\d{2}):?(\\d{2})) map.Z = index - return '(Z|[+-]\\d{2}:\\d{2})' + return '(Z|[+-]\\d{2}:\\d{2}|)' case 'ZZ': map.ZZ = index - return '(Z|[+-]\\d{2}\\d{2})' + return '(Z|[+-]\\d{2}\\d{2}|)' case 'X': map.X = index - return '(-?\\d+)' + return '(-?\\d+|)' case 'x': map.x = index - return '(-?\\d{4,})' + return '(-?\\d{4,}|)' default: index-- @@ -213,16 +213,23 @@ export function __splitDate (str, mask, dateLocale, calendar, defaultModel) { return date } + const idx = { ...map } + Object.keys(idx).forEach(key => { + if (match[idx[key]] === '') { + idx[key] = void 0 + } + }) + let tzString = '' - if (map.X !== void 0 || map.x !== void 0) { - const stamp = parseInt(match[map.X !== void 0 ? map.X : map.x], 10) + if (idx.X !== void 0 || idx.x !== void 0) { + const stamp = parseInt(match[idx.X !== void 0 ? idx.X : idx.x], 10) if (isNaN(stamp) === true || stamp < 0) { return date } - const d = new Date(stamp * (map.X !== void 0 ? 1000 : 1)) + const d = new Date(stamp * (idx.X !== void 0 ? 1000 : 1)) date.year = d.getFullYear() date.month = d.getMonth() + 1 @@ -233,29 +240,29 @@ export function __splitDate (str, mask, dateLocale, calendar, defaultModel) { date.millisecond = d.getMilliseconds() } else { - if (map.YYYY !== void 0) { - date.year = parseInt(match[map.YYYY], 10) + if (idx.YYYY !== void 0) { + date.year = parseInt(match[idx.YYYY], 10) } - else if (map.YY !== void 0) { - const y = parseInt(match[map.YY], 10) + else if (idx.YY !== void 0) { + const y = parseInt(match[idx.YY], 10) date.year = y < 0 ? y : 2000 + y } - if (map.M !== void 0) { - date.month = parseInt(match[map.M], 10) + if (idx.M !== void 0) { + date.month = parseInt(match[idx.M], 10) if (date.month < 1 || date.month > 12) { return date } } - else if (map.MMM !== void 0) { - date.month = monthsShort.indexOf(match[map.MMM]) + 1 + else if (idx.MMM !== void 0) { + date.month = monthsShort.indexOf(match[idx.MMM]) + 1 } - else if (map.MMMM !== void 0) { - date.month = months.indexOf(match[map.MMMM]) + 1 + else if (idx.MMMM !== void 0) { + date.month = months.indexOf(match[idx.MMMM]) + 1 } - if (map.D !== void 0) { - date.day = parseInt(match[map.D], 10) + if (idx.D !== void 0) { + date.day = parseInt(match[idx.D], 10) if (date.year === null || date.month === null || date.day < 1) { return date @@ -270,35 +277,35 @@ export function __splitDate (str, mask, dateLocale, calendar, defaultModel) { } } - if (map.H !== void 0) { - date.hour = parseInt(match[map.H], 10) % 24 + if (idx.H !== void 0) { + date.hour = parseInt(match[idx.H], 10) % 24 } - else if (map.h !== void 0) { - date.hour = parseInt(match[map.h], 10) % 12 + else if (idx.h !== void 0) { + date.hour = parseInt(match[idx.h], 10) % 12 if ( - (map.A && match[map.A] === 'PM') || - (map.a && match[map.a] === 'pm') || - (map.aa && match[map.aa] === 'p.m.') + (idx.A && match[idx.A] === 'PM') || + (idx.a && match[idx.a] === 'pm') || + (idx.aa && match[idx.aa] === 'p.m.') ) { date.hour += 12 } date.hour = date.hour % 24 } - if (map.m !== void 0) { - date.minute = parseInt(match[map.m], 10) % 60 + if (idx.m !== void 0) { + date.minute = parseInt(match[idx.m], 10) % 60 } - if (map.s !== void 0) { - date.second = parseInt(match[map.s], 10) % 60 + if (idx.s !== void 0) { + date.second = parseInt(match[idx.s], 10) % 60 } - if (map.S !== void 0) { - date.millisecond = parseInt(match[map.S], 10) * 10 ** (3 - match[map.S].length) + if (idx.S !== void 0) { + date.millisecond = parseInt(match[idx.S], 10) * 10 ** (3 - match[idx.S].length) } - if (map.Z !== void 0 || map.ZZ !== void 0) { - tzString = (map.Z !== void 0 ? match[map.Z].replace(':', '') : match[map.ZZ]) + if (idx.Z !== void 0 || idx.ZZ !== void 0) { + tzString = (idx.Z !== void 0 ? match[idx.Z].replace(':', '') : match[idx.ZZ]) date.timezoneOffset = (tzString[0] === '+' ? -1 : 1) * (60 * tzString.slice(1, 3) + 1 * tzString.slice(3, 5)) } }