feat(ui) : MalioDate/DateTime exposent update:valid + saisie clavier DateTime (#MUI-43)
- MalioDate : event update:valid (malforme/hors-plage => false), emis au montage - MalioDateTime : prop editable (saisie JJ/MM/AAAA HH:MM) + meme update:valid - CalendarField : masque maska configurable via prop mask - datetimeFormat : nouveau parseur parseDisplayToIsoDateTime - fix test Date « Entree » (key 'Enter' reel vs trigger keydown.enter) - doc COMPONENTS.md + CHANGELOG.md + champ editable dans le playground Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -338,7 +338,9 @@ describe('MalioDate', () => {
|
||||
await input.trigger('focus')
|
||||
expect(wrapper.find('[data-test="popover"]').exists()).toBe(true)
|
||||
await input.setValue('19/05/2026')
|
||||
await input.trigger('keydown.enter')
|
||||
// Valeur DOM réelle de la touche Entrée ('Enter') ; `trigger('keydown.enter')`
|
||||
// produirait `key: 'enter'`, qui ne matche pas le handler manuel `e.key === 'Enter'`.
|
||||
await input.trigger('keydown', {key: 'Enter'})
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-05-19'])
|
||||
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
|
||||
})
|
||||
@@ -351,4 +353,73 @@ describe('MalioDate', () => {
|
||||
expect(wrapper.text()).toContain('Format incorrect')
|
||||
})
|
||||
})
|
||||
|
||||
describe('état de validité (update:valid)', () => {
|
||||
it('émet valid=true au montage avec une valeur valide', () => {
|
||||
const wrapper = mountDate({modelValue: '2026-05-19'})
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
|
||||
it('émet valid=true au montage quand le champ est vide', () => {
|
||||
const wrapper = mountDate()
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
|
||||
it('émet valid=true sur saisie clavier valide', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('19/05/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
|
||||
it('émet valid=false sur saisie malformée sans émettre modelValue', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([false])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('émet valid=false sur saisie hors min/max', async () => {
|
||||
const wrapper = mountDate({editable: true, min: '2026-05-10', max: '2026-05-20'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('25/12/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([false])
|
||||
})
|
||||
|
||||
it('émet valid=true sur saisie vidée même si le champ est requis', async () => {
|
||||
const wrapper = mountDate({editable: true, required: true, modelValue: '2026-05-19'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([null])
|
||||
})
|
||||
|
||||
it('émet valid=true sur clear', async () => {
|
||||
const wrapper = mountDate({modelValue: '2026-05-19'})
|
||||
await wrapper.get('[data-test="clear"]').trigger('click')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
|
||||
it('émet valid=true quand on sélectionne une date au calendrier', async () => {
|
||||
const wrapper = mountDate()
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
await wrapper.get('[data-test="day"][data-iso="2026-05-19"]').trigger('click')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
|
||||
it('repasse valid=true quand modelValue change de l\'extérieur après une saisie invalide', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([false])
|
||||
await wrapper.setProps({modelValue: '2026-05-19'})
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -87,44 +87,56 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{(e: 'update:modelValue', value: string | null): void}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string | null): void
|
||||
(e: 'update:valid', value: boolean): void
|
||||
}>()
|
||||
|
||||
const displayValue = computed(() => formatIsoToDisplay(props.modelValue ?? null))
|
||||
|
||||
const internalError = ref('')
|
||||
const mergedError = computed(() => props.error || internalError.value)
|
||||
|
||||
// La validité ne reflète que la saisie : malformée/hors plage → false. Un champ
|
||||
// vide est valide (l'obligation `required` reste à la charge du parent).
|
||||
const setError = (message: string) => {
|
||||
internalError.value = message
|
||||
emit('update:valid', message === '')
|
||||
}
|
||||
|
||||
const onCommit = (text: string) => {
|
||||
const trimmed = text.trim()
|
||||
if (trimmed === '') {
|
||||
internalError.value = ''
|
||||
setError('')
|
||||
emit('update:modelValue', null)
|
||||
return
|
||||
}
|
||||
const iso = parseDisplayToIso(trimmed)
|
||||
if (iso && isDateInRange(iso, props.min, props.max)) {
|
||||
internalError.value = ''
|
||||
setError('')
|
||||
emit('update:modelValue', iso)
|
||||
return
|
||||
}
|
||||
internalError.value = props.invalidMessage
|
||||
setError(props.invalidMessage)
|
||||
}
|
||||
|
||||
const onClear = () => {
|
||||
internalError.value = ''
|
||||
setError('')
|
||||
emit('update:modelValue', null)
|
||||
}
|
||||
|
||||
const onSelect = (iso: string, close: () => void) => {
|
||||
internalError.value = ''
|
||||
setError('')
|
||||
emit('update:modelValue', iso)
|
||||
close()
|
||||
}
|
||||
|
||||
// immediate : émet aussi la validité au montage, pour que le parent connaisse
|
||||
// l'état d'un champ pré-rempli (formulaire d'édition) sans interaction préalable.
|
||||
watch(() => props.modelValue, (val) => {
|
||||
internalError.value = ''
|
||||
setError('')
|
||||
if (val && !isValidIso(val) && import.meta.dev) {
|
||||
console.warn(`[MalioDate] modelValue invalide ignoré : "${val}"`)
|
||||
}
|
||||
})
|
||||
}, {immediate: true})
|
||||
</script>
|
||||
|
||||
@@ -19,6 +19,8 @@ type DateTimeProps = {
|
||||
min?: string
|
||||
max?: string
|
||||
clearable?: boolean
|
||||
editable?: boolean
|
||||
invalidMessage?: string
|
||||
inputClass?: string
|
||||
labelClass?: string
|
||||
groupClass?: string
|
||||
@@ -120,4 +122,141 @@ describe('MalioDateTime', () => {
|
||||
expect(wrapper.text()).toContain('Date requise')
|
||||
})
|
||||
})
|
||||
|
||||
describe('saisie manuelle (editable)', () => {
|
||||
it('par défaut (editable=false) l\'input reste readonly', () => {
|
||||
const wrapper = mountDateTime({modelValue: '2026-05-20T14:30:00'})
|
||||
expect(wrapper.get('[data-test="date-input"]').attributes('readonly')).toBeDefined()
|
||||
})
|
||||
|
||||
it('editable=true : l\'input n\'est plus readonly', () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
expect(wrapper.get('[data-test="date-input"]').attributes('readonly')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('émet le datetime ISO sur saisie clavier valide au blur', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('20/05/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-05-20T14:30:00'])
|
||||
})
|
||||
|
||||
it('garde le texte et affiche « Date invalide » sur saisie invalide au blur', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
expect((input.element as HTMLInputElement).value).toBe('32/13/2026 14:30')
|
||||
expect(input.attributes('aria-invalid')).toBe('true')
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
})
|
||||
|
||||
it('passe en erreur si le datetime saisi est hors min/max', async () => {
|
||||
const wrapper = mountDateTime({editable: true, min: '2026-05-10T00:00:00', max: '2026-05-20T00:00:00'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('25/12/2026 10:00')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
})
|
||||
|
||||
it('émet null sur saisie vidée au blur', async () => {
|
||||
const wrapper = mountDateTime({editable: true, modelValue: '2026-05-20T14:30:00'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([null])
|
||||
})
|
||||
|
||||
it('valide et ferme le popover sur Entrée', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.trigger('focus')
|
||||
expect(wrapper.find('[data-test="popover"]').exists()).toBe(true)
|
||||
await input.setValue('20/05/2026 14:30')
|
||||
await input.trigger('keydown', {key: 'Enter'})
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-05-20T14:30:00'])
|
||||
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('utilise le message invalidMessage personnalisé', async () => {
|
||||
const wrapper = mountDateTime({editable: true, invalidMessage: 'Format incorrect'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('99/99/9999 10:00')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.text()).toContain('Format incorrect')
|
||||
})
|
||||
|
||||
it('efface l\'erreur de saisie quand modelValue change de l\'extérieur', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
await wrapper.setProps({modelValue: '2026-05-20T14:30:00'})
|
||||
expect(wrapper.text()).not.toContain('Date invalide')
|
||||
})
|
||||
})
|
||||
|
||||
describe('état de validité (update:valid)', () => {
|
||||
it('émet valid=true au montage avec une valeur valide', () => {
|
||||
const wrapper = mountDateTime({modelValue: '2026-05-20T14:30:00'})
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
|
||||
it('émet valid=true au montage quand le champ est vide', () => {
|
||||
const wrapper = mountDateTime()
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
|
||||
it('émet valid=true sur saisie clavier valide', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('20/05/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
|
||||
it('émet valid=false sur saisie malformée sans émettre modelValue', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([false])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('émet valid=true sur saisie vidée même si le champ est requis', async () => {
|
||||
const wrapper = mountDateTime({editable: true, required: true, modelValue: '2026-05-20T14:30:00'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
|
||||
it('émet valid=true sur clear', async () => {
|
||||
const wrapper = mountDateTime({modelValue: '2026-05-20T14:30:00'})
|
||||
await wrapper.get('[data-test="clear"]').trigger('click')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
|
||||
it('émet valid=true quand on sélectionne une date au calendrier', async () => {
|
||||
const wrapper = mountDateTime()
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
await wrapper.get('[data-test="day"][data-iso="2026-05-19"]').trigger('click')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
|
||||
it('repasse valid=true quand modelValue change de l\'extérieur après une saisie invalide', async () => {
|
||||
const wrapper = mountDateTime({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026 14:30')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([false])
|
||||
await wrapper.setProps({modelValue: '2026-05-20T14:30:00'})
|
||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,14 +10,17 @@
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:hint="hint"
|
||||
:error="error"
|
||||
:error="mergedError"
|
||||
:success="success"
|
||||
:clearable="clearable"
|
||||
:editable="editable"
|
||||
mask="##/##/#### ##:##"
|
||||
:input-class="inputClass"
|
||||
:label-class="labelClass"
|
||||
:group-class="groupClass"
|
||||
v-bind="$attrs"
|
||||
@clear="onClear"
|
||||
@commit="onCommit"
|
||||
>
|
||||
<template #default="{ currentMonth, currentYear }">
|
||||
<MonthGrid
|
||||
@@ -47,7 +50,8 @@ import CalendarField from './internal/CalendarField.vue'
|
||||
import MonthGrid from './internal/MonthGrid.vue'
|
||||
import MalioTimePicker from '../time/TimePicker.vue'
|
||||
import {formatTime} from '../time/composables/timeFormat'
|
||||
import {composeDateTime, formatIsoDateTimeToDisplay, isValidIsoDateTime, splitDateTime} from './composables/datetimeFormat'
|
||||
import {isDateInRange} from './composables/dateFormat'
|
||||
import {composeDateTime, formatIsoDateTimeToDisplay, isValidIsoDateTime, parseDisplayToIsoDateTime, splitDateTime} from './composables/datetimeFormat'
|
||||
|
||||
defineOptions({name: 'MalioDateTime', inheritAttrs: false})
|
||||
|
||||
@@ -67,6 +71,8 @@ const props = withDefaults(
|
||||
min?: string
|
||||
max?: string
|
||||
clearable?: boolean
|
||||
editable?: boolean
|
||||
invalidMessage?: string
|
||||
inputClass?: string
|
||||
labelClass?: string
|
||||
groupClass?: string
|
||||
@@ -86,13 +92,18 @@ const props = withDefaults(
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
clearable: true,
|
||||
editable: false,
|
||||
invalidMessage: 'Date invalide',
|
||||
inputClass: '',
|
||||
labelClass: '',
|
||||
groupClass: '',
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{(e: 'update:modelValue', value: string | null): void}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string | null): void
|
||||
(e: 'update:valid', value: boolean): void
|
||||
}>()
|
||||
|
||||
// pendingTime : heure réglée avant qu'un jour ne soit choisi (sinon on ne peut pas émettre).
|
||||
const pendingTime = ref('')
|
||||
@@ -102,17 +113,29 @@ const datePart = computed(() => parts.value.date)
|
||||
const displayValue = computed(() => formatIsoDateTimeToDisplay(props.modelValue ?? null))
|
||||
const timeValue = computed(() => parts.value.time || pendingTime.value)
|
||||
|
||||
const internalError = ref('')
|
||||
const mergedError = computed(() => props.error || internalError.value)
|
||||
|
||||
// La validité ne reflète que la saisie clavier : malformée/hors plage → false. Un
|
||||
// champ vide est valide (l'obligation `required` reste à la charge du parent).
|
||||
const setError = (message: string) => {
|
||||
internalError.value = message
|
||||
emit('update:valid', message === '')
|
||||
}
|
||||
|
||||
function onSelectDay(iso: string) {
|
||||
// Si aucune heure n'a été choisie, on prend l'heure actuelle (pas 00:00).
|
||||
// (heure courante au moment du clic)
|
||||
const now = new Date()
|
||||
const time = parts.value.time || pendingTime.value || formatTime(now.getHours(), now.getMinutes())
|
||||
setError('')
|
||||
emit('update:modelValue', composeDateTime(iso, time))
|
||||
}
|
||||
|
||||
function onTimeChange(value: string | null) {
|
||||
if (!value) return
|
||||
if (datePart.value) {
|
||||
setError('')
|
||||
emit('update:modelValue', composeDateTime(datePart.value, value))
|
||||
}
|
||||
else {
|
||||
@@ -120,14 +143,34 @@ function onTimeChange(value: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
function onCommit(text: string) {
|
||||
const trimmed = text.trim()
|
||||
if (trimmed === '') {
|
||||
setError('')
|
||||
emit('update:modelValue', null)
|
||||
return
|
||||
}
|
||||
const iso = parseDisplayToIsoDateTime(trimmed)
|
||||
if (iso && isDateInRange(iso, props.min, props.max)) {
|
||||
setError('')
|
||||
emit('update:modelValue', iso)
|
||||
return
|
||||
}
|
||||
setError(props.invalidMessage)
|
||||
}
|
||||
|
||||
function onClear() {
|
||||
setError('')
|
||||
pendingTime.value = ''
|
||||
emit('update:modelValue', null)
|
||||
}
|
||||
|
||||
// immediate : émet aussi la validité au montage, pour que le parent connaisse
|
||||
// l'état d'un champ pré-rempli (formulaire d'édition) sans interaction préalable.
|
||||
watch(() => props.modelValue, (val) => {
|
||||
setError('')
|
||||
if (val && !isValidIsoDateTime(val) && import.meta.dev) {
|
||||
console.warn(`[MalioDateTime] modelValue invalide ignoré : "${val}"`)
|
||||
}
|
||||
})
|
||||
}, {immediate: true})
|
||||
</script>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
composeDateTime,
|
||||
formatIsoDateTimeToDisplay,
|
||||
isValidIsoDateTime,
|
||||
parseDisplayToIsoDateTime,
|
||||
splitDateTime,
|
||||
} from './datetimeFormat'
|
||||
|
||||
@@ -49,6 +50,34 @@ describe('datetimeFormat', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseDisplayToIsoDateTime', () => {
|
||||
it('parse un JJ/MM/AAAA HH:MM valide en datetime ISO', () => {
|
||||
expect(parseDisplayToIsoDateTime('20/05/2026 14:30')).toBe('2026-05-20T14:30:00')
|
||||
expect(parseDisplayToIsoDateTime('01/01/2026 00:00')).toBe('2026-01-01T00:00:00')
|
||||
expect(parseDisplayToIsoDateTime('31/12/2026 23:59')).toBe('2026-12-31T23:59:00')
|
||||
})
|
||||
|
||||
it('tolère les espaces autour', () => {
|
||||
expect(parseDisplayToIsoDateTime(' 20/05/2026 14:30 ')).toBe('2026-05-20T14:30:00')
|
||||
})
|
||||
|
||||
it('rejette une date malformée', () => {
|
||||
expect(parseDisplayToIsoDateTime('32/01/2026 10:00')).toBeNull()
|
||||
expect(parseDisplayToIsoDateTime('10/13/2026 10:00')).toBeNull()
|
||||
})
|
||||
|
||||
it('rejette une heure hors bornes', () => {
|
||||
expect(parseDisplayToIsoDateTime('20/05/2026 24:00')).toBeNull()
|
||||
expect(parseDisplayToIsoDateTime('20/05/2026 12:60')).toBeNull()
|
||||
})
|
||||
|
||||
it('rejette un format incomplet ou sans heure', () => {
|
||||
expect(parseDisplayToIsoDateTime('20/05/2026')).toBeNull()
|
||||
expect(parseDisplayToIsoDateTime('20/05/2026 14')).toBeNull()
|
||||
expect(parseDisplayToIsoDateTime('')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('composeDateTime', () => {
|
||||
it('recompose un datetime ISO avec secondes à 00', () => {
|
||||
expect(composeDateTime('2026-05-20', '14:30')).toBe('2026-05-20T14:30:00')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {isValidIso} from './dateFormat'
|
||||
import {isValidIso, parseDisplayToIso} from './dateFormat'
|
||||
|
||||
const DATETIME_RE = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2}):(\d{2})$/
|
||||
|
||||
@@ -27,6 +27,16 @@ export function splitDateTime(s: string | null): {date: string | null; time: str
|
||||
return {date, time: time.slice(0, 5)}
|
||||
}
|
||||
|
||||
export function parseDisplayToIsoDateTime(display: string): string | null {
|
||||
const match = /^(\d{2}\/\d{2}\/\d{4}) (\d{2}):(\d{2})$/.exec(display.trim())
|
||||
if (!match) return null
|
||||
const [, datePart, hh, mm] = match
|
||||
const iso = parseDisplayToIso(datePart)
|
||||
if (!iso) return null
|
||||
if (Number(hh) > 23 || Number(mm) > 59) return null
|
||||
return `${iso}T${hh}:${mm}:00`
|
||||
}
|
||||
|
||||
export function composeDateTime(date: string, time: string): string {
|
||||
const t = time || '00:00'
|
||||
return `${date}T${t}:00`
|
||||
|
||||
@@ -137,6 +137,7 @@ const props = withDefaults(
|
||||
success?: string
|
||||
clearable?: boolean
|
||||
editable?: boolean
|
||||
mask?: string
|
||||
inputClass?: string
|
||||
labelClass?: string
|
||||
groupClass?: string
|
||||
@@ -155,6 +156,7 @@ const props = withDefaults(
|
||||
success: '',
|
||||
clearable: true,
|
||||
editable: false,
|
||||
mask: '##/##/####',
|
||||
inputClass: '',
|
||||
labelClass: '',
|
||||
groupClass: '',
|
||||
@@ -172,7 +174,7 @@ const generatedId = useId()
|
||||
const root = ref<HTMLElement | null>(null)
|
||||
|
||||
const draft = ref(props.displayValue)
|
||||
const maskaOptions = computed<MaskInputOptions>(() => ({mask: props.editable ? '##/##/####' : undefined}))
|
||||
const maskaOptions = computed<MaskInputOptions>(() => ({mask: props.editable ? props.mask : undefined}))
|
||||
const inputReadonly = computed(() => !props.editable || props.readonly || props.disabled)
|
||||
|
||||
watch(() => props.displayValue, (value) => {
|
||||
|
||||
Reference in New Issue
Block a user