diff --git a/docs/src/examples/QDate/DaySlot.vue b/docs/src/examples/QDate/DaySlot.vue new file mode 100644 index 000000000000..612a116e584f --- /dev/null +++ b/docs/src/examples/QDate/DaySlot.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/docs/src/examples/QDate/Input.vue b/docs/src/examples/QDate/Input.vue index 666d99de505c..b50feb1939bf 100644 --- a/docs/src/examples/QDate/Input.vue +++ b/docs/src/examples/QDate/Input.vue @@ -4,11 +4,7 @@ diff --git a/docs/src/examples/QDate/InputFull.vue b/docs/src/examples/QDate/InputFull.vue index eb0fb1d03b13..2a8875197419 100644 --- a/docs/src/examples/QDate/InputFull.vue +++ b/docs/src/examples/QDate/InputFull.vue @@ -4,11 +4,7 @@ @@ -16,11 +12,7 @@ diff --git a/docs/src/examples/QDate/IntervalSelection.vue b/docs/src/examples/QDate/IntervalSelection.vue new file mode 100644 index 000000000000..04df2cde474e --- /dev/null +++ b/docs/src/examples/QDate/IntervalSelection.vue @@ -0,0 +1,124 @@ + + + diff --git a/docs/src/examples/QDate/IntervalSelectionConstrained.vue b/docs/src/examples/QDate/IntervalSelectionConstrained.vue new file mode 100644 index 000000000000..f137e7125cbb --- /dev/null +++ b/docs/src/examples/QDate/IntervalSelectionConstrained.vue @@ -0,0 +1,167 @@ + + + diff --git a/docs/src/examples/QDate/WeekSelection.vue b/docs/src/examples/QDate/WeekSelection.vue new file mode 100644 index 000000000000..6e544a85f7cf --- /dev/null +++ b/docs/src/examples/QDate/WeekSelection.vue @@ -0,0 +1,39 @@ + + + diff --git a/docs/src/examples/QTime/Input.vue b/docs/src/examples/QTime/Input.vue index 720440bcb728..58c39b89db47 100644 --- a/docs/src/examples/QTime/Input.vue +++ b/docs/src/examples/QTime/Input.vue @@ -5,11 +5,7 @@ @@ -23,11 +19,7 @@ v-model="timeWithSeconds" with-seconds format24h - > -
- -
- + /> diff --git a/docs/src/examples/QTime/InputFull.vue b/docs/src/examples/QTime/InputFull.vue index eb0fb1d03b13..2a8875197419 100644 --- a/docs/src/examples/QTime/InputFull.vue +++ b/docs/src/examples/QTime/InputFull.vue @@ -4,11 +4,7 @@ @@ -16,11 +12,7 @@ diff --git a/docs/src/pages/vue-components/date.md b/docs/src/pages/vue-components/date.md index e19207cc1406..2b427218f1af 100644 --- a/docs/src/pages/vue-components/date.md +++ b/docs/src/pages/vue-components/date.md @@ -47,7 +47,7 @@ Notice in the examples below that the model is an Object (single selection) or a ::: tip TIPS * Clicking on an already selected day will deselect it. * The user's current editing range can also be set programmatic through the `setEditingRange` method (check the API card). -* There are two useful events in regards to the current editing range: `range-start` and `range-end` (check the API card). +* There are three useful events in regards to the current editing range: `range-start`, `range-change` and `range-end` (check the API card). ::: ::: warning @@ -134,6 +134,12 @@ The first example is using an array and the second example is using a function. +### Day scoped slot + +You can use the `day` scoped slot to render custom event markers or tooltips specific to each day. + + + ### Limiting options * You can use the `options` prop to limit user selection to certain times. @@ -157,6 +163,18 @@ You can use the default slot for adding buttons: +* Use 2 components to allow easy interval selection + + + +* Adjust the selection to match a fixed interval + + + +* Limit the minimum and maximum selection length + + + ### With QSplitter and QTabPanels diff --git a/ui/dev/src/pages/form/date-day-slot.vue b/ui/dev/src/pages/form/date-day-slot.vue new file mode 100644 index 000000000000..bf6c7f7fc7b0 --- /dev/null +++ b/ui/dev/src/pages/form/date-day-slot.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/ui/dev/src/pages/form/date-interval-selection-constrained.vue b/ui/dev/src/pages/form/date-interval-selection-constrained.vue new file mode 100644 index 000000000000..f137e7125cbb --- /dev/null +++ b/ui/dev/src/pages/form/date-interval-selection-constrained.vue @@ -0,0 +1,167 @@ + + + diff --git a/ui/dev/src/pages/form/date-interval-selection.vue b/ui/dev/src/pages/form/date-interval-selection.vue new file mode 100644 index 000000000000..04df2cde474e --- /dev/null +++ b/ui/dev/src/pages/form/date-interval-selection.vue @@ -0,0 +1,124 @@ + + + diff --git a/ui/dev/src/pages/form/date-part1-basic.vue b/ui/dev/src/pages/form/date-part1-basic.vue index 147ed13ba647..2d4ecfde1c9f 100644 --- a/ui/dev/src/pages/form/date-part1-basic.vue +++ b/ui/dev/src/pages/form/date-part1-basic.vue @@ -22,8 +22,8 @@ :style="style" @input="inputLog" flat bordered - navigation-min-year-month="2018/05" - navigation-max-year-month="2019/03" + :navigation-min-year-month="navigationMinYM" + :navigation-max-year-month="navigationMaxYM" >
@@ -135,25 +135,61 @@
- Limited options + Day slot
+ + +
+ +
+ Limited options +
+
+ @@ -335,6 +370,43 @@
+ + diff --git a/ui/src/components/date/QDate.js b/ui/src/components/date/QDate.js index 02c0be7a7b87..45d47086a186 100644 --- a/ui/src/components/date/QDate.js +++ b/ui/src/components/date/QDate.js @@ -5,7 +5,7 @@ import QBtn from '../btn/QBtn.js' import DateTimeMixin from '../../mixins/datetime.js' import { slot } from '../../utils/slot.js' -import { formatDate, __splitDate, getDateDiff } from '../../utils/date.js' +import { formatDate, __splitDate, getDateDiff, __safeCreateDate } from '../../utils/date.js' import { pad } from '../../utils/format.js' import { jalaaliMonthLength, toGregorian } from '../../utils/date-persian.js' import cache from '../../utils/cache.js' @@ -14,6 +14,16 @@ const yearsInterval = 20 const views = [ 'Calendar', 'Years', 'Months' ] const viewIsValid = v => views.includes(v) const yearMonthValidator = v => /^-?[\d]+\/[0-1]\d$/.test(v) +const modelNavigationValidator = v => [ 'from', 'to', false ].indexOf(v) > -1 +const hashToInt = s => { + if (typeof s !== 'string') { + return NaN + } + + const splits = s.split('/') + + return parseInt(splits[0], 10) * 10000 + parseInt(splits.slice(1).join(''), 10) +} const lineStr = ' \u2014 ' export default Vue.extend({ @@ -29,6 +39,8 @@ export default Vue.extend({ multiple: Boolean, range: Boolean, + dayAsRange: Boolean, + title: String, subtitle: String, @@ -52,6 +64,12 @@ export default Vue.extend({ options: [ Array, Function ], + modelNavigation: { + type: [ String, Boolean ], + default: 'from', + validator: modelNavigationValidator + }, + navigationMinYearMonth: { type: String, validator: yearMonthValidator @@ -164,16 +182,29 @@ export default Vue.extend({ const fn = date => this.__decodeString(date, this.innerMask, this.innerLocale) return this.normalizedModel .filter(date => Object(date) === date && 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) + .map(range => { + const from = fn(range.from) + const to = fn(range.to) + + return hashToInt(from.dateHash) <= hashToInt(to.dateHash) + ? { from, to } + : { from: to, to: from } + }) + .filter(range => range.from.dateHash !== null && range.to.dateHash !== null) }, getNativeDateFn () { return this.calendar !== 'persian' - ? model => new Date(model.year, model.month - 1, model.day) + ? model => { + const date = __safeCreateDate(model.year, model.month - 1, model.day) + date.setFullYear(model.year) + return date + } : model => { const gDate = toGregorian(model.year, model.month, model.day) - return new Date(gDate.gy, gDate.gm - 1, gDate.gd) + const date = __safeCreateDate(gDate.gy, gDate.gm - 1, gDate.gd) + date.setFullYear(model.year) + return date } }, @@ -181,7 +212,7 @@ export default Vue.extend({ return this.calendar === 'persian' ? this.__getDayHash : (date, mask, locale) => formatDate( - new Date( + __safeCreateDate( date.year, date.month - 1, date.day, @@ -229,7 +260,7 @@ export default Vue.extend({ return `${this.daysInModel} ${this.innerLocale.pluralDay}` } - const model = this.daysModel[0] + const model = this.minSelectedModel const date = this.getNativeDateFn(model) if (isNaN(date.valueOf()) === true) { @@ -270,7 +301,7 @@ export default Vue.extend({ ) + ' ' + to.year } - return this.daysModel[0].year + return this.minSelectedModel.year }, minSelectedModel () { @@ -309,10 +340,21 @@ export default Vue.extend({ }, daysInMonth () { - const date = this.viewModel - return this.calendar !== 'persian' - ? (new Date(date.year, date.month, 0)).getDate() - : jalaaliMonthLength(date.year, date.month) + const { year, month } = this.viewModel + + if (this.calendar !== 'persian') { + return { + prev: (__safeCreateDate(year, month - 1, 0)).getDate(), + cur: (__safeCreateDate(year, month, 0)).getDate(), + next: (__safeCreateDate(year, month + 1, 0)).getDate() + } + } + + return { + prev: jalaaliMonthLength(month === 1 ? year - 1 : year, month === 1 ? 12 : month - 1), + cur: jalaaliMonthLength(year, month), + next: jalaaliMonthLength(month === 12 ? year + 1 : year, month === 12 ? 1 : month + 1) + } }, today () { @@ -382,34 +424,35 @@ export default Vue.extend({ const map = {} this.rangeModel.forEach(entry => { - const hashFrom = this.__getMonthHash(entry.from) - const hashTo = this.__getMonthHash(entry.to) + const fromHash = this.__getMonthHash(entry.from) + const toHash = this.__getMonthHash(entry.to) + const toHashInt = hashToInt(toHash) - if (map[hashFrom] === void 0) { - map[hashFrom] = [] + if (map[fromHash] === void 0) { + map[fromHash] = [] } - map[hashFrom].push({ + map[fromHash].push({ from: entry.from.day, - to: hashFrom === hashTo ? entry.to.day : void 0, + to: fromHash === toHash ? entry.to.day : void 0, range: entry }) - if (hashFrom < hashTo) { - let hash + if (hashToInt(fromHash) < toHashInt) { const { year, month } = entry.from const cur = month < 12 ? { year, month: month + 1 } : { year: year + 1, month: 1 } + let hash = this.__getMonthHash(cur) - while ((hash = this.__getMonthHash(cur)) <= hashTo) { + while (hashToInt(hash) <= toHashInt) { if (map[hash] === void 0) { map[hash] = [] } map[hash].push({ from: void 0, - to: hash === hashTo ? entry.to.day : void 0, + to: hash === toHash ? entry.to.day : void 0, range: entry }) @@ -418,6 +461,8 @@ export default Vue.extend({ cur.year++ cur.month = 1 } + + hash = this.__getMonthHash(cur) } } }) @@ -425,54 +470,64 @@ export default Vue.extend({ return map }, - rangeView () { + rangeViewMap () { if (this.editRange === void 0) { - return + return {} } + const map = {} const { init, initHash, final, finalHash } = this.editRange - const [ from, to ] = initHash <= finalHash + const [ from, to ] = hashToInt(initHash) <= hashToInt(finalHash) ? [ init, final ] : [ final, init ] - const fromHash = this.__getMonthHash(from) - const toHash = this.__getMonthHash(to) + const fromHashInt = hashToInt(this.__getMonthHash(from)) + const toHashInt = hashToInt(this.__getMonthHash(to)) - if (fromHash !== this.viewMonthHash && toHash !== this.viewMonthHash) { - return - } + const months = [ 'prev', 'cur', 'next' ] - const view = {} + months.forEach(month => { + const monthHashInt = hashToInt(this.viewMonthHash[month]) - if (fromHash === this.viewMonthHash) { - view.from = from.day - view.includeFrom = true - } - else { - view.from = 1 - } + if (fromHashInt > monthHashInt || toHashInt < monthHashInt) { + return + } - if (toHash === this.viewMonthHash) { - view.to = to.day - view.includeTo = true - } - else { - view.to = this.daysInMonth - } + const view = { + includeFrom: fromHashInt === monthHashInt, + includeTo: toHashInt === monthHashInt + } + + view.from = view.includeFrom ? from.day : 1 + view.to = view.includeTo ? to.day : this.daysInMonth[month] - return view + map[this.viewMonthHash[month]] = view + }) + + return map }, viewMonthHash () { - return this.__getMonthHash(this.viewModel) + const { year, month } = this.viewModel + return { + prev: this.__getMonthHash({ + year: month === 1 ? year - 1 : year, + month: month === 1 ? 12 : month - 1 + }), + cur: this.__getMonthHash({ year, month }), + next: this.__getMonthHash({ + year: month === 12 ? year + 1 : year, + month: month === 12 ? 1 : month + 1 + }) + } }, selectionDaysMap () { const map = {} if (this.options === void 0) { - for (let i = 1; i <= this.daysInMonth; i++) { + for (let i = 1; i <= this.daysInMonth.cur; i++) { map[i] = true } @@ -483,8 +538,8 @@ export default Vue.extend({ ? this.options : date => this.options.includes(date) - for (let i = 1; i <= this.daysInMonth; i++) { - const dayHash = this.viewMonthHash + '/' + pad(i) + for (let i = 1; i <= this.daysInMonth.cur; i++) { + const dayHash = this.viewMonthHash.cur + '/' + pad(i) map[i] = fn(dayHash) } @@ -495,7 +550,7 @@ export default Vue.extend({ const map = {} if (this.events === void 0) { - for (let i = 1; i <= this.daysInMonth; i++) { + for (let i = 1; i <= this.daysInMonth.cur; i++) { map[i] = false } } @@ -504,8 +559,8 @@ export default Vue.extend({ ? this.events : date => this.events.includes(date) - for (let i = 1; i <= this.daysInMonth; i++) { - const dayHash = this.viewMonthHash + '/' + pad(i) + for (let i = 1; i <= this.daysInMonth.cur; i++) { + const dayHash = this.viewMonthHash.cur + '/' + pad(i) map[i] = fn(dayHash) === true && this.evtColor(dayHash) } } @@ -513,47 +568,42 @@ export default Vue.extend({ return map }, - viewDays () { - let date, endDay + startFillDays () { + let date const { year, month } = this.viewModel if (this.calendar !== 'persian') { - date = new Date(year, month - 1, 1) - endDay = (new Date(year, month - 1, 0)).getDate() + date = __safeCreateDate(year, month - 1, 1) } 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) + date = __safeCreateDate(gDate.gy, gDate.gm - 1, gDate.gd) } + const days = date.getDay() - this.computedFirstDayOfWeek - 1 + return { - days: date.getDay() - this.computedFirstDayOfWeek - 1, - endDay + days: days < 0 ? days + 7 : days, + endDay: this.daysInMonth.prev } }, days () { const res = [] - const { days, endDay } = this.viewDays + const { days, endDay } = this.startFillDays - const len = days < 0 ? days + 7 : days - if (len < 6) { - for (let i = endDay - len; i <= endDay; i++) { - res.push({ i, fill: true }) + if (days < 6) { + for (let i = endDay - days; i <= endDay; i++) { + res.push({ i, day: this.viewMonthHash.prev + '/' + pad(i), fill: true }) } + + this.__fillDaysMeta(res, this.viewMonthHash.prev, endDay, -endDay + days + 1, endDay - days, endDay) } const index = res.length - for (let i = 1; i <= this.daysInMonth; i++) { - const day = { i, event: this.eventDaysMap[i], classes: [] } + for (let i = 1; i <= this.daysInMonth.cur; i++) { + const day = { i, day: this.viewMonthHash.cur + '/' + pad(i), event: this.eventDaysMap[i], classes: [] } if (this.selectionDaysMap[i] === true) { day.in = true @@ -563,93 +613,7 @@ export default Vue.extend({ res.push(day) } - // if current view has days in model - if (this.daysMap[this.viewMonthHash] !== void 0) { - this.daysMap[this.viewMonthHash].forEach(day => { - const i = index + day - 1 - Object.assign(res[i], { - selected: true, - unelevated: true, - flat: false, - color: this.computedColor, - textColor: this.computedTextColor - }) - }) - } - - // if current view has ranges in model - if (this.rangeMap[this.viewMonthHash] !== void 0) { - this.rangeMap[this.viewMonthHash].forEach(entry => { - if (entry.from !== void 0) { - const from = index + entry.from - 1 - const to = index + (entry.to || this.daysInMonth) - 1 - - for (let day = from; day <= to; day++) { - Object.assign(res[day], { - range: entry.range, - unelevated: true, - color: this.computedColor, - textColor: this.computedTextColor - }) - } - - Object.assign(res[from], { - rangeFrom: true, - flat: false - }) - - 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: this.computedColor, - textColor: this.computedTextColor - }) - } - - Object.assign(res[to], { - flat: false, - rangeTo: true - }) - } - else { - const to = index + this.daysInMonth - 1 - for (let day = index; day <= to; day++) { - Object.assign(res[day], { - range: entry.range, - unelevated: true, - color: this.computedColor, - textColor: this.computedTextColor - }) - } - } - }) - } - - if (this.rangeView !== void 0) { - const from = index + this.rangeView.from - 1 - const to = index + this.rangeView.to - 1 - - for (let day = from; day <= to; day++) { - res[day].color = this.computedColor - res[day].editRange = true - } - - if (this.rangeView.includeFrom === true) { - res[from].editRangeFrom = true - } - if (this.rangeView.includeTo === true) { - res[to].editRangeTo = true - } - } + this.__fillDaysMeta(res, this.viewMonthHash.cur, this.daysInMonth.cur, index, 1, this.daysInMonth.cur) if (this.viewModel.year === this.today.year && this.viewModel.month === this.today.month) { res[index + this.today.day - 1].today = true @@ -659,30 +623,29 @@ export default Vue.extend({ if (left > 0) { const afterDays = 7 - left for (let i = 1; i <= afterDays; i++) { - res.push({ i, fill: true }) + res.push({ i, day: this.viewMonthHash.next + '/' + pad(i), fill: true }) } + + this.__fillDaysMeta(res, this.viewMonthHash.next, this.daysInMonth.next, index + this.daysInMonth.cur, 1, afterDays) } res.forEach(day => { - let cls = `q-date__calendar-item ` + 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'}` + cls += day.fill === true + ? 'q-date__calendar-item--fill' + : `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.range !== void 0 || day.editRange === true) { + cls += ` text-${day.color}` + } - if (day.editRange === true) { - cls += ` q-date__edit-range${day.editRangeFrom === true ? '-from' : ''}${day.editRangeTo === true ? '-to' : ''}` - } + if (day.range !== void 0 && (day.rangeTo !== true || day.rangeFrom !== true)) { + cls += ` q-date__range${day.rangeFrom === true ? '-from' : (day.rangeTo === true ? '-to' : '')}` + } - if (day.range !== void 0 || day.editRange === true) { - cls += ` text-${day.color}` - } + if (day.editRange === true) { + cls += ` q-date__edit-range${day.editRangeFrom === true ? '-from' : ''}${day.editRangeTo === true ? '-to' : ''}` } day.classes = cls @@ -726,7 +689,11 @@ export default Vue.extend({ this.__updateViewModel(year, month) }, - setEditingRange (from, to) { + setEditingRange (from, to, modelNavigation) { + if (modelNavigation === void 0) { + modelNavigation = this.modelNavigation + } + if (this.range === false || !from) { this.editRange = void 0 return @@ -744,7 +711,14 @@ export default Vue.extend({ finalHash: this.__getDayHash(final) } - this.setCalendarTo(init.year, init.month) + if ([ 'from', 'to' ].indexOf(modelNavigation) > -1) { + this.$nextTick(() => { + this.setCalendarTo( + modelNavigation === 'from' ? init.year : final.year, + modelNavigation === 'from' ? init.month : final.month + ) + }) + } }, __getMask () { @@ -767,6 +741,12 @@ export default Vue.extend({ }, __getViewModel (mask, locale) { + if (this.modelNavigation === false) { + return this.viewModel === void 0 + ? this.__getDefaultViewModel() + : this.viewModel + } + const model = Array.isArray(this.value) === true ? this.value : (this.value ? [ this.value ] : []) @@ -775,8 +755,12 @@ export default Vue.extend({ return this.__getDefaultViewModel() } + const viewModel = this.modelNavigation === 'from' + ? model[0].from !== void 0 ? model[0].from : model[0] + : model[model.length - 1].from !== void 0 ? model[model.length - 1].from : model[model.length - 1] + const decoded = this.__decodeString( - model[0].from !== void 0 ? model[0].from : model[0], + viewModel, mask, locale ) @@ -947,9 +931,19 @@ export default Vue.extend({ }, __getCalendarView (h) { + const dayContentFn = this.$scopedSlots.day !== void 0 + ? this.$scopedSlots.day + : day => (day.event !== false ? [ + h('div', { staticClass: 'q-date__event bg-' + day.event }) + ] : null) + const dayFillContentFn = this.$scopedSlots.day !== void 0 + ? this.$scopedSlots.day + : day => h('div', [ day.i ]) + const selectedDay = this.days.find(day => day.unelevated === true) const viewDay = selectedDay === void 0 ? this.days.find(day => day.today === true) : selectedDay const viewTarget = viewDay === void 0 ? 1 : viewDay.i + const calCachePrefix = this.calendar === 'persian' ? 'dayP#' : 'day#' return [ h('div', { @@ -994,7 +988,7 @@ export default Vue.extend({ } }, [ h('div', { - key: this.viewMonthHash, + key: this.viewMonthHash.cur, staticClass: 'q-date__calendar-days fit' }, this.days.map(day => h('div', { staticClass: day.classes }, [ day.in === true @@ -1010,15 +1004,13 @@ export default Vue.extend({ label: day.i, tabindex: this.computedTabindex }, - on: cache(this, 'day#' + day.i, { + on: cache(this, calCachePrefix + day.i, { click: () => { this.__onDayClick(day.i) }, focusin: () => { this.__onDayMouseover(day.i) }, mouseenter: () => { this.__onDayMouseover(day.i) } }) - }, day.event !== false ? [ - h('div', { staticClass: 'q-date__event bg-' + day.event }) - ] : null) - : h('div', [ day.i ]) + }, dayContentFn(day)) + : dayFillContentFn(day) ]))) ]) ]) @@ -1174,6 +1166,81 @@ export default Vue.extend({ ]) }, + __fillDaysMeta (res, monthHash, daysInMonth, index, dayFrom, dayTo) { + // if current view has days in model + if (this.daysMap[monthHash] !== void 0) { + this.daysMap[monthHash] + .filter(day => day >= dayFrom && day <= dayTo) + .forEach(day => { + const i = index + day - 1 + Object.assign(res[i], { + selected: true, + unelevated: true, + flat: false, + color: this.computedColor, + textColor: this.computedTextColor + }) + }) + } + + // if current view has ranges in model + if (this.rangeMap[monthHash] !== void 0) { + this.rangeMap[monthHash].forEach(entry => { + const from = index + Math.max(dayFrom, entry.from === void 0 ? 1 : entry.from) - 1 + const to = index + Math.min(dayTo, entry.to === void 0 ? daysInMonth : entry.to) - 1 + + for (let day = from; day <= to; day++) { + Object.assign(res[day], { + range: entry.range, + unelevated: true, + color: this.computedColor, + textColor: this.computedTextColor + }) + } + + if (entry.from >= dayFrom && entry.from <= dayTo) { + Object.assign(res[from], { + rangeFrom: true, + flat: false + }) + } + + if (entry.to >= dayFrom && entry.to <= dayTo) { + Object.assign(res[to], { + rangeTo: true, + flat: false + }) + } + }) + } + + if (this.rangeViewMap[monthHash] !== void 0) { + const from = index + Math.max(dayFrom, this.rangeViewMap[monthHash].from) - 1 + const to = index + Math.min(dayTo, this.rangeViewMap[monthHash].to) - 1 + + for (let day = from; day <= to; day++) { + res[day].color = this.computedColor + res[day].editRange = true + } + + if ( + this.rangeViewMap[monthHash].includeFrom === true && + this.rangeViewMap[monthHash].from >= dayFrom && + this.rangeViewMap[monthHash].from <= dayTo + ) { + res[from].editRangeFrom = true + } + + if ( + this.rangeViewMap[monthHash].includeTo === true && + this.rangeViewMap[monthHash].to >= dayFrom && + this.rangeViewMap[monthHash].to <= dayTo + ) { + res[to].editRangeTo = true + } + } + }, + __goToMonth (offset) { let year = this.viewModel.year let month = Number(this.viewModel.month) + offset @@ -1230,7 +1297,7 @@ export default Vue.extend({ const day = { ...this.viewModel, day: dayIndex } if (this.range === false) { - this.__toggleDate(day, this.viewMonthHash) + this.__toggleDate(day, this.viewMonthHash.cur) return } @@ -1262,12 +1329,12 @@ export default Vue.extend({ const initHash = this.editRange.initHash, finalHash = this.__getDayHash(day), - payload = initHash <= finalHash + payload = hashToInt(initHash) <= hashToInt(finalHash) ? { from: this.editRange.init, to: day } : { from: day, to: this.editRange.init } this.editRange = void 0 - this.__addToModel(initHash === finalHash ? day : { target: day, ...payload }) + this.__addToModel(initHash === finalHash && this.dayAsRange !== true ? day : { target: day, ...payload }) this.$emit('range-end', { from: this.__getShortDate(payload.from), @@ -1284,10 +1351,18 @@ export default Vue.extend({ final, finalHash: this.__getDayHash(final) }) + + this.$emit('range-change', { + from: this.__getShortDate(this.editRange.init), + to: this.__getShortDate(this.editRange.final) + }) } }, __updateViewModel (year, month) { + year = parseInt(year, 10) + month = parseInt(month, 10) + if (this.minNav !== void 0 && year <= this.minNav.year) { year = this.minNav.year if (month < this.minNav.month) { @@ -1344,7 +1419,7 @@ export default Vue.extend({ date.month = this.viewModel.month const maxDay = this.calendar !== 'persian' - ? (new Date(date.year, date.month, 0)).getDate() + ? (__safeCreateDate(date.year, date.month, 0)).getDate() : jalaaliMonthLength(date.year, date.month) date.day = Math.min(Math.max(1, date.day), maxDay) @@ -1390,14 +1465,18 @@ export default Vue.extend({ if (date.from !== void 0) { // we also need to filter out intersections - const fromHash = this.__getDayHash(date.from) - const toHash = this.__getDayHash(date.to) + const fromHashInt = hashToInt(this.__getDayHash(date.from)) + const toHashInt = hashToInt(this.__getDayHash(date.to)) const days = this.daysModel - .filter(day => day.dateHash < fromHash || day.dateHash > toHash) + .filter(day => { + const dayHashInt = hashToInt(day.dateHash) + + return dayHashInt < fromHashInt || dayHashInt > toHashInt + }) const ranges = this.rangeModel - .filter(({ from, to }) => to.dateHash < fromHash || from.dateHash > toHash) + .filter(({ from, to }) => hashToInt(to.dateHash) < fromHashInt || hashToInt(from.dateHash) > toHashInt) value = days.concat(ranges).concat(date).map(entry => this.__encodeEntry(entry)) } diff --git a/ui/src/components/date/QDate.json b/ui/src/components/date/QDate.json index 2025d91ba905..ae481031e562 100644 --- a/ui/src/components/date/QDate.json +++ b/ui/src/components/date/QDate.json @@ -96,6 +96,15 @@ "category": "model" }, + "model-navigation": { + "type": [ "String", "Boolean" ], + "desc": "On which end of the range to navigate the calendar when the model changes; Use `false` to skip navigation", + "values": [ "from", "to", "(Boolean) false" ], + "default": "from", + "category": "behavior", + "addedIn": "v1.14.8" + }, + "navigation-min-year-month": { "type": "String", "desc": "Lock user from navigating below a specific year+month (in YYYY/MM format); This prop is not used to correct the model; You might want to also use 'default-year-month' prop", @@ -156,6 +165,13 @@ "addedIn": "v1.13.0" }, + "day-as-range": { + "type": "Boolean", + "desc": "Model single days as a range (object) instead of string", + "category": "model|selection", + "addedIn": "v1.15.2" + }, + "emit-immediately": { "type": "Boolean", "desc": "Emit model when user browses month and year too; ONLY for single selection (non-multiple, non-range)", @@ -170,6 +186,129 @@ } }, + "scopedSlots": { + "day": { + "desc": "Override default day content slot; Suggestion: tooltips and / or multiple event markers", + "scope": { + "i": { + "type": "Number", + "desc": "The day number in month", + "examples": [ 23 ] + }, + "day": { + "type": "String", + "desc": "Full date in YYYY/MM/DD form", + "examples": [ "2020/05/20" ] + }, + "fill": { + "type": "Boolean", + "desc": "The day does not belong to the currently displayed month / year (no QBtn and hidden by default)" + }, + "in": { + "type": "Boolean", + "desc": "The day is selectable (QBtn is not disabled)" + }, + "today": { + "type": "Boolean", + "desc": "The day is today" + }, + "selected": { + "type": "Boolean", + "desc": "The day is selected" + }, + "unelevated": { + "type": "Boolean", + "desc": "The day is selected" + }, + "flat": { + "type": "Boolean", + "desc": "The day is not selected" + }, + "classes": { + "type": "String", + "desc": "The classes applied to the day", + "__exemption": "examples" + }, + "color": { + "type": "String", + "desc": "The color of the QBtn used for the day", + "examples": [ "primary" ] + }, + "textColor": { + "type": "String", + "desc": "The text color of the QBtn used for the day", + "examples": [ "orange-2" ] + }, + "event": { + "type": [ "Boolean", "String", "Any" ], + "desc": "Boolean false if there is no event for the day; The event-color or the value returned by the event-color function", + "examples": [ "red" ] + }, + "range": { + "type": "Object", + "desc": "The range this day belongs to", + "definition": { + "from": { + "type": "Object", + "desc": "Object of properties of the range starting point (only if range)", + "definition": { + "year": { + "type": "Number", + "desc": "The year", + "__exemption": [ + "examples" + ] + }, + "month": { + "type": "Number", + "desc": "The month", + "__exemption": [ + "examples" + ] + }, + "day": { + "type": "Number", + "desc": "The day of month", + "__exemption": [ + "examples" + ] + } + } + }, + "to": { + "type": "Object", + "desc": "Object of properties of the range ending point (only if range)", + "definition": { + "year": { + "type": "Number", + "desc": "The year", + "__exemption": [ + "examples" + ] + }, + "month": { + "type": "Number", + "desc": "The month", + "__exemption": [ + "examples" + ] + }, + "day": { + "type": "Number", + "desc": "The day of month", + "__exemption": [ + "examples" + ] + } + } + } + } + } + }, + "addedIn": "v1.14.8" + } + }, + "events": { "input": { "extends": "input", @@ -306,6 +445,61 @@ "addedIn": "v1.13.0" }, + "range-change": { + "desc": "User has changed a range selection", + "params": { + "range": { + "type": "Object", + "desc": "Definition of the range", + "definition": { + "from": { + "type": "Object", + "desc": "Definition of date from where the range begins", + "definition": { + "year": { + "type": "Number", + "desc": "The year", + "__exemption": [ "examples" ] + }, + "month": { + "type": "Number", + "desc": "The month", + "__exemption": [ "examples" ] + }, + "day": { + "type": "Number", + "desc": "The day of month", + "__exemption": [ "examples" ] + } + } + }, + "to": { + "type": "Object", + "desc": "Definition of date to where the range ends", + "definition": { + "year": { + "type": "Number", + "desc": "The year", + "__exemption": [ "examples" ] + }, + "month": { + "type": "Number", + "desc": "The month", + "__exemption": [ "examples" ] + }, + "day": { + "type": "Number", + "desc": "The day of month", + "__exemption": [ "examples" ] + } + } + } + } + } + }, + "addedIn": "v1.14.8" + }, + "range-end": { "desc": "User has ended a range selection", "params": { @@ -459,6 +653,13 @@ "__exemption": [ "examples" ] } } + }, + "modelNavigation": { + "type": [ "String", "Boolean" ], + "desc": "On which end of the range to navigate the calendar; Use `false` to skip navigation", + "values": [ "from", "to", "(Boolean) false" ], + "default": "from", + "addedIn": "v1.14.8" } } } diff --git a/ui/src/components/date/QDate.sass b/ui/src/components/date/QDate.sass index 269703053b9b..b99e7027c536 100644 --- a/ui/src/components/date/QDate.sass +++ b/ui/src/components/date/QDate.sass @@ -99,6 +99,12 @@ &--fill visibility: hidden + min-height: 32px + + &.q-date + &__range, &__range-from, &__range-to + &:before + opacity: .05 &__range, &__range-from, &__range-to @@ -111,7 +117,9 @@ left: 0 right: 0 opacity: .3 + pointer-events: none + &__range &:nth-child(7n-6) &:before border-top-left-radius: 0 @@ -124,10 +132,15 @@ &__range-from &:before - left: 50% + left: calc(50% - 15px) + border-top-left-radius: $button-rounded-border-radius + border-bottom-left-radius: $button-rounded-border-radius + &__range-to &:before - right: 50% + right: calc(50% - 15px) + border-top-right-radius: $button-rounded-border-radius + border-bottom-right-radius: $button-rounded-border-radius &__edit-range &:after diff --git a/ui/src/components/date/QDate.styl b/ui/src/components/date/QDate.styl index 269703053b9b..b99e7027c536 100644 --- a/ui/src/components/date/QDate.styl +++ b/ui/src/components/date/QDate.styl @@ -99,6 +99,12 @@ &--fill visibility: hidden + min-height: 32px + + &.q-date + &__range, &__range-from, &__range-to + &:before + opacity: .05 &__range, &__range-from, &__range-to @@ -111,7 +117,9 @@ left: 0 right: 0 opacity: .3 + pointer-events: none + &__range &:nth-child(7n-6) &:before border-top-left-radius: 0 @@ -124,10 +132,15 @@ &__range-from &:before - left: 50% + left: calc(50% - 15px) + border-top-left-radius: $button-rounded-border-radius + border-bottom-left-radius: $button-rounded-border-radius + &__range-to &:before - right: 50% + right: calc(50% - 15px) + border-top-right-radius: $button-rounded-border-radius + border-bottom-right-radius: $button-rounded-border-radius &__edit-range &:after diff --git a/ui/src/components/time/QTime.js b/ui/src/components/time/QTime.js index 95f11bd9ebc9..123e73f9b2eb 100644 --- a/ui/src/components/time/QTime.js +++ b/ui/src/components/time/QTime.js @@ -4,7 +4,7 @@ import QBtn from '../btn/QBtn.js' import TouchPan from '../../directives/TouchPan.js' import { slot } from '../../utils/slot.js' -import { formatDate, __splitDate } from '../../utils/date.js' +import { formatDate, __splitDate, __safeCreateDate } from '../../utils/date.js' import { position } from '../../utils/event.js' import { pad } from '../../utils/format.js' import cache from '../../utils/cache.js' @@ -851,7 +851,7 @@ export default Vue.extend({ pad(date.minute) + (this.withSeconds === true ? ':' + pad(date.second) : '') : formatDate( - new Date( + __safeCreateDate( date.year, date.month === null ? null : date.month - 1, date.day, diff --git a/ui/src/utils/date.js b/ui/src/utils/date.js index bcd4852712ca..7dcc619c7126 100644 --- a/ui/src/utils/date.js +++ b/ui/src/utils/date.js @@ -151,10 +151,20 @@ function getRegexData (mask, dateLocale) { return res } +export function __safeCreateDate (...args) { + const d = new Date(...args) + + if (args.length > 1 && args[0] >= 0 && args[0] <= 99) { + d.setFullYear(args[0]) + } + + return d +} + export function extractDate (str, mask, dateLocale) { const d = __splitDate(str, mask, dateLocale) - const date = new Date( + const date = __safeCreateDate( d.year, d.month === null ? null : d.month - 1, d.day, @@ -262,7 +272,7 @@ export function __splitDate (str, mask, dateLocale, calendar, defaultModel) { } const maxDay = calendar !== 'persian' - ? (new Date(date.year, date.month, 0)).getDate() + ? (__safeCreateDate(date.year, date.month, 0)).getDate() : jalaaliMonthLength(date.year, date.month) if (date.day > maxDay) { @@ -321,7 +331,7 @@ function formatTimezone (offset, delimeter = '') { function setMonth (date, newMonth /* 1-based */) { const - test = new Date(date.getFullYear(), newMonth, 0, 0, 0, 0, 0), + test = __safeCreateDate(date.getFullYear(), newMonth, 0, 0, 0, 0, 0), days = test.getDate() date.setMonth(newMonth - 1, Math.min(days, date.getDate())) @@ -363,13 +373,13 @@ export function getDayOfWeek (date) { export function getWeekOfYear (date) { // Remove time components of date - const thursday = new Date(date.getFullYear(), date.getMonth(), date.getDate()) + const thursday = __safeCreateDate(date.getFullYear(), date.getMonth(), date.getDate()) // Change date to Thursday same week thursday.setDate(thursday.getDate() - ((thursday.getDay() + 6) % 7) + 3) // Take January 4th as it is always in week 1 (see ISO 8601) - const firstThursday = new Date(thursday.getFullYear(), 0, 4) + const firstThursday = __safeCreateDate(thursday.getFullYear(), 0, 4) // Change date to Thursday same week firstThursday.setDate(firstThursday.getDate() - ((firstThursday.getDay() + 6) % 7) + 3) @@ -490,10 +500,12 @@ export function getMinDate (date /*, ...args */) { } function getDiff (t, sub, interval) { - return ( - (t.getTime() - t.getTimezoneOffset() * MILLISECONDS_IN_MINUTE) - - (sub.getTime() - sub.getTimezoneOffset() * MILLISECONDS_IN_MINUTE) - ) / interval + return Math.floor( + ( + (t.getTime() - t.getTimezoneOffset() * MILLISECONDS_IN_MINUTE) - + (sub.getTime() - sub.getTimezoneOffset() * MILLISECONDS_IN_MINUTE) + ) / interval + ) } export function getDateDiff (date, subtract, unit = 'days') { @@ -595,7 +607,7 @@ export function isSameDate (date, date2, unit) { } export function daysInMonth (date) { - return (new Date(date.getFullYear(), date.getMonth() + 1, 0)).getDate() + return (__safeCreateDate(date.getFullYear(), date.getMonth() + 1, 0)).getDate() } function getOrdinal (n) {