import { createDateField } from '../../index.js'; import { areAllDaysBetweenValid, dateStore, getAnnouncer, getDefaultDate, getFirstSegment, isBefore, isBeforeOrSame, } from '../../internal/helpers/date/index.js'; import { addMeltEventListener, makeElement, createElHelpers, effect, executeCallbacks, omit, overridable, sleep, styleToString, toWritableStores, } from '../../internal/helpers/index.js'; import { withGet } from '../../internal/helpers/withGet.js'; import { derived, writable } from 'svelte/store'; import { generateIds } from '../../internal/helpers/id.js'; import { removeDescriptionElement } from './_internal/helpers.js'; const defaults = { isDateUnavailable: undefined, value: undefined, hourCycle: undefined, locale: 'en', granularity: undefined, hideTimeZone: false, defaultValue: { start: undefined, end: undefined, }, startName: undefined, endName: undefined, disabled: false, readonly: false, readonlySegments: undefined, minValue: undefined, maxValue: undefined, }; const { name } = createElHelpers('dateField'); const rangeFieldIdParts = ['field', 'label', 'description', 'validation']; export function createDateRangeField(props) { const withDefaults = { ...defaults, ...props }; const options = toWritableStores(omit(withDefaults, 'value', 'placeholder')); const generatedIds = generateIds(rangeFieldIdParts); const ids = toWritableStores({ ...generatedIds, ...withDefaults.ids }); const defaultDate = getDefaultDate({ defaultValue: withDefaults.defaultValue?.start, defaultPlaceholder: withDefaults.defaultPlaceholder, granularity: withDefaults.granularity, }); const valueWritable = withDefaults.value ?? writable(withDefaults.defaultValue); const value = overridable(valueWritable, withDefaults.onValueChange); const startValue = withGet.writable(value.get()?.start ?? withDefaults.defaultValue?.start); const endValue = withGet.writable(value.get()?.end ?? withDefaults.defaultValue?.end); const isCompleted = derived(value, ($value) => { return $value?.start && $value?.end; }); const placeholderWritable = withDefaults.placeholder ?? writable(withDefaults.defaultPlaceholder ?? defaultDate); const placeholder = dateStore(overridable(placeholderWritable, withDefaults.onPlaceholderChange), withDefaults.defaultPlaceholder ?? defaultDate); const startField = createDateField({ ...omit(withDefaults, 'defaultValue', 'onValueChange', 'startName', 'endName', 'readonlySegments'), value: startValue, name: withDefaults.startName, readonlySegments: withDefaults.readonlySegments?.start, ids: { ...generatedIds, ...withDefaults.ids, ...withDefaults.startIds, }, }); const endField = createDateField({ ...omit(withDefaults, 'defaultValue', 'onValueChange', 'endName', 'startName', 'readonlySegments'), value: endValue, name: withDefaults.endName, readonlySegments: withDefaults.readonlySegments?.end, ids: { ...generatedIds, ...withDefaults.ids, ...withDefaults.endIds, }, }); const { elements: { segment: startSegment, hiddenInput: startHiddenInput }, states: { isInvalid: isStartInvalid, segmentContents: startSegmentContents, segmentValues: startSegmentValues, }, options: { name: startName }, } = startField; const { elements: { segment: endSegment, hiddenInput: endHiddenInput }, states: { isInvalid: isEndInvalid, segmentContents: endSegmentContents, segmentValues: endSegmentValues, }, options: { name: endName }, } = endField; const isInvalid = derived([value, isStartInvalid, isEndInvalid, options.isDateUnavailable], ([$value, $isStartInvalid, $isEndInvalid, $isDateUnavailable]) => { if ($isStartInvalid || $isEndInvalid) { return true; } if (!$value?.start || !$value?.end) { return false; } if (!isBeforeOrSame($value?.start, $value?.end)) { return true; } if ($isDateUnavailable !== undefined) { const allValid = areAllDaysBetweenValid($value?.start, $value?.end, $isDateUnavailable, undefined); if (!allValid) { return true; } } return false; }); const label = makeElement(name('label'), { stores: [isInvalid, options.disabled, ids.label], returned: ([$isInvalid, $disabled, $labelId]) => { return { id: $labelId, 'data-invalid': $isInvalid ? '' : undefined, 'data-disabled': $disabled ? '' : undefined, }; }, action: (node) => { const unsub = executeCallbacks(addMeltEventListener(node, 'click', () => { const firstSegment = getFirstSegment(ids.field.get()); if (!firstSegment) return; sleep(1).then(() => firstSegment.focus()); }), addMeltEventListener(node, 'mousedown', (e) => { if (!e.defaultPrevented && e.detail > 1) { e.preventDefault(); } })); return { destroy: unsub, }; }, }); const fieldIdDeps = derived([ids.field, ids.label, ids.description, ids.validation], ([$fieldId, $labelId, $descriptionId, $validationId]) => { return { field: $fieldId, label: $labelId, description: $descriptionId, validation: $validationId, }; }); const field = makeElement(name('field'), { stores: [isCompleted, isInvalid, fieldIdDeps], returned: ([$isCompleted, $isInvalid, $ids]) => { const describedBy = $isCompleted ? `${$ids.description}${$isInvalid ? ` ${$ids.validation}` : ''}` : `${$ids.description}`; return { role: 'group', id: $ids.field, 'aria-labelledby': $ids.label, 'aria-describedby': describedBy, 'data-invalid': $isInvalid ? '' : undefined, }; }, action: () => { getAnnouncer(); return { destroy() { removeDescriptionElement(ids.description.get()); }, }; }, }); const validation = makeElement(name('validation'), { stores: [isInvalid, ids.validation], returned: ([$isInvalid, $validationId]) => { const validStyle = styleToString({ display: 'none', }); return { id: $validationId, 'data-invalid': $isInvalid ? '' : undefined, style: $isInvalid ? undefined : validStyle, }; }, }); /** * Combine the `startSegmentContents` and `endSegmentContents` stores * into a single store that can be used to render the contents of the * date range field. * * Since contents are generated automatically based on the locale * and granularity props, this is not a writable store. If you wish * to control the contents of the field, you should use the * `startSegmentValues` and `endSegmentValues` stores returned * from this builder instead. */ const segmentContents = derived([startSegmentContents, endSegmentContents], ([$startSegmentContents, $endSegmentContents]) => { return { start: $startSegmentContents, end: $endSegmentContents, }; }); /** * Synchronize the `value` store with the individual `startValue` * and `endValue` stores that are used by the individual date fields. * * We only want to update the `value` store when both the `startValue` * and `endValue` stores are not `undefined`. This is because the * `value` store is used to determine if the date field is completed, * and we don't want to mark the date field as completed until both * the start and end dates have been selected. */ effect([value], ([$value]) => { const $startValue = startValue.get(); const $endValue = endValue.get(); if ($value?.start && $value?.end) { if ($value.start !== $startValue) { startValue.set($value.start); } if ($value.end !== $endValue) { endValue.set($value.end); } return; } }); effect([startValue, endValue], ([$startValue, $endValue]) => { const $value = value.get(); if ($value && $value?.start === $startValue && $value?.end === $endValue) return; if ($startValue && $endValue) { value.update((prev) => { if (prev?.start === $startValue && prev?.end === $endValue) { return prev; } return { start: $startValue, end: $endValue, }; }); } else if ($value && $value?.start && $value?.end) { value.set({ start: undefined, end: undefined, }); } }); effect([options.disabled], ([$disabled]) => { startField.options.disabled.set($disabled); endField.options.disabled.set($disabled); }); effect([options.readonly], ([$readonly]) => { startField.options.readonly.set($readonly); endField.options.readonly.set($readonly); }); effect([options.readonlySegments], ([$readonlySegments]) => { startField.options.readonlySegments.set($readonlySegments?.start); endField.options.readonlySegments.set($readonlySegments?.end); }); effect([options.minValue], ([$minValue]) => { startField.options.minValue.set($minValue); endField.options.minValue.set($minValue); }); effect([options.maxValue], ([$maxValue]) => { startField.options.maxValue.set($maxValue); endField.options.maxValue.set($maxValue); }); effect([options.granularity], ([$granularity]) => { startField.options.granularity.set($granularity); endField.options.granularity.set($granularity); }); effect([options.hideTimeZone], ([$hideTimeZone]) => { startField.options.hideTimeZone.set($hideTimeZone); endField.options.hideTimeZone.set($hideTimeZone); }); effect([options.hourCycle], ([$hourCycle]) => { startField.options.hourCycle.set($hourCycle); endField.options.hourCycle.set($hourCycle); }); effect([options.locale], ([$locale]) => { startField.options.locale.set($locale); endField.options.locale.set($locale); }); return { elements: { field, label, startSegment, endSegment, startHiddenInput, endHiddenInput, validation, }, states: { value, placeholder: placeholder.toWritable(), segmentContents, endSegmentValues, startSegmentValues, isInvalid, }, options: { ...options, endName, startName, }, ids: { field: ids, start: startField.ids, end: endField.ids, }, }; }