@rotki/ui-library 2.23.5 → 2.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/date-time-picker/RuiDateTimePicker.js.map +1 -1
- package/dist/components/date-time-picker/RuiDateTimePicker.vue.d.ts +16 -0
- package/dist/components/date-time-picker/RuiDateTimePicker.vue_vue_type_script_setup_true_lang.js +20 -3
- package/dist/components/date-time-picker/RuiDateTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/date-time-picker/partial-time.d.ts +35 -0
- package/dist/components/date-time-picker/partial-time.js +71 -0
- package/dist/components/date-time-picker/partial-time.js.map +1 -0
- package/dist/components/date-time-picker/use-date-bounds.d.ts +27 -0
- package/dist/components/date-time-picker/use-date-bounds.js +56 -0
- package/dist/components/date-time-picker/use-date-bounds.js.map +1 -0
- package/dist/components/date-time-picker/use-date-time-selection.d.ts +4 -0
- package/dist/components/date-time-picker/use-date-time-selection.js +39 -38
- package/dist/components/date-time-picker/use-date-time-selection.js.map +1 -1
- package/dist/web-types.json +10 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RuiDateTimePicker.js","names":[],"sources":["../../../src/components/date-time-picker/RuiDateTimePicker.vue"],"sourcesContent":["<script lang=\"ts\" setup>\nimport type { ComponentPublicInstance } from 'vue';\nimport type { DateTimePickerAction, DateTimeSegmentType } from '@/components/date-time-picker/types';\nimport type { TimePickerSelection } from '@/components/time-picker/RuiTimePicker.vue';\nimport RuiButton from '@/components/buttons/button/RuiButton.vue';\nimport { dateTimePickerStyles, type DateTimePickerVariant } from '@/components/date-time-picker/date-time-picker-styles';\nimport RuiDateTimePickerMenu from '@/components/date-time-picker/RuiDateTimePickerMenu.vue';\nimport { useDateTimeSelection } from '@/components/date-time-picker/use-date-time-selection';\nimport { useInputHandler } from '@/components/date-time-picker/use-input-handler';\nimport { useKeyboardHandler } from '@/components/date-time-picker/use-keyboard-handler';\nimport RuiIcon from '@/components/icons/RuiIcon.vue';\nimport RuiMenu from '@/components/overlays/menu/RuiMenu.vue';\nimport { type FloatingOptions, Placement } from '@/composables/floating';\nimport { useRuiI8n } from '@/composables/use-rui-i18n';\nimport { RUI_I18N_KEYS } from '@/i18n/keys';\nimport { useFormTextDetail } from '@/utils/form-text-detail';\nimport { getNonRootAttrs, getRootAttrs } from '@/utils/helpers';\nimport { cn } from '@/utils/tv';\n\ntype DateFormat = 'year-first' | 'month-first' | 'day-first';\n\ntype DateTimeModelType = 'date' | 'epoch-ms' | 'epoch';\n\ntype ModelValueType<T extends DateTimeModelType> = T extends 'date'\n ? Date | undefined\n : T extends 'epoch-ms'\n ? number | undefined\n : T extends 'epoch'\n ? number | undefined\n : Date | number | undefined;\n\nexport interface RuiDateTimePickerProps {\n minDate?: Date | number;\n maxDate?: Date | number | 'now';\n format?: DateFormat;\n type?: DateTimeModelType;\n accuracy?: 'minute' | 'second' | 'millisecond';\n disabled?: boolean;\n allowEmpty?: boolean;\n readonly?: boolean;\n dense?: boolean;\n label?: string;\n variant?: DateTimePickerVariant;\n hint?: string;\n errorMessages?: string | string[];\n successMessages?: string | string[];\n hideDetails?: boolean;\n required?: boolean;\n /**\n * Renders the timezone selector in the menu. Off by default: the value is\n * emitted as a date or an epoch, so the picked timezone does not survive a\n * round trip and most consumers work in local time.\n */\n showTimezone?: boolean;\n /**\n * Actions rendered in the menu footer. `clear` is only rendered when the\n * picker is `allowEmpty`. Pass an empty array to drop the footer entirely.\n */\n actions?: DateTimePickerAction[];\n /**\n * Focuses the field once it is mounted. The native attribute is ignored for\n * an input inserted into an already loaded document, which is the usual case\n * for a picker revealed by an editor or a dialog.\n */\n autofocus?: boolean;\n}\n\ndefineOptions({\n name: 'RuiDateTimePicker',\n inheritAttrs: false,\n});\n\nconst modelValue = defineModel<ModelValueType<DateTimeModelType>>({ required: true });\nconst menuOpen = defineModel<boolean>('menuOpen', { default: false });\n\nconst {\n disabled = false,\n readonly = false,\n allowEmpty = false,\n dense = false,\n type = 'epoch-ms',\n hideDetails = false,\n label,\n variant = 'default',\n hint,\n maxDate,\n minDate,\n format = 'day-first',\n accuracy = 'minute',\n errorMessages = [],\n successMessages = [],\n required = false,\n showTimezone = false,\n actions = ['now'],\n autofocus = false,\n} = defineProps<RuiDateTimePickerProps>();\n\ndefineSlots<{\n 'menu-content': () => any;\n}>();\n\nconst MENU_OPTIONS: FloatingOptions = { placement: Placement.bottomStart };\n\nconst baseFormats: Record<DateFormat, string> = {\n 'day-first': 'DD/MM/YYYY HH:mm',\n 'month-first': 'MM/DD/YYYY HH:mm',\n 'year-first': 'YYYY/MM/DD HH:mm',\n};\n\nconst isOpen = ref<boolean>(false);\nconst isHovered = ref<boolean>(false);\nconst cursorPosition = ref<number>(0);\nconst currentValue = ref<number>();\n\nconst { t } = useRuiI8n();\n\nconst keys = RUI_I18N_KEYS.dateTimePicker;\n\nconst fieldLabel = computed<string>(() => label ?? t(keys.label, 'Pick a date'));\nconst clearLabel = computed<string>(() => t(keys.clearValue, 'Clear the date'));\nconst toggleLabel = computed<string>(() => (get(isOpen)\n ? t(keys.closeCalendar, 'Close the calendar')\n : t(keys.openCalendar, 'Open the calendar')));\n\nconst textInput = useTemplateRef<HTMLInputElement>('textInput');\nconst activator = useTemplateRef<HTMLDivElement>('activator');\nconst menuWrapperRef = useTemplateRef<ComponentPublicInstance>('menuWrapperRef');\nconst calendarMenuOpen = ref<boolean>(false);\n\nconst { focused: activatorFocusedWithin } = useFocusWithin(activator);\nconst { focused: menuWrapperFocusedWithin } = useFocusWithin(menuWrapperRef);\nconst { focused: searchInputFocused } = useFocus(textInput);\n\nconst anyFocused = computed<boolean>(() => get(activatorFocusedWithin) || get(menuWrapperFocusedWithin));\n\nconst dateFormat = computed<string>(() => {\n const fmt = baseFormats[format];\n if (accuracy === 'second') {\n return fmt.replace('HH:mm', 'HH:mm:ss');\n }\n else if (accuracy === 'millisecond') {\n return fmt.replace('HH:mm', 'HH:mm:ss.SSS');\n }\n return fmt;\n});\n\nconst {\n clear: clearSelection,\n getDateTime,\n internalErrorMessages,\n maxAllowedDate,\n minAllowedDate,\n segmentData,\n selectedDate,\n selectedDay,\n selectedHour,\n selectedMillisecond,\n selectedMinute,\n selectedMonth,\n selectedSecond,\n selectedTime,\n selectedTimezone,\n selectedYear,\n setNow,\n setToday,\n valueSet,\n} = useDateTimeSelection({\n accuracy,\n allowEmpty,\n dateFormat,\n maxDate,\n minDate,\n modelValue,\n type,\n});\n\nconst { setValue, getCurrent } = useInputHandler(segmentData, currentValue);\n\nconst {\n clear: clearSegment,\n getCurrentSegment,\n handleBlur,\n handleClick,\n handleFocus,\n handleInput,\n handleInputSelection,\n handleKeyDown,\n handleMouseDown,\n handlePaste,\n selectFirstSegment,\n setSegment,\n} = useKeyboardHandler({\n accuracy,\n currentValue,\n cursorPosition,\n dateFormat,\n disabled,\n getCurrent,\n getDateTime,\n readonly,\n setValue,\n textInput,\n});\n\nconst { hasError, hasSuccess } = useFormTextDetail(\n () => errorMessages,\n () => successMessages,\n);\n\nconst isOutlined = computed<boolean>(() => variant === 'outlined');\n\n/** True once any segment holds a digit, even a partially typed date. */\nconst anySegmentSet = computed<boolean>(() => [\n selectedYear,\n selectedMonth,\n selectedDay,\n selectedHour,\n selectedMinute,\n selectedSecond,\n selectedMillisecond,\n].some(segment => isDefined(segment)));\n\nconst formattedDisplay = computed<string>(() => {\n // An untouched field shows its format through the placeholder rather than\n // holding the tokens as its value, where a screen reader reads them as\n // content and select-all copies them. The tokens stay while the field is\n // focused: that is when the segment machinery highlights them, and\n // collapsing the value mid-edit would blank the field under the cursor.\n // The guard is \"no segment set\" and not `valueSet`, so blurring a half\n // typed date keeps what was entered on screen.\n if (!get(anySegmentSet) && !get(searchInputFocused)) {\n return '';\n }\n\n let result = get(dateFormat);\n\n const replacements = [\n { pattern: 'YYYY', value: getDisplayValue(selectedYear, 4) },\n { pattern: 'MM', value: getDisplayValue(selectedMonth, 2) },\n { pattern: 'DD', value: getDisplayValue(selectedDay, 2) },\n { pattern: 'HH', value: getDisplayValue(selectedHour, 2) },\n { pattern: 'mm', value: getDisplayValue(selectedMinute, 2) },\n { pattern: 'ss', value: getDisplayValue(selectedSecond, 2) },\n { pattern: 'SSS', value: getDisplayValue(selectedMillisecond, 3) },\n ];\n\n for (const { pattern, value } of replacements) {\n if (value !== undefined) {\n result = result.replace(pattern, value);\n }\n }\n\n return result;\n});\n\n/**\n * An untouched field holds no value until focus expands the format tokens into\n * it. That patch overwrites the input's value, and writing a value drops any\n * selection with it, so the highlight `handleFocus` just set is gone by the\n * time the tokens are on screen and the caret ends up after them. Re-select\n * the first segment once the tokens have actually landed.\n */\nwatch(formattedDisplay, (value, previous) => {\n if (previous === '' && value !== '' && get(searchInputFocused))\n selectFirstSegment();\n}, { flush: 'post' });\n\nconst timeSelection = computed<TimePickerSelection>({\n get() {\n const type = getCurrentSegment()?.type;\n\n if (type === 'mm') {\n return 'minute';\n }\n else if (type === 'ss') {\n return 'second';\n }\n else if (type === 'SSS') {\n return 'millisecond';\n }\n return 'hour';\n },\n set(value: TimePickerSelection) {\n let segmentType: DateTimeSegmentType = 'HH';\n if (value === 'minute') {\n segmentType = 'mm';\n }\n else if (value === 'second') {\n segmentType = 'ss';\n }\n else if (value === 'millisecond') {\n segmentType = 'SSS';\n }\n\n setSegment(segmentType);\n },\n});\n\nconst float = computed<boolean>(() => (get(isOpen) || get(valueSet) || get(searchInputFocused)) && get(isOutlined));\n\nconst legendText = computed<string>(() => {\n if (!get(float))\n return '';\n const resolved = get(fieldLabel);\n return required ? `${resolved} ﹡` : resolved;\n});\n\nconst ui = computed<ReturnType<typeof dateTimePickerStyles>>(() => dateTimePickerStyles({\n filled: variant === 'filled',\n outlined: get(isOutlined),\n float: get(float),\n opened: get(isOpen),\n hovered: get(isHovered),\n dense,\n disabled,\n readonly,\n hasError: get(hasError),\n hasSuccess: get(hasSuccess) && !get(hasError),\n}));\n\nconst combinedErrorMessages = computed<string[]>(() => {\n if (!errorMessages)\n return get(internalErrorMessages);\n\n const propErrors = Array.isArray(errorMessages) ? errorMessages : [errorMessages];\n return [...propErrors, ...get(internalErrorMessages)];\n});\n\nfunction getDisplayValue(digit: Ref<number | undefined>, padding: number): string | undefined {\n return isDefined(digit) ? get(digit).toString().padStart(padding, '0') : undefined;\n}\n\nasync function setInputFocus(): Promise<void> {\n await nextTick(() => {\n set(searchInputFocused, true);\n });\n}\n\nfunction clear(segmentType?: string): void {\n if (!segmentType) {\n clearSelection();\n set(currentValue, undefined);\n return;\n }\n\n clearSegment(segmentType);\n}\n\nfunction handleInputClick(event: MouseEvent): void {\n // Handle segment selection first, before any DOM changes from menu opening\n handleClick(event);\n // Open menu if not already open\n if (!get(isOpen)) {\n set(isOpen, true);\n }\n}\n\n/**\n * The menu is teleported to the body, so it never sits next to the field in\n * the tab sequence. Opening it from the keyboard therefore moves focus into\n * the calendar; opening it with the mouse leaves focus in the field, which is\n * what `disable-auto-focus` on the menu is there for.\n */\nconst focusCalendarOnOpen = ref<boolean>(false);\n\nfunction focusCalendar(): void {\n // the menu is teleported and mounts a frame later, so this waits for the\n // wrapper to appear rather than guessing at a number of ticks\n set(focusCalendarOnOpen, true);\n}\n\nwatch(menuWrapperRef, (menu) => {\n if (!menu || !get(focusCalendarOnOpen))\n return;\n\n set(focusCalendarOnOpen, false);\n nextTick(() => {\n const el = menu.$el as HTMLElement | undefined;\n el?.querySelector<HTMLButtonElement>('[role=\"gridcell\"][tabindex=\"0\"]')?.focus({ preventScroll: true });\n });\n});\n\n/** Moves focus to the field. Exposed so a consumer can drive it from a ref. */\nfunction focus(): void {\n get(textInput)?.focus({ preventScroll: true });\n}\n\nfunction focusField(): void {\n nextTick(focus);\n}\n\nonMounted(() => {\n if (autofocus && !disabled)\n focusField();\n});\n\n/** Escape inside the calendar closes it and hands focus back to the field. */\nfunction closeFromMenu(): void {\n set(isOpen, false);\n focusField();\n}\n\n/**\n * The menu used to be mouse-only: the activator carried no key handlers, so\n * there was no way to reach the calendar from the keyboard. Alt+ArrowDown\n * opens it and Escape closes it, following the combobox convention, and the\n * append chevron is a real button for anyone who tabs to it instead.\n */\nfunction onKeyDown(event: KeyboardEvent): void {\n if (disabled || readonly)\n return;\n\n if (event.altKey && event.key === 'ArrowDown') {\n event.preventDefault();\n set(isOpen, true);\n focusCalendar();\n return;\n }\n\n if (event.key === 'Escape' && get(isOpen)) {\n set(isOpen, false);\n return;\n }\n\n handleKeyDown(event);\n}\n\nfunction arrowClicked(event: MouseEvent): void {\n if (get(isOpen)) {\n set(isOpen, false);\n event.stopPropagation();\n }\n}\n\nconst menuActions = computed<DateTimePickerAction[]>(\n () => actions.filter(action => action !== 'clear' || allowEmpty),\n);\n\nconst anyMenuOpen = computed<boolean>(() => get(isOpen) || get(calendarMenuOpen));\n\nwatch(anyMenuOpen, (value) => {\n set(menuOpen, value);\n});\n\ndefineExpose({\n focus,\n});\n</script>\n\n<template>\n <RuiMenu\n v-model=\"isOpen\"\n v-bind=\"getRootAttrs($attrs, [])\"\n :class=\"ui.wrapper({ class: cn($attrs.class) })\"\n :options=\"MENU_OPTIONS\"\n :dense=\"dense\"\n :hint=\"hint\"\n :disabled=\"disabled\"\n :success-messages=\"successMessages\"\n :error-messages=\"combinedErrorMessages\"\n :close-on-content-click=\"false\"\n :show-details=\"!hideDetails\"\n :persistent=\"calendarMenuOpen\"\n full-width\n disable-auto-focus\n >\n <template #activator=\"{ attrs, open }\">\n <div\n ref=\"activator\"\n :class=\"ui.activator()\"\n v-bind=\"{\n ...getNonRootAttrs($attrs, ['onClick', 'class']),\n ...(readonly ? {} : attrs),\n }\"\n data-id=\"activator\"\n :aria-invalid=\"hasError\"\n @mouseenter=\"isHovered = true\"\n @mouseleave=\"isHovered = false\"\n @click=\"setInputFocus()\"\n >\n <span\n v-if=\"isOutlined && (searchInputFocused || open || valueSet)\"\n data-id=\"label\"\n :class=\"[\n ui.label(),\n { 'pr-2': !valueSet && !open && isOutlined },\n ]\"\n >\n {{ fieldLabel }}\n <span\n v-if=\"required\"\n data-id=\"required-indicator\"\n :class=\"ui.required()\"\n >\n ﹡\n </span>\n </span>\n\n <span :class=\"ui.iconPrepend()\">\n <RuiIcon\n class=\"text-rui-text-secondary transition\"\n :size=\"dense ? 16 : 24\"\n name=\"lu-calendar-days\"\n />\n </span>\n\n <div :class=\"ui.value()\">\n <input\n ref=\"textInput\"\n :disabled=\"disabled\"\n :value=\"formattedDisplay\"\n class=\"bg-transparent outline-none flex-1 min-w-0\"\n type=\"text\"\n inputmode=\"numeric\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n :placeholder=\"dateFormat\"\n :readonly=\"readonly\"\n :aria-invalid=\"hasError\"\n :aria-label=\"fieldLabel\"\n :aria-required=\"required || undefined\"\n @mousedown=\"handleMouseDown($event)\"\n @focus=\"handleFocus()\"\n @blur=\"handleBlur()\"\n @select=\"handleInputSelection($event)\"\n @click.stop=\"handleInputClick($event)\"\n @keydown=\"onKeyDown($event)\"\n @paste=\"handlePaste($event)\"\n @input=\"handleInput($event)\"\n />\n </div>\n\n <RuiButton\n v-if=\"allowEmpty && valueSet && !disabled\"\n variant=\"text\"\n icon\n data-id=\"clear-button\"\n size=\"sm\"\n color=\"error\"\n :aria-label=\"clearLabel\"\n :class=\"[\n ui.clear(),\n anyFocused && '!visible',\n { 'mr-2': !dense },\n ]\"\n @click.stop.prevent=\"clear()\"\n >\n <RuiIcon\n name=\"lu-x\"\n size=\"18\"\n />\n </RuiButton>\n\n <button\n v-if=\"!disabled && !readonly\"\n type=\"button\"\n :class=\"ui.iconWrapper()\"\n data-id=\"append\"\n :aria-label=\"toggleLabel\"\n :aria-expanded=\"isOpen\"\n aria-haspopup=\"dialog\"\n @click=\"arrowClicked($event)\"\n >\n <RuiIcon\n :class=\"ui.icon()\"\n :size=\"dense ? 16 : 24\"\n name=\"lu-chevron-down\"\n />\n </button>\n <span\n v-else\n :class=\"ui.iconWrapper()\"\n data-id=\"append\"\n >\n <RuiIcon\n :class=\"ui.icon()\"\n :size=\"dense ? 16 : 24\"\n name=\"lu-chevron-down\"\n />\n </span>\n </div>\n <fieldset\n v-if=\"isOutlined\"\n :class=\"ui.fieldset()\"\n >\n <legend :class=\"ui.legend()\">\n {{ legendText }}\n </legend>\n </fieldset>\n </template>\n <template #default>\n <RuiDateTimePickerMenu\n ref=\"menuWrapperRef\"\n v-model:selected-date=\"selectedDate\"\n v-model:selected-time=\"selectedTime\"\n v-model:selected-hour=\"selectedHour\"\n v-model:selected-minute=\"selectedMinute\"\n v-model:selected-second=\"selectedSecond\"\n v-model:selected-millisecond=\"selectedMillisecond\"\n v-model:time-selection=\"timeSelection\"\n v-model:selected-timezone=\"selectedTimezone\"\n v-model:calendar-menu-open=\"calendarMenuOpen\"\n :accuracy=\"accuracy\"\n :max-date=\"maxAllowedDate\"\n :min-date=\"minAllowedDate\"\n :show-timezone=\"showTimezone\"\n :actions=\"menuActions\"\n @keydown.escape=\"closeFromMenu()\"\n @set-now=\"setNow()\"\n @set-today=\"setToday()\"\n @clear=\"clear()\"\n >\n <slot name=\"menu-content\" />\n </RuiDateTimePickerMenu>\n </template>\n </RuiMenu>\n</template>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"RuiDateTimePicker.js","names":[],"sources":["../../../src/components/date-time-picker/RuiDateTimePicker.vue"],"sourcesContent":["<script lang=\"ts\" setup>\nimport type { ComponentPublicInstance } from 'vue';\nimport type { DateTimePickerAction, DateTimeSegmentType } from '@/components/date-time-picker/types';\nimport type { TimePickerSelection } from '@/components/time-picker/RuiTimePicker.vue';\nimport RuiButton from '@/components/buttons/button/RuiButton.vue';\nimport { dateTimePickerStyles, type DateTimePickerVariant } from '@/components/date-time-picker/date-time-picker-styles';\nimport RuiDateTimePickerMenu from '@/components/date-time-picker/RuiDateTimePickerMenu.vue';\nimport { useDateTimeSelection } from '@/components/date-time-picker/use-date-time-selection';\nimport { useInputHandler } from '@/components/date-time-picker/use-input-handler';\nimport { useKeyboardHandler } from '@/components/date-time-picker/use-keyboard-handler';\nimport RuiIcon from '@/components/icons/RuiIcon.vue';\nimport RuiMenu from '@/components/overlays/menu/RuiMenu.vue';\nimport { type FloatingOptions, Placement } from '@/composables/floating';\nimport { useRuiI8n } from '@/composables/use-rui-i18n';\nimport { RUI_I18N_KEYS } from '@/i18n/keys';\nimport { useFormTextDetail } from '@/utils/form-text-detail';\nimport { getNonRootAttrs, getRootAttrs } from '@/utils/helpers';\nimport { cn } from '@/utils/tv';\n\ntype DateFormat = 'year-first' | 'month-first' | 'day-first';\n\ntype DateTimeModelType = 'date' | 'epoch-ms' | 'epoch';\n\ntype ModelValueType<T extends DateTimeModelType> = T extends 'date'\n ? Date | undefined\n : T extends 'epoch-ms'\n ? number | undefined\n : T extends 'epoch'\n ? number | undefined\n : Date | number | undefined;\n\nexport interface RuiDateTimePickerProps {\n minDate?: Date | number;\n maxDate?: Date | number | 'now';\n format?: DateFormat;\n type?: DateTimeModelType;\n accuracy?: 'minute' | 'second' | 'millisecond';\n disabled?: boolean;\n allowEmpty?: boolean;\n readonly?: boolean;\n dense?: boolean;\n label?: string;\n variant?: DateTimePickerVariant;\n hint?: string;\n errorMessages?: string | string[];\n successMessages?: string | string[];\n hideDetails?: boolean;\n required?: boolean;\n /**\n * Renders the timezone selector in the menu. Off by default: the value is\n * emitted as a date or an epoch, so the picked timezone does not survive a\n * round trip and most consumers work in local time.\n */\n showTimezone?: boolean;\n /**\n * Actions rendered in the menu footer. `clear` is only rendered when the\n * picker is `allowEmpty`. Pass an empty array to drop the footer entirely.\n */\n actions?: DateTimePickerAction[];\n /**\n * Focuses the field once it is mounted. The native attribute is ignored for\n * an input inserted into an already loaded document, which is the usual case\n * for a picker revealed by an editor or a dialog.\n */\n autofocus?: boolean;\n /**\n * Accepts an entry that stops short of the full format, filling the segments\n * it never reached from one end of the day: `start` gives a bare date\n * 00:00:00.000 and `end` gives it 23:59:59.999. Meant for a range, where the\n * two bounds want opposite ends. Left unset, an entry missing its time is not\n * a value and nothing is emitted.\n *\n * The fill runs when the user is done with the field - leaving it, pressing\n * enter, or closing the calendar - and is written back into the segments, so\n * what it decided on is on screen.\n *\n * Spelled out rather than written as `PartialTimeMode`: a consumer that hands\n * this whole interface to its own `defineProps` needs every member resolvable\n * by the SFC compiler, which does not follow the type into the package.\n */\n partialTime?: 'start' | 'end';\n}\n\ndefineOptions({\n name: 'RuiDateTimePicker',\n inheritAttrs: false,\n});\n\nconst modelValue = defineModel<ModelValueType<DateTimeModelType>>({ required: true });\nconst menuOpen = defineModel<boolean>('menuOpen', { default: false });\n\nconst {\n disabled = false,\n readonly = false,\n allowEmpty = false,\n dense = false,\n type = 'epoch-ms',\n hideDetails = false,\n label,\n variant = 'default',\n hint,\n maxDate,\n minDate,\n format = 'day-first',\n accuracy = 'minute',\n errorMessages = [],\n successMessages = [],\n required = false,\n showTimezone = false,\n actions = ['now'],\n autofocus = false,\n partialTime,\n} = defineProps<RuiDateTimePickerProps>();\n\ndefineSlots<{\n 'menu-content': () => any;\n}>();\n\nconst MENU_OPTIONS: FloatingOptions = { placement: Placement.bottomStart };\n\nconst baseFormats: Record<DateFormat, string> = {\n 'day-first': 'DD/MM/YYYY HH:mm',\n 'month-first': 'MM/DD/YYYY HH:mm',\n 'year-first': 'YYYY/MM/DD HH:mm',\n};\n\nconst isOpen = ref<boolean>(false);\nconst isHovered = ref<boolean>(false);\nconst cursorPosition = ref<number>(0);\nconst currentValue = ref<number>();\n\nconst { t } = useRuiI8n();\n\nconst keys = RUI_I18N_KEYS.dateTimePicker;\n\nconst fieldLabel = computed<string>(() => label ?? t(keys.label, 'Pick a date'));\nconst clearLabel = computed<string>(() => t(keys.clearValue, 'Clear the date'));\nconst toggleLabel = computed<string>(() => (get(isOpen)\n ? t(keys.closeCalendar, 'Close the calendar')\n : t(keys.openCalendar, 'Open the calendar')));\n\nconst textInput = useTemplateRef<HTMLInputElement>('textInput');\nconst activator = useTemplateRef<HTMLDivElement>('activator');\nconst menuWrapperRef = useTemplateRef<ComponentPublicInstance>('menuWrapperRef');\nconst calendarMenuOpen = ref<boolean>(false);\n\nconst { focused: activatorFocusedWithin } = useFocusWithin(activator);\nconst { focused: menuWrapperFocusedWithin } = useFocusWithin(menuWrapperRef);\nconst { focused: searchInputFocused } = useFocus(textInput);\n\nconst anyFocused = computed<boolean>(() => get(activatorFocusedWithin) || get(menuWrapperFocusedWithin));\n\nconst dateFormat = computed<string>(() => {\n const fmt = baseFormats[format];\n if (accuracy === 'second') {\n return fmt.replace('HH:mm', 'HH:mm:ss');\n }\n else if (accuracy === 'millisecond') {\n return fmt.replace('HH:mm', 'HH:mm:ss.SSS');\n }\n return fmt;\n});\n\nconst {\n clear: clearSelection,\n commitPartialTime,\n getDateTime,\n internalErrorMessages,\n maxAllowedDate,\n minAllowedDate,\n segmentData,\n selectedDate,\n selectedDay,\n selectedHour,\n selectedMillisecond,\n selectedMinute,\n selectedMonth,\n selectedSecond,\n selectedTime,\n selectedTimezone,\n selectedYear,\n setNow,\n setToday,\n valueSet,\n} = useDateTimeSelection({\n accuracy,\n allowEmpty,\n dateFormat,\n maxDate,\n minDate,\n modelValue,\n partialTime,\n type,\n});\n\nconst { setValue, getCurrent } = useInputHandler(segmentData, currentValue);\n\nconst {\n clear: clearSegment,\n getCurrentSegment,\n handleBlur,\n handleClick,\n handleFocus,\n handleInput,\n handleInputSelection,\n handleKeyDown,\n handleMouseDown,\n handlePaste,\n selectFirstSegment,\n setSegment,\n} = useKeyboardHandler({\n accuracy,\n currentValue,\n cursorPosition,\n dateFormat,\n disabled,\n getCurrent,\n getDateTime,\n readonly,\n setValue,\n textInput,\n});\n\nconst { hasError, hasSuccess } = useFormTextDetail(\n () => errorMessages,\n () => successMessages,\n);\n\nconst isOutlined = computed<boolean>(() => variant === 'outlined');\n\n/** True once any segment holds a digit, even a partially typed date. */\nconst anySegmentSet = computed<boolean>(() => [\n selectedYear,\n selectedMonth,\n selectedDay,\n selectedHour,\n selectedMinute,\n selectedSecond,\n selectedMillisecond,\n].some(segment => isDefined(segment)));\n\nconst formattedDisplay = computed<string>(() => {\n // An untouched field shows its format through the placeholder rather than\n // holding the tokens as its value, where a screen reader reads them as\n // content and select-all copies them. The tokens stay while the field is\n // focused: that is when the segment machinery highlights them, and\n // collapsing the value mid-edit would blank the field under the cursor.\n // The guard is \"no segment set\" and not `valueSet`, so blurring a half\n // typed date keeps what was entered on screen.\n if (!get(anySegmentSet) && !get(searchInputFocused)) {\n return '';\n }\n\n let result = get(dateFormat);\n\n const replacements = [\n { pattern: 'YYYY', value: getDisplayValue(selectedYear, 4) },\n { pattern: 'MM', value: getDisplayValue(selectedMonth, 2) },\n { pattern: 'DD', value: getDisplayValue(selectedDay, 2) },\n { pattern: 'HH', value: getDisplayValue(selectedHour, 2) },\n { pattern: 'mm', value: getDisplayValue(selectedMinute, 2) },\n { pattern: 'ss', value: getDisplayValue(selectedSecond, 2) },\n { pattern: 'SSS', value: getDisplayValue(selectedMillisecond, 3) },\n ];\n\n for (const { pattern, value } of replacements) {\n if (value !== undefined) {\n result = result.replace(pattern, value);\n }\n }\n\n return result;\n});\n\n/**\n * An untouched field holds no value until focus expands the format tokens into\n * it. That patch overwrites the input's value, and writing a value drops any\n * selection with it, so the highlight `handleFocus` just set is gone by the\n * time the tokens are on screen and the caret ends up after them. Re-select\n * the first segment once the tokens have actually landed.\n */\nwatch(formattedDisplay, (value, previous) => {\n if (previous === '' && value !== '' && get(searchInputFocused))\n selectFirstSegment();\n}, { flush: 'post' });\n\nconst timeSelection = computed<TimePickerSelection>({\n get() {\n const type = getCurrentSegment()?.type;\n\n if (type === 'mm') {\n return 'minute';\n }\n else if (type === 'ss') {\n return 'second';\n }\n else if (type === 'SSS') {\n return 'millisecond';\n }\n return 'hour';\n },\n set(value: TimePickerSelection) {\n let segmentType: DateTimeSegmentType = 'HH';\n if (value === 'minute') {\n segmentType = 'mm';\n }\n else if (value === 'second') {\n segmentType = 'ss';\n }\n else if (value === 'millisecond') {\n segmentType = 'SSS';\n }\n\n setSegment(segmentType);\n },\n});\n\nconst float = computed<boolean>(() => (get(isOpen) || get(valueSet) || get(searchInputFocused)) && get(isOutlined));\n\nconst legendText = computed<string>(() => {\n if (!get(float))\n return '';\n const resolved = get(fieldLabel);\n return required ? `${resolved} ﹡` : resolved;\n});\n\nconst ui = computed<ReturnType<typeof dateTimePickerStyles>>(() => dateTimePickerStyles({\n filled: variant === 'filled',\n outlined: get(isOutlined),\n float: get(float),\n opened: get(isOpen),\n hovered: get(isHovered),\n dense,\n disabled,\n readonly,\n hasError: get(hasError),\n hasSuccess: get(hasSuccess) && !get(hasError),\n}));\n\nconst combinedErrorMessages = computed<string[]>(() => {\n if (!errorMessages)\n return get(internalErrorMessages);\n\n const propErrors = Array.isArray(errorMessages) ? errorMessages : [errorMessages];\n return [...propErrors, ...get(internalErrorMessages)];\n});\n\nfunction getDisplayValue(digit: Ref<number | undefined>, padding: number): string | undefined {\n return isDefined(digit) ? get(digit).toString().padStart(padding, '0') : undefined;\n}\n\nasync function setInputFocus(): Promise<void> {\n await nextTick(() => {\n set(searchInputFocused, true);\n });\n}\n\nfunction clear(segmentType?: string): void {\n if (!segmentType) {\n clearSelection();\n set(currentValue, undefined);\n return;\n }\n\n clearSegment(segmentType);\n}\n\n/**\n * An entry left incomplete is completed once the user is done with the field,\n * never while they are still filling it in: a half typed hour is not a segment\n * they left out. Leaving the field counts, unless the calendar is open, since\n * then the focus is only moving into it and the menu closing covers that.\n */\nfunction onBlur(): void {\n handleBlur();\n if (!get(isOpen))\n commitPartialTime();\n}\n\nwatch(isOpen, (open) => {\n if (!open && !get(searchInputFocused))\n commitPartialTime();\n});\n\nfunction handleInputClick(event: MouseEvent): void {\n // Handle segment selection first, before any DOM changes from menu opening\n handleClick(event);\n // Open menu if not already open\n if (!get(isOpen)) {\n set(isOpen, true);\n }\n}\n\n/**\n * The menu is teleported to the body, so it never sits next to the field in\n * the tab sequence. Opening it from the keyboard therefore moves focus into\n * the calendar; opening it with the mouse leaves focus in the field, which is\n * what `disable-auto-focus` on the menu is there for.\n */\nconst focusCalendarOnOpen = ref<boolean>(false);\n\nfunction focusCalendar(): void {\n // the menu is teleported and mounts a frame later, so this waits for the\n // wrapper to appear rather than guessing at a number of ticks\n set(focusCalendarOnOpen, true);\n}\n\nwatch(menuWrapperRef, (menu) => {\n if (!menu || !get(focusCalendarOnOpen))\n return;\n\n set(focusCalendarOnOpen, false);\n nextTick(() => {\n const el = menu.$el as HTMLElement | undefined;\n el?.querySelector<HTMLButtonElement>('[role=\"gridcell\"][tabindex=\"0\"]')?.focus({ preventScroll: true });\n });\n});\n\n/** Moves focus to the field. Exposed so a consumer can drive it from a ref. */\nfunction focus(): void {\n get(textInput)?.focus({ preventScroll: true });\n}\n\nfunction focusField(): void {\n nextTick(focus);\n}\n\nonMounted(() => {\n if (autofocus && !disabled)\n focusField();\n});\n\n/** Escape inside the calendar closes it and hands focus back to the field. */\nfunction closeFromMenu(): void {\n set(isOpen, false);\n focusField();\n}\n\n/**\n * The menu used to be mouse-only: the activator carried no key handlers, so\n * there was no way to reach the calendar from the keyboard. Alt+ArrowDown\n * opens it and Escape closes it, following the combobox convention, and the\n * append chevron is a real button for anyone who tabs to it instead.\n */\nfunction onKeyDown(event: KeyboardEvent): void {\n if (disabled || readonly)\n return;\n\n if (event.altKey && event.key === 'ArrowDown') {\n event.preventDefault();\n set(isOpen, true);\n focusCalendar();\n return;\n }\n\n if (event.key === 'Escape' && get(isOpen)) {\n set(isOpen, false);\n return;\n }\n\n // Enter and Escape are the other ways to be done with the field, and the keys\n // a consumer tends to close its editor on. Escape reaches here only once the\n // calendar is already shut, and it discards nothing - the segments stay on\n // screen either way - so it commits what was entered rather than dropping it\n // when the field goes. Both write the value before the key carries on\n // bubbling, so it survives that close.\n if (event.key === 'Enter' || event.key === 'Escape')\n commitPartialTime();\n\n handleKeyDown(event);\n}\n\nfunction arrowClicked(event: MouseEvent): void {\n if (get(isOpen)) {\n set(isOpen, false);\n event.stopPropagation();\n }\n}\n\nconst menuActions = computed<DateTimePickerAction[]>(\n () => actions.filter(action => action !== 'clear' || allowEmpty),\n);\n\nconst anyMenuOpen = computed<boolean>(() => get(isOpen) || get(calendarMenuOpen));\n\nwatch(anyMenuOpen, (value) => {\n set(menuOpen, value);\n});\n\ndefineExpose({\n focus,\n});\n</script>\n\n<template>\n <RuiMenu\n v-model=\"isOpen\"\n v-bind=\"getRootAttrs($attrs, [])\"\n :class=\"ui.wrapper({ class: cn($attrs.class) })\"\n :options=\"MENU_OPTIONS\"\n :dense=\"dense\"\n :hint=\"hint\"\n :disabled=\"disabled\"\n :success-messages=\"successMessages\"\n :error-messages=\"combinedErrorMessages\"\n :close-on-content-click=\"false\"\n :show-details=\"!hideDetails\"\n :persistent=\"calendarMenuOpen\"\n full-width\n disable-auto-focus\n >\n <template #activator=\"{ attrs, open }\">\n <div\n ref=\"activator\"\n :class=\"ui.activator()\"\n v-bind=\"{\n ...getNonRootAttrs($attrs, ['onClick', 'class']),\n ...(readonly ? {} : attrs),\n }\"\n data-id=\"activator\"\n :aria-invalid=\"hasError\"\n @mouseenter=\"isHovered = true\"\n @mouseleave=\"isHovered = false\"\n @click=\"setInputFocus()\"\n >\n <span\n v-if=\"isOutlined && (searchInputFocused || open || valueSet)\"\n data-id=\"label\"\n :class=\"[\n ui.label(),\n { 'pr-2': !valueSet && !open && isOutlined },\n ]\"\n >\n {{ fieldLabel }}\n <span\n v-if=\"required\"\n data-id=\"required-indicator\"\n :class=\"ui.required()\"\n >\n ﹡\n </span>\n </span>\n\n <span :class=\"ui.iconPrepend()\">\n <RuiIcon\n class=\"text-rui-text-secondary transition\"\n :size=\"dense ? 16 : 24\"\n name=\"lu-calendar-days\"\n />\n </span>\n\n <div :class=\"ui.value()\">\n <input\n ref=\"textInput\"\n :disabled=\"disabled\"\n :value=\"formattedDisplay\"\n class=\"bg-transparent outline-none flex-1 min-w-0\"\n type=\"text\"\n inputmode=\"numeric\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n :placeholder=\"dateFormat\"\n :readonly=\"readonly\"\n :aria-invalid=\"hasError\"\n :aria-label=\"fieldLabel\"\n :aria-required=\"required || undefined\"\n @mousedown=\"handleMouseDown($event)\"\n @focus=\"handleFocus()\"\n @blur=\"onBlur()\"\n @select=\"handleInputSelection($event)\"\n @click.stop=\"handleInputClick($event)\"\n @keydown=\"onKeyDown($event)\"\n @paste=\"handlePaste($event)\"\n @input=\"handleInput($event)\"\n />\n </div>\n\n <RuiButton\n v-if=\"allowEmpty && valueSet && !disabled\"\n variant=\"text\"\n icon\n data-id=\"clear-button\"\n size=\"sm\"\n color=\"error\"\n :aria-label=\"clearLabel\"\n :class=\"[\n ui.clear(),\n anyFocused && '!visible',\n { 'mr-2': !dense },\n ]\"\n @click.stop.prevent=\"clear()\"\n >\n <RuiIcon\n name=\"lu-x\"\n size=\"18\"\n />\n </RuiButton>\n\n <!-- Out of the tab sequence, following the combobox pattern: the button only duplicates\n Alt+ArrowDown on the field, so leaving it in cost two stops to cross one date field\n and four to cross a from/to pair, for nothing a keyboard user could not already do.\n It stays a real button, so it keeps its label and its expanded state for anyone\n reaching it by any other means. -->\n <button\n v-if=\"!disabled && !readonly\"\n type=\"button\"\n tabindex=\"-1\"\n :class=\"ui.iconWrapper()\"\n data-id=\"append\"\n :aria-label=\"toggleLabel\"\n :aria-expanded=\"isOpen\"\n aria-haspopup=\"dialog\"\n @click=\"arrowClicked($event)\"\n >\n <RuiIcon\n :class=\"ui.icon()\"\n :size=\"dense ? 16 : 24\"\n name=\"lu-chevron-down\"\n />\n </button>\n <span\n v-else\n :class=\"ui.iconWrapper()\"\n data-id=\"append\"\n >\n <RuiIcon\n :class=\"ui.icon()\"\n :size=\"dense ? 16 : 24\"\n name=\"lu-chevron-down\"\n />\n </span>\n </div>\n <fieldset\n v-if=\"isOutlined\"\n :class=\"ui.fieldset()\"\n >\n <legend :class=\"ui.legend()\">\n {{ legendText }}\n </legend>\n </fieldset>\n </template>\n <template #default>\n <RuiDateTimePickerMenu\n ref=\"menuWrapperRef\"\n v-model:selected-date=\"selectedDate\"\n v-model:selected-time=\"selectedTime\"\n v-model:selected-hour=\"selectedHour\"\n v-model:selected-minute=\"selectedMinute\"\n v-model:selected-second=\"selectedSecond\"\n v-model:selected-millisecond=\"selectedMillisecond\"\n v-model:time-selection=\"timeSelection\"\n v-model:selected-timezone=\"selectedTimezone\"\n v-model:calendar-menu-open=\"calendarMenuOpen\"\n :accuracy=\"accuracy\"\n :max-date=\"maxAllowedDate\"\n :min-date=\"minAllowedDate\"\n :show-timezone=\"showTimezone\"\n :actions=\"menuActions\"\n @keydown.escape=\"closeFromMenu()\"\n @set-now=\"setNow()\"\n @set-today=\"setToday()\"\n @clear=\"clear()\"\n >\n <slot name=\"menu-content\" />\n </RuiDateTimePickerMenu>\n </template>\n </RuiMenu>\n</template>\n"],"mappings":""}
|
|
@@ -37,6 +37,22 @@ export interface RuiDateTimePickerProps {
|
|
|
37
37
|
* for a picker revealed by an editor or a dialog.
|
|
38
38
|
*/
|
|
39
39
|
autofocus?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Accepts an entry that stops short of the full format, filling the segments
|
|
42
|
+
* it never reached from one end of the day: `start` gives a bare date
|
|
43
|
+
* 00:00:00.000 and `end` gives it 23:59:59.999. Meant for a range, where the
|
|
44
|
+
* two bounds want opposite ends. Left unset, an entry missing its time is not
|
|
45
|
+
* a value and nothing is emitted.
|
|
46
|
+
*
|
|
47
|
+
* The fill runs when the user is done with the field - leaving it, pressing
|
|
48
|
+
* enter, or closing the calendar - and is written back into the segments, so
|
|
49
|
+
* what it decided on is on screen.
|
|
50
|
+
*
|
|
51
|
+
* Spelled out rather than written as `PartialTimeMode`: a consumer that hands
|
|
52
|
+
* this whole interface to its own `defineProps` needs every member resolvable
|
|
53
|
+
* by the SFC compiler, which does not follow the type into the package.
|
|
54
|
+
*/
|
|
55
|
+
partialTime?: 'start' | 'end';
|
|
40
56
|
}
|
|
41
57
|
type __VLS_Props = RuiDateTimePickerProps;
|
|
42
58
|
type __VLS_Slots = {
|
package/dist/components/date-time-picker/RuiDateTimePicker.vue_vue_type_script_setup_true_lang.js
CHANGED
|
@@ -74,7 +74,8 @@ var RuiDateTimePicker_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*
|
|
|
74
74
|
autofocus: {
|
|
75
75
|
type: Boolean,
|
|
76
76
|
default: false
|
|
77
|
-
}
|
|
77
|
+
},
|
|
78
|
+
partialTime: {}
|
|
78
79
|
}, {
|
|
79
80
|
"modelValue": { required: true },
|
|
80
81
|
"modelModifiers": {},
|
|
@@ -117,13 +118,14 @@ var RuiDateTimePicker_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*
|
|
|
117
118
|
else if (__props.accuracy === "millisecond") return fmt.replace("HH:mm", "HH:mm:ss.SSS");
|
|
118
119
|
return fmt;
|
|
119
120
|
});
|
|
120
|
-
const { clear: clearSelection, getDateTime, internalErrorMessages, maxAllowedDate, minAllowedDate, segmentData, selectedDate, selectedDay, selectedHour, selectedMillisecond, selectedMinute, selectedMonth, selectedSecond, selectedTime, selectedTimezone, selectedYear, setNow, setToday, valueSet } = useDateTimeSelection({
|
|
121
|
+
const { clear: clearSelection, commitPartialTime, getDateTime, internalErrorMessages, maxAllowedDate, minAllowedDate, segmentData, selectedDate, selectedDay, selectedHour, selectedMillisecond, selectedMinute, selectedMonth, selectedSecond, selectedTime, selectedTimezone, selectedYear, setNow, setToday, valueSet } = useDateTimeSelection({
|
|
121
122
|
accuracy: __props.accuracy,
|
|
122
123
|
allowEmpty: __props.allowEmpty,
|
|
123
124
|
dateFormat,
|
|
124
125
|
maxDate: __props.maxDate,
|
|
125
126
|
minDate: __props.minDate,
|
|
126
127
|
modelValue,
|
|
128
|
+
partialTime: __props.partialTime,
|
|
127
129
|
type: __props.type
|
|
128
130
|
});
|
|
129
131
|
const { setValue, getCurrent } = useInputHandler(segmentData, currentValue);
|
|
@@ -251,6 +253,19 @@ var RuiDateTimePicker_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*
|
|
|
251
253
|
}
|
|
252
254
|
clearSegment(segmentType);
|
|
253
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* An entry left incomplete is completed once the user is done with the field,
|
|
258
|
+
* never while they are still filling it in: a half typed hour is not a segment
|
|
259
|
+
* they left out. Leaving the field counts, unless the calendar is open, since
|
|
260
|
+
* then the focus is only moving into it and the menu closing covers that.
|
|
261
|
+
*/
|
|
262
|
+
function onBlur() {
|
|
263
|
+
handleBlur();
|
|
264
|
+
if (!get$1(isOpen)) commitPartialTime();
|
|
265
|
+
}
|
|
266
|
+
watch(isOpen, (open) => {
|
|
267
|
+
if (!open && !get$1(searchInputFocused)) commitPartialTime();
|
|
268
|
+
});
|
|
254
269
|
function handleInputClick(event) {
|
|
255
270
|
handleClick(event);
|
|
256
271
|
if (!get$1(isOpen)) set$1(isOpen, true);
|
|
@@ -305,6 +320,7 @@ var RuiDateTimePicker_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*
|
|
|
305
320
|
set$1(isOpen, false);
|
|
306
321
|
return;
|
|
307
322
|
}
|
|
323
|
+
if (event.key === "Enter" || event.key === "Escape") commitPartialTime();
|
|
308
324
|
handleKeyDown(event);
|
|
309
325
|
}
|
|
310
326
|
function arrowClicked(event) {
|
|
@@ -383,7 +399,7 @@ var RuiDateTimePicker_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*
|
|
|
383
399
|
"aria-required": __props.required || void 0,
|
|
384
400
|
onMousedown: _cache[0] || (_cache[0] = ($event) => unref(handleMouseDown)($event)),
|
|
385
401
|
onFocus: _cache[1] || (_cache[1] = ($event) => unref(handleFocus)()),
|
|
386
|
-
onBlur: _cache[2] || (_cache[2] = ($event) =>
|
|
402
|
+
onBlur: _cache[2] || (_cache[2] = ($event) => onBlur()),
|
|
387
403
|
onSelect: _cache[3] || (_cache[3] = ($event) => unref(handleInputSelection)($event)),
|
|
388
404
|
onClick: _cache[4] || (_cache[4] = withModifiers(($event) => handleInputClick($event), ["stop"])),
|
|
389
405
|
onKeydown: _cache[5] || (_cache[5] = ($event) => onKeyDown($event)),
|
|
@@ -414,6 +430,7 @@ var RuiDateTimePicker_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*
|
|
|
414
430
|
!__props.disabled && !__props.readonly ? (openBlock(), createElementBlock("button", {
|
|
415
431
|
key: 2,
|
|
416
432
|
type: "button",
|
|
433
|
+
tabindex: "-1",
|
|
417
434
|
class: normalizeClass(unref(ui).iconWrapper()),
|
|
418
435
|
"data-id": "append",
|
|
419
436
|
"aria-label": unref(toggleLabel),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RuiDateTimePicker.vue_vue_type_script_setup_true_lang.js","names":["$attrs"],"sources":["../../../src/components/date-time-picker/RuiDateTimePicker.vue"],"sourcesContent":["<script lang=\"ts\" setup>\nimport type { ComponentPublicInstance } from 'vue';\nimport type { DateTimePickerAction, DateTimeSegmentType } from '@/components/date-time-picker/types';\nimport type { TimePickerSelection } from '@/components/time-picker/RuiTimePicker.vue';\nimport RuiButton from '@/components/buttons/button/RuiButton.vue';\nimport { dateTimePickerStyles, type DateTimePickerVariant } from '@/components/date-time-picker/date-time-picker-styles';\nimport RuiDateTimePickerMenu from '@/components/date-time-picker/RuiDateTimePickerMenu.vue';\nimport { useDateTimeSelection } from '@/components/date-time-picker/use-date-time-selection';\nimport { useInputHandler } from '@/components/date-time-picker/use-input-handler';\nimport { useKeyboardHandler } from '@/components/date-time-picker/use-keyboard-handler';\nimport RuiIcon from '@/components/icons/RuiIcon.vue';\nimport RuiMenu from '@/components/overlays/menu/RuiMenu.vue';\nimport { type FloatingOptions, Placement } from '@/composables/floating';\nimport { useRuiI8n } from '@/composables/use-rui-i18n';\nimport { RUI_I18N_KEYS } from '@/i18n/keys';\nimport { useFormTextDetail } from '@/utils/form-text-detail';\nimport { getNonRootAttrs, getRootAttrs } from '@/utils/helpers';\nimport { cn } from '@/utils/tv';\n\ntype DateFormat = 'year-first' | 'month-first' | 'day-first';\n\ntype DateTimeModelType = 'date' | 'epoch-ms' | 'epoch';\n\ntype ModelValueType<T extends DateTimeModelType> = T extends 'date'\n ? Date | undefined\n : T extends 'epoch-ms'\n ? number | undefined\n : T extends 'epoch'\n ? number | undefined\n : Date | number | undefined;\n\nexport interface RuiDateTimePickerProps {\n minDate?: Date | number;\n maxDate?: Date | number | 'now';\n format?: DateFormat;\n type?: DateTimeModelType;\n accuracy?: 'minute' | 'second' | 'millisecond';\n disabled?: boolean;\n allowEmpty?: boolean;\n readonly?: boolean;\n dense?: boolean;\n label?: string;\n variant?: DateTimePickerVariant;\n hint?: string;\n errorMessages?: string | string[];\n successMessages?: string | string[];\n hideDetails?: boolean;\n required?: boolean;\n /**\n * Renders the timezone selector in the menu. Off by default: the value is\n * emitted as a date or an epoch, so the picked timezone does not survive a\n * round trip and most consumers work in local time.\n */\n showTimezone?: boolean;\n /**\n * Actions rendered in the menu footer. `clear` is only rendered when the\n * picker is `allowEmpty`. Pass an empty array to drop the footer entirely.\n */\n actions?: DateTimePickerAction[];\n /**\n * Focuses the field once it is mounted. The native attribute is ignored for\n * an input inserted into an already loaded document, which is the usual case\n * for a picker revealed by an editor or a dialog.\n */\n autofocus?: boolean;\n}\n\ndefineOptions({\n name: 'RuiDateTimePicker',\n inheritAttrs: false,\n});\n\nconst modelValue = defineModel<ModelValueType<DateTimeModelType>>({ required: true });\nconst menuOpen = defineModel<boolean>('menuOpen', { default: false });\n\nconst {\n disabled = false,\n readonly = false,\n allowEmpty = false,\n dense = false,\n type = 'epoch-ms',\n hideDetails = false,\n label,\n variant = 'default',\n hint,\n maxDate,\n minDate,\n format = 'day-first',\n accuracy = 'minute',\n errorMessages = [],\n successMessages = [],\n required = false,\n showTimezone = false,\n actions = ['now'],\n autofocus = false,\n} = defineProps<RuiDateTimePickerProps>();\n\ndefineSlots<{\n 'menu-content': () => any;\n}>();\n\nconst MENU_OPTIONS: FloatingOptions = { placement: Placement.bottomStart };\n\nconst baseFormats: Record<DateFormat, string> = {\n 'day-first': 'DD/MM/YYYY HH:mm',\n 'month-first': 'MM/DD/YYYY HH:mm',\n 'year-first': 'YYYY/MM/DD HH:mm',\n};\n\nconst isOpen = ref<boolean>(false);\nconst isHovered = ref<boolean>(false);\nconst cursorPosition = ref<number>(0);\nconst currentValue = ref<number>();\n\nconst { t } = useRuiI8n();\n\nconst keys = RUI_I18N_KEYS.dateTimePicker;\n\nconst fieldLabel = computed<string>(() => label ?? t(keys.label, 'Pick a date'));\nconst clearLabel = computed<string>(() => t(keys.clearValue, 'Clear the date'));\nconst toggleLabel = computed<string>(() => (get(isOpen)\n ? t(keys.closeCalendar, 'Close the calendar')\n : t(keys.openCalendar, 'Open the calendar')));\n\nconst textInput = useTemplateRef<HTMLInputElement>('textInput');\nconst activator = useTemplateRef<HTMLDivElement>('activator');\nconst menuWrapperRef = useTemplateRef<ComponentPublicInstance>('menuWrapperRef');\nconst calendarMenuOpen = ref<boolean>(false);\n\nconst { focused: activatorFocusedWithin } = useFocusWithin(activator);\nconst { focused: menuWrapperFocusedWithin } = useFocusWithin(menuWrapperRef);\nconst { focused: searchInputFocused } = useFocus(textInput);\n\nconst anyFocused = computed<boolean>(() => get(activatorFocusedWithin) || get(menuWrapperFocusedWithin));\n\nconst dateFormat = computed<string>(() => {\n const fmt = baseFormats[format];\n if (accuracy === 'second') {\n return fmt.replace('HH:mm', 'HH:mm:ss');\n }\n else if (accuracy === 'millisecond') {\n return fmt.replace('HH:mm', 'HH:mm:ss.SSS');\n }\n return fmt;\n});\n\nconst {\n clear: clearSelection,\n getDateTime,\n internalErrorMessages,\n maxAllowedDate,\n minAllowedDate,\n segmentData,\n selectedDate,\n selectedDay,\n selectedHour,\n selectedMillisecond,\n selectedMinute,\n selectedMonth,\n selectedSecond,\n selectedTime,\n selectedTimezone,\n selectedYear,\n setNow,\n setToday,\n valueSet,\n} = useDateTimeSelection({\n accuracy,\n allowEmpty,\n dateFormat,\n maxDate,\n minDate,\n modelValue,\n type,\n});\n\nconst { setValue, getCurrent } = useInputHandler(segmentData, currentValue);\n\nconst {\n clear: clearSegment,\n getCurrentSegment,\n handleBlur,\n handleClick,\n handleFocus,\n handleInput,\n handleInputSelection,\n handleKeyDown,\n handleMouseDown,\n handlePaste,\n selectFirstSegment,\n setSegment,\n} = useKeyboardHandler({\n accuracy,\n currentValue,\n cursorPosition,\n dateFormat,\n disabled,\n getCurrent,\n getDateTime,\n readonly,\n setValue,\n textInput,\n});\n\nconst { hasError, hasSuccess } = useFormTextDetail(\n () => errorMessages,\n () => successMessages,\n);\n\nconst isOutlined = computed<boolean>(() => variant === 'outlined');\n\n/** True once any segment holds a digit, even a partially typed date. */\nconst anySegmentSet = computed<boolean>(() => [\n selectedYear,\n selectedMonth,\n selectedDay,\n selectedHour,\n selectedMinute,\n selectedSecond,\n selectedMillisecond,\n].some(segment => isDefined(segment)));\n\nconst formattedDisplay = computed<string>(() => {\n // An untouched field shows its format through the placeholder rather than\n // holding the tokens as its value, where a screen reader reads them as\n // content and select-all copies them. The tokens stay while the field is\n // focused: that is when the segment machinery highlights them, and\n // collapsing the value mid-edit would blank the field under the cursor.\n // The guard is \"no segment set\" and not `valueSet`, so blurring a half\n // typed date keeps what was entered on screen.\n if (!get(anySegmentSet) && !get(searchInputFocused)) {\n return '';\n }\n\n let result = get(dateFormat);\n\n const replacements = [\n { pattern: 'YYYY', value: getDisplayValue(selectedYear, 4) },\n { pattern: 'MM', value: getDisplayValue(selectedMonth, 2) },\n { pattern: 'DD', value: getDisplayValue(selectedDay, 2) },\n { pattern: 'HH', value: getDisplayValue(selectedHour, 2) },\n { pattern: 'mm', value: getDisplayValue(selectedMinute, 2) },\n { pattern: 'ss', value: getDisplayValue(selectedSecond, 2) },\n { pattern: 'SSS', value: getDisplayValue(selectedMillisecond, 3) },\n ];\n\n for (const { pattern, value } of replacements) {\n if (value !== undefined) {\n result = result.replace(pattern, value);\n }\n }\n\n return result;\n});\n\n/**\n * An untouched field holds no value until focus expands the format tokens into\n * it. That patch overwrites the input's value, and writing a value drops any\n * selection with it, so the highlight `handleFocus` just set is gone by the\n * time the tokens are on screen and the caret ends up after them. Re-select\n * the first segment once the tokens have actually landed.\n */\nwatch(formattedDisplay, (value, previous) => {\n if (previous === '' && value !== '' && get(searchInputFocused))\n selectFirstSegment();\n}, { flush: 'post' });\n\nconst timeSelection = computed<TimePickerSelection>({\n get() {\n const type = getCurrentSegment()?.type;\n\n if (type === 'mm') {\n return 'minute';\n }\n else if (type === 'ss') {\n return 'second';\n }\n else if (type === 'SSS') {\n return 'millisecond';\n }\n return 'hour';\n },\n set(value: TimePickerSelection) {\n let segmentType: DateTimeSegmentType = 'HH';\n if (value === 'minute') {\n segmentType = 'mm';\n }\n else if (value === 'second') {\n segmentType = 'ss';\n }\n else if (value === 'millisecond') {\n segmentType = 'SSS';\n }\n\n setSegment(segmentType);\n },\n});\n\nconst float = computed<boolean>(() => (get(isOpen) || get(valueSet) || get(searchInputFocused)) && get(isOutlined));\n\nconst legendText = computed<string>(() => {\n if (!get(float))\n return '';\n const resolved = get(fieldLabel);\n return required ? `${resolved} ﹡` : resolved;\n});\n\nconst ui = computed<ReturnType<typeof dateTimePickerStyles>>(() => dateTimePickerStyles({\n filled: variant === 'filled',\n outlined: get(isOutlined),\n float: get(float),\n opened: get(isOpen),\n hovered: get(isHovered),\n dense,\n disabled,\n readonly,\n hasError: get(hasError),\n hasSuccess: get(hasSuccess) && !get(hasError),\n}));\n\nconst combinedErrorMessages = computed<string[]>(() => {\n if (!errorMessages)\n return get(internalErrorMessages);\n\n const propErrors = Array.isArray(errorMessages) ? errorMessages : [errorMessages];\n return [...propErrors, ...get(internalErrorMessages)];\n});\n\nfunction getDisplayValue(digit: Ref<number | undefined>, padding: number): string | undefined {\n return isDefined(digit) ? get(digit).toString().padStart(padding, '0') : undefined;\n}\n\nasync function setInputFocus(): Promise<void> {\n await nextTick(() => {\n set(searchInputFocused, true);\n });\n}\n\nfunction clear(segmentType?: string): void {\n if (!segmentType) {\n clearSelection();\n set(currentValue, undefined);\n return;\n }\n\n clearSegment(segmentType);\n}\n\nfunction handleInputClick(event: MouseEvent): void {\n // Handle segment selection first, before any DOM changes from menu opening\n handleClick(event);\n // Open menu if not already open\n if (!get(isOpen)) {\n set(isOpen, true);\n }\n}\n\n/**\n * The menu is teleported to the body, so it never sits next to the field in\n * the tab sequence. Opening it from the keyboard therefore moves focus into\n * the calendar; opening it with the mouse leaves focus in the field, which is\n * what `disable-auto-focus` on the menu is there for.\n */\nconst focusCalendarOnOpen = ref<boolean>(false);\n\nfunction focusCalendar(): void {\n // the menu is teleported and mounts a frame later, so this waits for the\n // wrapper to appear rather than guessing at a number of ticks\n set(focusCalendarOnOpen, true);\n}\n\nwatch(menuWrapperRef, (menu) => {\n if (!menu || !get(focusCalendarOnOpen))\n return;\n\n set(focusCalendarOnOpen, false);\n nextTick(() => {\n const el = menu.$el as HTMLElement | undefined;\n el?.querySelector<HTMLButtonElement>('[role=\"gridcell\"][tabindex=\"0\"]')?.focus({ preventScroll: true });\n });\n});\n\n/** Moves focus to the field. Exposed so a consumer can drive it from a ref. */\nfunction focus(): void {\n get(textInput)?.focus({ preventScroll: true });\n}\n\nfunction focusField(): void {\n nextTick(focus);\n}\n\nonMounted(() => {\n if (autofocus && !disabled)\n focusField();\n});\n\n/** Escape inside the calendar closes it and hands focus back to the field. */\nfunction closeFromMenu(): void {\n set(isOpen, false);\n focusField();\n}\n\n/**\n * The menu used to be mouse-only: the activator carried no key handlers, so\n * there was no way to reach the calendar from the keyboard. Alt+ArrowDown\n * opens it and Escape closes it, following the combobox convention, and the\n * append chevron is a real button for anyone who tabs to it instead.\n */\nfunction onKeyDown(event: KeyboardEvent): void {\n if (disabled || readonly)\n return;\n\n if (event.altKey && event.key === 'ArrowDown') {\n event.preventDefault();\n set(isOpen, true);\n focusCalendar();\n return;\n }\n\n if (event.key === 'Escape' && get(isOpen)) {\n set(isOpen, false);\n return;\n }\n\n handleKeyDown(event);\n}\n\nfunction arrowClicked(event: MouseEvent): void {\n if (get(isOpen)) {\n set(isOpen, false);\n event.stopPropagation();\n }\n}\n\nconst menuActions = computed<DateTimePickerAction[]>(\n () => actions.filter(action => action !== 'clear' || allowEmpty),\n);\n\nconst anyMenuOpen = computed<boolean>(() => get(isOpen) || get(calendarMenuOpen));\n\nwatch(anyMenuOpen, (value) => {\n set(menuOpen, value);\n});\n\ndefineExpose({\n focus,\n});\n</script>\n\n<template>\n <RuiMenu\n v-model=\"isOpen\"\n v-bind=\"getRootAttrs($attrs, [])\"\n :class=\"ui.wrapper({ class: cn($attrs.class) })\"\n :options=\"MENU_OPTIONS\"\n :dense=\"dense\"\n :hint=\"hint\"\n :disabled=\"disabled\"\n :success-messages=\"successMessages\"\n :error-messages=\"combinedErrorMessages\"\n :close-on-content-click=\"false\"\n :show-details=\"!hideDetails\"\n :persistent=\"calendarMenuOpen\"\n full-width\n disable-auto-focus\n >\n <template #activator=\"{ attrs, open }\">\n <div\n ref=\"activator\"\n :class=\"ui.activator()\"\n v-bind=\"{\n ...getNonRootAttrs($attrs, ['onClick', 'class']),\n ...(readonly ? {} : attrs),\n }\"\n data-id=\"activator\"\n :aria-invalid=\"hasError\"\n @mouseenter=\"isHovered = true\"\n @mouseleave=\"isHovered = false\"\n @click=\"setInputFocus()\"\n >\n <span\n v-if=\"isOutlined && (searchInputFocused || open || valueSet)\"\n data-id=\"label\"\n :class=\"[\n ui.label(),\n { 'pr-2': !valueSet && !open && isOutlined },\n ]\"\n >\n {{ fieldLabel }}\n <span\n v-if=\"required\"\n data-id=\"required-indicator\"\n :class=\"ui.required()\"\n >\n ﹡\n </span>\n </span>\n\n <span :class=\"ui.iconPrepend()\">\n <RuiIcon\n class=\"text-rui-text-secondary transition\"\n :size=\"dense ? 16 : 24\"\n name=\"lu-calendar-days\"\n />\n </span>\n\n <div :class=\"ui.value()\">\n <input\n ref=\"textInput\"\n :disabled=\"disabled\"\n :value=\"formattedDisplay\"\n class=\"bg-transparent outline-none flex-1 min-w-0\"\n type=\"text\"\n inputmode=\"numeric\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n :placeholder=\"dateFormat\"\n :readonly=\"readonly\"\n :aria-invalid=\"hasError\"\n :aria-label=\"fieldLabel\"\n :aria-required=\"required || undefined\"\n @mousedown=\"handleMouseDown($event)\"\n @focus=\"handleFocus()\"\n @blur=\"handleBlur()\"\n @select=\"handleInputSelection($event)\"\n @click.stop=\"handleInputClick($event)\"\n @keydown=\"onKeyDown($event)\"\n @paste=\"handlePaste($event)\"\n @input=\"handleInput($event)\"\n />\n </div>\n\n <RuiButton\n v-if=\"allowEmpty && valueSet && !disabled\"\n variant=\"text\"\n icon\n data-id=\"clear-button\"\n size=\"sm\"\n color=\"error\"\n :aria-label=\"clearLabel\"\n :class=\"[\n ui.clear(),\n anyFocused && '!visible',\n { 'mr-2': !dense },\n ]\"\n @click.stop.prevent=\"clear()\"\n >\n <RuiIcon\n name=\"lu-x\"\n size=\"18\"\n />\n </RuiButton>\n\n <button\n v-if=\"!disabled && !readonly\"\n type=\"button\"\n :class=\"ui.iconWrapper()\"\n data-id=\"append\"\n :aria-label=\"toggleLabel\"\n :aria-expanded=\"isOpen\"\n aria-haspopup=\"dialog\"\n @click=\"arrowClicked($event)\"\n >\n <RuiIcon\n :class=\"ui.icon()\"\n :size=\"dense ? 16 : 24\"\n name=\"lu-chevron-down\"\n />\n </button>\n <span\n v-else\n :class=\"ui.iconWrapper()\"\n data-id=\"append\"\n >\n <RuiIcon\n :class=\"ui.icon()\"\n :size=\"dense ? 16 : 24\"\n name=\"lu-chevron-down\"\n />\n </span>\n </div>\n <fieldset\n v-if=\"isOutlined\"\n :class=\"ui.fieldset()\"\n >\n <legend :class=\"ui.legend()\">\n {{ legendText }}\n </legend>\n </fieldset>\n </template>\n <template #default>\n <RuiDateTimePickerMenu\n ref=\"menuWrapperRef\"\n v-model:selected-date=\"selectedDate\"\n v-model:selected-time=\"selectedTime\"\n v-model:selected-hour=\"selectedHour\"\n v-model:selected-minute=\"selectedMinute\"\n v-model:selected-second=\"selectedSecond\"\n v-model:selected-millisecond=\"selectedMillisecond\"\n v-model:time-selection=\"timeSelection\"\n v-model:selected-timezone=\"selectedTimezone\"\n v-model:calendar-menu-open=\"calendarMenuOpen\"\n :accuracy=\"accuracy\"\n :max-date=\"maxAllowedDate\"\n :min-date=\"minAllowedDate\"\n :show-timezone=\"showTimezone\"\n :actions=\"menuActions\"\n @keydown.escape=\"closeFromMenu()\"\n @set-now=\"setNow()\"\n @set-today=\"setToday()\"\n @clear=\"clear()\"\n >\n <slot name=\"menu-content\" />\n </RuiDateTimePickerMenu>\n </template>\n </RuiMenu>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RuiDateTimePicker.vue_vue_type_script_setup_true_lang.js","names":["$attrs"],"sources":["../../../src/components/date-time-picker/RuiDateTimePicker.vue"],"sourcesContent":["<script lang=\"ts\" setup>\nimport type { ComponentPublicInstance } from 'vue';\nimport type { DateTimePickerAction, DateTimeSegmentType } from '@/components/date-time-picker/types';\nimport type { TimePickerSelection } from '@/components/time-picker/RuiTimePicker.vue';\nimport RuiButton from '@/components/buttons/button/RuiButton.vue';\nimport { dateTimePickerStyles, type DateTimePickerVariant } from '@/components/date-time-picker/date-time-picker-styles';\nimport RuiDateTimePickerMenu from '@/components/date-time-picker/RuiDateTimePickerMenu.vue';\nimport { useDateTimeSelection } from '@/components/date-time-picker/use-date-time-selection';\nimport { useInputHandler } from '@/components/date-time-picker/use-input-handler';\nimport { useKeyboardHandler } from '@/components/date-time-picker/use-keyboard-handler';\nimport RuiIcon from '@/components/icons/RuiIcon.vue';\nimport RuiMenu from '@/components/overlays/menu/RuiMenu.vue';\nimport { type FloatingOptions, Placement } from '@/composables/floating';\nimport { useRuiI8n } from '@/composables/use-rui-i18n';\nimport { RUI_I18N_KEYS } from '@/i18n/keys';\nimport { useFormTextDetail } from '@/utils/form-text-detail';\nimport { getNonRootAttrs, getRootAttrs } from '@/utils/helpers';\nimport { cn } from '@/utils/tv';\n\ntype DateFormat = 'year-first' | 'month-first' | 'day-first';\n\ntype DateTimeModelType = 'date' | 'epoch-ms' | 'epoch';\n\ntype ModelValueType<T extends DateTimeModelType> = T extends 'date'\n ? Date | undefined\n : T extends 'epoch-ms'\n ? number | undefined\n : T extends 'epoch'\n ? number | undefined\n : Date | number | undefined;\n\nexport interface RuiDateTimePickerProps {\n minDate?: Date | number;\n maxDate?: Date | number | 'now';\n format?: DateFormat;\n type?: DateTimeModelType;\n accuracy?: 'minute' | 'second' | 'millisecond';\n disabled?: boolean;\n allowEmpty?: boolean;\n readonly?: boolean;\n dense?: boolean;\n label?: string;\n variant?: DateTimePickerVariant;\n hint?: string;\n errorMessages?: string | string[];\n successMessages?: string | string[];\n hideDetails?: boolean;\n required?: boolean;\n /**\n * Renders the timezone selector in the menu. Off by default: the value is\n * emitted as a date or an epoch, so the picked timezone does not survive a\n * round trip and most consumers work in local time.\n */\n showTimezone?: boolean;\n /**\n * Actions rendered in the menu footer. `clear` is only rendered when the\n * picker is `allowEmpty`. Pass an empty array to drop the footer entirely.\n */\n actions?: DateTimePickerAction[];\n /**\n * Focuses the field once it is mounted. The native attribute is ignored for\n * an input inserted into an already loaded document, which is the usual case\n * for a picker revealed by an editor or a dialog.\n */\n autofocus?: boolean;\n /**\n * Accepts an entry that stops short of the full format, filling the segments\n * it never reached from one end of the day: `start` gives a bare date\n * 00:00:00.000 and `end` gives it 23:59:59.999. Meant for a range, where the\n * two bounds want opposite ends. Left unset, an entry missing its time is not\n * a value and nothing is emitted.\n *\n * The fill runs when the user is done with the field - leaving it, pressing\n * enter, or closing the calendar - and is written back into the segments, so\n * what it decided on is on screen.\n *\n * Spelled out rather than written as `PartialTimeMode`: a consumer that hands\n * this whole interface to its own `defineProps` needs every member resolvable\n * by the SFC compiler, which does not follow the type into the package.\n */\n partialTime?: 'start' | 'end';\n}\n\ndefineOptions({\n name: 'RuiDateTimePicker',\n inheritAttrs: false,\n});\n\nconst modelValue = defineModel<ModelValueType<DateTimeModelType>>({ required: true });\nconst menuOpen = defineModel<boolean>('menuOpen', { default: false });\n\nconst {\n disabled = false,\n readonly = false,\n allowEmpty = false,\n dense = false,\n type = 'epoch-ms',\n hideDetails = false,\n label,\n variant = 'default',\n hint,\n maxDate,\n minDate,\n format = 'day-first',\n accuracy = 'minute',\n errorMessages = [],\n successMessages = [],\n required = false,\n showTimezone = false,\n actions = ['now'],\n autofocus = false,\n partialTime,\n} = defineProps<RuiDateTimePickerProps>();\n\ndefineSlots<{\n 'menu-content': () => any;\n}>();\n\nconst MENU_OPTIONS: FloatingOptions = { placement: Placement.bottomStart };\n\nconst baseFormats: Record<DateFormat, string> = {\n 'day-first': 'DD/MM/YYYY HH:mm',\n 'month-first': 'MM/DD/YYYY HH:mm',\n 'year-first': 'YYYY/MM/DD HH:mm',\n};\n\nconst isOpen = ref<boolean>(false);\nconst isHovered = ref<boolean>(false);\nconst cursorPosition = ref<number>(0);\nconst currentValue = ref<number>();\n\nconst { t } = useRuiI8n();\n\nconst keys = RUI_I18N_KEYS.dateTimePicker;\n\nconst fieldLabel = computed<string>(() => label ?? t(keys.label, 'Pick a date'));\nconst clearLabel = computed<string>(() => t(keys.clearValue, 'Clear the date'));\nconst toggleLabel = computed<string>(() => (get(isOpen)\n ? t(keys.closeCalendar, 'Close the calendar')\n : t(keys.openCalendar, 'Open the calendar')));\n\nconst textInput = useTemplateRef<HTMLInputElement>('textInput');\nconst activator = useTemplateRef<HTMLDivElement>('activator');\nconst menuWrapperRef = useTemplateRef<ComponentPublicInstance>('menuWrapperRef');\nconst calendarMenuOpen = ref<boolean>(false);\n\nconst { focused: activatorFocusedWithin } = useFocusWithin(activator);\nconst { focused: menuWrapperFocusedWithin } = useFocusWithin(menuWrapperRef);\nconst { focused: searchInputFocused } = useFocus(textInput);\n\nconst anyFocused = computed<boolean>(() => get(activatorFocusedWithin) || get(menuWrapperFocusedWithin));\n\nconst dateFormat = computed<string>(() => {\n const fmt = baseFormats[format];\n if (accuracy === 'second') {\n return fmt.replace('HH:mm', 'HH:mm:ss');\n }\n else if (accuracy === 'millisecond') {\n return fmt.replace('HH:mm', 'HH:mm:ss.SSS');\n }\n return fmt;\n});\n\nconst {\n clear: clearSelection,\n commitPartialTime,\n getDateTime,\n internalErrorMessages,\n maxAllowedDate,\n minAllowedDate,\n segmentData,\n selectedDate,\n selectedDay,\n selectedHour,\n selectedMillisecond,\n selectedMinute,\n selectedMonth,\n selectedSecond,\n selectedTime,\n selectedTimezone,\n selectedYear,\n setNow,\n setToday,\n valueSet,\n} = useDateTimeSelection({\n accuracy,\n allowEmpty,\n dateFormat,\n maxDate,\n minDate,\n modelValue,\n partialTime,\n type,\n});\n\nconst { setValue, getCurrent } = useInputHandler(segmentData, currentValue);\n\nconst {\n clear: clearSegment,\n getCurrentSegment,\n handleBlur,\n handleClick,\n handleFocus,\n handleInput,\n handleInputSelection,\n handleKeyDown,\n handleMouseDown,\n handlePaste,\n selectFirstSegment,\n setSegment,\n} = useKeyboardHandler({\n accuracy,\n currentValue,\n cursorPosition,\n dateFormat,\n disabled,\n getCurrent,\n getDateTime,\n readonly,\n setValue,\n textInput,\n});\n\nconst { hasError, hasSuccess } = useFormTextDetail(\n () => errorMessages,\n () => successMessages,\n);\n\nconst isOutlined = computed<boolean>(() => variant === 'outlined');\n\n/** True once any segment holds a digit, even a partially typed date. */\nconst anySegmentSet = computed<boolean>(() => [\n selectedYear,\n selectedMonth,\n selectedDay,\n selectedHour,\n selectedMinute,\n selectedSecond,\n selectedMillisecond,\n].some(segment => isDefined(segment)));\n\nconst formattedDisplay = computed<string>(() => {\n // An untouched field shows its format through the placeholder rather than\n // holding the tokens as its value, where a screen reader reads them as\n // content and select-all copies them. The tokens stay while the field is\n // focused: that is when the segment machinery highlights them, and\n // collapsing the value mid-edit would blank the field under the cursor.\n // The guard is \"no segment set\" and not `valueSet`, so blurring a half\n // typed date keeps what was entered on screen.\n if (!get(anySegmentSet) && !get(searchInputFocused)) {\n return '';\n }\n\n let result = get(dateFormat);\n\n const replacements = [\n { pattern: 'YYYY', value: getDisplayValue(selectedYear, 4) },\n { pattern: 'MM', value: getDisplayValue(selectedMonth, 2) },\n { pattern: 'DD', value: getDisplayValue(selectedDay, 2) },\n { pattern: 'HH', value: getDisplayValue(selectedHour, 2) },\n { pattern: 'mm', value: getDisplayValue(selectedMinute, 2) },\n { pattern: 'ss', value: getDisplayValue(selectedSecond, 2) },\n { pattern: 'SSS', value: getDisplayValue(selectedMillisecond, 3) },\n ];\n\n for (const { pattern, value } of replacements) {\n if (value !== undefined) {\n result = result.replace(pattern, value);\n }\n }\n\n return result;\n});\n\n/**\n * An untouched field holds no value until focus expands the format tokens into\n * it. That patch overwrites the input's value, and writing a value drops any\n * selection with it, so the highlight `handleFocus` just set is gone by the\n * time the tokens are on screen and the caret ends up after them. Re-select\n * the first segment once the tokens have actually landed.\n */\nwatch(formattedDisplay, (value, previous) => {\n if (previous === '' && value !== '' && get(searchInputFocused))\n selectFirstSegment();\n}, { flush: 'post' });\n\nconst timeSelection = computed<TimePickerSelection>({\n get() {\n const type = getCurrentSegment()?.type;\n\n if (type === 'mm') {\n return 'minute';\n }\n else if (type === 'ss') {\n return 'second';\n }\n else if (type === 'SSS') {\n return 'millisecond';\n }\n return 'hour';\n },\n set(value: TimePickerSelection) {\n let segmentType: DateTimeSegmentType = 'HH';\n if (value === 'minute') {\n segmentType = 'mm';\n }\n else if (value === 'second') {\n segmentType = 'ss';\n }\n else if (value === 'millisecond') {\n segmentType = 'SSS';\n }\n\n setSegment(segmentType);\n },\n});\n\nconst float = computed<boolean>(() => (get(isOpen) || get(valueSet) || get(searchInputFocused)) && get(isOutlined));\n\nconst legendText = computed<string>(() => {\n if (!get(float))\n return '';\n const resolved = get(fieldLabel);\n return required ? `${resolved} ﹡` : resolved;\n});\n\nconst ui = computed<ReturnType<typeof dateTimePickerStyles>>(() => dateTimePickerStyles({\n filled: variant === 'filled',\n outlined: get(isOutlined),\n float: get(float),\n opened: get(isOpen),\n hovered: get(isHovered),\n dense,\n disabled,\n readonly,\n hasError: get(hasError),\n hasSuccess: get(hasSuccess) && !get(hasError),\n}));\n\nconst combinedErrorMessages = computed<string[]>(() => {\n if (!errorMessages)\n return get(internalErrorMessages);\n\n const propErrors = Array.isArray(errorMessages) ? errorMessages : [errorMessages];\n return [...propErrors, ...get(internalErrorMessages)];\n});\n\nfunction getDisplayValue(digit: Ref<number | undefined>, padding: number): string | undefined {\n return isDefined(digit) ? get(digit).toString().padStart(padding, '0') : undefined;\n}\n\nasync function setInputFocus(): Promise<void> {\n await nextTick(() => {\n set(searchInputFocused, true);\n });\n}\n\nfunction clear(segmentType?: string): void {\n if (!segmentType) {\n clearSelection();\n set(currentValue, undefined);\n return;\n }\n\n clearSegment(segmentType);\n}\n\n/**\n * An entry left incomplete is completed once the user is done with the field,\n * never while they are still filling it in: a half typed hour is not a segment\n * they left out. Leaving the field counts, unless the calendar is open, since\n * then the focus is only moving into it and the menu closing covers that.\n */\nfunction onBlur(): void {\n handleBlur();\n if (!get(isOpen))\n commitPartialTime();\n}\n\nwatch(isOpen, (open) => {\n if (!open && !get(searchInputFocused))\n commitPartialTime();\n});\n\nfunction handleInputClick(event: MouseEvent): void {\n // Handle segment selection first, before any DOM changes from menu opening\n handleClick(event);\n // Open menu if not already open\n if (!get(isOpen)) {\n set(isOpen, true);\n }\n}\n\n/**\n * The menu is teleported to the body, so it never sits next to the field in\n * the tab sequence. Opening it from the keyboard therefore moves focus into\n * the calendar; opening it with the mouse leaves focus in the field, which is\n * what `disable-auto-focus` on the menu is there for.\n */\nconst focusCalendarOnOpen = ref<boolean>(false);\n\nfunction focusCalendar(): void {\n // the menu is teleported and mounts a frame later, so this waits for the\n // wrapper to appear rather than guessing at a number of ticks\n set(focusCalendarOnOpen, true);\n}\n\nwatch(menuWrapperRef, (menu) => {\n if (!menu || !get(focusCalendarOnOpen))\n return;\n\n set(focusCalendarOnOpen, false);\n nextTick(() => {\n const el = menu.$el as HTMLElement | undefined;\n el?.querySelector<HTMLButtonElement>('[role=\"gridcell\"][tabindex=\"0\"]')?.focus({ preventScroll: true });\n });\n});\n\n/** Moves focus to the field. Exposed so a consumer can drive it from a ref. */\nfunction focus(): void {\n get(textInput)?.focus({ preventScroll: true });\n}\n\nfunction focusField(): void {\n nextTick(focus);\n}\n\nonMounted(() => {\n if (autofocus && !disabled)\n focusField();\n});\n\n/** Escape inside the calendar closes it and hands focus back to the field. */\nfunction closeFromMenu(): void {\n set(isOpen, false);\n focusField();\n}\n\n/**\n * The menu used to be mouse-only: the activator carried no key handlers, so\n * there was no way to reach the calendar from the keyboard. Alt+ArrowDown\n * opens it and Escape closes it, following the combobox convention, and the\n * append chevron is a real button for anyone who tabs to it instead.\n */\nfunction onKeyDown(event: KeyboardEvent): void {\n if (disabled || readonly)\n return;\n\n if (event.altKey && event.key === 'ArrowDown') {\n event.preventDefault();\n set(isOpen, true);\n focusCalendar();\n return;\n }\n\n if (event.key === 'Escape' && get(isOpen)) {\n set(isOpen, false);\n return;\n }\n\n // Enter and Escape are the other ways to be done with the field, and the keys\n // a consumer tends to close its editor on. Escape reaches here only once the\n // calendar is already shut, and it discards nothing - the segments stay on\n // screen either way - so it commits what was entered rather than dropping it\n // when the field goes. Both write the value before the key carries on\n // bubbling, so it survives that close.\n if (event.key === 'Enter' || event.key === 'Escape')\n commitPartialTime();\n\n handleKeyDown(event);\n}\n\nfunction arrowClicked(event: MouseEvent): void {\n if (get(isOpen)) {\n set(isOpen, false);\n event.stopPropagation();\n }\n}\n\nconst menuActions = computed<DateTimePickerAction[]>(\n () => actions.filter(action => action !== 'clear' || allowEmpty),\n);\n\nconst anyMenuOpen = computed<boolean>(() => get(isOpen) || get(calendarMenuOpen));\n\nwatch(anyMenuOpen, (value) => {\n set(menuOpen, value);\n});\n\ndefineExpose({\n focus,\n});\n</script>\n\n<template>\n <RuiMenu\n v-model=\"isOpen\"\n v-bind=\"getRootAttrs($attrs, [])\"\n :class=\"ui.wrapper({ class: cn($attrs.class) })\"\n :options=\"MENU_OPTIONS\"\n :dense=\"dense\"\n :hint=\"hint\"\n :disabled=\"disabled\"\n :success-messages=\"successMessages\"\n :error-messages=\"combinedErrorMessages\"\n :close-on-content-click=\"false\"\n :show-details=\"!hideDetails\"\n :persistent=\"calendarMenuOpen\"\n full-width\n disable-auto-focus\n >\n <template #activator=\"{ attrs, open }\">\n <div\n ref=\"activator\"\n :class=\"ui.activator()\"\n v-bind=\"{\n ...getNonRootAttrs($attrs, ['onClick', 'class']),\n ...(readonly ? {} : attrs),\n }\"\n data-id=\"activator\"\n :aria-invalid=\"hasError\"\n @mouseenter=\"isHovered = true\"\n @mouseleave=\"isHovered = false\"\n @click=\"setInputFocus()\"\n >\n <span\n v-if=\"isOutlined && (searchInputFocused || open || valueSet)\"\n data-id=\"label\"\n :class=\"[\n ui.label(),\n { 'pr-2': !valueSet && !open && isOutlined },\n ]\"\n >\n {{ fieldLabel }}\n <span\n v-if=\"required\"\n data-id=\"required-indicator\"\n :class=\"ui.required()\"\n >\n ﹡\n </span>\n </span>\n\n <span :class=\"ui.iconPrepend()\">\n <RuiIcon\n class=\"text-rui-text-secondary transition\"\n :size=\"dense ? 16 : 24\"\n name=\"lu-calendar-days\"\n />\n </span>\n\n <div :class=\"ui.value()\">\n <input\n ref=\"textInput\"\n :disabled=\"disabled\"\n :value=\"formattedDisplay\"\n class=\"bg-transparent outline-none flex-1 min-w-0\"\n type=\"text\"\n inputmode=\"numeric\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n :placeholder=\"dateFormat\"\n :readonly=\"readonly\"\n :aria-invalid=\"hasError\"\n :aria-label=\"fieldLabel\"\n :aria-required=\"required || undefined\"\n @mousedown=\"handleMouseDown($event)\"\n @focus=\"handleFocus()\"\n @blur=\"onBlur()\"\n @select=\"handleInputSelection($event)\"\n @click.stop=\"handleInputClick($event)\"\n @keydown=\"onKeyDown($event)\"\n @paste=\"handlePaste($event)\"\n @input=\"handleInput($event)\"\n />\n </div>\n\n <RuiButton\n v-if=\"allowEmpty && valueSet && !disabled\"\n variant=\"text\"\n icon\n data-id=\"clear-button\"\n size=\"sm\"\n color=\"error\"\n :aria-label=\"clearLabel\"\n :class=\"[\n ui.clear(),\n anyFocused && '!visible',\n { 'mr-2': !dense },\n ]\"\n @click.stop.prevent=\"clear()\"\n >\n <RuiIcon\n name=\"lu-x\"\n size=\"18\"\n />\n </RuiButton>\n\n <!-- Out of the tab sequence, following the combobox pattern: the button only duplicates\n Alt+ArrowDown on the field, so leaving it in cost two stops to cross one date field\n and four to cross a from/to pair, for nothing a keyboard user could not already do.\n It stays a real button, so it keeps its label and its expanded state for anyone\n reaching it by any other means. -->\n <button\n v-if=\"!disabled && !readonly\"\n type=\"button\"\n tabindex=\"-1\"\n :class=\"ui.iconWrapper()\"\n data-id=\"append\"\n :aria-label=\"toggleLabel\"\n :aria-expanded=\"isOpen\"\n aria-haspopup=\"dialog\"\n @click=\"arrowClicked($event)\"\n >\n <RuiIcon\n :class=\"ui.icon()\"\n :size=\"dense ? 16 : 24\"\n name=\"lu-chevron-down\"\n />\n </button>\n <span\n v-else\n :class=\"ui.iconWrapper()\"\n data-id=\"append\"\n >\n <RuiIcon\n :class=\"ui.icon()\"\n :size=\"dense ? 16 : 24\"\n name=\"lu-chevron-down\"\n />\n </span>\n </div>\n <fieldset\n v-if=\"isOutlined\"\n :class=\"ui.fieldset()\"\n >\n <legend :class=\"ui.legend()\">\n {{ legendText }}\n </legend>\n </fieldset>\n </template>\n <template #default>\n <RuiDateTimePickerMenu\n ref=\"menuWrapperRef\"\n v-model:selected-date=\"selectedDate\"\n v-model:selected-time=\"selectedTime\"\n v-model:selected-hour=\"selectedHour\"\n v-model:selected-minute=\"selectedMinute\"\n v-model:selected-second=\"selectedSecond\"\n v-model:selected-millisecond=\"selectedMillisecond\"\n v-model:time-selection=\"timeSelection\"\n v-model:selected-timezone=\"selectedTimezone\"\n v-model:calendar-menu-open=\"calendarMenuOpen\"\n :accuracy=\"accuracy\"\n :max-date=\"maxAllowedDate\"\n :min-date=\"minAllowedDate\"\n :show-timezone=\"showTimezone\"\n :actions=\"menuActions\"\n @keydown.escape=\"closeFromMenu()\"\n @set-now=\"setNow()\"\n @set-today=\"setToday()\"\n @clear=\"clear()\"\n >\n <slot name=\"menu-content\" />\n </RuiDateTimePickerMenu>\n </template>\n </RuiMenu>\n</template>\n"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Dayjs } from 'dayjs';
|
|
2
|
+
import type { TimeAccuracy } from '../../consts/time-accuracy.js';
|
|
3
|
+
import { type SegmentValues } from '../../components/date-time-picker/segment-utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Which end of the entered precision an incomplete entry stands for. `start`
|
|
6
|
+
* fills the segments it never reached with their lowest value and `end` with
|
|
7
|
+
* their highest, so a bare date means the whole day from either side.
|
|
8
|
+
*/
|
|
9
|
+
export type PartialTimeMode = 'start' | 'end';
|
|
10
|
+
interface PartialTimeOptions {
|
|
11
|
+
mode: PartialTimeMode;
|
|
12
|
+
accuracy: TimeAccuracy;
|
|
13
|
+
minDate: Date;
|
|
14
|
+
maxDate: Date | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Completes an entry that stopped short of the full format, so a bare date - or
|
|
18
|
+
* a date and an hour - can still become a value. Returns nothing when there is
|
|
19
|
+
* nothing to complete: no date to build on, or every segment already entered.
|
|
20
|
+
*
|
|
21
|
+
* The result is pulled back inside the bounds when the fill overshot them. The
|
|
22
|
+
* date is the user's and the filled time is ours, so an `end` fill on today
|
|
23
|
+
* against a `now` maximum is answered with that maximum rather than refused.
|
|
24
|
+
* A clamp landing on another day means the date itself is out of range, which
|
|
25
|
+
* is the user's to fix, so that is handed back unclamped to be rejected with an
|
|
26
|
+
* error the user can read.
|
|
27
|
+
*
|
|
28
|
+
* The fill reaches as far as the entry does. A date and a time down to the
|
|
29
|
+
* minute is already a value, which its field emits and reads back with a zero
|
|
30
|
+
* second, and from then on that second is part of the entry rather than a gap
|
|
31
|
+
* in it: `end` therefore means the last second of a bare date, and the zeroth
|
|
32
|
+
* second of a date entered with its minutes.
|
|
33
|
+
*/
|
|
34
|
+
export declare function completePartialEntry(segments: SegmentValues, { accuracy, maxDate, minDate, mode }: PartialTimeOptions): Dayjs | undefined;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { includeMilliseconds, includeSeconds } from "./utils.js";
|
|
2
|
+
import { buildDateTime, clampToBounds } from "./segment-utils.js";
|
|
3
|
+
//#region src/components/date-time-picker/partial-time.ts
|
|
4
|
+
/** What each side of the day fills a segment the entry never reached with. */
|
|
5
|
+
var FALLBACKS = {
|
|
6
|
+
end: {
|
|
7
|
+
hour: 23,
|
|
8
|
+
millisecond: 999,
|
|
9
|
+
minute: 59,
|
|
10
|
+
second: 59
|
|
11
|
+
},
|
|
12
|
+
start: {
|
|
13
|
+
hour: 0,
|
|
14
|
+
millisecond: 0,
|
|
15
|
+
minute: 0,
|
|
16
|
+
second: 0
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Reads the segments an entry does hold, with the ones its accuracy does not
|
|
21
|
+
* expose already answered: those cannot be left out, so they never count as
|
|
22
|
+
* missing.
|
|
23
|
+
*/
|
|
24
|
+
function timeSegments(segments, accuracy) {
|
|
25
|
+
return {
|
|
26
|
+
hour: segments.hour,
|
|
27
|
+
millisecond: includeMilliseconds(accuracy) ? segments.millisecond : 0,
|
|
28
|
+
minute: segments.minute,
|
|
29
|
+
second: includeSeconds(accuracy) ? segments.second : 0
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Completes an entry that stopped short of the full format, so a bare date - or
|
|
34
|
+
* a date and an hour - can still become a value. Returns nothing when there is
|
|
35
|
+
* nothing to complete: no date to build on, or every segment already entered.
|
|
36
|
+
*
|
|
37
|
+
* The result is pulled back inside the bounds when the fill overshot them. The
|
|
38
|
+
* date is the user's and the filled time is ours, so an `end` fill on today
|
|
39
|
+
* against a `now` maximum is answered with that maximum rather than refused.
|
|
40
|
+
* A clamp landing on another day means the date itself is out of range, which
|
|
41
|
+
* is the user's to fix, so that is handed back unclamped to be rejected with an
|
|
42
|
+
* error the user can read.
|
|
43
|
+
*
|
|
44
|
+
* The fill reaches as far as the entry does. A date and a time down to the
|
|
45
|
+
* minute is already a value, which its field emits and reads back with a zero
|
|
46
|
+
* second, and from then on that second is part of the entry rather than a gap
|
|
47
|
+
* in it: `end` therefore means the last second of a bare date, and the zeroth
|
|
48
|
+
* second of a date entered with its minutes.
|
|
49
|
+
*/
|
|
50
|
+
function completePartialEntry(segments, { accuracy, maxDate, minDate, mode }) {
|
|
51
|
+
const { day, month, year } = segments;
|
|
52
|
+
if (year === void 0 || month === void 0 || day === void 0) return void 0;
|
|
53
|
+
const time = timeSegments(segments, accuracy);
|
|
54
|
+
if (time.hour !== void 0 && time.minute !== void 0 && time.second !== void 0 && time.millisecond !== void 0) return void 0;
|
|
55
|
+
const fallback = FALLBACKS[mode];
|
|
56
|
+
const candidate = buildDateTime({
|
|
57
|
+
day,
|
|
58
|
+
hour: time.hour ?? fallback.hour,
|
|
59
|
+
millisecond: time.millisecond ?? fallback.millisecond,
|
|
60
|
+
minute: time.minute ?? fallback.minute,
|
|
61
|
+
month,
|
|
62
|
+
second: time.second ?? fallback.second,
|
|
63
|
+
year
|
|
64
|
+
}, accuracy);
|
|
65
|
+
const clamped = clampToBounds(candidate, minDate, maxDate);
|
|
66
|
+
return clamped.isSame(candidate, "day") ? clamped : candidate;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { completePartialEntry };
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=partial-time.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"partial-time.js","names":[],"sources":["../../../src/components/date-time-picker/partial-time.ts"],"sourcesContent":["import type { Dayjs } from 'dayjs';\nimport type { TimeAccuracy } from '@/consts/time-accuracy';\nimport { buildDateTime, clampToBounds, type SegmentValues } from '@/components/date-time-picker/segment-utils';\nimport { includeMilliseconds, includeSeconds } from '@/components/date-time-picker/utils';\n\n/**\n * Which end of the entered precision an incomplete entry stands for. `start`\n * fills the segments it never reached with their lowest value and `end` with\n * their highest, so a bare date means the whole day from either side.\n */\nexport type PartialTimeMode = 'start' | 'end';\n\ninterface PartialTimeOptions {\n mode: PartialTimeMode;\n accuracy: TimeAccuracy;\n minDate: Date;\n maxDate: Date | undefined;\n}\n\ntype TimeSegments = Pick<SegmentValues, 'hour' | 'minute' | 'second' | 'millisecond'>;\n\n/** What each side of the day fills a segment the entry never reached with. */\nconst FALLBACKS: Record<PartialTimeMode, Required<TimeSegments>> = {\n end: { hour: 23, millisecond: 999, minute: 59, second: 59 },\n start: { hour: 0, millisecond: 0, minute: 0, second: 0 },\n};\n\n/**\n * Reads the segments an entry does hold, with the ones its accuracy does not\n * expose already answered: those cannot be left out, so they never count as\n * missing.\n */\nfunction timeSegments(segments: SegmentValues, accuracy: TimeAccuracy): TimeSegments {\n return {\n hour: segments.hour,\n millisecond: includeMilliseconds(accuracy) ? segments.millisecond : 0,\n minute: segments.minute,\n second: includeSeconds(accuracy) ? segments.second : 0,\n };\n}\n\n/**\n * Completes an entry that stopped short of the full format, so a bare date - or\n * a date and an hour - can still become a value. Returns nothing when there is\n * nothing to complete: no date to build on, or every segment already entered.\n *\n * The result is pulled back inside the bounds when the fill overshot them. The\n * date is the user's and the filled time is ours, so an `end` fill on today\n * against a `now` maximum is answered with that maximum rather than refused.\n * A clamp landing on another day means the date itself is out of range, which\n * is the user's to fix, so that is handed back unclamped to be rejected with an\n * error the user can read.\n *\n * The fill reaches as far as the entry does. A date and a time down to the\n * minute is already a value, which its field emits and reads back with a zero\n * second, and from then on that second is part of the entry rather than a gap\n * in it: `end` therefore means the last second of a bare date, and the zeroth\n * second of a date entered with its minutes.\n */\nexport function completePartialEntry(\n segments: SegmentValues,\n { accuracy, maxDate, minDate, mode }: PartialTimeOptions,\n): Dayjs | undefined {\n const { day, month, year } = segments;\n if (year === undefined || month === undefined || day === undefined)\n return undefined;\n\n const time = timeSegments(segments, accuracy);\n const complete = time.hour !== undefined && time.minute !== undefined\n && time.second !== undefined && time.millisecond !== undefined;\n if (complete)\n return undefined;\n\n const fallback = FALLBACKS[mode];\n const candidate = buildDateTime({\n day,\n hour: time.hour ?? fallback.hour,\n millisecond: time.millisecond ?? fallback.millisecond,\n minute: time.minute ?? fallback.minute,\n month,\n second: time.second ?? fallback.second,\n year,\n }, accuracy);\n\n const clamped = clampToBounds(candidate, minDate, maxDate);\n return clamped.isSame(candidate, 'day') ? clamped : candidate;\n}\n"],"mappings":";;;;AAsBA,IAAM,YAA6D;CACjE,KAAK;EAAE,MAAM;EAAI,aAAa;EAAK,QAAQ;EAAI,QAAQ;CAAG;CAC1D,OAAO;EAAE,MAAM;EAAG,aAAa;EAAG,QAAQ;EAAG,QAAQ;CAAE;AACzD;;;;;;AAOA,SAAS,aAAa,UAAyB,UAAsC;CACnF,OAAO;EACL,MAAM,SAAS;EACf,aAAa,oBAAoB,QAAQ,IAAI,SAAS,cAAc;EACpE,QAAQ,SAAS;EACjB,QAAQ,eAAe,QAAQ,IAAI,SAAS,SAAS;CACvD;AACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,qBACd,UACA,EAAE,UAAU,SAAS,SAAS,QACX;CACnB,MAAM,EAAE,KAAK,OAAO,SAAS;CAC7B,IAAI,SAAS,KAAA,KAAa,UAAU,KAAA,KAAa,QAAQ,KAAA,GACvD,OAAO,KAAA;CAET,MAAM,OAAO,aAAa,UAAU,QAAQ;CAG5C,IAFiB,KAAK,SAAS,KAAA,KAAa,KAAK,WAAW,KAAA,KACvD,KAAK,WAAW,KAAA,KAAa,KAAK,gBAAgB,KAAA,GAErD,OAAO,KAAA;CAET,MAAM,WAAW,UAAU;CAC3B,MAAM,YAAY,cAAc;EAC9B;EACA,MAAM,KAAK,QAAQ,SAAS;EAC5B,aAAa,KAAK,eAAe,SAAS;EAC1C,QAAQ,KAAK,UAAU,SAAS;EAChC;EACA,QAAQ,KAAK,UAAU,SAAS;EAChC;CACF,GAAG,QAAQ;CAEX,MAAM,UAAU,cAAc,WAAW,SAAS,OAAO;CACzD,OAAO,QAAQ,OAAO,WAAW,KAAK,IAAI,UAAU;AACtD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ComputedRef, Ref } from 'vue';
|
|
2
|
+
import { type Dayjs } from 'dayjs';
|
|
3
|
+
interface DateBoundsOptions {
|
|
4
|
+
minDate: Date | number | undefined;
|
|
5
|
+
maxDate: Date | number | 'now' | undefined;
|
|
6
|
+
/** Whether a numeric bound is stated in whole seconds rather than milliseconds. */
|
|
7
|
+
epochSeconds: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* The field's own format, so a bound named in an error message is written the
|
|
10
|
+
* same way round as the value the user is looking at.
|
|
11
|
+
*/
|
|
12
|
+
dateFormat: Ref<string>;
|
|
13
|
+
/** The clock a `now` maximum follows, kept current by the caller. */
|
|
14
|
+
now: Ref<Dayjs>;
|
|
15
|
+
}
|
|
16
|
+
interface DateBoundsReturn {
|
|
17
|
+
minAllowedDate: ComputedRef<Date>;
|
|
18
|
+
maxAllowedDate: ComputedRef<Date | undefined>;
|
|
19
|
+
internalErrorMessages: Ref<string[]>;
|
|
20
|
+
isDateValid: (date: Dayjs) => boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The range a picked date has to sit in, and the message explaining a date that
|
|
24
|
+
* does not.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useDateBounds({ dateFormat, epochSeconds, maxDate, minDate, now, }: DateBoundsOptions): DateBoundsReturn;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useRuiI8n } from "../../composables/use-rui-i18n.js";
|
|
2
|
+
import { RUI_I18N_KEYS } from "../../i18n/keys.js";
|
|
3
|
+
import { resolveBound } from "./segment-utils.js";
|
|
4
|
+
import { computed, ref } from "vue";
|
|
5
|
+
import { get, set } from "@vueuse/shared";
|
|
6
|
+
import dayjs from "dayjs";
|
|
7
|
+
//#region src/components/date-time-picker/use-date-bounds.ts
|
|
8
|
+
/**
|
|
9
|
+
* The range a picked date has to sit in, and the message explaining a date that
|
|
10
|
+
* does not.
|
|
11
|
+
*/
|
|
12
|
+
function useDateBounds({ dateFormat, epochSeconds, maxDate, minDate, now }) {
|
|
13
|
+
const { t } = useRuiI8n();
|
|
14
|
+
const internalErrorMessages = ref([]);
|
|
15
|
+
const minAllowedDate = computed(() => resolveBound(minDate, epochSeconds) ?? new Date(1970, 0, 1));
|
|
16
|
+
const maxAllowedDate = computed(() => maxDate === "now" ? get(now).toDate() : resolveBound(maxDate, epochSeconds));
|
|
17
|
+
/**
|
|
18
|
+
* Writes a bound the way the field writes its value. `toLocaleDateString()`
|
|
19
|
+
* followed the browser locale and ignored the picker's own format, so a
|
|
20
|
+
* day-first field could report its limit month-first, and it dropped the time
|
|
21
|
+
* entirely, which left a mid-day bound unable to explain itself.
|
|
22
|
+
*/
|
|
23
|
+
function formatBound(bound) {
|
|
24
|
+
return dayjs(bound).format(get(dateFormat));
|
|
25
|
+
}
|
|
26
|
+
function isDateValid(date) {
|
|
27
|
+
const min = get(minAllowedDate);
|
|
28
|
+
const max = get(maxAllowedDate);
|
|
29
|
+
set(internalErrorMessages, []);
|
|
30
|
+
if (min && date.isBefore(min)) {
|
|
31
|
+
const formatted = formatBound(min);
|
|
32
|
+
const errorMessage = t(RUI_I18N_KEYS.dateTimePicker.dateBeforeMin, { date: formatted }, `Date cannot be before ${formatted}`);
|
|
33
|
+
set(internalErrorMessages, [...get(internalErrorMessages), errorMessage]);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (max && date.isAfter(max)) {
|
|
37
|
+
const formatted = formatBound(max);
|
|
38
|
+
const nowError = t(RUI_I18N_KEYS.dateTimePicker.dateInFuture, "The selected date cannot be in the future");
|
|
39
|
+
const maxError = t(RUI_I18N_KEYS.dateTimePicker.dateAfterMax, { date: formatted }, `Date cannot be after ${formatted}`);
|
|
40
|
+
const errorMessage = maxDate === "now" ? nowError : maxError;
|
|
41
|
+
set(internalErrorMessages, [...get(internalErrorMessages), errorMessage]);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
internalErrorMessages,
|
|
48
|
+
isDateValid,
|
|
49
|
+
maxAllowedDate,
|
|
50
|
+
minAllowedDate
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { useDateBounds };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=use-date-bounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-date-bounds.js","names":[],"sources":["../../../src/components/date-time-picker/use-date-bounds.ts"],"sourcesContent":["import type { ComputedRef, Ref } from 'vue';\nimport dayjs, { type Dayjs } from 'dayjs';\nimport { resolveBound } from '@/components/date-time-picker/segment-utils';\nimport { useRuiI8n } from '@/composables/use-rui-i18n';\nimport { RUI_I18N_KEYS } from '@/i18n/keys';\n\ninterface DateBoundsOptions {\n minDate: Date | number | undefined;\n maxDate: Date | number | 'now' | undefined;\n /** Whether a numeric bound is stated in whole seconds rather than milliseconds. */\n epochSeconds: boolean;\n /**\n * The field's own format, so a bound named in an error message is written the\n * same way round as the value the user is looking at.\n */\n dateFormat: Ref<string>;\n /** The clock a `now` maximum follows, kept current by the caller. */\n now: Ref<Dayjs>;\n}\n\ninterface DateBoundsReturn {\n minAllowedDate: ComputedRef<Date>;\n maxAllowedDate: ComputedRef<Date | undefined>;\n internalErrorMessages: Ref<string[]>;\n isDateValid: (date: Dayjs) => boolean;\n}\n\n/**\n * The range a picked date has to sit in, and the message explaining a date that\n * does not.\n */\nexport function useDateBounds({\n dateFormat,\n epochSeconds,\n maxDate,\n minDate,\n now,\n}: DateBoundsOptions): DateBoundsReturn {\n const { t } = useRuiI8n();\n\n const internalErrorMessages = ref<string[]>([]);\n\n const minAllowedDate = computed<Date>(\n () => resolveBound(minDate, epochSeconds) ?? new Date(1970, 0, 1),\n );\n\n const maxAllowedDate = computed<Date | undefined>(\n () => (maxDate === 'now' ? get(now).toDate() : resolveBound(maxDate, epochSeconds)),\n );\n\n /**\n * Writes a bound the way the field writes its value. `toLocaleDateString()`\n * followed the browser locale and ignored the picker's own format, so a\n * day-first field could report its limit month-first, and it dropped the time\n * entirely, which left a mid-day bound unable to explain itself.\n */\n function formatBound(bound: Date): string {\n return dayjs(bound).format(get(dateFormat));\n }\n\n function isDateValid(date: Dayjs): boolean {\n const min = get(minAllowedDate);\n const max = get(maxAllowedDate);\n\n set(internalErrorMessages, []);\n\n if (min && date.isBefore(min)) {\n const formatted = formatBound(min);\n const errorMessage = t(RUI_I18N_KEYS.dateTimePicker.dateBeforeMin, {\n date: formatted,\n }, `Date cannot be before ${formatted}`);\n set(internalErrorMessages, [...get(internalErrorMessages), errorMessage]);\n return false;\n }\n\n if (max && date.isAfter(max)) {\n const formatted = formatBound(max);\n const nowError = t(RUI_I18N_KEYS.dateTimePicker.dateInFuture, 'The selected date cannot be in the future');\n const maxError = t(RUI_I18N_KEYS.dateTimePicker.dateAfterMax, { date: formatted }, `Date cannot be after ${formatted}`);\n const errorMessage = maxDate === 'now' ? nowError : maxError;\n set(internalErrorMessages, [...get(internalErrorMessages), errorMessage]);\n return false;\n }\n\n return true;\n }\n\n return {\n internalErrorMessages,\n isDateValid,\n maxAllowedDate,\n minAllowedDate,\n };\n}\n"],"mappings":";;;;;;;;;;;AA+BA,SAAgB,cAAc,EAC5B,YACA,cACA,SACA,SACA,OACsC;CACtC,MAAM,EAAE,MAAM,UAAU;CAExB,MAAM,wBAAwB,IAAc,CAAC,CAAC;CAE9C,MAAM,iBAAiB,eACf,aAAa,SAAS,YAAY,KAAK,IAAI,KAAK,MAAM,GAAG,CAAC,CAClE;CAEA,MAAM,iBAAiB,eACd,YAAY,QAAQ,IAAI,GAAG,CAAC,CAAC,OAAO,IAAI,aAAa,SAAS,YAAY,CACnF;;;;;;;CAQA,SAAS,YAAY,OAAqB;EACxC,OAAO,MAAM,KAAK,CAAC,CAAC,OAAO,IAAI,UAAU,CAAC;CAC5C;CAEA,SAAS,YAAY,MAAsB;EACzC,MAAM,MAAM,IAAI,cAAc;EAC9B,MAAM,MAAM,IAAI,cAAc;EAE9B,IAAI,uBAAuB,CAAC,CAAC;EAE7B,IAAI,OAAO,KAAK,SAAS,GAAG,GAAG;GAC7B,MAAM,YAAY,YAAY,GAAG;GACjC,MAAM,eAAe,EAAE,cAAc,eAAe,eAAe,EACjE,MAAM,UACR,GAAG,yBAAyB,WAAW;GACvC,IAAI,uBAAuB,CAAC,GAAG,IAAI,qBAAqB,GAAG,YAAY,CAAC;GACxE,OAAO;EACT;EAEA,IAAI,OAAO,KAAK,QAAQ,GAAG,GAAG;GAC5B,MAAM,YAAY,YAAY,GAAG;GACjC,MAAM,WAAW,EAAE,cAAc,eAAe,cAAc,2CAA2C;GACzG,MAAM,WAAW,EAAE,cAAc,eAAe,cAAc,EAAE,MAAM,UAAU,GAAG,wBAAwB,WAAW;GACtH,MAAM,eAAe,YAAY,QAAQ,WAAW;GACpD,IAAI,uBAAuB,CAAC,GAAG,IAAI,qBAAqB,GAAG,YAAY,CAAC;GACxE,OAAO;EACT;EAEA,OAAO;CACT;CAEA,OAAO;EACL;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -2,6 +2,7 @@ import type { ComputedRef, Ref, WritableComputedRef } from 'vue';
|
|
|
2
2
|
import type { SegmentData } from '../../components/date-time-picker/types.js';
|
|
3
3
|
import type { TimeAccuracy } from '../../consts/time-accuracy.js';
|
|
4
4
|
import { type Dayjs } from 'dayjs';
|
|
5
|
+
import { type PartialTimeMode } from '../../components/date-time-picker/partial-time.js';
|
|
5
6
|
import '../../components/date-time-picker/dayjs-setup.js';
|
|
6
7
|
type DateTimeModelType = 'date' | 'epoch-ms' | 'epoch';
|
|
7
8
|
type ModelValueType<T extends DateTimeModelType> = T extends 'date' ? Date | undefined : T extends 'epoch-ms' ? number | undefined : T extends 'epoch' ? number | undefined : Date | number | undefined;
|
|
@@ -17,6 +18,8 @@ interface DateTimeSelectionOptions<T extends DateTimeModelType> {
|
|
|
17
18
|
* same way round as the value the user is looking at.
|
|
18
19
|
*/
|
|
19
20
|
dateFormat: Ref<string>;
|
|
21
|
+
/** See {@link completePartialEntry}; unset, an incomplete entry is not a value. */
|
|
22
|
+
partialTime?: PartialTimeMode;
|
|
20
23
|
}
|
|
21
24
|
interface DateTimeSelectionReturn {
|
|
22
25
|
selectedYear: Ref<number | undefined>;
|
|
@@ -38,6 +41,7 @@ interface DateTimeSelectionReturn {
|
|
|
38
41
|
getDateTime: () => Dayjs;
|
|
39
42
|
setNow: () => void;
|
|
40
43
|
setToday: () => void;
|
|
44
|
+
commitPartialTime: () => void;
|
|
41
45
|
clear: () => void;
|
|
42
46
|
isDateValid: (date: Dayjs) => boolean;
|
|
43
47
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useRuiI8n } from "../../composables/use-rui-i18n.js";
|
|
2
|
-
import { RUI_I18N_KEYS } from "../../i18n/keys.js";
|
|
3
1
|
import "./dayjs-setup.js";
|
|
4
2
|
import { formatWallClock, guessTimezone, includeMilliseconds, includeSeconds } from "./utils.js";
|
|
5
|
-
import { buildDateTime, clampToBounds
|
|
3
|
+
import { buildDateTime, clampToBounds } from "./segment-utils.js";
|
|
4
|
+
import { completePartialEntry } from "./partial-time.js";
|
|
5
|
+
import { useDateBounds } from "./use-date-bounds.js";
|
|
6
6
|
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
|
7
7
|
import { isDefined, watchIgnorable } from "@vueuse/core";
|
|
8
8
|
import { get as get$1, set as set$1 } from "@vueuse/shared";
|
|
@@ -10,8 +10,7 @@ import dayjs from "dayjs";
|
|
|
10
10
|
//#region src/components/date-time-picker/use-date-time-selection.ts
|
|
11
11
|
var MILLISECONDS = 1e3;
|
|
12
12
|
function useDateTimeSelection(options) {
|
|
13
|
-
const { accuracy, allowEmpty, dateFormat, maxDate, minDate, modelValue, type } = options;
|
|
14
|
-
const { t } = useRuiI8n();
|
|
13
|
+
const { accuracy, allowEmpty, dateFormat, maxDate, minDate, modelValue, partialTime, type } = options;
|
|
15
14
|
const selectedYear = ref();
|
|
16
15
|
const selectedMonth = ref();
|
|
17
16
|
const selectedDay = ref();
|
|
@@ -20,11 +19,14 @@ function useDateTimeSelection(options) {
|
|
|
20
19
|
const selectedSecond = ref();
|
|
21
20
|
const selectedMillisecond = ref();
|
|
22
21
|
const selectedTimezone = ref(guessTimezone());
|
|
23
|
-
const internalErrorMessages = ref([]);
|
|
24
22
|
const now = ref(dayjs.tz(void 0, guessTimezone()));
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const { internalErrorMessages, isDateValid, maxAllowedDate, minAllowedDate } = useDateBounds({
|
|
24
|
+
dateFormat,
|
|
25
|
+
epochSeconds: type === "epoch",
|
|
26
|
+
maxDate,
|
|
27
|
+
minDate,
|
|
28
|
+
now
|
|
29
|
+
});
|
|
28
30
|
const segmentData = {
|
|
29
31
|
DD: selectedDay,
|
|
30
32
|
HH: selectedHour,
|
|
@@ -76,35 +78,6 @@ function useDateTimeSelection(options) {
|
|
|
76
78
|
year: get$1(selectedYear)
|
|
77
79
|
}, accuracy);
|
|
78
80
|
}
|
|
79
|
-
/**
|
|
80
|
-
* Writes a bound the way the field writes its value. `toLocaleDateString()`
|
|
81
|
-
* followed the browser locale and ignored the picker's own format, so a
|
|
82
|
-
* day-first field could report its limit month-first, and it dropped the time
|
|
83
|
-
* entirely, which left a mid-day bound unable to explain itself.
|
|
84
|
-
*/
|
|
85
|
-
function formatBound(bound) {
|
|
86
|
-
return dayjs(bound).format(get$1(dateFormat));
|
|
87
|
-
}
|
|
88
|
-
function isDateValid(date) {
|
|
89
|
-
const min = get$1(minAllowedDate);
|
|
90
|
-
const max = get$1(maxAllowedDate);
|
|
91
|
-
set$1(internalErrorMessages, []);
|
|
92
|
-
if (min && date.isBefore(min)) {
|
|
93
|
-
const formatted = formatBound(min);
|
|
94
|
-
const errorMessage = t(RUI_I18N_KEYS.dateTimePicker.dateBeforeMin, { date: formatted }, `Date cannot be before ${formatted}`);
|
|
95
|
-
set$1(internalErrorMessages, [...get$1(internalErrorMessages), errorMessage]);
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
if (max && date.isAfter(max)) {
|
|
99
|
-
const formatted = formatBound(max);
|
|
100
|
-
const nowError = t(RUI_I18N_KEYS.dateTimePicker.dateInFuture, "The selected date cannot be in the future");
|
|
101
|
-
const maxError = t(RUI_I18N_KEYS.dateTimePicker.dateAfterMax, { date: formatted }, `Date cannot be after ${formatted}`);
|
|
102
|
-
const errorMessage = maxDate === "now" ? nowError : maxError;
|
|
103
|
-
set$1(internalErrorMessages, [...get$1(internalErrorMessages), errorMessage]);
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
81
|
function emitUpdate(updatedModel) {
|
|
109
82
|
set$1(modelValue, {
|
|
110
83
|
"date": () => updatedModel.toDate(),
|
|
@@ -197,6 +170,33 @@ function useDateTimeSelection(options) {
|
|
|
197
170
|
set$1(now, dayjs());
|
|
198
171
|
updateModelValue();
|
|
199
172
|
});
|
|
173
|
+
/**
|
|
174
|
+
* Fills in the segments an incomplete entry never reached and commits it, so
|
|
175
|
+
* a bare date can become a value. Called when the user is done with the field
|
|
176
|
+
* rather than on every keystroke, since a segment still being typed is not
|
|
177
|
+
* yet one they left out. The fill is written back into the field, so the
|
|
178
|
+
* value it decided on is the one on screen.
|
|
179
|
+
*/
|
|
180
|
+
function commitPartialTime() {
|
|
181
|
+
if (partialTime === void 0) return;
|
|
182
|
+
const target = completePartialEntry({
|
|
183
|
+
day: get$1(selectedDay),
|
|
184
|
+
hour: get$1(selectedHour),
|
|
185
|
+
millisecond: get$1(selectedMillisecond),
|
|
186
|
+
minute: get$1(selectedMinute),
|
|
187
|
+
month: get$1(selectedMonth),
|
|
188
|
+
second: get$1(selectedSecond),
|
|
189
|
+
year: get$1(selectedYear)
|
|
190
|
+
}, {
|
|
191
|
+
accuracy,
|
|
192
|
+
maxDate: get$1(maxAllowedDate),
|
|
193
|
+
minDate: get$1(minAllowedDate),
|
|
194
|
+
mode: partialTime
|
|
195
|
+
});
|
|
196
|
+
if (!target) return;
|
|
197
|
+
ignoreUpdates(() => applySegments(target));
|
|
198
|
+
updateModelValue();
|
|
199
|
+
}
|
|
200
200
|
function updateInternalModel(value) {
|
|
201
201
|
ignoreUpdates(() => {
|
|
202
202
|
const updatedValue = type === "epoch" && typeof value === "number" ? value * MILLISECONDS : value;
|
|
@@ -222,6 +222,7 @@ function useDateTimeSelection(options) {
|
|
|
222
222
|
});
|
|
223
223
|
return {
|
|
224
224
|
clear,
|
|
225
|
+
commitPartialTime,
|
|
225
226
|
getDateTime,
|
|
226
227
|
internalErrorMessages,
|
|
227
228
|
isDateValid,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-date-time-selection.js","names":[],"sources":["../../../src/components/date-time-picker/use-date-time-selection.ts"],"sourcesContent":["import type { ComputedRef, Ref, WritableComputedRef } from 'vue';\nimport type { SegmentData } from '@/components/date-time-picker/types';\nimport type { TimeAccuracy } from '@/consts/time-accuracy';\nimport dayjs, { type Dayjs } from 'dayjs';\nimport { buildDateTime, clampToBounds, resolveBound } from '@/components/date-time-picker/segment-utils';\nimport { formatWallClock, guessTimezone, includeMilliseconds, includeSeconds } from '@/components/date-time-picker/utils';\nimport { useRuiI8n } from '@/composables/use-rui-i18n';\nimport { RUI_I18N_KEYS } from '@/i18n/keys';\nimport '@/components/date-time-picker/dayjs-setup';\n\ntype DateTimeModelType = 'date' | 'epoch-ms' | 'epoch';\n\ntype ModelValueType<T extends DateTimeModelType> =\n T extends 'date' ? Date | undefined :\n T extends 'epoch-ms' ? number | undefined :\n T extends 'epoch' ? number | undefined :\n Date | number | undefined;\n\ninterface DateTimeSelectionOptions<T extends DateTimeModelType> {\n modelValue: Ref<ModelValueType<T>>;\n type: T;\n accuracy: TimeAccuracy;\n minDate: Date | number | undefined;\n maxDate: Date | number | 'now' | undefined;\n allowEmpty: boolean;\n /**\n * The field's own format, so a bound named in an error message is written the\n * same way round as the value the user is looking at.\n */\n dateFormat: Ref<string>;\n}\n\ninterface DateTimeSelectionReturn {\n selectedYear: Ref<number | undefined>;\n selectedMonth: Ref<number | undefined>;\n selectedDay: Ref<number | undefined>;\n selectedHour: Ref<number | undefined>;\n selectedMinute: Ref<number | undefined>;\n selectedSecond: Ref<number | undefined>;\n selectedMillisecond: Ref<number | undefined>;\n selectedTimezone: Ref<string | undefined>;\n selectedDate: WritableComputedRef<Date | undefined>;\n selectedTime: WritableComputedRef<Date | undefined>;\n valueSet: ComputedRef<boolean>;\n internalErrorMessages: Ref<string[]>;\n now: Ref<Dayjs>;\n segmentData: SegmentData;\n minAllowedDate: ComputedRef<Date>;\n maxAllowedDate: ComputedRef<Date | undefined>;\n getDateTime: () => Dayjs;\n setNow: () => void;\n setToday: () => void;\n clear: () => void;\n isDateValid: (date: Dayjs) => boolean;\n}\n\nconst MILLISECONDS = 1000;\n\nexport function useDateTimeSelection<T extends DateTimeModelType>(\n options: DateTimeSelectionOptions<T>,\n): DateTimeSelectionReturn {\n const {\n accuracy,\n allowEmpty,\n dateFormat,\n maxDate,\n minDate,\n modelValue,\n type,\n } = options;\n\n const { t } = useRuiI8n();\n\n const selectedYear = ref<number | undefined>();\n const selectedMonth = ref<number | undefined>();\n const selectedDay = ref<number | undefined>();\n\n const selectedHour = ref<number | undefined>();\n const selectedMinute = ref<number | undefined>();\n const selectedSecond = ref<number | undefined>();\n const selectedMillisecond = ref<number | undefined>();\n const selectedTimezone = ref<string | undefined>(guessTimezone());\n\n const internalErrorMessages = ref<string[]>([]);\n const now = ref<Dayjs>(dayjs.tz(undefined, guessTimezone()));\n\n const epochSeconds = type === 'epoch';\n\n const minAllowedDate = computed<Date>(\n () => resolveBound(minDate, epochSeconds) ?? new Date(1970, 0, 1),\n );\n\n const maxAllowedDate = computed<Date | undefined>(\n () => (maxDate === 'now' ? get(now).toDate() : resolveBound(maxDate, epochSeconds)),\n );\n\n const segmentData: SegmentData = {\n DD: selectedDay,\n HH: selectedHour,\n MM: selectedMonth,\n SSS: selectedMillisecond,\n YYYY: selectedYear,\n mm: selectedMinute,\n ss: selectedSecond,\n };\n\n const selectedDate = computed<Date | undefined>({\n get() {\n if (!(isDefined(selectedYear) && isDefined(selectedMonth) && isDefined(selectedDay))) {\n return undefined;\n }\n const date = new Date();\n date.setFullYear(get(selectedYear));\n // Set day to 1 first to prevent month overflow when today's day > days in target month\n // e.g., if today is Dec 30 and we set month to Feb, day 30 would overflow to March\n date.setDate(1);\n date.setMonth(get(selectedMonth) - 1);\n date.setDate(get(selectedDay));\n return date;\n },\n set(value?: Date) {\n set(selectedYear, value?.getFullYear());\n set(selectedMonth, value ? value.getMonth() + 1 : undefined);\n set(selectedDay, value?.getDate());\n },\n });\n\n const selectedTime = computed<Date | undefined>({\n get() {\n if (!(isDefined(selectedHour) && isDefined(selectedMinute))) {\n return undefined;\n }\n const date = new Date();\n date.setHours(\n get(selectedHour),\n get(selectedMinute),\n get(selectedSecond) ?? 0,\n get(selectedMillisecond) ?? 0,\n );\n return date;\n },\n set(value?: Date) {\n set(selectedHour, value?.getHours());\n set(selectedMinute, value?.getMinutes());\n set(selectedSecond, value?.getSeconds());\n set(selectedMillisecond, value?.getMilliseconds());\n },\n });\n\n const valueSet = computed<boolean>(() => isDefined(selectedDate) && isDefined(selectedTime));\n\n function getDateTime(): Dayjs {\n return buildDateTime({\n day: get(selectedDay),\n hour: get(selectedHour),\n millisecond: get(selectedMillisecond),\n minute: get(selectedMinute),\n month: get(selectedMonth),\n second: get(selectedSecond),\n year: get(selectedYear),\n }, accuracy);\n }\n\n /**\n * Writes a bound the way the field writes its value. `toLocaleDateString()`\n * followed the browser locale and ignored the picker's own format, so a\n * day-first field could report its limit month-first, and it dropped the time\n * entirely, which left a mid-day bound unable to explain itself.\n */\n function formatBound(bound: Date): string {\n return dayjs(bound).format(get(dateFormat));\n }\n\n function isDateValid(date: Dayjs): boolean {\n const min = get(minAllowedDate);\n const max = get(maxAllowedDate);\n\n set(internalErrorMessages, []);\n\n if (min && date.isBefore(min)) {\n const formatted = formatBound(min);\n const errorMessage = t(RUI_I18N_KEYS.dateTimePicker.dateBeforeMin, {\n date: formatted,\n }, `Date cannot be before ${formatted}`);\n set(internalErrorMessages, [...get(internalErrorMessages), errorMessage]);\n return false;\n }\n\n if (max && date.isAfter(max)) {\n const formatted = formatBound(max);\n const nowError = t(RUI_I18N_KEYS.dateTimePicker.dateInFuture, 'The selected date cannot be in the future');\n const maxError = t(RUI_I18N_KEYS.dateTimePicker.dateAfterMax, { date: formatted }, `Date cannot be after ${formatted}`);\n const errorMessage = maxDate === 'now' ? nowError : maxError;\n set(internalErrorMessages, [...get(internalErrorMessages), errorMessage]);\n return false;\n }\n\n return true;\n }\n\n function emitUpdate(updatedModel: Dayjs): void {\n const typeMap = {\n 'date': () => updatedModel.toDate(),\n // an epoch is whole seconds; `millisecond` accuracy would otherwise emit a fraction\n 'epoch': () => Math.floor(updatedModel.valueOf() / MILLISECONDS),\n 'epoch-ms': () => updatedModel.valueOf(),\n } as const;\n\n set(modelValue, typeMap[type]() as ModelValueType<T>);\n }\n\n function updateModelValue(): void {\n if (!isDefined(selectedDate) || !isDefined(selectedTime)) {\n return;\n }\n\n // The segments are a wall-clock reading, so they are formatted and parsed\n // in the selected timezone. Mutating a `dayjs.tz()` built from the old\n // value instead would keep that value's UTC offset, and moving the date\n // across a DST boundary then shifted the time by an hour.\n const updatedModel = dayjs.tz(\n formatWallClock(get(selectedDate), get(selectedTime), accuracy),\n get(selectedTimezone),\n );\n\n if (!isDateValid(updatedModel)) {\n return;\n }\n\n emitUpdate(updatedModel);\n }\n\n function clear(): void {\n set(internalErrorMessages, []);\n set(selectedYear, undefined);\n set(selectedMonth, undefined);\n set(selectedDay, undefined);\n set(selectedHour, undefined);\n set(selectedMinute, undefined);\n set(selectedSecond, undefined);\n set(selectedMillisecond, undefined);\n set(modelValue, undefined as ModelValueType<T>);\n }\n\n function clampToAllowed(date: Dayjs): Dayjs {\n return clampToBounds(date, get(minAllowedDate), get(maxAllowedDate));\n }\n\n function applySegments(date: Dayjs): void {\n set(selectedYear, date.year());\n set(selectedMonth, date.month() + 1);\n set(selectedDay, date.date());\n set(selectedHour, date.hour());\n set(selectedMinute, date.minute());\n set(selectedSecond, includeSeconds(accuracy) ? date.second() : 0);\n set(selectedMillisecond, includeMilliseconds(accuracy) ? date.millisecond() : 0);\n }\n\n function setNow(): void {\n set(internalErrorMessages, []);\n\n const date = dayjs();\n set(now, date);\n applySegments(clampToAllowed(date));\n\n nextTick(() => {\n updateModelValue();\n });\n }\n\n /**\n * Moves the date part to today and leaves the time part alone, so a picked\n * time survives. Falls back to midnight when no time has been entered yet.\n */\n function setToday(): void {\n set(internalErrorMessages, []);\n\n const date = dayjs();\n set(now, date);\n\n const target = buildDateTime({\n day: date.date(),\n hour: get(selectedHour) ?? 0,\n millisecond: get(selectedMillisecond) ?? 0,\n minute: get(selectedMinute) ?? 0,\n month: date.month() + 1,\n second: get(selectedSecond) ?? 0,\n year: date.year(),\n }, accuracy, date);\n\n applySegments(clampToAllowed(target));\n\n nextTick(() => {\n updateModelValue();\n });\n }\n\n function updateSegments(date: Dayjs): void {\n const year = date.year();\n const month = date.month() + 1;\n const day = date.date();\n const hour = date.hour();\n const minute = date.minute();\n const second = includeSeconds(accuracy) ? date.second() : undefined;\n const millisecond = includeMilliseconds(accuracy) ? date.millisecond() : undefined;\n\n if (get(selectedYear) !== year)\n set(selectedYear, year);\n if (get(selectedMonth) !== month)\n set(selectedMonth, month);\n if (get(selectedDay) !== day)\n set(selectedDay, day);\n if (get(selectedHour) !== hour)\n set(selectedHour, hour);\n if (get(selectedMinute) !== minute)\n set(selectedMinute, minute);\n if (get(selectedSecond) !== second)\n set(selectedSecond, second);\n if (get(selectedMillisecond) !== millisecond)\n set(selectedMillisecond, millisecond);\n }\n\n const { ignoreUpdates } = watchIgnorable([selectedDate, selectedTime], ([newSelectedDate, newSelectedTime], [prevSelectedDate, prevSelectedTime]) => {\n const currentTimezone = get(selectedTimezone);\n const newDate = dayjs.tz(newSelectedDate, currentTimezone);\n const oldDate = dayjs.tz(prevSelectedDate, currentTimezone);\n const newTime = dayjs.tz(newSelectedTime, currentTimezone);\n const oldTime = dayjs.tz(prevSelectedTime, currentTimezone);\n\n if (newDate.isSame(oldDate) && newTime.isSame(oldTime)) {\n return;\n }\n\n set(now, dayjs());\n updateModelValue();\n });\n\n function updateInternalModel(value: ModelValueType<T>): void {\n ignoreUpdates(() => {\n const updatedValue = type === 'epoch' && typeof value === 'number'\n ? value * MILLISECONDS\n : value;\n const date = dayjs.tz(updatedValue, get(selectedTimezone));\n isDateValid(date);\n updateSegments(date);\n });\n }\n\n watch(selectedTimezone, (newTimezone: string | undefined) => {\n if (newTimezone && isDefined(selectedDate) && isDefined(selectedTime)) {\n set(now, dayjs());\n updateModelValue();\n }\n });\n\n watch(modelValue, (value) => {\n set(now, dayjs());\n if (value === undefined) {\n clear();\n }\n else {\n updateInternalModel(value);\n }\n });\n\n onMounted(() => {\n if (isDefined(modelValue)) {\n updateInternalModel(get(modelValue));\n }\n else if (!allowEmpty) {\n setNow();\n }\n });\n\n return {\n clear,\n getDateTime,\n internalErrorMessages,\n isDateValid,\n maxAllowedDate,\n minAllowedDate,\n now,\n segmentData,\n selectedDate,\n selectedDay,\n selectedHour,\n selectedMillisecond,\n selectedMinute,\n selectedMonth,\n selectedSecond,\n selectedTime,\n selectedTimezone,\n selectedYear,\n setNow,\n setToday,\n valueSet,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-date-time-selection.js","names":[],"sources":["../../../src/components/date-time-picker/use-date-time-selection.ts"],"sourcesContent":["import type { ComputedRef, Ref, WritableComputedRef } from 'vue';\nimport type { SegmentData } from '@/components/date-time-picker/types';\nimport type { TimeAccuracy } from '@/consts/time-accuracy';\nimport dayjs, { type Dayjs } from 'dayjs';\nimport { completePartialEntry, type PartialTimeMode } from '@/components/date-time-picker/partial-time';\nimport { buildDateTime, clampToBounds } from '@/components/date-time-picker/segment-utils';\nimport { useDateBounds } from '@/components/date-time-picker/use-date-bounds';\nimport { formatWallClock, guessTimezone, includeMilliseconds, includeSeconds } from '@/components/date-time-picker/utils';\nimport '@/components/date-time-picker/dayjs-setup';\n\ntype DateTimeModelType = 'date' | 'epoch-ms' | 'epoch';\n\ntype ModelValueType<T extends DateTimeModelType> =\n T extends 'date' ? Date | undefined :\n T extends 'epoch-ms' ? number | undefined :\n T extends 'epoch' ? number | undefined :\n Date | number | undefined;\n\ninterface DateTimeSelectionOptions<T extends DateTimeModelType> {\n modelValue: Ref<ModelValueType<T>>;\n type: T;\n accuracy: TimeAccuracy;\n minDate: Date | number | undefined;\n maxDate: Date | number | 'now' | undefined;\n allowEmpty: boolean;\n /**\n * The field's own format, so a bound named in an error message is written the\n * same way round as the value the user is looking at.\n */\n dateFormat: Ref<string>;\n /** See {@link completePartialEntry}; unset, an incomplete entry is not a value. */\n partialTime?: PartialTimeMode;\n}\n\ninterface DateTimeSelectionReturn {\n selectedYear: Ref<number | undefined>;\n selectedMonth: Ref<number | undefined>;\n selectedDay: Ref<number | undefined>;\n selectedHour: Ref<number | undefined>;\n selectedMinute: Ref<number | undefined>;\n selectedSecond: Ref<number | undefined>;\n selectedMillisecond: Ref<number | undefined>;\n selectedTimezone: Ref<string | undefined>;\n selectedDate: WritableComputedRef<Date | undefined>;\n selectedTime: WritableComputedRef<Date | undefined>;\n valueSet: ComputedRef<boolean>;\n internalErrorMessages: Ref<string[]>;\n now: Ref<Dayjs>;\n segmentData: SegmentData;\n minAllowedDate: ComputedRef<Date>;\n maxAllowedDate: ComputedRef<Date | undefined>;\n getDateTime: () => Dayjs;\n setNow: () => void;\n setToday: () => void;\n commitPartialTime: () => void;\n clear: () => void;\n isDateValid: (date: Dayjs) => boolean;\n}\n\nconst MILLISECONDS = 1000;\n\nexport function useDateTimeSelection<T extends DateTimeModelType>(\n options: DateTimeSelectionOptions<T>,\n): DateTimeSelectionReturn {\n const {\n accuracy,\n allowEmpty,\n dateFormat,\n maxDate,\n minDate,\n modelValue,\n partialTime,\n type,\n } = options;\n\n const selectedYear = ref<number | undefined>();\n const selectedMonth = ref<number | undefined>();\n const selectedDay = ref<number | undefined>();\n\n const selectedHour = ref<number | undefined>();\n const selectedMinute = ref<number | undefined>();\n const selectedSecond = ref<number | undefined>();\n const selectedMillisecond = ref<number | undefined>();\n const selectedTimezone = ref<string | undefined>(guessTimezone());\n\n const now = ref<Dayjs>(dayjs.tz(undefined, guessTimezone()));\n\n const { internalErrorMessages, isDateValid, maxAllowedDate, minAllowedDate } = useDateBounds({\n dateFormat,\n epochSeconds: type === 'epoch',\n maxDate,\n minDate,\n now,\n });\n\n const segmentData: SegmentData = {\n DD: selectedDay,\n HH: selectedHour,\n MM: selectedMonth,\n SSS: selectedMillisecond,\n YYYY: selectedYear,\n mm: selectedMinute,\n ss: selectedSecond,\n };\n\n const selectedDate = computed<Date | undefined>({\n get() {\n if (!(isDefined(selectedYear) && isDefined(selectedMonth) && isDefined(selectedDay))) {\n return undefined;\n }\n const date = new Date();\n date.setFullYear(get(selectedYear));\n // Set day to 1 first to prevent month overflow when today's day > days in target month\n // e.g., if today is Dec 30 and we set month to Feb, day 30 would overflow to March\n date.setDate(1);\n date.setMonth(get(selectedMonth) - 1);\n date.setDate(get(selectedDay));\n return date;\n },\n set(value?: Date) {\n set(selectedYear, value?.getFullYear());\n set(selectedMonth, value ? value.getMonth() + 1 : undefined);\n set(selectedDay, value?.getDate());\n },\n });\n\n const selectedTime = computed<Date | undefined>({\n get() {\n if (!(isDefined(selectedHour) && isDefined(selectedMinute))) {\n return undefined;\n }\n const date = new Date();\n date.setHours(\n get(selectedHour),\n get(selectedMinute),\n get(selectedSecond) ?? 0,\n get(selectedMillisecond) ?? 0,\n );\n return date;\n },\n set(value?: Date) {\n set(selectedHour, value?.getHours());\n set(selectedMinute, value?.getMinutes());\n set(selectedSecond, value?.getSeconds());\n set(selectedMillisecond, value?.getMilliseconds());\n },\n });\n\n const valueSet = computed<boolean>(() => isDefined(selectedDate) && isDefined(selectedTime));\n\n function getDateTime(): Dayjs {\n return buildDateTime({\n day: get(selectedDay),\n hour: get(selectedHour),\n millisecond: get(selectedMillisecond),\n minute: get(selectedMinute),\n month: get(selectedMonth),\n second: get(selectedSecond),\n year: get(selectedYear),\n }, accuracy);\n }\n\n function emitUpdate(updatedModel: Dayjs): void {\n const typeMap = {\n 'date': () => updatedModel.toDate(),\n // an epoch is whole seconds; `millisecond` accuracy would otherwise emit a fraction\n 'epoch': () => Math.floor(updatedModel.valueOf() / MILLISECONDS),\n 'epoch-ms': () => updatedModel.valueOf(),\n } as const;\n\n set(modelValue, typeMap[type]() as ModelValueType<T>);\n }\n\n function updateModelValue(): void {\n if (!isDefined(selectedDate) || !isDefined(selectedTime)) {\n return;\n }\n\n // The segments are a wall-clock reading, so they are formatted and parsed\n // in the selected timezone. Mutating a `dayjs.tz()` built from the old\n // value instead would keep that value's UTC offset, and moving the date\n // across a DST boundary then shifted the time by an hour.\n const updatedModel = dayjs.tz(\n formatWallClock(get(selectedDate), get(selectedTime), accuracy),\n get(selectedTimezone),\n );\n\n if (!isDateValid(updatedModel)) {\n return;\n }\n\n emitUpdate(updatedModel);\n }\n\n function clear(): void {\n set(internalErrorMessages, []);\n set(selectedYear, undefined);\n set(selectedMonth, undefined);\n set(selectedDay, undefined);\n set(selectedHour, undefined);\n set(selectedMinute, undefined);\n set(selectedSecond, undefined);\n set(selectedMillisecond, undefined);\n set(modelValue, undefined as ModelValueType<T>);\n }\n\n function clampToAllowed(date: Dayjs): Dayjs {\n return clampToBounds(date, get(minAllowedDate), get(maxAllowedDate));\n }\n\n function applySegments(date: Dayjs): void {\n set(selectedYear, date.year());\n set(selectedMonth, date.month() + 1);\n set(selectedDay, date.date());\n set(selectedHour, date.hour());\n set(selectedMinute, date.minute());\n set(selectedSecond, includeSeconds(accuracy) ? date.second() : 0);\n set(selectedMillisecond, includeMilliseconds(accuracy) ? date.millisecond() : 0);\n }\n\n function setNow(): void {\n set(internalErrorMessages, []);\n\n const date = dayjs();\n set(now, date);\n applySegments(clampToAllowed(date));\n\n nextTick(() => {\n updateModelValue();\n });\n }\n\n /**\n * Moves the date part to today and leaves the time part alone, so a picked\n * time survives. Falls back to midnight when no time has been entered yet.\n */\n function setToday(): void {\n set(internalErrorMessages, []);\n\n const date = dayjs();\n set(now, date);\n\n const target = buildDateTime({\n day: date.date(),\n hour: get(selectedHour) ?? 0,\n millisecond: get(selectedMillisecond) ?? 0,\n minute: get(selectedMinute) ?? 0,\n month: date.month() + 1,\n second: get(selectedSecond) ?? 0,\n year: date.year(),\n }, accuracy, date);\n\n applySegments(clampToAllowed(target));\n\n nextTick(() => {\n updateModelValue();\n });\n }\n\n function updateSegments(date: Dayjs): void {\n const year = date.year();\n const month = date.month() + 1;\n const day = date.date();\n const hour = date.hour();\n const minute = date.minute();\n const second = includeSeconds(accuracy) ? date.second() : undefined;\n const millisecond = includeMilliseconds(accuracy) ? date.millisecond() : undefined;\n\n if (get(selectedYear) !== year)\n set(selectedYear, year);\n if (get(selectedMonth) !== month)\n set(selectedMonth, month);\n if (get(selectedDay) !== day)\n set(selectedDay, day);\n if (get(selectedHour) !== hour)\n set(selectedHour, hour);\n if (get(selectedMinute) !== minute)\n set(selectedMinute, minute);\n if (get(selectedSecond) !== second)\n set(selectedSecond, second);\n if (get(selectedMillisecond) !== millisecond)\n set(selectedMillisecond, millisecond);\n }\n\n const { ignoreUpdates } = watchIgnorable([selectedDate, selectedTime], ([newSelectedDate, newSelectedTime], [prevSelectedDate, prevSelectedTime]) => {\n const currentTimezone = get(selectedTimezone);\n const newDate = dayjs.tz(newSelectedDate, currentTimezone);\n const oldDate = dayjs.tz(prevSelectedDate, currentTimezone);\n const newTime = dayjs.tz(newSelectedTime, currentTimezone);\n const oldTime = dayjs.tz(prevSelectedTime, currentTimezone);\n\n if (newDate.isSame(oldDate) && newTime.isSame(oldTime)) {\n return;\n }\n\n set(now, dayjs());\n updateModelValue();\n });\n\n /**\n * Fills in the segments an incomplete entry never reached and commits it, so\n * a bare date can become a value. Called when the user is done with the field\n * rather than on every keystroke, since a segment still being typed is not\n * yet one they left out. The fill is written back into the field, so the\n * value it decided on is the one on screen.\n */\n function commitPartialTime(): void {\n if (partialTime === undefined)\n return;\n\n const target = completePartialEntry({\n day: get(selectedDay),\n hour: get(selectedHour),\n millisecond: get(selectedMillisecond),\n minute: get(selectedMinute),\n month: get(selectedMonth),\n second: get(selectedSecond),\n year: get(selectedYear),\n }, {\n accuracy,\n maxDate: get(maxAllowedDate),\n minDate: get(minAllowedDate),\n mode: partialTime,\n });\n\n if (!target)\n return;\n\n // The model is written here rather than left to the watcher: this runs on blur and on enter,\n // and a consumer that closes the field on that key would drop a value the watcher only emits\n // on the next tick.\n ignoreUpdates(() => applySegments(target));\n updateModelValue();\n }\n\n function updateInternalModel(value: ModelValueType<T>): void {\n ignoreUpdates(() => {\n const updatedValue = type === 'epoch' && typeof value === 'number'\n ? value * MILLISECONDS\n : value;\n const date = dayjs.tz(updatedValue, get(selectedTimezone));\n isDateValid(date);\n updateSegments(date);\n });\n }\n\n watch(selectedTimezone, (newTimezone: string | undefined) => {\n if (newTimezone && isDefined(selectedDate) && isDefined(selectedTime)) {\n set(now, dayjs());\n updateModelValue();\n }\n });\n\n watch(modelValue, (value) => {\n set(now, dayjs());\n if (value === undefined) {\n clear();\n }\n else {\n updateInternalModel(value);\n }\n });\n\n onMounted(() => {\n if (isDefined(modelValue)) {\n updateInternalModel(get(modelValue));\n }\n else if (!allowEmpty) {\n setNow();\n }\n });\n\n return {\n clear,\n commitPartialTime,\n getDateTime,\n internalErrorMessages,\n isDateValid,\n maxAllowedDate,\n minAllowedDate,\n now,\n segmentData,\n selectedDate,\n selectedDay,\n selectedHour,\n selectedMillisecond,\n selectedMinute,\n selectedMonth,\n selectedSecond,\n selectedTime,\n selectedTimezone,\n selectedYear,\n setNow,\n setToday,\n valueSet,\n };\n}\n"],"mappings":"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"}
|
package/dist/web-types.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "http://json.schemastore.org/web-types",
|
|
3
3
|
"framework": "vue",
|
|
4
4
|
"name": "@rotki/ui-library",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.24.0",
|
|
6
6
|
"js-types-syntax": "typescript",
|
|
7
7
|
"description-markup": "markdown",
|
|
8
8
|
"contributions": {
|
|
@@ -1277,6 +1277,15 @@
|
|
|
1277
1277
|
"type": "boolean | undefined"
|
|
1278
1278
|
}
|
|
1279
1279
|
},
|
|
1280
|
+
{
|
|
1281
|
+
"name": "partial-time",
|
|
1282
|
+
"description": "Accepts an entry that stops short of the full format, filling the segments\nit never reached from one end of the day: `start` gives a bare date\n00:00:00.000 and `end` gives it 23:59:59.999. Meant for a range, where the\ntwo bounds want opposite ends. Left unset, an entry missing its time is not\na value and nothing is emitted.\n\nThe fill runs when the user is done with the field - leaving it, pressing\nenter, or closing the calendar - and is written back into the segments, so\nwhat it decided on is on screen.\n\nSpelled out rather than written as `PartialTimeMode`: a consumer that hands\nthis whole interface to its own `defineProps` needs every member resolvable\nby the SFC compiler, which does not follow the type into the package.",
|
|
1283
|
+
"required": false,
|
|
1284
|
+
"value": {
|
|
1285
|
+
"kind": "expression",
|
|
1286
|
+
"type": "'start' | 'end' | undefined"
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1280
1289
|
{
|
|
1281
1290
|
"name": "model-value",
|
|
1282
1291
|
"description": "",
|