Skip to content

Commit e204bfa

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 e204bfa

2 files changed

Lines changed: 99 additions & 35 deletions

File tree

src/components/NcDateTimePicker/NcDateTimePicker.vue

Lines changed: 96 additions & 33 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,
@@ -245,11 +257,14 @@ const props = withDefaults(defineProps<{
245257
246258
/**
247259
* Type of the picker.
248-
* The 'range' type will enable a range picker for dates,
249-
* while 'range-datetime' will allow picking a date range with times.
260+
* There is some special handling for ranges as those types require a `[Date, Date]` model value.
261+
* - The 'date-range' type will enable a range picker for dates
262+
* - The 'time-range' allows picking a time range.
263+
* - The 'datetime-range' allows picking dates with times assigned.
264+
*
250265
* @default 'date'
251266
*/
252-
type?: 'date' | 'datetime' | 'time' | 'week' | 'month' | 'year' | 'range' | 'range-datetime'
267+
type?: 'date' | 'datetime' | 'time' | 'week' | 'month' | 'year' | 'date-range' | 'time-range' | 'datetime-range'
253268
254269
appendToBody?: boolean
255270
@@ -281,8 +296,9 @@ const emit = defineEmits<{
281296
/**
282297
* If range picker is enabled then an array containing start and end date are emitted.
283298
* Otherwise the selected date is emitted.
299+
* `null` is emitted if `clearable` is set to `true` and the value was cleared.
284300
*/
285-
'update:modelValue': [Date | [Date, Date]]
301+
'update:modelValue': [Date | [Date, Date] | null]
286302
'update:timezoneId': [string]
287303
}>()
288304
@@ -296,20 +312,32 @@ const value = computed(() => {
296312
const end = new Date(date)
297313
end.setUTCDate(date.getUTCDate() + 6)
298314
return [date, end]
299-
} else if (props.type.startsWith('range')) {
315+
} else if (props.type === 'year') {
316+
const date = props.modelValue instanceof Date ? props.modelValue : new Date()
317+
return date.getUTCFullYear()
318+
} else if (props.type === 'month') {
319+
const date = props.modelValue instanceof Date ? props.modelValue : new Date()
320+
return { year: date.getUTCFullYear(), month: date.getUTCMonth() }
321+
} else if (props.type === 'time' || props.type === 'time-range') {
322+
const time = [props.modelValue ?? (props.type === 'time-range' ? [new Date(), new Date()] : new Date())].flat()
323+
// default time range is 1 hour
324+
if (props.modelValue === undefined && props.type === 'time-range') {
325+
time[1].setHours(time[1].getHours() + 1)
326+
}
327+
const timeValue = time.map((date) => ({
328+
hours: date.getHours(),
329+
minutes: date.getMinutes(),
330+
seconds: date.getSeconds(),
331+
} as LibraryTimeObject))
332+
return props.type === 'time' ? timeValue[0] : timeValue
333+
} else if (props.type.endsWith('-range')) {
300334
if (props.modelValue === undefined) {
301335
const start = new Date()
302336
const end = new Date(start)
303337
end.setUTCDate(start.getUTCDate() + 7)
304338
return [start, end]
305339
}
306340
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() }
313341
}
314342
315343
// no special handling for other types needed
@@ -329,7 +357,7 @@ const placeholderFallback = computed(() => {
329357
return t('Select month')
330358
} else if (props.type === 'year') {
331359
return t('Select year')
332-
} else if (props.type.startsWith('range')) {
360+
} else if (props.type.endsWith('-range')) {
333361
return t('Select time range')
334362
}
335363
// should not be reached
@@ -350,10 +378,12 @@ const realFormat = computed(() => {
350378
}
351379
352380
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') {
381+
if (props.type === 'date' || props.type === 'date-range') {
356382
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { dateStyle: 'medium' })
383+
} else if (props.type === 'time' || props.type === 'time-range') {
384+
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { timeStyle: 'short' })
385+
} else if (props.type === 'datetime' || props.type === 'datetime-range') {
386+
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { dateStyle: 'medium', timeStyle: 'short' })
357387
} else if (props.type === 'month') {
358388
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { year: 'numeric', month: '2-digit' })
359389
} else if (props.type === 'year') {
@@ -371,12 +401,12 @@ const realFormat = computed(() => {
371401
})
372402
373403
const pickerType = computed(() => ({
374-
timePicker: props.type === 'time',
404+
timePicker: props.type === 'time' || props.type === 'time-range',
375405
yearPicker: props.type === 'year',
376406
monthPicker: props.type === 'month',
377407
weekPicker: props.type === 'week',
378-
range: props.type.startsWith('range'),
379-
enableTimePicker: !(props.type === 'date' || props.type === 'range'),
408+
range: props.type.endsWith('-range'),
409+
enableTimePicker: !(props.type === 'date' || props.type === 'date-range'),
380410
flow: props.type === 'datetime'
381411
? ['calendar', 'time'] as ['calendar', 'time']
382412
: undefined,
@@ -386,17 +416,50 @@ const pickerType = computed(() => ({
386416
* Called on model value update of the library.
387417
* @param value The value emitted from the underlying library
388418
*/
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') {
419+
function onUpdateModelValue(value: LibraryModelValue): void {
420+
if (value === null) {
421+
return emit('update:modelValue', null)
422+
}
423+
424+
if (props.type === 'time') {
425+
// time is provided as an object
426+
emit('update:modelValue', formatLibraryTime(value as LibraryTimeObject))
427+
} else if (props.type === 'time-range') {
428+
// same as time but as an array with two elements
429+
const start = formatLibraryTime(value[0])
430+
const end = formatLibraryTime(value[1])
431+
// ensure end is beyond the start
432+
if (end.getTime() < start.getTime()) {
433+
end.setDate(end.getDate() + 1)
434+
}
435+
emit('update:modelValue', [start, end])
436+
} else if (props.type === 'month') {
437+
// month is emitted as an object with month and year attribute
392438
const data = value as { month: number, year: number }
393-
date = new Date(data.year, data.month, 1)
439+
emit('update:modelValue', new Date(data.year, data.month, 1))
394440
} else if (props.type === 'year') {
395-
date = new Date(value as number, 0)
441+
// Years are emitted as the numeric year e.g. 2022
442+
emit('update:modelValue', new Date(value as number, 0))
396443
} else if (props.type === 'week') {
397-
date = value[0]
444+
// weeks are emitted as [Date, Date]
445+
emit('update:modelValue', value[0])
446+
} else {
447+
// otherwise it already emits the correct format
448+
emit('update:modelValue', value as Date | [Date, Date])
398449
}
399-
emit('update:modelValue', date)
450+
}
451+
452+
/**
453+
* Format a vuepick time object to native JS Date object.
454+
*
455+
* @param time - The library time value object
456+
*/
457+
function formatLibraryTime(time: LibraryTimeObject): Date {
458+
const date = new Date()
459+
date.setHours(time.hours)
460+
date.setMinutes(time.minutes)
461+
date.setSeconds(time.seconds)
462+
return date
400463
}
401464
402465
// 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)