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 @@
+
+
+
+
+
+
+ {{ day.i }}
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
-
-
+ $refs.qDateProxy.hide()" />
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 @@
+
+
+
+
+ { syncCalendars('start', yearMonth) }"
+ @range-start="onSelectionStart"
+ @range-change="onSelectionChange"
+ @range-end="onSelectionEnd"
+ />
+
+ { syncCalendars('end', yearMonth) }"
+ @range-start="onSelectionStart"
+ @range-change="onSelectionChange"
+ @range-end="onSelectionEnd"
+ />
+
+
+
+
+
+
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 @@
+
+
+
+
+ { syncCalendars('start', yearMonth) }"
+ @range-start="onSelectionStart"
+ @range-change="onSelectionChange"
+ @range-end="onSelectionEnd"
+ />
+
+ { syncCalendars('end', yearMonth) }"
+ @range-start="onSelectionStart"
+ @range-change="onSelectionChange"
+ @range-end="onSelectionEnd"
+ />
+
+
+
+ - Click once to start selection mode
+ - Click again to select the range
+ - The selected range must be at least 3 days and at most 14 days
+
+
+
+
+
+
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 b2bb5d914d0d..5de65b798e75 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 @@
+
+
+
+
+
+
+ {{ day.i }}
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ { syncCalendars('start', yearMonth) }"
+ @range-start="onSelectionStart"
+ @range-change="onSelectionChange"
+ @range-end="onSelectionEnd"
+ />
+
+ { syncCalendars('end', yearMonth) }"
+ @range-start="onSelectionStart"
+ @range-change="onSelectionChange"
+ @range-end="onSelectionEnd"
+ />
+
+
+
+ - Click once to start selection mode
+ - Click again to select the range
+ - The selected range must be at least 3 days and at most 14 days
+
+
+
+
+
+
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 @@
+
+
+
+
+ { syncCalendars('start', yearMonth) }"
+ @range-start="onSelectionStart"
+ @range-change="onSelectionChange"
+ @range-end="onSelectionEnd"
+ />
+
+ { syncCalendars('end', yearMonth) }"
+ @range-start="onSelectionStart"
+ @range-change="onSelectionChange"
+ @range-end="onSelectionEnd"
+ />
+
+
+
+
+
+
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
+
+
+ {{ day.i }}
+
+
+
+
+
+
+
+ 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) {