stimeo-ui 0.2.0 → 0.3.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/CHANGELOG.md +163 -0
- package/dist/cable/index.d.ts +16 -16
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +3 -2
- package/dist/controllers/accordion_controller.js +10 -0
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +64 -0
- package/dist/controllers/alert_dialog_controller.js +318 -0
- package/dist/controllers/alert_dialog_controller.js.map +1 -0
- package/dist/controllers/breadcrumb_controller.d.ts +97 -10
- package/dist/controllers/breadcrumb_controller.js +225 -13
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +11 -1
- package/dist/controllers/calendar_controller.js +89 -22
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +115 -0
- package/dist/controllers/carousel_controller.js +313 -0
- package/dist/controllers/carousel_controller.js.map +1 -0
- package/dist/controllers/clipboard_controller.d.ts +72 -0
- package/dist/controllers/clipboard_controller.js +144 -0
- package/dist/controllers/clipboard_controller.js.map +1 -0
- package/dist/controllers/collapsible_controller.d.ts +70 -0
- package/dist/controllers/collapsible_controller.js +327 -0
- package/dist/controllers/collapsible_controller.js.map +1 -0
- package/dist/controllers/color_picker_controller.d.ts +87 -0
- package/dist/controllers/color_picker_controller.js +252 -0
- package/dist/controllers/color_picker_controller.js.map +1 -0
- package/dist/controllers/combobox_controller.d.ts +14 -1
- package/dist/controllers/combobox_controller.js +162 -23
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +2 -0
- package/dist/controllers/command_palette_controller.js +194 -17
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +7 -6
- package/dist/controllers/context_menu_controller.js +32 -10
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js +8 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +72 -0
- package/dist/controllers/currency_input_controller.js +147 -0
- package/dist/controllers/currency_input_controller.js.map +1 -0
- package/dist/controllers/data_grid_controller.d.ts +89 -0
- package/dist/controllers/data_grid_controller.js +246 -0
- package/dist/controllers/data_grid_controller.js.map +1 -0
- package/dist/controllers/date_range_picker_controller.d.ts +88 -0
- package/dist/controllers/date_range_picker_controller.js +441 -0
- package/dist/controllers/date_range_picker_controller.js.map +1 -0
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +57 -0
- package/dist/controllers/dismissible_controller.js +117 -0
- package/dist/controllers/dismissible_controller.js.map +1 -0
- package/dist/controllers/drawer_controller.d.ts +96 -0
- package/dist/controllers/drawer_controller.js +630 -0
- package/dist/controllers/drawer_controller.js.map +1 -0
- package/dist/controllers/dropdown_controller.d.ts +1 -2
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +73 -0
- package/dist/controllers/editable_controller.js +169 -0
- package/dist/controllers/editable_controller.js.map +1 -0
- package/dist/controllers/file_dropzone_controller.d.ts +92 -0
- package/dist/controllers/file_dropzone_controller.js +165 -0
- package/dist/controllers/file_dropzone_controller.js.map +1 -0
- package/dist/controllers/filter_controller.d.ts +60 -0
- package/dist/controllers/filter_controller.js +86 -0
- package/dist/controllers/filter_controller.js.map +1 -0
- package/dist/controllers/flash_controller.js +36 -5
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +1 -1
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +6 -4
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +1 -1
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +67 -19
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +54 -11
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +9 -2
- package/dist/controllers/listbox_controller.js +257 -53
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +2 -2
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +54 -0
- package/dist/controllers/masonry_controller.js +142 -0
- package/dist/controllers/masonry_controller.js.map +1 -0
- package/dist/controllers/menu_controller.d.ts +29 -11
- package/dist/controllers/menu_controller.js +104 -17
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +142 -0
- package/dist/controllers/menubar_controller.js +785 -0
- package/dist/controllers/menubar_controller.js.map +1 -0
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +141 -0
- package/dist/controllers/multi_select_controller.js +755 -0
- package/dist/controllers/multi_select_controller.js.map +1 -0
- package/dist/controllers/navigation_menu_controller.d.ts +146 -0
- package/dist/controllers/navigation_menu_controller.js +511 -0
- package/dist/controllers/navigation_menu_controller.js.map +1 -0
- package/dist/controllers/nested_form_controller.d.ts +4 -5
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +1 -1
- package/dist/controllers/number_input_controller.js +7 -0
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js +18 -1
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +15 -6
- package/dist/controllers/overflow_indicator_controller.js +246 -27
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +78 -19
- package/dist/controllers/overflow_menu_controller.js +381 -57
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +36 -5
- package/dist/controllers/pagination_controller.js +163 -32
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +49 -0
- package/dist/controllers/password_reveal_controller.js +117 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -0
- package/dist/controllers/persist_controller.js +6 -6
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +2 -3
- package/dist/controllers/pointer_drag_controller.js +9 -1
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +1 -2
- package/dist/controllers/popover_controller.js +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +4 -4
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.js +22 -3
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +97 -0
- package/dist/controllers/range_slider_controller.js +192 -0
- package/dist/controllers/range_slider_controller.js.map +1 -0
- package/dist/controllers/rating_controller.js +16 -2
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +52 -0
- package/dist/controllers/read_more_controller.js +238 -0
- package/dist/controllers/read_more_controller.js.map +1 -0
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +3 -1
- package/dist/controllers/resizable_controller.js +65 -1
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +17 -2
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +101 -14
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +54 -0
- package/dist/controllers/scroll_restore_controller.js +93 -0
- package/dist/controllers/scroll_restore_controller.js.map +1 -0
- package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
- package/dist/controllers/scroll_visibility_controller.js +40 -6
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +64 -15
- package/dist/controllers/scrollspy_controller.js +369 -74
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +69 -0
- package/dist/controllers/separator_controller.js +96 -0
- package/dist/controllers/separator_controller.js.map +1 -0
- package/dist/controllers/sidebar_controller.d.ts +107 -0
- package/dist/controllers/sidebar_controller.js +761 -0
- package/dist/controllers/sidebar_controller.js.map +1 -0
- package/dist/controllers/skeleton_controller.js +1 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +11 -1
- package/dist/controllers/slider_controller.js +32 -6
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.d.ts +3 -1
- package/dist/controllers/sortable_controller.js +34 -3
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +1 -1
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +4 -0
- package/dist/controllers/stepper_controller.js +28 -12
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +5 -6
- package/dist/controllers/stick_to_bottom_controller.js +9 -4
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +17 -4
- package/dist/controllers/sticky_observer_controller.js +109 -20
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +2 -2
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js +1 -0
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +13 -4
- package/dist/controllers/tabs_controller.js +26 -3
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +91 -0
- package/dist/controllers/tags_input_controller.js +295 -0
- package/dist/controllers/tags_input_controller.js.map +1 -0
- package/dist/controllers/theme_controller.d.ts +1 -1
- package/dist/controllers/theme_controller.js +42 -13
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +67 -0
- package/dist/controllers/time_picker_controller.js +231 -0
- package/dist/controllers/time_picker_controller.js.map +1 -0
- package/dist/controllers/toast_controller.d.ts +5 -14
- package/dist/controllers/toast_controller.js +40 -14
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js +23 -2
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +53 -13
- package/dist/controllers/toolbar_controller.js +230 -31
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +1 -1
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +14 -8
- package/dist/controllers/transition_controller.js +153 -38
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +135 -0
- package/dist/controllers/tree_view_controller.js +691 -0
- package/dist/controllers/tree_view_controller.js.map +1 -0
- package/dist/index.d.ts +27 -1818
- package/dist/index.js +4256 -915
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +375 -39
- package/dist/inspector/cli.js +228 -14
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +419 -48
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +26 -26
- package/dist/inspector/manifest.json +1054 -127
- package/package.json +5 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/nested_form_controller.ts"],"names":[],"mappings":"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","file":"nested_form_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless **nested / dynamic fields** for Rails `fields_for` +\n * `accepts_nested_attributes_for` (no dedicated APG pattern — form editing). The\n * Headless successor to the cocoon / nested_form gems: clone a `<template>` row,\n * renumber its index, and remove rows by flagging `_destroy` (persisted) or\n * dropping them from the DOM (unsaved).\n *\n * Markup contract (identifier: `stimeo--nested-form`):\n * <div data-controller=\"stimeo--nested-form\" data-stimeo--nested-form-min-value=\"1\">\n * <div data-stimeo--nested-form-target=\"list\">\n * <!-- existing rows; a persisted row carries a _destroy flag + remove button -->\n * </div>\n * <template data-stimeo--nested-form-target=\"template\">\n * <fieldset>\n * <input name=\"order[items_attributes][__INDEX__][name]\">\n * <button type=\"button\" data-stimeo--nested-form-target=\"remove\">Remove</button>\n * </fieldset>\n * </template>\n * <button type=\"button\" data-stimeo--nested-form-target=\"add\"\n * data-action=\"click->stimeo--nested-form#add\">Add</button>\n * </div>\n *\n * @remarks\n * Behavior only — server-side `accepts_nested_attributes_for`, per-field\n * validation, and reordering are out of scope. Row state lives **only** in the DOM\n * (inserted nodes + each `_destroy` hidden input); there is no module-scope index\n * counter, so the controller stays idempotent across Turbo swaps. Remove buttons\n * are handled by **delegation** on the container, so dynamically-added rows work\n * without per-row `data-action` (see `stimulus-lifecycle-turbo`). Adding a row moves\n * focus to its first control and removing returns focus to a neighbor (WCAG 2.2\n * 2.4.3); count changes are announced through the shared `stimeo--announcer`\n * (WCAG 2.2 4.1.3) when `announce` + `countMessage` are set. The delegated listener\n * is removed on `disconnect()`.\n */\nexport class NestedFormController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"template\", \"add\", \"remove\", \"destroyFlag\"];\n static override values = {\n min: { type: Number, default: 0 },\n max: { type: Number, default: 0 },\n indexPlaceholder: { type: String, default: \"__INDEX__\" },\n announce: { type: Boolean, default: true },\n countMessage: { type: String, default: \"\" },\n };\n static actions = [\"add\"] as const;\n static events = [\"add\", \"remove\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly templateTarget: HTMLTemplateElement;\n declare readonly addTarget: HTMLButtonElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasTemplateTarget: boolean;\n declare readonly hasAddTarget: boolean;\n\n declare minValue: number;\n declare maxValue: number;\n declare indexPlaceholderValue: string;\n declare announceValue: boolean;\n declare countMessageValue: string;\n\n /** Monotonic source for unique row indices; never a row-state counter. */\n #lastIndex = 0;\n\n /** Delegated click handler for the per-row remove buttons (dynamic-safe). */\n readonly #onClick = (event: Event): void => {\n const target = event.target as HTMLElement | null;\n const button = target?.closest('[data-stimeo--nested-form-target=\"remove\"]');\n if (!button || !this.element.contains(button)) return;\n const row = this.#rowContaining(button);\n if (row) this.#removeRow(row);\n };\n\n override connect(): void {\n this.element.addEventListener(\"click\", this.#onClick);\n this.#refresh();\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"click\", this.#onClick);\n }\n\n /**\n * Clones the template row, replaces the index placeholder with a unique value,\n * appends it, focuses its first control, and announces the new count. No-ops at\n * `max`.\n */\n add(): void {\n if (!this.hasTemplateTarget || !this.hasListTarget || this.#atMax) return;\n\n const index = this.#nextIndex();\n const markup = this.templateTarget.innerHTML.replaceAll(\n this.indexPlaceholderValue,\n String(index),\n );\n this.listTarget.insertAdjacentHTML(\"beforeend\", markup);\n const row = this.listTarget.lastElementChild as HTMLElement | null;\n if (!row) return;\n\n this.#refresh();\n this.#firstControl(row)?.focus();\n this.dispatch(\"add\", { detail: { index, element: row } });\n this.#announce();\n }\n\n /**\n * Removes a row: a persisted row (one carrying a `destroyFlag`) has its flag set\n * to `1` and is hidden so Rails destroys it on submit; an unsaved row is dropped\n * from the DOM. Returns focus to a neighboring row. No-ops at `min`.\n */\n #removeRow(row: HTMLElement): void {\n if (this.#effectiveRows.length <= this.minValue) return;\n\n const neighbors = this.#effectiveRows;\n const position = neighbors.indexOf(row);\n const neighbor = neighbors[position + 1] ?? neighbors[position - 1] ?? null;\n\n const flag = row.querySelector<HTMLInputElement>(\n '[data-stimeo--nested-form-target=\"destroyFlag\"]',\n );\n const persisted = flag !== null;\n if (persisted) {\n flag.value = \"1\";\n row.hidden = true;\n } else {\n row.remove();\n }\n\n this.#refresh();\n const focusTarget = neighbor\n ? this.#firstControl(neighbor)\n : this.hasAddTarget\n ? this.addTarget\n : null;\n focusTarget?.focus();\n this.dispatch(\"remove\", { detail: { element: row, persisted } });\n this.#announce();\n }\n\n /** Recomputes the live count and the min/max state hooks from the DOM. */\n #refresh(): void {\n const count = this.#effectiveRows.length;\n this.element.setAttribute(\"data-nested-count\", String(count));\n this.#reflect(\"data-nested-at-max\", this.maxValue > 0 && count >= this.maxValue);\n this.#reflect(\"data-nested-at-min\", count <= this.minValue);\n if (this.hasAddTarget) this.addTarget.disabled = this.#atMax;\n }\n\n /** Bridges the count change to the shared announcer when configured. */\n #announce(): void {\n if (!this.announceValue || !this.countMessageValue) return;\n const message = this.countMessageValue.replaceAll(\n \"{count}\",\n String(this.#effectiveRows.length),\n );\n window.dispatchEvent(new CustomEvent(\"stimeo--announcer:announce\", { detail: { message } }));\n }\n\n /** Sets `attribute` to `\"true\"` when `on`, else removes it. */\n #reflect(attribute: string, on: boolean): void {\n if (on) this.element.setAttribute(attribute, \"true\");\n else this.element.removeAttribute(attribute);\n }\n\n /** A strictly-increasing unique index (collision-free even on rapid adds). */\n #nextIndex(): number {\n const index = Math.max(Date.now(), this.#lastIndex + 1);\n this.#lastIndex = index;\n return index;\n }\n\n get #atMax(): boolean {\n return this.maxValue > 0 && this.#effectiveRows.length >= this.maxValue;\n }\n\n /** Direct child rows of the list that are not flagged for destruction. */\n get #effectiveRows(): HTMLElement[] {\n return (Array.from(this.listTarget.children) as HTMLElement[]).filter((row) => !row.hidden);\n }\n\n /** The nearest ancestor of `el` that is a direct child of the list, else null. */\n #rowContaining(el: Element): HTMLElement | null {\n let node: Element | null = el;\n while (node && node.parentElement !== this.listTarget) {\n node = node.parentElement;\n }\n return node as HTMLElement | null;\n }\n\n /** First visible focusable control inside `row` (skips hidden inputs). */\n #firstControl(row: HTMLElement): HTMLElement | null {\n return row.querySelector<HTMLElement>(\n 'input:not([type=\"hidden\"]), select, textarea, button, [tabindex]',\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/controllers/nested_form_controller.ts"],"names":[],"mappings":"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","file":"nested_form_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless **nested / dynamic fields** for Rails `fields_for` +\n * `accepts_nested_attributes_for` (no dedicated APG pattern — form editing). The\n * Headless successor to the cocoon / nested_form gems: clone a `<template>` row,\n * renumber its index, and remove rows by flagging `_destroy` (persisted) or\n * dropping them from the DOM (unsaved).\n *\n * Markup contract (identifier: `stimeo--nested-form`):\n * <div data-controller=\"stimeo--nested-form\" data-stimeo--nested-form-min-value=\"1\">\n * <div data-stimeo--nested-form-target=\"list\">\n * <!-- existing rows; a persisted row carries a _destroy flag + remove button -->\n * </div>\n * <template data-stimeo--nested-form-target=\"template\">\n * <fieldset>\n * <input name=\"order[items_attributes][__INDEX__][name]\">\n * <button type=\"button\" data-stimeo--nested-form-target=\"remove\">Remove</button>\n * </fieldset>\n * </template>\n * <button type=\"button\" data-stimeo--nested-form-target=\"add\"\n * data-action=\"click->stimeo--nested-form#add\">Add</button>\n * </div>\n *\n * @remarks\n * Behavior only — server-side `accepts_nested_attributes_for`, per-field\n * validation, and reordering are out of scope. Row state lives **only** in the DOM\n * (inserted nodes + each `_destroy` hidden input); there is no module-scope index\n * counter, so the controller stays idempotent across Turbo swaps. Remove buttons\n * are handled by **delegation** on the container, so dynamically-added rows work\n * without per-row `data-action`. Adding a row moves focus to its first control and\n * removing returns focus to a neighbor (WCAG 2.2 2.4.3); count changes are announced\n * through the shared `stimeo--announcer` (WCAG 2.2 4.1.3) when `announce` +\n * `countMessage` are set. The delegated listener is removed on `disconnect()`.\n */\nexport class NestedFormController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"template\", \"add\", \"remove\", \"destroyFlag\"];\n static override values = {\n min: { type: Number, default: 0 },\n max: { type: Number, default: 0 },\n indexPlaceholder: { type: String, default: \"__INDEX__\" },\n announce: { type: Boolean, default: true },\n countMessage: { type: String, default: \"\" },\n };\n static actions = [\"add\"] as const;\n static events = [\"add\", \"remove\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly templateTarget: HTMLTemplateElement;\n declare readonly addTarget: HTMLButtonElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasTemplateTarget: boolean;\n declare readonly hasAddTarget: boolean;\n\n declare minValue: number;\n declare maxValue: number;\n declare indexPlaceholderValue: string;\n declare announceValue: boolean;\n declare countMessageValue: string;\n\n /** Monotonic source for unique row indices; never a row-state counter. */\n #lastIndex = 0;\n\n /** Delegated click handler for the per-row remove buttons (dynamic-safe). */\n readonly #onClick = (event: Event): void => {\n const target = event.target as HTMLElement | null;\n const button = target?.closest('[data-stimeo--nested-form-target=\"remove\"]');\n if (!button || !this.element.contains(button)) return;\n const row = this.#rowContaining(button);\n if (row) this.#removeRow(row);\n };\n\n override connect(): void {\n this.element.addEventListener(\"click\", this.#onClick);\n this.#refresh();\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"click\", this.#onClick);\n }\n\n /**\n * Clones the template row, replaces the index placeholder with a unique value,\n * appends it, focuses its first control, and announces the new count. No-ops at\n * `max`.\n */\n add(): void {\n if (!this.hasTemplateTarget || !this.hasListTarget || this.#atMax) return;\n\n const index = this.#nextIndex();\n const markup = this.templateTarget.innerHTML.replaceAll(\n this.indexPlaceholderValue,\n String(index),\n );\n this.listTarget.insertAdjacentHTML(\"beforeend\", markup);\n const row = this.listTarget.lastElementChild as HTMLElement | null;\n if (!row) return;\n\n this.#refresh();\n this.#firstControl(row)?.focus();\n this.dispatch(\"add\", { detail: { index, element: row } });\n this.#announce();\n }\n\n /**\n * Removes a row: a persisted row (one carrying a `destroyFlag`) has its flag set\n * to `1` and is hidden so Rails destroys it on submit; an unsaved row is dropped\n * from the DOM. Returns focus to a neighboring row. No-ops at `min`.\n */\n #removeRow(row: HTMLElement): void {\n if (this.#effectiveRows.length <= this.minValue) return;\n\n const neighbors = this.#effectiveRows;\n const position = neighbors.indexOf(row);\n const neighbor = neighbors[position + 1] ?? neighbors[position - 1] ?? null;\n\n const flag = row.querySelector<HTMLInputElement>(\n '[data-stimeo--nested-form-target=\"destroyFlag\"]',\n );\n const persisted = flag !== null;\n if (persisted) {\n flag.value = \"1\";\n row.hidden = true;\n } else {\n row.remove();\n }\n\n this.#refresh();\n const focusTarget = neighbor\n ? this.#firstControl(neighbor)\n : this.hasAddTarget\n ? this.addTarget\n : null;\n focusTarget?.focus();\n this.dispatch(\"remove\", { detail: { element: row, persisted } });\n this.#announce();\n }\n\n /** Recomputes the live count and the min/max state hooks from the DOM. */\n #refresh(): void {\n const count = this.#effectiveRows.length;\n this.element.setAttribute(\"data-nested-count\", String(count));\n this.#reflect(\"data-nested-at-max\", this.maxValue > 0 && count >= this.maxValue);\n this.#reflect(\"data-nested-at-min\", count <= this.minValue);\n if (this.hasAddTarget) this.addTarget.disabled = this.#atMax;\n }\n\n /** Bridges the count change to the shared announcer when configured. */\n #announce(): void {\n if (!this.announceValue || !this.countMessageValue) return;\n const message = this.countMessageValue.replaceAll(\n \"{count}\",\n String(this.#effectiveRows.length),\n );\n window.dispatchEvent(new CustomEvent(\"stimeo--announcer:announce\", { detail: { message } }));\n }\n\n /** Sets `attribute` to `\"true\"` when `on`, else removes it. */\n #reflect(attribute: string, on: boolean): void {\n if (on) this.element.setAttribute(attribute, \"true\");\n else this.element.removeAttribute(attribute);\n }\n\n /** A strictly-increasing unique index (collision-free even on rapid adds). */\n #nextIndex(): number {\n const index = Math.max(Date.now(), this.#lastIndex + 1);\n this.#lastIndex = index;\n return index;\n }\n\n get #atMax(): boolean {\n return this.maxValue > 0 && this.#effectiveRows.length >= this.maxValue;\n }\n\n /** Direct child rows of the list that are not flagged for destruction. */\n get #effectiveRows(): HTMLElement[] {\n return (Array.from(this.listTarget.children) as HTMLElement[]).filter((row) => !row.hidden);\n }\n\n /** The nearest ancestor of `el` that is a direct child of the list, else null. */\n #rowContaining(el: Element): HTMLElement | null {\n let node: Element | null = el;\n while (node && node.parentElement !== this.listTarget) {\n node = node.parentElement;\n }\n return node as HTMLElement | null;\n }\n\n /** First visible focusable control inside `row` (skips hidden inputs). */\n #firstControl(row: HTMLElement): HTMLElement | null {\n return row.querySelector<HTMLElement>(\n 'input:not([type=\"hidden\"]), select, textarea, button, [tabindex]',\n );\n }\n}\n"]}
|
|
@@ -23,7 +23,7 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
23
23
|
* Implements the WAI-ARIA APG **Spinbutton** pattern. The arrow/step logic is
|
|
24
24
|
* fully owned by the controller (not delegated to the browser's native number
|
|
25
25
|
* stepping) so the behavior is identical for a native `<input type="number">` and
|
|
26
|
-
* a custom `role="spinbutton"` host
|
|
26
|
+
* a custom `role="spinbutton"` host.
|
|
27
27
|
*
|
|
28
28
|
* @remarks
|
|
29
29
|
* Behavior only — the consumer styles the field and buttons. The input is the
|
|
@@ -2,6 +2,12 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/number_input_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/arrow_step.ts
|
|
6
|
+
function isReservedArrowChord(event, allow = []) {
|
|
7
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
8
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
// src/utils/safe_timeout.ts
|
|
6
12
|
var TimerRegistry = class {
|
|
7
13
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -148,6 +154,7 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
148
154
|
}
|
|
149
155
|
/** Keyboard stepping per the APG spinbutton model. */
|
|
150
156
|
onKeydown(event) {
|
|
157
|
+
if (isReservedArrowChord(event)) return;
|
|
151
158
|
const page = this.pageStepValue > 0 ? this.pageStepValue : this.stepValue * 10;
|
|
152
159
|
let next = null;
|
|
153
160
|
switch (event.key) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/number_input_controller.ts"],"names":[],"mappings":";;;;;AAwBA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AAmBO,IAAM,WAAA,GAAN,cAA0B,aAAA,CAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,MAAM;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAClB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EACxB;AACF,CAAA;AAoBO,IAAM,YAAA,GAAN,cAA2B,aAAA,CAAc;AAAA;AAAA,EAE9C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,QAAA,EAAU,KAAK,CAAA;AACxC,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,WAAA,CAAY,QAAA,EAAU,KAAK,CAAA;AAAA,EAC3C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,cAAc,EAAE,CAAA;AAAA,EACzB;AACF,CAAA;;;AC5FO,IAAM,qBAAA,GAAN,MAAM,sBAAA,SAA8B,UAAA,CAAwB;AAAA,EACjE,OAAgB,OAAA,GAAU,CAAC,OAAA,EAAS,aAAa,WAAW,CAAA;AAAA,EAC5D,OAAgB,MAAA,GAAS;AAAA,IACvB,KAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,OAAO,iBAAA,EAAkB;AAAA,IACvD,KAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,OAAO,iBAAA,EAAkB;AAAA,IACvD,IAAA,EAAM,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA,EAAE;AAAA,IACjC,QAAA,EAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA;AAAE,GACvC;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,WAAA,EAAa,WAAA,EAAa,WAAW,WAAW,CAAA;AAAA,EAClE,OAAO,MAAA,GAAS,CAAC,QAAQ,CAAA;AAAA;AAAA,EAczB,OAAgB,cAAA,GAAiB,GAAA;AAAA;AAAA,EAEjC,OAAgB,eAAA,GAAkB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlC,OAAgB,kBAAA,GAAqB,GAAA;AAAA;AAAA,EAGrC,OAAA,GAAkC,IAAA;AAAA;AAAA,EAEzB,aAAA,GAAgB,IAAI,WAAA,EAAY;AAAA;AAAA,EAEhC,cAAA,GAAiB,IAAI,YAAA,EAAa;AAAA;AAAA,EAE3C,WAAA,GAAc,KAAA;AAAA;AAAA,EAEd,mBAAA,GAAsB,KAAA;AAAA;AAAA,EAEtB,kBAAA,GAAqB,KAAA;AAAA;AAAA,EAGZ,OAAA,GAAgB;AACvB,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AAC1B,IAAA,IAAI,IAAA,CAAK,WAAA,CAAY,KAAA,CAAM,IAAA,OAAW,EAAA,EAAI;AACxC,MAAA,IAAA,CAAK,OAAO,IAAA,CAAK,UAAA,CAAW,IAAA,CAAK,aAAA,EAAe,CAAC,CAAA;AAAA,IACnD,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,cAAA,CAAe,IAAA,CAAK,aAAA,EAAe,CAAA;AAAA,IAC1C;AACA,IAAA,IAAA,CAAK,OAAA,GAAU,IAAI,eAAA,EAAgB;AACnC,IAAA,MAAM,EAAE,MAAA,EAAO,GAAI,IAAA,CAAK,OAAA;AACxB,IAAA,IAAI,IAAA,CAAK,oBAAoB,IAAA,CAAK,WAAA,CAAY,KAAK,eAAA,EAAiB,IAAA,CAAK,WAAW,MAAM,CAAA;AAC1F,IAAA,IAAI,IAAA,CAAK,oBAAoB,IAAA,CAAK,WAAA,CAAY,KAAK,eAAA,EAAiB,CAAC,IAAA,CAAK,SAAA,EAAW,MAAM,CAAA;AAI3F,IAAA,MAAA,CAAO,iBAAiB,WAAA,EAAa,IAAA,CAAK,SAAA,EAAW,EAAE,QAAQ,CAAA;AAC/D,IAAA,MAAA,CAAO,iBAAiB,eAAA,EAAiB,IAAA,CAAK,SAAA,EAAW,EAAE,QAAQ,CAAA;AACnE,IAAA,MAAA,CAAO,iBAAiB,MAAA,EAAQ,IAAA,CAAK,SAAA,EAAW,EAAE,QAAQ,CAAA;AAAA,EAC5D;AAAA;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,SAAS,KAAA,EAAM;AACpB,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,IAAA,IAAA,CAAK,WAAA,GAAc,KAAA;AACnB,IAAA,IAAA,CAAK,mBAAA,GAAsB,KAAA;AAC3B,IAAA,IAAA,CAAK,kBAAA,GAAqB,KAAA;AAC1B,IAAA,IAAA,CAAK,cAAc,QAAA,EAAS;AAC5B,IAAA,IAAA,CAAK,eAAe,QAAA,EAAS;AAAA,EAC/B;AAAA;AAAA,EAGA,SAAA,GAAkB;AAChB,IAAA,IAAI,IAAA,CAAK,yBAAwB,EAAG;AACpC,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,aAAA,EAAc,GAAI,KAAK,SAAS,CAAA;AAClD,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AAAA,EACzB;AAAA;AAAA,EAGA,SAAA,GAAkB;AAChB,IAAA,IAAI,IAAA,CAAK,yBAAwB,EAAG;AACpC,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,aAAA,EAAc,GAAI,KAAK,SAAS,CAAA;AAClD,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AAAA,EACzB;AAAA;AAAA,EAGA,OAAA,GAAgB;AACd,IAAA,IAAI,IAAA,CAAK,WAAA,CAAY,KAAA,CAAM,IAAA,OAAW,EAAA,EAAI;AAC1C,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,aAAA,EAAe,CAAA;AAAA,EACnC;AAAA;AAAA,EAGA,UAAU,KAAA,EAA4B;AACpC,IAAA,MAAM,OAAO,IAAA,CAAK,aAAA,GAAgB,IAAI,IAAA,CAAK,aAAA,GAAgB,KAAK,SAAA,GAAY,EAAA;AAC5E,IAAA,IAAI,IAAA,GAAsB,IAAA;AAC1B,IAAA,QAAQ,MAAM,GAAA;AAAK,MACjB,KAAK,SAAA;AACH,QAAA,IAAA,GAAO,IAAA,CAAK,aAAA,EAAc,GAAI,IAAA,CAAK,SAAA;AACnC,QAAA;AAAA,MACF,KAAK,WAAA;AACH,QAAA,IAAA,GAAO,IAAA,CAAK,aAAA,EAAc,GAAI,IAAA,CAAK,SAAA;AACnC,QAAA;AAAA,MACF,KAAK,QAAA;AACH,QAAA,IAAA,GAAO,IAAA,CAAK,eAAc,GAAI,IAAA;AAC9B,QAAA;AAAA,MACF,KAAK,UAAA;AACH,QAAA,IAAA,GAAO,IAAA,CAAK,eAAc,GAAI,IAAA;AAC9B,QAAA;AAAA,MACF,KAAK,MAAA;AACH,QAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,EAAG;AACrC,QAAA,IAAA,GAAO,IAAA,CAAK,QAAA;AACZ,QAAA;AAAA,MACF,KAAK,KAAA;AACH,QAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,EAAG;AACrC,QAAA,IAAA,GAAO,IAAA,CAAK,QAAA;AACZ,QAAA;AAAA,MACF;AACE,QAAA;AAAA;AAEJ,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,IAAA,CAAK,QAAQ,IAAI,CAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAA,CAAY,MAAA,EAA2B,KAAA,EAAe,MAAA,EAA2B;AAC/E,IAAA,MAAA,CAAO,gBAAA,CAAiB,eAAe,CAAC,KAAA,KAAU,KAAK,QAAA,CAAS,KAAA,EAAO,MAAA,EAAQ,KAAK,CAAA,EAAG;AAAA,MACrF;AAAA,KACD,CAAA;AACD,IAAA,MAAA,CAAO,iBAAiB,cAAA,EAAgB,IAAA,CAAK,SAAA,EAAW,EAAE,QAAQ,CAAA;AAAA,EACpE;AAAA;AAAA,EAGA,QAAA,CAAS,KAAA,EAAc,MAAA,EAA2B,KAAA,EAAqB;AAErE,IAAA,MAAM,gBAAiB,KAAA,CAAuB,MAAA;AAC9C,IAAA,IAAI,OAAO,aAAA,KAAkB,QAAA,IAAY,aAAA,KAAkB,CAAA,EAAG;AAC9D,IAAA,IAAI,OAAO,QAAA,EAAU;AACrB,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AAEvB,IAAA,IAAA,CAAK,SAAA,EAAU;AACf,IAAA,IAAA,CAAK,WAAA,GAAc,IAAA;AACnB,IAAA,IAAA,CAAK,mBAAA,GAAsB,KAAA;AAC3B,IAAA,IAAA,CAAK,kBAAA,GAAqB,KAAA;AAC1B,IAAA,IAAA,CAAK,aAAA,CAAc,IAAI,MAAM;AAC3B,MAAA,IAAI,CAAC,IAAA,CAAK,OAAA,CAAQ,KAAK,aAAA,EAAc,GAAI,KAAK,CAAA,EAAG;AAC/C,QAAA,IAAA,CAAK,SAAA,EAAU;AACf,QAAA;AAAA,MACF;AACA,MAAA,IAAA,CAAK,mBAAA,GAAsB,IAAA;AAC3B,MAAA,IAAA,CAAK,cAAA,CAAe,IAAI,MAAM;AAC5B,QAAA,IAAI,CAAC,KAAK,OAAA,CAAQ,IAAA,CAAK,eAAc,GAAI,KAAK,CAAA,EAAG,IAAA,CAAK,SAAA,EAAU;AAAA,MAClE,CAAA,EAAG,uBAAsB,eAAe,CAAA;AAAA,IAC1C,CAAA,EAAG,uBAAsB,cAAc,CAAA;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,YAAY,MAAY;AAC/B,IAAA,IAAI,CAAC,KAAK,WAAA,EAAa;AACvB,IAAA,IAAA,CAAK,WAAA,GAAc,KAAA;AACnB,IAAA,IAAA,CAAK,cAAc,QAAA,EAAS;AAC5B,IAAA,IAAA,CAAK,eAAe,QAAA,EAAS;AAC7B,IAAA,IAAI,KAAK,mBAAA,EAAqB;AAC5B,MAAA,IAAA,CAAK,kBAAA,GAAqB,IAAA;AAC1B,MAAA,IAAA,CAAK,aAAA,CAAc,IAAI,MAAM;AAC3B,QAAA,IAAA,CAAK,kBAAA,GAAqB,KAAA;AAAA,MAC5B,CAAA,EAAG,uBAAsB,kBAAkB,CAAA;AAAA,IAC7C;AAAA,EACF,CAAA;AAAA;AAAA,EAGA,uBAAA,GAAmC;AACjC,IAAA,IAAI,CAAC,IAAA,CAAK,kBAAA,EAAoB,OAAO,KAAA;AACrC,IAAA,IAAA,CAAK,kBAAA,GAAqB,KAAA;AAC1B,IAAA,IAAA,CAAK,cAAc,QAAA,EAAS;AAC5B,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,QAAQ,GAAA,EAAsB;AAC5B,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA;AACjC,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,WAAA,CAAY,KAAA,KAAU,OAAO,KAAK,CAAA;AACvD,IAAA,IAAA,CAAK,OAAO,KAAK,CAAA;AACjB,IAAA,IAAI,OAAA,OAAc,QAAA,CAAS,QAAA,EAAU,EAAE,MAAA,EAAQ,EAAE,KAAA,EAAM,EAAG,CAAA;AAC1D,IAAA,OAAO,OAAA;AAAA,EACT;AAAA;AAAA,EAGA,OAAO,KAAA,EAAqB;AAC1B,IAAA,IAAA,CAAK,WAAA,CAAY,KAAA,GAAQ,MAAA,CAAO,KAAK,CAAA;AACrC,IAAA,IAAI,IAAA,CAAK,WAAA,CAAY,YAAA,CAAa,MAAM,MAAM,YAAA,EAAc;AAC1D,MAAA,IAAA,CAAK,WAAA,CAAY,YAAA,CAAa,eAAA,EAAiB,MAAA,CAAO,KAAK,CAAC,CAAA;AAC5D,MAAA,IAAI,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,EAAG;AAClC,QAAA,IAAA,CAAK,YAAY,YAAA,CAAa,eAAA,EAAiB,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAC,CAAA;AAAA,MACtE;AACA,MAAA,IAAI,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,EAAG;AAClC,QAAA,IAAA,CAAK,YAAY,YAAA,CAAa,eAAA,EAAiB,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAC,CAAA;AAAA,MACtE;AAAA,IACF;AACA,IAAA,IAAA,CAAK,eAAe,KAAK,CAAA;AAAA,EAC3B;AAAA;AAAA,EAGA,eAAe,KAAA,EAAqB;AAClC,IAAA,IAAI,IAAA,CAAK,oBAAoB,IAAA,CAAK,aAAA,CAAc,KAAK,eAAA,EAAiB,KAAA,GAAQ,KAAK,QAAQ,CAAA;AAC3F,IAAA,IAAI,IAAA,CAAK,oBAAoB,IAAA,CAAK,aAAA,CAAc,KAAK,eAAA,EAAiB,KAAA,GAAQ,KAAK,QAAQ,CAAA;AAAA,EAC7F;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,aAAA,CAAc,QAA2B,OAAA,EAAwB;AAC/D,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,IAAI,MAAA,CAAO,YAAA,CAAa,4BAA4B,CAAA,EAAG;AACrD,QAAA,MAAA,CAAO,QAAA,GAAW,KAAA;AAClB,QAAA,MAAA,CAAO,gBAAgB,4BAA4B,CAAA;AAAA,MACrD;AACA,MAAA;AAAA,IACF;AACA,IAAA,IAAI,OAAO,QAAA,EAAU;AACrB,IAAA,IAAI,QAAA,CAAS,aAAA,KAAkB,MAAA,EAAQ,IAAA,CAAK,YAAY,KAAA,EAAM;AAC9D,IAAA,MAAA,CAAO,QAAA,GAAW,IAAA;AAClB,IAAA,MAAA,CAAO,YAAA,CAAa,8BAA8B,EAAE,CAAA;AAAA,EACtD;AAAA;AAAA,EAGA,aAAA,GAAwB;AACtB,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,IAAA,CAAK,WAAA,CAAY,KAAK,CAAA;AAC5C,IAAA,IAAI,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA,IAAK,IAAA,CAAK,YAAY,KAAA,CAAM,IAAA,EAAK,KAAM,EAAA,EAAI,OAAO,MAAA;AAC5E,IAAA,OAAO,OAAO,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,GAAI,KAAK,QAAA,GAAW,CAAA;AAAA,EAC1D;AAAA;AAAA,EAGA,WAAW,GAAA,EAAqB;AAC9B,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,KAAK,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,GAAG,CAAC,CAAA;AACpE,IAAA,IAAI,IAAA,CAAK,SAAA,IAAa,CAAA,EAAG,OAAO,OAAA;AAChC,IAAA,MAAM,OAAO,MAAA,CAAO,QAAA,CAAS,KAAK,QAAQ,CAAA,GAAI,KAAK,QAAA,GAAW,CAAA;AAC9D,IAAA,MAAM,OAAA,GAAU,KAAK,KAAA,CAAA,CAAO,OAAA,GAAU,QAAQ,IAAA,CAAK,SAAS,CAAA,GAAI,IAAA,CAAK,SAAA,GAAY,IAAA;AACjF,IAAA,OAAO,IAAA,CAAK,IAAI,IAAA,CAAK,QAAA,EAAU,KAAK,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,OAAO,CAAC,CAAA;AAAA,EACjE;AACF","file":"number_input_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeInterval, SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless, accessible number / spin-button behavior.\n *\n * Markup contract (identifier: `stimeo--number-input`):\n * <div data-controller=\"stimeo--number-input\"\n * data-stimeo--number-input-min-value=\"0\"\n * data-stimeo--number-input-max-value=\"100\"\n * data-stimeo--number-input-step-value=\"1\">\n * <button type=\"button\" aria-label=\"Decrease\" tabindex=\"-1\"\n * data-stimeo--number-input-target=\"decrement\"\n * data-action=\"click->stimeo--number-input#decrement\">−</button>\n * <input type=\"number\" min=\"0\" max=\"100\" step=\"1\" value=\"0\"\n * data-stimeo--number-input-target=\"input\"\n * data-action=\"change->stimeo--number-input#onInput\n * keydown->stimeo--number-input#onKeydown\" />\n * <button type=\"button\" aria-label=\"Increase\" tabindex=\"-1\"\n * data-stimeo--number-input-target=\"increment\"\n * data-action=\"click->stimeo--number-input#increment\">+</button>\n * </div>\n *\n * Implements the WAI-ARIA APG **Spinbutton** pattern. The arrow/step logic is\n * fully owned by the controller (not delegated to the browser's native number\n * stepping) so the behavior is identical for a native `<input type=\"number\">` and\n * a custom `role=\"spinbutton\"` host, and deterministic under happy-dom.\n *\n * @remarks\n * Behavior only — the consumer styles the field and buttons. The input is the\n * sole Tab stop; the buttons are `tabindex=\"-1\"` and keep focus on the input\n * (pointerdown is suppressed), so stepping never moves focus away.\n *\n * Behavior provided:\n * - `ArrowUp`/`ArrowDown` step by `step`; `PageUp`/`PageDown` by `pageStep`\n * (default `step × 10`); `Home`/`End` jump to a finite `min`/`max`.\n * - Increment/decrement buttons step too, and are `disabled` at the bounds (focus\n * is returned to the input before a focused button is disabled).\n * - **Press-and-hold auto-repeat**: holding a step button steps once, then after a\n * short delay repeats until release, the bound is reached, or the element\n * disconnects. The `click` binding stays the single-step path (a quick click, a\n * synthesized/programmatic click, or assistive activation), so a normal click\n * never double-steps — the trailing click after a hold is swallowed.\n * - Typed input is clamped and snapped to the step grid on `change`;\n * `stimeo--number-input:change` is dispatched on every committed change.\n */\nexport class NumberInputController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"increment\", \"decrement\"];\n static override values = {\n min: { type: Number, default: Number.NEGATIVE_INFINITY },\n max: { type: Number, default: Number.POSITIVE_INFINITY },\n step: { type: Number, default: 1 },\n pageStep: { type: Number, default: 0 },\n };\n static actions = [\"decrement\", \"increment\", \"onInput\", \"onKeydown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly hasInputTarget: boolean;\n declare readonly incrementTarget: HTMLButtonElement;\n declare readonly hasIncrementTarget: boolean;\n declare readonly decrementTarget: HTMLButtonElement;\n declare readonly hasDecrementTarget: boolean;\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare pageStepValue: number;\n\n /** Delay (ms) a button must be held before auto-repeat starts. */\n static readonly #HOLD_DELAY_MS = 400;\n /** Interval (ms) between auto-repeat steps once a hold has started. */\n static readonly #HOLD_REPEAT_MS = 80;\n /**\n * Window (ms) after a hold ends during which the trailing synthetic `click` is\n * swallowed. A safety net for the (rare) case where that click never arrives\n * (e.g. the pointer was released off the button): the flag self-clears instead\n * of poisoning the next legitimate click.\n */\n static readonly #SUPPRESS_RESET_MS = 250;\n\n /** Aborts the pointer listeners on disconnect so none outlive the element. */\n #guards: AbortController | null = null;\n /** Timers for the hold delay and the suppress-reset safety net. */\n readonly #holdTimeouts = new SafeTimeout();\n /** The running auto-repeat interval (one at a time). */\n readonly #holdIntervals = new SafeInterval();\n /** True while a hold is armed/running, making `#stopHold` idempotent. */\n #holdActive = false;\n /** True once a hold actually produced a repeated step (vs. a quick click). */\n #repeatedDuringHold = false;\n /** True when the next `click` is the trailing one after a hold and must be ignored. */\n #suppressNextClick = false;\n\n /** Normalizes any initial value and wires the focus/hold pointer guards. */\n override connect(): void {\n if (!this.hasInputTarget) return;\n if (this.inputTarget.value.trim() !== \"\") {\n this.#write(this.#normalize(this.#currentValue()));\n } else {\n this.#updateButtons(this.#currentValue());\n }\n this.#guards = new AbortController();\n const { signal } = this.#guards;\n if (this.hasIncrementTarget) this.#wireButton(this.incrementTarget, this.stepValue, signal);\n if (this.hasDecrementTarget) this.#wireButton(this.decrementTarget, -this.stepValue, signal);\n // A pointer released or focus lost anywhere stops a running hold, even when\n // the button stopped receiving its own pointerup (e.g. it became disabled at\n // the bound, or the pointer was released off it).\n window.addEventListener(\"pointerup\", this.#stopHold, { signal });\n window.addEventListener(\"pointercancel\", this.#stopHold, { signal });\n window.addEventListener(\"blur\", this.#stopHold, { signal });\n }\n\n /** Releases the pointer guards and tears down every pending timer and hold state. */\n override disconnect(): void {\n this.#guards?.abort();\n this.#guards = null;\n this.#holdActive = false;\n this.#repeatedDuringHold = false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.clearAll();\n this.#holdIntervals.clearAll();\n }\n\n /** Increases by one step. Bound via `data-action` (click). */\n increment(): void {\n if (this.#consumeSuppressedClick()) return;\n this.#commit(this.#currentValue() + this.stepValue);\n this.inputTarget.focus();\n }\n\n /** Decreases by one step. Bound via `data-action` (click). */\n decrement(): void {\n if (this.#consumeSuppressedClick()) return;\n this.#commit(this.#currentValue() - this.stepValue);\n this.inputTarget.focus();\n }\n\n /** Clamps and snaps a typed value. Bound via `data-action` (change). */\n onInput(): void {\n if (this.inputTarget.value.trim() === \"\") return;\n this.#commit(this.#currentValue());\n }\n\n /** Keyboard stepping per the APG spinbutton model. */\n onKeydown(event: KeyboardEvent): void {\n const page = this.pageStepValue > 0 ? this.pageStepValue : this.stepValue * 10;\n let next: number | null = null;\n switch (event.key) {\n case \"ArrowUp\":\n next = this.#currentValue() + this.stepValue;\n break;\n case \"ArrowDown\":\n next = this.#currentValue() - this.stepValue;\n break;\n case \"PageUp\":\n next = this.#currentValue() + page;\n break;\n case \"PageDown\":\n next = this.#currentValue() - page;\n break;\n case \"Home\":\n if (!Number.isFinite(this.minValue)) return;\n next = this.minValue;\n break;\n case \"End\":\n if (!Number.isFinite(this.maxValue)) return;\n next = this.maxValue;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#commit(next);\n }\n\n /**\n * Wires a step button: `pointerdown` keeps focus on the input and arms the\n * hold; leaving the button while held stops it (the global listeners cover\n * release/cancel/blur).\n */\n #wireButton(button: HTMLButtonElement, delta: number, signal: AbortSignal): void {\n button.addEventListener(\"pointerdown\", (event) => this.#armHold(event, button, delta), {\n signal,\n });\n button.addEventListener(\"pointerleave\", this.#stopHold, { signal });\n }\n\n /** Starts a hold: focus the input, then schedule the first repeat after a delay. */\n #armHold(event: Event, button: HTMLButtonElement, delta: number): void {\n // Ignore secondary buttons (right/middle) when the event exposes one.\n const pointerButton = (event as PointerEvent).button;\n if (typeof pointerButton === \"number\" && pointerButton !== 0) return;\n if (button.disabled) return;\n event.preventDefault(); // keep focus on the input rather than the button\n this.inputTarget.focus();\n\n this.#stopHold();\n this.#holdActive = true;\n this.#repeatedDuringHold = false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.set(() => {\n if (!this.#commit(this.#currentValue() + delta)) {\n this.#stopHold();\n return;\n }\n this.#repeatedDuringHold = true;\n this.#holdIntervals.set(() => {\n if (!this.#commit(this.#currentValue() + delta)) this.#stopHold();\n }, NumberInputController.#HOLD_REPEAT_MS);\n }, NumberInputController.#HOLD_DELAY_MS);\n }\n\n /**\n * Stops the current hold (idempotent). When the hold actually repeated, the\n * trailing synthetic `click` must be ignored, so it is suppressed until the\n * click consumes it or a short safety-net timeout clears it.\n */\n readonly #stopHold = (): void => {\n if (!this.#holdActive) return;\n this.#holdActive = false;\n this.#holdTimeouts.clearAll();\n this.#holdIntervals.clearAll();\n if (this.#repeatedDuringHold) {\n this.#suppressNextClick = true;\n this.#holdTimeouts.set(() => {\n this.#suppressNextClick = false;\n }, NumberInputController.#SUPPRESS_RESET_MS);\n }\n };\n\n /** Consumes a pending trailing-click suppression; returns true if the click was swallowed. */\n #consumeSuppressedClick(): boolean {\n if (!this.#suppressNextClick) return false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.clearAll();\n return true;\n }\n\n /**\n * Normalizes `raw`, reflects it, and dispatches `change` only when the\n * displayed value actually changes.\n *\n * @returns Whether the value changed (drives the auto-repeat's bound stop).\n */\n #commit(raw: number): boolean {\n const value = this.#normalize(raw);\n const changed = this.inputTarget.value !== String(value);\n this.#write(value);\n if (changed) this.dispatch(\"change\", { detail: { value } });\n return changed;\n }\n\n /** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */\n #write(value: number): void {\n this.inputTarget.value = String(value);\n if (this.inputTarget.getAttribute(\"role\") === \"spinbutton\") {\n this.inputTarget.setAttribute(\"aria-valuenow\", String(value));\n if (Number.isFinite(this.minValue)) {\n this.inputTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n }\n if (Number.isFinite(this.maxValue)) {\n this.inputTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n }\n }\n this.#updateButtons(value);\n }\n\n /** Disables a step button at its bound, returning focus to the input first. */\n #updateButtons(value: number): void {\n if (this.hasIncrementTarget) this.#toggleButton(this.incrementTarget, value < this.maxValue);\n if (this.hasDecrementTarget) this.#toggleButton(this.decrementTarget, value > this.minValue);\n }\n\n /**\n * Enables or disables a step button at its bound, never disabling one while it\n * holds focus. Owns only the `disabled` it sets itself via a marker\n * (`data-number-input-disabled`, like `conditional-fields`/`submit-once`), so an\n * author-disabled button (e.g. the whole control disabled) is never re-enabled.\n */\n #toggleButton(button: HTMLButtonElement, enabled: boolean): void {\n if (enabled) {\n if (button.hasAttribute(\"data-number-input-disabled\")) {\n button.disabled = false;\n button.removeAttribute(\"data-number-input-disabled\");\n }\n return;\n }\n if (button.disabled) return; // already disabled (possibly by the author) — leave it\n if (document.activeElement === button) this.inputTarget.focus();\n button.disabled = true;\n button.setAttribute(\"data-number-input-disabled\", \"\");\n }\n\n /** The current numeric value, falling back to a finite min (else 0) when blank. */\n #currentValue(): number {\n const parsed = Number(this.inputTarget.value);\n if (Number.isFinite(parsed) && this.inputTarget.value.trim() !== \"\") return parsed;\n return Number.isFinite(this.minValue) ? this.minValue : 0;\n }\n\n /** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */\n #normalize(raw: number): number {\n const clamped = Math.min(this.maxValue, Math.max(this.minValue, raw));\n if (this.stepValue <= 0) return clamped;\n const base = Number.isFinite(this.minValue) ? this.minValue : 0;\n const stepped = Math.round((clamped - base) / this.stepValue) * this.stepValue + base;\n return Math.min(this.maxValue, Math.max(this.minValue, stepped));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/safe_timeout.ts","../../src/controllers/number_input_controller.ts"],"names":[],"mappings":";;;;;AA+FO,SAAS,oBAAA,CACd,KAAA,EACA,KAAA,GAAkC,EAAC,EAC1B;AACT,EAAA,IAAI,CAAC,KAAA,CAAM,GAAA,CAAI,UAAA,CAAW,OAAO,GAAG,OAAO,KAAA;AAC3C,EAAA,OACG,KAAA,CAAM,MAAA,IAAU,CAAC,KAAA,CAAM,QAAA,CAAS,KAAK,CAAA,IACrC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,KACvC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,IACvC,KAAA,CAAM,QAAA,IAAY,CAAC,KAAA,CAAM,QAAA,CAAS,OAAO,CAAA;AAE9C;;;AClFA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AAmBO,IAAM,WAAA,GAAN,cAA0B,aAAA,CAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,MAAM;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAClB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EACxB;AACF,CAAA;AAoBO,IAAM,YAAA,GAAN,cAA2B,aAAA,CAAc;AAAA;AAAA,EAE9C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,QAAA,EAAU,KAAK,CAAA;AACxC,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,WAAA,CAAY,QAAA,EAAU,KAAK,CAAA;AAAA,EAC3C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,cAAc,EAAE,CAAA;AAAA,EACzB;AACF,CAAA;;;AC3FO,IAAM,qBAAA,GAAN,MAAM,sBAAA,SAA8B,UAAA,CAAwB;AAAA,EACjE,OAAgB,OAAA,GAAU,CAAC,OAAA,EAAS,aAAa,WAAW,CAAA;AAAA,EAC5D,OAAgB,MAAA,GAAS;AAAA,IACvB,KAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,OAAO,iBAAA,EAAkB;AAAA,IACvD,KAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,OAAO,iBAAA,EAAkB;AAAA,IACvD,IAAA,EAAM,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA,EAAE;AAAA,IACjC,QAAA,EAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA;AAAE,GACvC;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,WAAA,EAAa,WAAA,EAAa,WAAW,WAAW,CAAA;AAAA,EAClE,OAAO,MAAA,GAAS,CAAC,QAAQ,CAAA;AAAA;AAAA,EAczB,OAAgB,cAAA,GAAiB,GAAA;AAAA;AAAA,EAEjC,OAAgB,eAAA,GAAkB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlC,OAAgB,kBAAA,GAAqB,GAAA;AAAA;AAAA,EAGrC,OAAA,GAAkC,IAAA;AAAA;AAAA,EAEzB,aAAA,GAAgB,IAAI,WAAA,EAAY;AAAA;AAAA,EAEhC,cAAA,GAAiB,IAAI,YAAA,EAAa;AAAA;AAAA,EAE3C,WAAA,GAAc,KAAA;AAAA;AAAA,EAEd,mBAAA,GAAsB,KAAA;AAAA;AAAA,EAEtB,kBAAA,GAAqB,KAAA;AAAA;AAAA,EAGZ,OAAA,GAAgB;AACvB,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AAC1B,IAAA,IAAI,IAAA,CAAK,WAAA,CAAY,KAAA,CAAM,IAAA,OAAW,EAAA,EAAI;AACxC,MAAA,IAAA,CAAK,OAAO,IAAA,CAAK,UAAA,CAAW,IAAA,CAAK,aAAA,EAAe,CAAC,CAAA;AAAA,IACnD,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,cAAA,CAAe,IAAA,CAAK,aAAA,EAAe,CAAA;AAAA,IAC1C;AACA,IAAA,IAAA,CAAK,OAAA,GAAU,IAAI,eAAA,EAAgB;AACnC,IAAA,MAAM,EAAE,MAAA,EAAO,GAAI,IAAA,CAAK,OAAA;AACxB,IAAA,IAAI,IAAA,CAAK,oBAAoB,IAAA,CAAK,WAAA,CAAY,KAAK,eAAA,EAAiB,IAAA,CAAK,WAAW,MAAM,CAAA;AAC1F,IAAA,IAAI,IAAA,CAAK,oBAAoB,IAAA,CAAK,WAAA,CAAY,KAAK,eAAA,EAAiB,CAAC,IAAA,CAAK,SAAA,EAAW,MAAM,CAAA;AAI3F,IAAA,MAAA,CAAO,iBAAiB,WAAA,EAAa,IAAA,CAAK,SAAA,EAAW,EAAE,QAAQ,CAAA;AAC/D,IAAA,MAAA,CAAO,iBAAiB,eAAA,EAAiB,IAAA,CAAK,SAAA,EAAW,EAAE,QAAQ,CAAA;AACnE,IAAA,MAAA,CAAO,iBAAiB,MAAA,EAAQ,IAAA,CAAK,SAAA,EAAW,EAAE,QAAQ,CAAA;AAAA,EAC5D;AAAA;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,SAAS,KAAA,EAAM;AACpB,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,IAAA,IAAA,CAAK,WAAA,GAAc,KAAA;AACnB,IAAA,IAAA,CAAK,mBAAA,GAAsB,KAAA;AAC3B,IAAA,IAAA,CAAK,kBAAA,GAAqB,KAAA;AAC1B,IAAA,IAAA,CAAK,cAAc,QAAA,EAAS;AAC5B,IAAA,IAAA,CAAK,eAAe,QAAA,EAAS;AAAA,EAC/B;AAAA;AAAA,EAGA,SAAA,GAAkB;AAChB,IAAA,IAAI,IAAA,CAAK,yBAAwB,EAAG;AACpC,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,aAAA,EAAc,GAAI,KAAK,SAAS,CAAA;AAClD,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AAAA,EACzB;AAAA;AAAA,EAGA,SAAA,GAAkB;AAChB,IAAA,IAAI,IAAA,CAAK,yBAAwB,EAAG;AACpC,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,aAAA,EAAc,GAAI,KAAK,SAAS,CAAA;AAClD,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AAAA,EACzB;AAAA;AAAA,EAGA,OAAA,GAAgB;AACd,IAAA,IAAI,IAAA,CAAK,WAAA,CAAY,KAAA,CAAM,IAAA,OAAW,EAAA,EAAI;AAC1C,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,aAAA,EAAe,CAAA;AAAA,EACnC;AAAA;AAAA,EAGA,UAAU,KAAA,EAA4B;AACpC,IAAA,IAAI,oBAAA,CAAqB,KAAK,CAAA,EAAG;AACjC,IAAA,MAAM,OAAO,IAAA,CAAK,aAAA,GAAgB,IAAI,IAAA,CAAK,aAAA,GAAgB,KAAK,SAAA,GAAY,EAAA;AAC5E,IAAA,IAAI,IAAA,GAAsB,IAAA;AAC1B,IAAA,QAAQ,MAAM,GAAA;AAAK,MACjB,KAAK,SAAA;AACH,QAAA,IAAA,GAAO,IAAA,CAAK,aAAA,EAAc,GAAI,IAAA,CAAK,SAAA;AACnC,QAAA;AAAA,MACF,KAAK,WAAA;AACH,QAAA,IAAA,GAAO,IAAA,CAAK,aAAA,EAAc,GAAI,IAAA,CAAK,SAAA;AACnC,QAAA;AAAA,MACF,KAAK,QAAA;AACH,QAAA,IAAA,GAAO,IAAA,CAAK,eAAc,GAAI,IAAA;AAC9B,QAAA;AAAA,MACF,KAAK,UAAA;AACH,QAAA,IAAA,GAAO,IAAA,CAAK,eAAc,GAAI,IAAA;AAC9B,QAAA;AAAA,MACF,KAAK,MAAA;AACH,QAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,EAAG;AACrC,QAAA,IAAA,GAAO,IAAA,CAAK,QAAA;AACZ,QAAA;AAAA,MACF,KAAK,KAAA;AACH,QAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,EAAG;AACrC,QAAA,IAAA,GAAO,IAAA,CAAK,QAAA;AACZ,QAAA;AAAA,MACF;AACE,QAAA;AAAA;AAEJ,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,IAAA,CAAK,QAAQ,IAAI,CAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAA,CAAY,MAAA,EAA2B,KAAA,EAAe,MAAA,EAA2B;AAC/E,IAAA,MAAA,CAAO,gBAAA,CAAiB,eAAe,CAAC,KAAA,KAAU,KAAK,QAAA,CAAS,KAAA,EAAO,MAAA,EAAQ,KAAK,CAAA,EAAG;AAAA,MACrF;AAAA,KACD,CAAA;AACD,IAAA,MAAA,CAAO,iBAAiB,cAAA,EAAgB,IAAA,CAAK,SAAA,EAAW,EAAE,QAAQ,CAAA;AAAA,EACpE;AAAA;AAAA,EAGA,QAAA,CAAS,KAAA,EAAc,MAAA,EAA2B,KAAA,EAAqB;AAErE,IAAA,MAAM,gBAAiB,KAAA,CAAuB,MAAA;AAC9C,IAAA,IAAI,OAAO,aAAA,KAAkB,QAAA,IAAY,aAAA,KAAkB,CAAA,EAAG;AAC9D,IAAA,IAAI,OAAO,QAAA,EAAU;AACrB,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AAEvB,IAAA,IAAA,CAAK,SAAA,EAAU;AACf,IAAA,IAAA,CAAK,WAAA,GAAc,IAAA;AACnB,IAAA,IAAA,CAAK,mBAAA,GAAsB,KAAA;AAC3B,IAAA,IAAA,CAAK,kBAAA,GAAqB,KAAA;AAC1B,IAAA,IAAA,CAAK,aAAA,CAAc,IAAI,MAAM;AAC3B,MAAA,IAAI,CAAC,IAAA,CAAK,OAAA,CAAQ,KAAK,aAAA,EAAc,GAAI,KAAK,CAAA,EAAG;AAC/C,QAAA,IAAA,CAAK,SAAA,EAAU;AACf,QAAA;AAAA,MACF;AACA,MAAA,IAAA,CAAK,mBAAA,GAAsB,IAAA;AAC3B,MAAA,IAAA,CAAK,cAAA,CAAe,IAAI,MAAM;AAC5B,QAAA,IAAI,CAAC,KAAK,OAAA,CAAQ,IAAA,CAAK,eAAc,GAAI,KAAK,CAAA,EAAG,IAAA,CAAK,SAAA,EAAU;AAAA,MAClE,CAAA,EAAG,uBAAsB,eAAe,CAAA;AAAA,IAC1C,CAAA,EAAG,uBAAsB,cAAc,CAAA;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,YAAY,MAAY;AAC/B,IAAA,IAAI,CAAC,KAAK,WAAA,EAAa;AACvB,IAAA,IAAA,CAAK,WAAA,GAAc,KAAA;AACnB,IAAA,IAAA,CAAK,cAAc,QAAA,EAAS;AAC5B,IAAA,IAAA,CAAK,eAAe,QAAA,EAAS;AAC7B,IAAA,IAAI,KAAK,mBAAA,EAAqB;AAC5B,MAAA,IAAA,CAAK,kBAAA,GAAqB,IAAA;AAC1B,MAAA,IAAA,CAAK,aAAA,CAAc,IAAI,MAAM;AAC3B,QAAA,IAAA,CAAK,kBAAA,GAAqB,KAAA;AAAA,MAC5B,CAAA,EAAG,uBAAsB,kBAAkB,CAAA;AAAA,IAC7C;AAAA,EACF,CAAA;AAAA;AAAA,EAGA,uBAAA,GAAmC;AACjC,IAAA,IAAI,CAAC,IAAA,CAAK,kBAAA,EAAoB,OAAO,KAAA;AACrC,IAAA,IAAA,CAAK,kBAAA,GAAqB,KAAA;AAC1B,IAAA,IAAA,CAAK,cAAc,QAAA,EAAS;AAC5B,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,QAAQ,GAAA,EAAsB;AAC5B,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA;AACjC,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,WAAA,CAAY,KAAA,KAAU,OAAO,KAAK,CAAA;AACvD,IAAA,IAAA,CAAK,OAAO,KAAK,CAAA;AACjB,IAAA,IAAI,OAAA,OAAc,QAAA,CAAS,QAAA,EAAU,EAAE,MAAA,EAAQ,EAAE,KAAA,EAAM,EAAG,CAAA;AAC1D,IAAA,OAAO,OAAA;AAAA,EACT;AAAA;AAAA,EAGA,OAAO,KAAA,EAAqB;AAC1B,IAAA,IAAA,CAAK,WAAA,CAAY,KAAA,GAAQ,MAAA,CAAO,KAAK,CAAA;AACrC,IAAA,IAAI,IAAA,CAAK,WAAA,CAAY,YAAA,CAAa,MAAM,MAAM,YAAA,EAAc;AAC1D,MAAA,IAAA,CAAK,WAAA,CAAY,YAAA,CAAa,eAAA,EAAiB,MAAA,CAAO,KAAK,CAAC,CAAA;AAC5D,MAAA,IAAI,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,EAAG;AAClC,QAAA,IAAA,CAAK,YAAY,YAAA,CAAa,eAAA,EAAiB,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAC,CAAA;AAAA,MACtE;AACA,MAAA,IAAI,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,EAAG;AAClC,QAAA,IAAA,CAAK,YAAY,YAAA,CAAa,eAAA,EAAiB,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAC,CAAA;AAAA,MACtE;AAAA,IACF;AACA,IAAA,IAAA,CAAK,eAAe,KAAK,CAAA;AAAA,EAC3B;AAAA;AAAA,EAGA,eAAe,KAAA,EAAqB;AAClC,IAAA,IAAI,IAAA,CAAK,oBAAoB,IAAA,CAAK,aAAA,CAAc,KAAK,eAAA,EAAiB,KAAA,GAAQ,KAAK,QAAQ,CAAA;AAC3F,IAAA,IAAI,IAAA,CAAK,oBAAoB,IAAA,CAAK,aAAA,CAAc,KAAK,eAAA,EAAiB,KAAA,GAAQ,KAAK,QAAQ,CAAA;AAAA,EAC7F;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,aAAA,CAAc,QAA2B,OAAA,EAAwB;AAC/D,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,IAAI,MAAA,CAAO,YAAA,CAAa,4BAA4B,CAAA,EAAG;AACrD,QAAA,MAAA,CAAO,QAAA,GAAW,KAAA;AAClB,QAAA,MAAA,CAAO,gBAAgB,4BAA4B,CAAA;AAAA,MACrD;AACA,MAAA;AAAA,IACF;AACA,IAAA,IAAI,OAAO,QAAA,EAAU;AACrB,IAAA,IAAI,QAAA,CAAS,aAAA,KAAkB,MAAA,EAAQ,IAAA,CAAK,YAAY,KAAA,EAAM;AAC9D,IAAA,MAAA,CAAO,QAAA,GAAW,IAAA;AAClB,IAAA,MAAA,CAAO,YAAA,CAAa,8BAA8B,EAAE,CAAA;AAAA,EACtD;AAAA;AAAA,EAGA,aAAA,GAAwB;AACtB,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,IAAA,CAAK,WAAA,CAAY,KAAK,CAAA;AAC5C,IAAA,IAAI,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA,IAAK,IAAA,CAAK,YAAY,KAAA,CAAM,IAAA,EAAK,KAAM,EAAA,EAAI,OAAO,MAAA;AAC5E,IAAA,OAAO,OAAO,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,GAAI,KAAK,QAAA,GAAW,CAAA;AAAA,EAC1D;AAAA;AAAA,EAGA,WAAW,GAAA,EAAqB;AAC9B,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,KAAK,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,GAAG,CAAC,CAAA;AACpE,IAAA,IAAI,IAAA,CAAK,SAAA,IAAa,CAAA,EAAG,OAAO,OAAA;AAChC,IAAA,MAAM,OAAO,MAAA,CAAO,QAAA,CAAS,KAAK,QAAQ,CAAA,GAAI,KAAK,QAAA,GAAW,CAAA;AAC9D,IAAA,MAAM,OAAA,GAAU,KAAK,KAAA,CAAA,CAAO,OAAA,GAAU,QAAQ,IAAA,CAAK,SAAS,CAAA,GAAI,IAAA,CAAK,SAAA,GAAY,IAAA;AACjF,IAAA,OAAO,IAAA,CAAK,IAAI,IAAA,CAAK,QAAA,EAAU,KAAK,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,OAAO,CAAC,CAAA;AAAA,EACjE;AACF","file":"number_input_controller.js","sourcesContent":["import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { SafeInterval, SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless, accessible number / spin-button behavior.\n *\n * Markup contract (identifier: `stimeo--number-input`):\n * <div data-controller=\"stimeo--number-input\"\n * data-stimeo--number-input-min-value=\"0\"\n * data-stimeo--number-input-max-value=\"100\"\n * data-stimeo--number-input-step-value=\"1\">\n * <button type=\"button\" aria-label=\"Decrease\" tabindex=\"-1\"\n * data-stimeo--number-input-target=\"decrement\"\n * data-action=\"click->stimeo--number-input#decrement\">−</button>\n * <input type=\"number\" min=\"0\" max=\"100\" step=\"1\" value=\"0\"\n * data-stimeo--number-input-target=\"input\"\n * data-action=\"change->stimeo--number-input#onInput\n * keydown->stimeo--number-input#onKeydown\" />\n * <button type=\"button\" aria-label=\"Increase\" tabindex=\"-1\"\n * data-stimeo--number-input-target=\"increment\"\n * data-action=\"click->stimeo--number-input#increment\">+</button>\n * </div>\n *\n * Implements the WAI-ARIA APG **Spinbutton** pattern. The arrow/step logic is\n * fully owned by the controller (not delegated to the browser's native number\n * stepping) so the behavior is identical for a native `<input type=\"number\">` and\n * a custom `role=\"spinbutton\"` host.\n *\n * @remarks\n * Behavior only — the consumer styles the field and buttons. The input is the\n * sole Tab stop; the buttons are `tabindex=\"-1\"` and keep focus on the input\n * (pointerdown is suppressed), so stepping never moves focus away.\n *\n * Behavior provided:\n * - `ArrowUp`/`ArrowDown` step by `step`; `PageUp`/`PageDown` by `pageStep`\n * (default `step × 10`); `Home`/`End` jump to a finite `min`/`max`.\n * - Increment/decrement buttons step too, and are `disabled` at the bounds (focus\n * is returned to the input before a focused button is disabled).\n * - **Press-and-hold auto-repeat**: holding a step button steps once, then after a\n * short delay repeats until release, the bound is reached, or the element\n * disconnects. The `click` binding stays the single-step path (a quick click, a\n * synthesized/programmatic click, or assistive activation), so a normal click\n * never double-steps — the trailing click after a hold is swallowed.\n * - Typed input is clamped and snapped to the step grid on `change`;\n * `stimeo--number-input:change` is dispatched on every committed change.\n */\nexport class NumberInputController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"increment\", \"decrement\"];\n static override values = {\n min: { type: Number, default: Number.NEGATIVE_INFINITY },\n max: { type: Number, default: Number.POSITIVE_INFINITY },\n step: { type: Number, default: 1 },\n pageStep: { type: Number, default: 0 },\n };\n static actions = [\"decrement\", \"increment\", \"onInput\", \"onKeydown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly hasInputTarget: boolean;\n declare readonly incrementTarget: HTMLButtonElement;\n declare readonly hasIncrementTarget: boolean;\n declare readonly decrementTarget: HTMLButtonElement;\n declare readonly hasDecrementTarget: boolean;\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare pageStepValue: number;\n\n /** Delay (ms) a button must be held before auto-repeat starts. */\n static readonly #HOLD_DELAY_MS = 400;\n /** Interval (ms) between auto-repeat steps once a hold has started. */\n static readonly #HOLD_REPEAT_MS = 80;\n /**\n * Window (ms) after a hold ends during which the trailing synthetic `click` is\n * swallowed. A safety net for the (rare) case where that click never arrives\n * (e.g. the pointer was released off the button): the flag self-clears instead\n * of poisoning the next legitimate click.\n */\n static readonly #SUPPRESS_RESET_MS = 250;\n\n /** Aborts the pointer listeners on disconnect so none outlive the element. */\n #guards: AbortController | null = null;\n /** Timers for the hold delay and the suppress-reset safety net. */\n readonly #holdTimeouts = new SafeTimeout();\n /** The running auto-repeat interval (one at a time). */\n readonly #holdIntervals = new SafeInterval();\n /** True while a hold is armed/running, making `#stopHold` idempotent. */\n #holdActive = false;\n /** True once a hold actually produced a repeated step (vs. a quick click). */\n #repeatedDuringHold = false;\n /** True when the next `click` is the trailing one after a hold and must be ignored. */\n #suppressNextClick = false;\n\n /** Normalizes any initial value and wires the focus/hold pointer guards. */\n override connect(): void {\n if (!this.hasInputTarget) return;\n if (this.inputTarget.value.trim() !== \"\") {\n this.#write(this.#normalize(this.#currentValue()));\n } else {\n this.#updateButtons(this.#currentValue());\n }\n this.#guards = new AbortController();\n const { signal } = this.#guards;\n if (this.hasIncrementTarget) this.#wireButton(this.incrementTarget, this.stepValue, signal);\n if (this.hasDecrementTarget) this.#wireButton(this.decrementTarget, -this.stepValue, signal);\n // A pointer released or focus lost anywhere stops a running hold, even when\n // the button stopped receiving its own pointerup (e.g. it became disabled at\n // the bound, or the pointer was released off it).\n window.addEventListener(\"pointerup\", this.#stopHold, { signal });\n window.addEventListener(\"pointercancel\", this.#stopHold, { signal });\n window.addEventListener(\"blur\", this.#stopHold, { signal });\n }\n\n /** Releases the pointer guards and tears down every pending timer and hold state. */\n override disconnect(): void {\n this.#guards?.abort();\n this.#guards = null;\n this.#holdActive = false;\n this.#repeatedDuringHold = false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.clearAll();\n this.#holdIntervals.clearAll();\n }\n\n /** Increases by one step. Bound via `data-action` (click). */\n increment(): void {\n if (this.#consumeSuppressedClick()) return;\n this.#commit(this.#currentValue() + this.stepValue);\n this.inputTarget.focus();\n }\n\n /** Decreases by one step. Bound via `data-action` (click). */\n decrement(): void {\n if (this.#consumeSuppressedClick()) return;\n this.#commit(this.#currentValue() - this.stepValue);\n this.inputTarget.focus();\n }\n\n /** Clamps and snaps a typed value. Bound via `data-action` (change). */\n onInput(): void {\n if (this.inputTarget.value.trim() === \"\") return;\n this.#commit(this.#currentValue());\n }\n\n /** Keyboard stepping per the APG spinbutton model. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n const page = this.pageStepValue > 0 ? this.pageStepValue : this.stepValue * 10;\n let next: number | null = null;\n switch (event.key) {\n case \"ArrowUp\":\n next = this.#currentValue() + this.stepValue;\n break;\n case \"ArrowDown\":\n next = this.#currentValue() - this.stepValue;\n break;\n case \"PageUp\":\n next = this.#currentValue() + page;\n break;\n case \"PageDown\":\n next = this.#currentValue() - page;\n break;\n case \"Home\":\n if (!Number.isFinite(this.minValue)) return;\n next = this.minValue;\n break;\n case \"End\":\n if (!Number.isFinite(this.maxValue)) return;\n next = this.maxValue;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#commit(next);\n }\n\n /**\n * Wires a step button: `pointerdown` keeps focus on the input and arms the\n * hold; leaving the button while held stops it (the global listeners cover\n * release/cancel/blur).\n */\n #wireButton(button: HTMLButtonElement, delta: number, signal: AbortSignal): void {\n button.addEventListener(\"pointerdown\", (event) => this.#armHold(event, button, delta), {\n signal,\n });\n button.addEventListener(\"pointerleave\", this.#stopHold, { signal });\n }\n\n /** Starts a hold: focus the input, then schedule the first repeat after a delay. */\n #armHold(event: Event, button: HTMLButtonElement, delta: number): void {\n // Ignore secondary buttons (right/middle) when the event exposes one.\n const pointerButton = (event as PointerEvent).button;\n if (typeof pointerButton === \"number\" && pointerButton !== 0) return;\n if (button.disabled) return;\n event.preventDefault(); // keep focus on the input rather than the button\n this.inputTarget.focus();\n\n this.#stopHold();\n this.#holdActive = true;\n this.#repeatedDuringHold = false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.set(() => {\n if (!this.#commit(this.#currentValue() + delta)) {\n this.#stopHold();\n return;\n }\n this.#repeatedDuringHold = true;\n this.#holdIntervals.set(() => {\n if (!this.#commit(this.#currentValue() + delta)) this.#stopHold();\n }, NumberInputController.#HOLD_REPEAT_MS);\n }, NumberInputController.#HOLD_DELAY_MS);\n }\n\n /**\n * Stops the current hold (idempotent). When the hold actually repeated, the\n * trailing synthetic `click` must be ignored, so it is suppressed until the\n * click consumes it or a short safety-net timeout clears it.\n */\n readonly #stopHold = (): void => {\n if (!this.#holdActive) return;\n this.#holdActive = false;\n this.#holdTimeouts.clearAll();\n this.#holdIntervals.clearAll();\n if (this.#repeatedDuringHold) {\n this.#suppressNextClick = true;\n this.#holdTimeouts.set(() => {\n this.#suppressNextClick = false;\n }, NumberInputController.#SUPPRESS_RESET_MS);\n }\n };\n\n /** Consumes a pending trailing-click suppression; returns true if the click was swallowed. */\n #consumeSuppressedClick(): boolean {\n if (!this.#suppressNextClick) return false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.clearAll();\n return true;\n }\n\n /**\n * Normalizes `raw`, reflects it, and dispatches `change` only when the\n * displayed value actually changes.\n *\n * @returns Whether the value changed (drives the auto-repeat's bound stop).\n */\n #commit(raw: number): boolean {\n const value = this.#normalize(raw);\n const changed = this.inputTarget.value !== String(value);\n this.#write(value);\n if (changed) this.dispatch(\"change\", { detail: { value } });\n return changed;\n }\n\n /** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */\n #write(value: number): void {\n this.inputTarget.value = String(value);\n if (this.inputTarget.getAttribute(\"role\") === \"spinbutton\") {\n this.inputTarget.setAttribute(\"aria-valuenow\", String(value));\n if (Number.isFinite(this.minValue)) {\n this.inputTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n }\n if (Number.isFinite(this.maxValue)) {\n this.inputTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n }\n }\n this.#updateButtons(value);\n }\n\n /** Disables a step button at its bound, returning focus to the input first. */\n #updateButtons(value: number): void {\n if (this.hasIncrementTarget) this.#toggleButton(this.incrementTarget, value < this.maxValue);\n if (this.hasDecrementTarget) this.#toggleButton(this.decrementTarget, value > this.minValue);\n }\n\n /**\n * Enables or disables a step button at its bound, never disabling one while it\n * holds focus. Owns only the `disabled` it sets itself via a marker\n * (`data-number-input-disabled`, like `conditional-fields`/`submit-once`), so an\n * author-disabled button (e.g. the whole control disabled) is never re-enabled.\n */\n #toggleButton(button: HTMLButtonElement, enabled: boolean): void {\n if (enabled) {\n if (button.hasAttribute(\"data-number-input-disabled\")) {\n button.disabled = false;\n button.removeAttribute(\"data-number-input-disabled\");\n }\n return;\n }\n if (button.disabled) return; // already disabled (possibly by the author) — leave it\n if (document.activeElement === button) this.inputTarget.focus();\n button.disabled = true;\n button.setAttribute(\"data-number-input-disabled\", \"\");\n }\n\n /** The current numeric value, falling back to a finite min (else 0) when blank. */\n #currentValue(): number {\n const parsed = Number(this.inputTarget.value);\n if (Number.isFinite(parsed) && this.inputTarget.value.trim() !== \"\") return parsed;\n return Number.isFinite(this.minValue) ? this.minValue : 0;\n }\n\n /** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */\n #normalize(raw: number): number {\n const clamped = Math.min(this.maxValue, Math.max(this.minValue, raw));\n if (this.stepValue <= 0) return clamped;\n const base = Number.isFinite(this.minValue) ? this.minValue : 0;\n const stepped = Math.round((clamped - base) / this.stepValue) * this.stepValue + base;\n return Math.min(this.maxValue, Math.max(this.minValue, stepped));\n }\n}\n"]}
|
|
@@ -2,6 +2,22 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/otp_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/logical_scroll.ts
|
|
6
|
+
function isRtl(element) {
|
|
7
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/utils/arrow_step.ts
|
|
11
|
+
function logicalArrowKey(key, element) {
|
|
12
|
+
if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
|
|
13
|
+
if (!isRtl(element)) return key;
|
|
14
|
+
return key === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
|
|
15
|
+
}
|
|
16
|
+
function isReservedArrowChord(event, allow = []) {
|
|
17
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
18
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
// src/utils/composition_tracker.ts
|
|
6
22
|
var CompositionTracker = class {
|
|
7
23
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
@@ -99,12 +115,13 @@ var OtpController = class extends Controller {
|
|
|
99
115
|
}
|
|
100
116
|
/** Handles Backspace retreating, arrows, and home/end navigation. */
|
|
101
117
|
onKeydown(event) {
|
|
118
|
+
if (isReservedArrowChord(event)) return;
|
|
102
119
|
const input = event.currentTarget;
|
|
103
120
|
if (!input) return;
|
|
104
121
|
const index = this.fieldTargets.indexOf(input);
|
|
105
122
|
if (index === -1) return;
|
|
106
123
|
if (this.#composition.isComposing(event)) return;
|
|
107
|
-
switch (event.key) {
|
|
124
|
+
switch (logicalArrowKey(event.key, this.element)) {
|
|
108
125
|
case "Backspace":
|
|
109
126
|
if (!input.value) {
|
|
110
127
|
if (index > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/composition_tracker.ts","../../src/controllers/otp_controller.ts"],"names":[],"mappings":"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","file":"otp_controller.js","sourcesContent":["/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\n\n/**\n * Headless, accessible One-Time Password / PIN input logic.\n *\n * Markup contract (identifier: `stimeo--otp`):\n * <div data-controller=\"stimeo--otp\"\n * data-stimeo--otp-length-value=\"6\"\n * data-stimeo--otp-pattern-value=\"[0-9]\"\n * role=\"group\" aria-label=\"One-time passcode\">\n * <input data-stimeo--otp-target=\"field\" inputmode=\"numeric\" maxlength=\"1\"\n * data-action=\"input->stimeo--otp#onInput\n * keydown->stimeo--otp#onKeydown\n * paste->stimeo--otp#onPaste\" />\n * <input type=\"hidden\" data-stimeo--otp-target=\"value\" />\n * </div>\n *\n * Implements highly polished UX conventions for single-digit field groups:\n * - Dynamic autofocus advancement upon valid inputs.\n * - Auto-selection `select()` upon gaining focus to enable frictionless digit overwrites.\n * - Retroactive Backspace key focus stepping to clear preceding values safely.\n * - Clipboard intercepting paste division across fields with trailing focus positioning.\n * - Hidden form sync and complete/change event dispatching.\n *\n * @remarks\n * Behavior only. The controller binds events dynamically to pre-rendered inputs,\n * updates `data-filled` and hidden value bindings, and emits Stimulus custom events.\n */\nexport class OtpController extends Controller<HTMLElement> {\n static override targets = [\"field\", \"value\", \"error\"];\n static override values = {\n length: { type: Number, default: 6 },\n pattern: { type: String, default: \"[0-9]\" },\n };\n static actions = [\"onInput\", \"onKeydown\", \"onPaste\"] as const;\n static events = [\"change\", \"complete\", \"invalid\"] as const;\n\n declare readonly fieldTargets: HTMLInputElement[];\n declare readonly valueTarget: HTMLInputElement;\n declare readonly errorTarget: HTMLElement;\n declare readonly hasValueTarget: boolean;\n declare readonly hasErrorTarget: boolean;\n\n declare lengthValue: number;\n declare patternValue: string;\n\n /** Owns IME lifecycle state across every digit field. */\n readonly #composition = new CompositionTracker({\n onEnd: (event) => {\n const input = event.currentTarget as HTMLInputElement | null;\n if (input) this.#handleInputValidation(input);\n },\n });\n\n override connect(): void {\n // Dynamically bind auto-selection and IME tracking logic to all connected inputs\n for (const field of this.fieldTargets) {\n field.addEventListener(\"focus\", this.#onFieldFocus);\n this.#composition.observe(field);\n }\n }\n\n override disconnect(): void {\n for (const field of this.fieldTargets) {\n field.removeEventListener(\"focus\", this.#onFieldFocus);\n }\n this.#composition.disconnect();\n }\n\n /**\n * Stimulus lifecycle callback when a new field target enters the DOM.\n * Ensures new additions are also wired with overwriting support.\n */\n fieldTargetConnected(element: HTMLInputElement): void {\n element.addEventListener(\"focus\", this.#onFieldFocus);\n this.#composition.observe(element);\n }\n\n /** Removes focus listeners when fields are dropped. */\n fieldTargetDisconnected(element: HTMLInputElement): void {\n element.removeEventListener(\"focus\", this.#onFieldFocus);\n this.#composition.unobserve(element);\n }\n\n /** Handles keystroke inputs and advances focus to the next field. */\n onInput(event: Event): void {\n const input = event.currentTarget as HTMLInputElement | null;\n if (!input) return;\n\n // Guard during active composition to prevent premature focus switching\n if (this.#composition.isComposing(event as InputEvent)) return;\n\n this.#handleInputValidation(input);\n }\n\n /** Handles Backspace retreating, arrows, and home/end navigation. */\n onKeydown(event: KeyboardEvent): void {\n const input = event.currentTarget as HTMLInputElement | null;\n if (!input) return;\n\n const index = this.fieldTargets.indexOf(input);\n if (index === -1) return;\n\n // Do not trigger keydown actions during composition\n if (this.#composition.isComposing(event)) return;\n\n switch (event.key) {\n case \"Backspace\":\n if (!input.value) {\n // Empty field: step backward, wipe previous digit, and focus it\n if (index > 0) {\n event.preventDefault();\n const prevField = this.fieldTargets[index - 1];\n if (prevField) {\n prevField.value = \"\";\n prevField.removeAttribute(\"data-filled\");\n prevField.focus();\n this.#clearError();\n this.#syncAndDispatch();\n }\n }\n } else {\n // Filled field: clear current value\n input.value = \"\";\n input.removeAttribute(\"data-filled\");\n this.#clearError();\n this.#syncAndDispatch();\n }\n break;\n\n case \"ArrowLeft\":\n if (index > 0) {\n event.preventDefault();\n this.fieldTargets[index - 1]?.focus();\n }\n break;\n\n case \"ArrowRight\":\n if (index < this.lengthValue - 1) {\n event.preventDefault();\n this.fieldTargets[index + 1]?.focus();\n }\n break;\n\n case \"Home\":\n event.preventDefault();\n this.fieldTargets[0]?.focus();\n break;\n\n case \"End\":\n event.preventDefault();\n this.fieldTargets[this.lengthValue - 1]?.focus();\n break;\n\n default:\n break;\n }\n }\n\n /** Divides pasted string characters across available input fields. */\n onPaste(event: ClipboardEvent): void {\n const input = event.currentTarget as HTMLInputElement | null;\n if (!input) return;\n\n const startIndex = this.fieldTargets.indexOf(input);\n if (startIndex === -1) return;\n\n event.preventDefault();\n\n const rawText = event.clipboardData?.getData(\"text\") || \"\";\n const text = this.#normalizeValue(rawText);\n const regex = new RegExp(`^${this.patternValue}$`);\n const validChars = Array.from(text).filter((char) => regex.test(char));\n\n if (validChars.length === 0) {\n this.#showError();\n return;\n }\n\n const limit = Math.min(validChars.length, this.lengthValue - startIndex);\n let lastFocusedIndex = startIndex;\n\n for (let i = 0; i < limit; i++) {\n const fieldIndex = startIndex + i;\n const field = this.fieldTargets[fieldIndex];\n const char = validChars[i];\n\n if (field && char) {\n field.value = char;\n field.setAttribute(\"data-filled\", \"true\");\n lastFocusedIndex = fieldIndex;\n }\n }\n\n this.#clearError();\n\n // Set trailing focus position to the last filled box (or next empty if one exists)\n const focusTargetIndex =\n lastFocusedIndex < this.lengthValue - 1 ? lastFocusedIndex + 1 : lastFocusedIndex;\n this.fieldTargets[focusTargetIndex]?.focus();\n\n this.#syncAndDispatch();\n }\n\n readonly #onFieldFocus = (event: FocusEvent): void => {\n const input = event.currentTarget as HTMLInputElement | null;\n if (input) {\n // Auto-selection enables effortless character overwrites\n input.select();\n }\n };\n\n #handleInputValidation(input: HTMLInputElement): void {\n const index = this.fieldTargets.indexOf(input);\n if (index === -1) return;\n\n const rawValue = input.value;\n const normalized = this.#normalizeValue(rawValue);\n const regex = new RegExp(`^${this.patternValue}$`);\n\n if (normalized && regex.test(normalized)) {\n input.value = normalized;\n input.setAttribute(\"data-filled\", \"true\");\n this.#clearError();\n\n // Step focus forward\n if (index < this.lengthValue - 1) {\n const nextField = this.fieldTargets[index + 1];\n nextField?.focus();\n }\n } else if (normalized) {\n input.value = \"\";\n input.removeAttribute(\"data-filled\");\n this.#showError();\n } else {\n input.removeAttribute(\"data-filled\");\n }\n\n this.#syncAndDispatch();\n }\n\n #normalizeValue(val: string): string {\n // Replaces full-width numbers (0-9) with standard half-width ones (0-9)\n return val.replace(/[0-9]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0));\n }\n\n #showError(): void {\n if (this.hasErrorTarget) {\n this.errorTarget.removeAttribute(\"hidden\");\n }\n // Behavior only: emit a neutral payload and let the consumer compose/localize\n // the user-facing message. The pattern is reported so consumers can word it.\n this.dispatch(\"invalid\", { detail: { pattern: this.patternValue } });\n }\n\n #clearError(): void {\n if (this.hasErrorTarget) {\n this.errorTarget.setAttribute(\"hidden\", \"true\");\n }\n }\n\n #syncAndDispatch(): void {\n const fields = this.fieldTargets;\n const combinedValue = fields.map((f) => f.value).join(\"\");\n\n if (this.hasValueTarget) {\n this.valueTarget.value = combinedValue;\n }\n\n this.dispatch(\"change\", { detail: { value: combinedValue } });\n\n // Completed state when all fields contain a digit\n const isCompleted =\n fields.every((f) => f.value.length > 0) && combinedValue.length === this.lengthValue;\n if (isCompleted) {\n this.dispatch(\"complete\", { detail: { value: combinedValue } });\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/composition_tracker.ts","../../src/controllers/otp_controller.ts"],"names":[],"mappings":"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","file":"otp_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\n\n/**\n * Headless, accessible One-Time Password / PIN input logic.\n *\n * Markup contract (identifier: `stimeo--otp`):\n * <div data-controller=\"stimeo--otp\"\n * data-stimeo--otp-length-value=\"6\"\n * data-stimeo--otp-pattern-value=\"[0-9]\"\n * role=\"group\" aria-label=\"One-time passcode\">\n * <input data-stimeo--otp-target=\"field\" inputmode=\"numeric\" maxlength=\"1\"\n * data-action=\"input->stimeo--otp#onInput\n * keydown->stimeo--otp#onKeydown\n * paste->stimeo--otp#onPaste\" />\n * <input type=\"hidden\" data-stimeo--otp-target=\"value\" />\n * </div>\n *\n * Implements highly polished UX conventions for single-digit field groups:\n * - Dynamic autofocus advancement upon valid inputs.\n * - Auto-selection `select()` upon gaining focus to enable frictionless digit overwrites.\n * - Retroactive Backspace key focus stepping to clear preceding values safely.\n * - Clipboard intercepting paste division across fields with trailing focus positioning.\n * - Hidden form sync and complete/change event dispatching.\n *\n * @remarks\n * Behavior only. The controller binds events dynamically to pre-rendered inputs,\n * updates `data-filled` and hidden value bindings, and emits Stimulus custom events.\n */\nexport class OtpController extends Controller<HTMLElement> {\n static override targets = [\"field\", \"value\", \"error\"];\n static override values = {\n length: { type: Number, default: 6 },\n pattern: { type: String, default: \"[0-9]\" },\n };\n static actions = [\"onInput\", \"onKeydown\", \"onPaste\"] as const;\n static events = [\"change\", \"complete\", \"invalid\"] as const;\n\n declare readonly fieldTargets: HTMLInputElement[];\n declare readonly valueTarget: HTMLInputElement;\n declare readonly errorTarget: HTMLElement;\n declare readonly hasValueTarget: boolean;\n declare readonly hasErrorTarget: boolean;\n\n declare lengthValue: number;\n declare patternValue: string;\n\n /** Owns IME lifecycle state across every digit field. */\n readonly #composition = new CompositionTracker({\n onEnd: (event) => {\n const input = event.currentTarget as HTMLInputElement | null;\n if (input) this.#handleInputValidation(input);\n },\n });\n\n override connect(): void {\n // Dynamically bind auto-selection and IME tracking logic to all connected inputs\n for (const field of this.fieldTargets) {\n field.addEventListener(\"focus\", this.#onFieldFocus);\n this.#composition.observe(field);\n }\n }\n\n override disconnect(): void {\n for (const field of this.fieldTargets) {\n field.removeEventListener(\"focus\", this.#onFieldFocus);\n }\n this.#composition.disconnect();\n }\n\n /**\n * Stimulus lifecycle callback when a new field target enters the DOM.\n * Ensures new additions are also wired with overwriting support.\n */\n fieldTargetConnected(element: HTMLInputElement): void {\n element.addEventListener(\"focus\", this.#onFieldFocus);\n this.#composition.observe(element);\n }\n\n /** Removes focus listeners when fields are dropped. */\n fieldTargetDisconnected(element: HTMLInputElement): void {\n element.removeEventListener(\"focus\", this.#onFieldFocus);\n this.#composition.unobserve(element);\n }\n\n /** Handles keystroke inputs and advances focus to the next field. */\n onInput(event: Event): void {\n const input = event.currentTarget as HTMLInputElement | null;\n if (!input) return;\n\n // Guard during active composition to prevent premature focus switching\n if (this.#composition.isComposing(event as InputEvent)) return;\n\n this.#handleInputValidation(input);\n }\n\n /** Handles Backspace retreating, arrows, and home/end navigation. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n const input = event.currentTarget as HTMLInputElement | null;\n if (!input) return;\n\n const index = this.fieldTargets.indexOf(input);\n if (index === -1) return;\n\n // Do not trigger keydown actions during composition\n if (this.#composition.isComposing(event)) return;\n // Logical, not physical. The key is normalised rather than the\n // delta negated: these two branches are not mirror images — their bounds\n // guards differ — so swapping the key keeps each guard with its own direction.\n\n switch (logicalArrowKey(event.key, this.element)) {\n case \"Backspace\":\n if (!input.value) {\n // Empty field: step backward, wipe previous digit, and focus it\n if (index > 0) {\n event.preventDefault();\n const prevField = this.fieldTargets[index - 1];\n if (prevField) {\n prevField.value = \"\";\n prevField.removeAttribute(\"data-filled\");\n prevField.focus();\n this.#clearError();\n this.#syncAndDispatch();\n }\n }\n } else {\n // Filled field: clear current value\n input.value = \"\";\n input.removeAttribute(\"data-filled\");\n this.#clearError();\n this.#syncAndDispatch();\n }\n break;\n\n case \"ArrowLeft\":\n if (index > 0) {\n event.preventDefault();\n this.fieldTargets[index - 1]?.focus();\n }\n break;\n\n case \"ArrowRight\":\n if (index < this.lengthValue - 1) {\n event.preventDefault();\n this.fieldTargets[index + 1]?.focus();\n }\n break;\n\n case \"Home\":\n event.preventDefault();\n this.fieldTargets[0]?.focus();\n break;\n\n case \"End\":\n event.preventDefault();\n this.fieldTargets[this.lengthValue - 1]?.focus();\n break;\n\n default:\n break;\n }\n }\n\n /** Divides pasted string characters across available input fields. */\n onPaste(event: ClipboardEvent): void {\n const input = event.currentTarget as HTMLInputElement | null;\n if (!input) return;\n\n const startIndex = this.fieldTargets.indexOf(input);\n if (startIndex === -1) return;\n\n event.preventDefault();\n\n const rawText = event.clipboardData?.getData(\"text\") || \"\";\n const text = this.#normalizeValue(rawText);\n const regex = new RegExp(`^${this.patternValue}$`);\n const validChars = Array.from(text).filter((char) => regex.test(char));\n\n if (validChars.length === 0) {\n this.#showError();\n return;\n }\n\n const limit = Math.min(validChars.length, this.lengthValue - startIndex);\n let lastFocusedIndex = startIndex;\n\n for (let i = 0; i < limit; i++) {\n const fieldIndex = startIndex + i;\n const field = this.fieldTargets[fieldIndex];\n const char = validChars[i];\n\n if (field && char) {\n field.value = char;\n field.setAttribute(\"data-filled\", \"true\");\n lastFocusedIndex = fieldIndex;\n }\n }\n\n this.#clearError();\n\n // Set trailing focus position to the last filled box (or next empty if one exists)\n const focusTargetIndex =\n lastFocusedIndex < this.lengthValue - 1 ? lastFocusedIndex + 1 : lastFocusedIndex;\n this.fieldTargets[focusTargetIndex]?.focus();\n\n this.#syncAndDispatch();\n }\n\n readonly #onFieldFocus = (event: FocusEvent): void => {\n const input = event.currentTarget as HTMLInputElement | null;\n if (input) {\n // Auto-selection enables effortless character overwrites\n input.select();\n }\n };\n\n #handleInputValidation(input: HTMLInputElement): void {\n const index = this.fieldTargets.indexOf(input);\n if (index === -1) return;\n\n const rawValue = input.value;\n const normalized = this.#normalizeValue(rawValue);\n const regex = new RegExp(`^${this.patternValue}$`);\n\n if (normalized && regex.test(normalized)) {\n input.value = normalized;\n input.setAttribute(\"data-filled\", \"true\");\n this.#clearError();\n\n // Step focus forward\n if (index < this.lengthValue - 1) {\n const nextField = this.fieldTargets[index + 1];\n nextField?.focus();\n }\n } else if (normalized) {\n input.value = \"\";\n input.removeAttribute(\"data-filled\");\n this.#showError();\n } else {\n input.removeAttribute(\"data-filled\");\n }\n\n this.#syncAndDispatch();\n }\n\n #normalizeValue(val: string): string {\n // Replaces full-width numbers (0-9) with standard half-width ones (0-9)\n return val.replace(/[0-9]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0));\n }\n\n #showError(): void {\n if (this.hasErrorTarget) {\n this.errorTarget.removeAttribute(\"hidden\");\n }\n // Behavior only: emit a neutral payload and let the consumer compose/localize\n // the user-facing message. The pattern is reported so consumers can word it.\n this.dispatch(\"invalid\", { detail: { pattern: this.patternValue } });\n }\n\n #clearError(): void {\n if (this.hasErrorTarget) {\n this.errorTarget.setAttribute(\"hidden\", \"true\");\n }\n }\n\n #syncAndDispatch(): void {\n const fields = this.fieldTargets;\n const combinedValue = fields.map((f) => f.value).join(\"\");\n\n if (this.hasValueTarget) {\n this.valueTarget.value = combinedValue;\n }\n\n this.dispatch(\"change\", { detail: { value: combinedValue } });\n\n // Completed state when all fields contain a digit\n const isCompleted =\n fields.every((f) => f.value.length > 0) && combinedValue.length === this.lengthValue;\n if (isCompleted) {\n this.dispatch(\"complete\", { detail: { value: combinedValue } });\n }\n }\n}\n"]}
|
|
@@ -22,15 +22,17 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
22
22
|
* </div>
|
|
23
23
|
*
|
|
24
24
|
* The viewport's scroll position and size are watched (via the wired `scroll`
|
|
25
|
-
* action, plus {@link LayoutObserver} for resize
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* action, plus {@link LayoutObserver} for viewport/content resize, a
|
|
26
|
+
* {@link MutationObserver}, and descendant load events). Optional page buttons
|
|
27
|
+
* scroll one logical viewport page at a time (including RTL) and have their
|
|
28
|
+
* disabled state synced to the matching direction's remaining room.
|
|
28
29
|
*
|
|
29
30
|
* @remarks
|
|
30
31
|
* Behavior only — shadows, arrows, and gradients are the consumer's CSS;
|
|
31
32
|
* `data-overflow-*` carry no ARIA semantics. All observers/listeners are released
|
|
32
|
-
* on `disconnect()` (Turbo navigation included). `scrollByPage`
|
|
33
|
-
* `prefers-reduced-motion
|
|
33
|
+
* on `disconnect()` (Turbo navigation included). `scrollByPage` scrolls smoothly
|
|
34
|
+
* by default and forces an instant jump under `prefers-reduced-motion`, regardless
|
|
35
|
+
* of the consumer's CSS `scroll-behavior`.
|
|
34
36
|
*/
|
|
35
37
|
declare class OverflowIndicatorController extends Controller<HTMLElement> {
|
|
36
38
|
#private;
|
|
@@ -53,7 +55,14 @@ declare class OverflowIndicatorController extends Controller<HTMLElement> {
|
|
|
53
55
|
thresholdValue: number;
|
|
54
56
|
connect(): void;
|
|
55
57
|
disconnect(): void;
|
|
56
|
-
|
|
58
|
+
viewportTargetConnected(): void;
|
|
59
|
+
viewportTargetDisconnected(viewport: HTMLElement): void;
|
|
60
|
+
orientationValueChanged(): void;
|
|
61
|
+
thresholdValueChanged(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Re-measures remaining scroll room and reflects the state hooks.
|
|
64
|
+
* Public so it can be wired to the viewport's `scroll`.
|
|
65
|
+
*/
|
|
57
66
|
update(): void;
|
|
58
67
|
/** Scrolls one viewport page toward the `direction` param (`start`/`end`). */
|
|
59
68
|
scrollByPage(event: Event): void;
|