Skip to content

Commit 5b4faa6

Browse files
committed
feat(NcDateTimePicker): add time range picker and align naming
1. Add a time range picker mode 2. Align type names to `TYPE(-range)?` 3. Fix some issues related to time mode (also without range) Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 6b82415 commit 5b4faa6

2 files changed

Lines changed: 93 additions & 32 deletions

File tree

src/components/NcDateTimePicker/NcDateTimePicker.vue

Lines changed: 90 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,13 @@ Meaning an array with two dates is used, the first date is the range start and t
8989
<div>
9090
<fieldset class="type-select">
9191
<legend>Picker mode</legend>
92-
<NcCheckboxRadioSwitch v-model="type" type="radio" value="range">Date</NcCheckboxRadioSwitch>
93-
<NcCheckboxRadioSwitch v-model="type" type="radio" value="range-datetime">Date and time</NcCheckboxRadioSwitch>
92+
<NcCheckboxRadioSwitch v-model="type" type="radio" value="date-range">Date</NcCheckboxRadioSwitch>
93+
<NcCheckboxRadioSwitch v-model="type" type="radio" value="time-range">Time</NcCheckboxRadioSwitch>
94+
<NcCheckboxRadioSwitch v-model="type" type="radio" value="datetime-range">Date and time</NcCheckboxRadioSwitch>
9495
</fieldset>
9596

9697
<NcDateTimePicker
98+
:key="type"
9799
v-model="time"
98100
:type />
99101
<div>
@@ -106,17 +108,20 @@ Meaning an array with two dates is used, the first date is the range start and t
106108
export default {
107109
data() {
108110
return {
109-
time: [new Date(2025, 3, 18), new Date(2025, 3, 21)],
110-
type: 'range',
111+
time: [new Date(2025, 3, 18, 12, 30), new Date(2025, 3, 21, 13, 30)],
112+
type: 'date-range',
111113
}
112114
},
113115
methods: {
114116
formatDate(date) {
115-
const text = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
116-
if (this.type === 'range') {
117-
return text
117+
const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
118+
const timeString = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
119+
if (this.type === 'date-range') {
120+
return dateString
121+
} else if (this.type === 'time-range') {
122+
return timeString
118123
}
119-
return `${text} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
124+
return `${dateString} ${timeString}`
120125
},
121126
},
122127
}
@@ -161,6 +166,13 @@ export default {
161166
</docs>
162167

163168
<script setup lang="ts">
169+
import type {
170+
// The emitted object for time picker
171+
TimeObj as LibraryTimeObject,
172+
// The accepted model value
173+
ModelValue as LibraryModelValue,
174+
} from '@vuepic/vue-datepicker'
175+
164176
import {
165177
mdiCalendarBlank,
166178
mdiChevronDown,
@@ -249,7 +261,7 @@ const props = withDefaults(defineProps<{
249261
* while 'range-datetime' will allow picking a date range with times.
250262
* @default 'date'
251263
*/
252-
type?: 'date' | 'datetime' | 'time' | 'week' | 'month' | 'year' | 'range' | 'range-datetime'
264+
type?: 'date' | 'datetime' | 'time' | 'week' | 'month' | 'year' | 'date-range' | 'time-range' | 'datetime-range'
253265
254266
appendToBody?: boolean
255267
@@ -281,8 +293,9 @@ const emit = defineEmits<{
281293
/**
282294
* If range picker is enabled then an array containing start and end date are emitted.
283295
* Otherwise the selected date is emitted.
296+
* `null` is emitted if `clearable` is set to `true` and the value was cleared.
284297
*/
285-
'update:modelValue': [Date | [Date, Date]]
298+
'update:modelValue': [Date | [Date, Date] | null]
286299
'update:timezoneId': [string]
287300
}>()
288301
@@ -296,20 +309,32 @@ const value = computed(() => {
296309
const end = new Date(date)
297310
end.setUTCDate(date.getUTCDate() + 6)
298311
return [date, end]
299-
} else if (props.type.startsWith('range')) {
312+
} else if (props.type === 'year') {
313+
const date = props.modelValue instanceof Date ? props.modelValue : new Date()
314+
return date.getUTCFullYear()
315+
} else if (props.type === 'month') {
316+
const date = props.modelValue instanceof Date ? props.modelValue : new Date()
317+
return { year: date.getUTCFullYear(), month: date.getUTCMonth() }
318+
} else if (props.type === 'time' || props.type === 'time-range') {
319+
const time = [props.modelValue ?? (props.type === 'time-range' ? [new Date(), new Date()] : new Date())].flat()
320+
// default time range is 1 hour
321+
if (props.modelValue === undefined && props.type === 'time-range') {
322+
time[1].setHours(time[1].getHours() + 1)
323+
}
324+
const timeValue = time.map((date) => ({
325+
hours: date.getHours(),
326+
minutes: date.getMinutes(),
327+
seconds: date.getSeconds(),
328+
} as LibraryTimeObject))
329+
return props.type === 'time' ? timeValue[0] : timeValue
330+
} else if (props.type.endsWith('-range')) {
300331
if (props.modelValue === undefined) {
301332
const start = new Date()
302333
const end = new Date(start)
303334
end.setUTCDate(start.getUTCDate() + 7)
304335
return [start, end]
305336
}
306337
return props.modelValue
307-
} else if (props.type === 'year') {
308-
const date = props.modelValue instanceof Date ? props.modelValue : new Date()
309-
return date.getUTCFullYear()
310-
} else if (props.type === 'month') {
311-
const date = props.modelValue instanceof Date ? props.modelValue : new Date()
312-
return { year: date.getUTCFullYear(), month: date.getUTCMonth() }
313338
}
314339
315340
// no special handling for other types needed
@@ -350,10 +375,12 @@ const realFormat = computed(() => {
350375
}
351376
352377
let formatter: Intl.DateTimeFormat | undefined
353-
if (props.type === 'datetime' || props.type === 'range-datetime') {
354-
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { dateStyle: 'medium', timeStyle: 'short' })
355-
} else if (props.type === 'date' || props.type === 'range') {
378+
if (props.type === 'date' || props.type === 'date-range') {
356379
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { dateStyle: 'medium' })
380+
} else if (props.type === 'time' || props.type === 'time-range') {
381+
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { timeStyle: 'short' })
382+
} else if (props.type === 'datetime' || props.type === 'datetime-range') {
383+
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { dateStyle: 'medium', timeStyle: 'short' })
357384
} else if (props.type === 'month') {
358385
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { year: 'numeric', month: '2-digit' })
359386
} else if (props.type === 'year') {
@@ -371,12 +398,12 @@ const realFormat = computed(() => {
371398
})
372399
373400
const pickerType = computed(() => ({
374-
timePicker: props.type === 'time',
401+
timePicker: props.type === 'time' || props.type === 'time-range',
375402
yearPicker: props.type === 'year',
376403
monthPicker: props.type === 'month',
377404
weekPicker: props.type === 'week',
378-
range: props.type.startsWith('range'),
379-
enableTimePicker: !(props.type === 'date' || props.type === 'range'),
405+
range: props.type.endsWith('-range'),
406+
enableTimePicker: !(props.type === 'date' || props.type === 'date-range'),
380407
flow: props.type === 'datetime'
381408
? ['calendar', 'time'] as ['calendar', 'time']
382409
: undefined,
@@ -386,17 +413,50 @@ const pickerType = computed(() => ({
386413
* Called on model value update of the library.
387414
* @param value The value emitted from the underlying library
388415
*/
389-
function onUpdateModelValue(value: Date | [Date, Date] | number | { month: number, year: number }): void {
390-
let date = value as Date | [Date, Date]
391-
if (props.type === 'month') {
416+
function onUpdateModelValue(value: LibraryModelValue): void {
417+
if (value === null) {
418+
return emit('update:modelValue', null)
419+
}
420+
421+
if (props.type === 'time') {
422+
// time is provided as an object
423+
emit('update:modelValue', formatLibraryTime(value as LibraryTimeObject))
424+
} else if (props.type === 'time-range') {
425+
// same as time but as an array with two elements
426+
const start = formatLibraryTime(value[0])
427+
const end = formatLibraryTime(value[1])
428+
// ensure end is beyond the start
429+
if (end.getTime() < start.getTime()) {
430+
end.setDate(end.getDate() + 1)
431+
}
432+
emit('update:modelValue', [start, end])
433+
} else if (props.type === 'month') {
434+
// month is emitted as an object with month and year attribute
392435
const data = value as { month: number, year: number }
393-
date = new Date(data.year, data.month, 1)
436+
emit('update:modelValue', new Date(data.year, data.month, 1))
394437
} else if (props.type === 'year') {
395-
date = new Date(value as number, 0)
438+
// Years are emitted as the numeric year e.g. 2022
439+
emit('update:modelValue', new Date(value as number, 0))
396440
} else if (props.type === 'week') {
397-
date = value[0]
441+
// weeks are emitted as [Date, Date]
442+
emit('update:modelValue', value[0])
443+
} else {
444+
// otherwise it already emits the correct format
445+
emit('update:modelValue', value as Date | [Date, Date])
398446
}
399-
emit('update:modelValue', date)
447+
}
448+
449+
/**
450+
* Format a vuepick time object to native JS Date object.
451+
*
452+
* @param time - The library time value object
453+
*/
454+
function formatLibraryTime(time: LibraryTimeObject): Date {
455+
const date = new Date()
456+
date.setHours(time.hours)
457+
date.setMinutes(time.minutes)
458+
date.setSeconds(time.seconds)
459+
return date
400460
}
401461
402462
// Localization

tests/component/components/NcDateTimePicker.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ const testcases = [
2323
['week', new Date(2000, 0, 2, 3, 4), '1999-52'],
2424
['month', new Date(2000, 0, 2, 3, 4), '01/2000'],
2525
['year', new Date(2000, 0, 2, 3, 4), '2000'],
26-
['range', [new Date(2000, 0, 1), new Date(2000, 0, 7)] as [Date, Date], /Jan 1\s\s7, 2000/i],
27-
['range-datetime', [new Date(2000, 0, 1, 2, 3), new Date(2000, 0, 7, 8, 9)] as [Date, Date], /Jan 1, 2000, 2:03\sAM\s\sJan 7, 2000, 8:09\sAM/i],
26+
['date-range', [new Date(2000, 0, 1), new Date(2000, 0, 7)] as [Date, Date], /Jan 1\s\s7, 2000/i],
27+
['time-range', [new Date(2000, 0, 1, 2, 3), new Date(2000, 0, 1, 8, 9)] as [Date, Date], /2:03\s(AM\s)?\s8:09\sAM/i],
28+
['datetime-range', [new Date(2000, 0, 1, 2, 3), new Date(2000, 0, 7, 8, 9)] as [Date, Date], /Jan 1, 2000, 2:03\sAM\s\sJan 7, 2000, 8:09\sAM/i],
2829
] as const
2930

3031
for (const [type, modelValue, expectedValue] of testcases) {

0 commit comments

Comments
 (0)