Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Especially the following are now provided as composables:
- `NcModal`
- `NcPopover`
- `NcDateTimePicker`
- The `range` property was removed in favor of `type="range"` (datetime ranges) and `type="range-date"` (date only ranges).
- The `range` property was removed in favor of `type="datetime-range"` (datetime ranges), `type="date-range"` (date only ranges), and `type="time-range"` (time only ranges).
- The `lang` property was replaced with the `locale` property.
- The `formatter` property was removed.

Expand Down
129 changes: 96 additions & 33 deletions src/components/NcDateTimePicker/NcDateTimePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,13 @@ Meaning an array with two dates is used, the first date is the range start and t
<div>
<fieldset class="type-select">
<legend>Picker mode</legend>
<NcCheckboxRadioSwitch v-model="type" type="radio" value="range">Date</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-model="type" type="radio" value="range-datetime">Date and time</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-model="type" type="radio" value="date-range">Date</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-model="type" type="radio" value="time-range">Time</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-model="type" type="radio" value="datetime-range">Date and time</NcCheckboxRadioSwitch>
</fieldset>

<NcDateTimePicker
:key="type"
v-model="time"
:type />
<div>
Expand All @@ -106,17 +108,20 @@ Meaning an array with two dates is used, the first date is the range start and t
export default {
data() {
return {
time: [new Date(2025, 3, 18), new Date(2025, 3, 21)],
type: 'range',
time: [new Date(2025, 3, 18, 12, 30), new Date(2025, 3, 21, 13, 30)],
type: 'date-range',
}
},
methods: {
formatDate(date) {
const text = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
if (this.type === 'range') {
return text
const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
const timeString = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
if (this.type === 'date-range') {
return dateString
} else if (this.type === 'time-range') {
return timeString
}
return `${text} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
return `${dateString} ${timeString}`
},
},
}
Expand Down Expand Up @@ -161,6 +166,13 @@ export default {
</docs>

<script setup lang="ts">
import type {
// The emitted object for time picker
TimeObj as LibraryTimeObject,
// The accepted model value
ModelValue as LibraryModelValue,
} from '@vuepic/vue-datepicker'

import {
mdiCalendarBlank,
mdiChevronDown,
Expand Down Expand Up @@ -271,11 +283,14 @@ const props = withDefaults(defineProps<{

/**
* Type of the picker.
* The 'range' type will enable a range picker for dates,
* while 'range-datetime' will allow picking a date range with times.
* There is some special handling for ranges as those types require a `[Date, Date]` model value.
* - The 'date-range' type will enable a range picker for dates
* - The 'time-range' allows picking a time range.
* - The 'datetime-range' allows picking dates with times assigned.
*
* @default 'date'
*/
type?: 'date' | 'datetime' | 'time' | 'week' | 'month' | 'year' | 'range' | 'range-datetime'
type?: 'date' | 'datetime' | 'time' | 'week' | 'month' | 'year' | 'date-range' | 'time-range' | 'datetime-range'
Comment thread
susnux marked this conversation as resolved.
}>(), {
ariaLabel: t('Datepicker input'),
ariaLabelMenu: t('Datepicker menu'),
Expand Down Expand Up @@ -303,8 +318,9 @@ const emit = defineEmits<{
/**
* If range picker is enabled then an array containing start and end date are emitted.
* Otherwise the selected date is emitted.
* `null` is emitted if `clearable` is set to `true` and the value was cleared.
*/
'update:modelValue': [Date | [Date, Date]]
'update:modelValue': [Date | [Date, Date] | null]
'update:timezoneId': [string]
}>()

Expand All @@ -323,20 +339,32 @@ const value = computed(() => {
const end = new Date(date)
end.setUTCDate(date.getUTCDate() + 6)
return [date, end]
} else if (props.type.startsWith('range')) {
} else if (props.type === 'year') {
const date = props.modelValue instanceof Date ? props.modelValue : new Date()
return date.getUTCFullYear()
} else if (props.type === 'month') {
const date = props.modelValue instanceof Date ? props.modelValue : new Date()
return { year: date.getUTCFullYear(), month: date.getUTCMonth() }
} else if (props.type === 'time' || props.type === 'time-range') {
const time = [props.modelValue ?? (props.type === 'time-range' ? [new Date(), new Date()] : new Date())].flat()
// default time range is 1 hour
if (props.modelValue === undefined && props.type === 'time-range') {
time[1].setHours(time[1].getHours() + 1)
}
const timeValue = time.map((date) => ({
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds(),
} as LibraryTimeObject))
return props.type === 'time' ? timeValue[0] : timeValue
} else if (props.type.endsWith('-range')) {
if (props.modelValue === undefined) {
const start = new Date()
const end = new Date(start)
end.setUTCDate(start.getUTCDate() + 7)
return [start, end]
}
return props.modelValue
} else if (props.type === 'year') {
const date = props.modelValue instanceof Date ? props.modelValue : new Date()
return date.getUTCFullYear()
} else if (props.type === 'month') {
const date = props.modelValue instanceof Date ? props.modelValue : new Date()
return { year: date.getUTCFullYear(), month: date.getUTCMonth() }
}

// no special handling for other types needed
Expand All @@ -356,7 +384,7 @@ const placeholderFallback = computed(() => {
return t('Select month')
} else if (props.type === 'year') {
return t('Select year')
} else if (props.type.startsWith('range')) {
} else if (props.type.endsWith('-range')) {
return t('Select time range')
}
// should not be reached
Expand All @@ -377,10 +405,12 @@ const realFormat = computed(() => {
}

let formatter: Intl.DateTimeFormat | undefined
if (props.type === 'datetime' || props.type === 'range-datetime') {
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { dateStyle: 'medium', timeStyle: 'short' })
} else if (props.type === 'date' || props.type === 'range') {
if (props.type === 'date' || props.type === 'date-range') {
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { dateStyle: 'medium' })
} else if (props.type === 'time' || props.type === 'time-range') {
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { timeStyle: 'short' })
} else if (props.type === 'datetime' || props.type === 'datetime-range') {
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { dateStyle: 'medium', timeStyle: 'short' })
} else if (props.type === 'month') {
formatter = new Intl.DateTimeFormat(getCanonicalLocale(), { year: 'numeric', month: '2-digit' })
} else if (props.type === 'year') {
Expand All @@ -398,17 +428,17 @@ const realFormat = computed(() => {
})

const pickerType = computed(() => ({
timePicker: props.type === 'time',
timePicker: props.type === 'time' || props.type === 'time-range',
yearPicker: props.type === 'year',
monthPicker: props.type === 'month',
weekPicker: props.type === 'week',
range: props.type.startsWith('range') && {
range: props.type.endsWith('-range') && {
// do not use partial ranges (meaning after selecting the start [Date, null] will be emitted)
// if this is needed someday we can enable it,
// but its not covered by our component interface (props / events) documentation so just disabled for now.
partialRange: false,
},
enableTimePicker: !(props.type === 'date' || props.type === 'range'),
enableTimePicker: !(props.type === 'date' || props.type === 'date-range'),
flow: props.type === 'datetime'
? ['calendar', 'time'] as ['calendar', 'time']
: undefined,
Expand All @@ -418,17 +448,50 @@ const pickerType = computed(() => ({
* Called on model value update of the library.
* @param value The value emitted from the underlying library
*/
function onUpdateModelValue(value: Date | [Date, Date] | number | { month: number, year: number }): void {
let date = value as Date | [Date, Date]
if (props.type === 'month') {
function onUpdateModelValue(value: LibraryModelValue): void {
if (value === null) {
return emit('update:modelValue', null)
}

if (props.type === 'time') {
// time is provided as an object
emit('update:modelValue', formatLibraryTime(value as LibraryTimeObject))
} else if (props.type === 'time-range') {
// same as time but as an array with two elements
const start = formatLibraryTime(value[0])
const end = formatLibraryTime(value[1])
// ensure end is beyond the start
if (end.getTime() < start.getTime()) {
end.setDate(end.getDate() + 1)
}
emit('update:modelValue', [start, end])
} else if (props.type === 'month') {
// month is emitted as an object with month and year attribute
const data = value as { month: number, year: number }
date = new Date(data.year, data.month, 1)
emit('update:modelValue', new Date(data.year, data.month, 1))
} else if (props.type === 'year') {
date = new Date(value as number, 0)
// Years are emitted as the numeric year e.g. 2022
emit('update:modelValue', new Date(value as number, 0))
} else if (props.type === 'week') {
date = value[0]
// weeks are emitted as [Date, Date]
emit('update:modelValue', value[0])
} else {
// otherwise it already emits the correct format
emit('update:modelValue', value as Date | [Date, Date])
}
emit('update:modelValue', date)
}

/**
* Format a vuepick time object to native JS Date object.
*
* @param time - The library time value object
*/
function formatLibraryTime(time: LibraryTimeObject): Date {
const date = new Date()
date.setHours(time.hours)
date.setMinutes(time.minutes)
date.setSeconds(time.seconds)
return date
}

// Localization
Expand Down
5 changes: 3 additions & 2 deletions tests/component/components/NcDateTimePicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ const testcases = [
['week', new Date(2000, 0, 2, 3, 4), '1999-52'],
['month', new Date(2000, 0, 2, 3, 4), '01/2000'],
['year', new Date(2000, 0, 2, 3, 4), '2000'],
['range', [new Date(2000, 0, 1), new Date(2000, 0, 7)] as [Date, Date], /Jan 1\s–\s7, 2000/i],
['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],
['date-range', [new Date(2000, 0, 1), new Date(2000, 0, 7)] as [Date, Date], /Jan 1\s–\s7, 2000/i],
['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],
['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],
] as const

for (const [type, modelValue, expectedValue] of testcases) {
Expand Down
Loading