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/pagination_controller.ts"],"names":[],"mappings":"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","file":"pagination_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless, accessible pagination behavior.\n *\n * Markup contract (identifier: `stimeo--pagination`):\n * <nav data-controller=\"stimeo--pagination\" aria-label=\"Pagination\"\n * data-stimeo--pagination-page-value=\"1\"\n * data-stimeo--pagination-total-value=\"5\">\n * <button type=\"button\" data-stimeo--pagination-target=\"prev\"\n * data-action=\"stimeo--pagination#prev\">Prev</button>\n * <button type=\"button\" data-page=\"1\" aria-current=\"page\"\n * data-stimeo--pagination-target=\"page\"\n * data-action=\"stimeo--pagination#select\">1</button>\n * <!-- more page buttons -->\n * <button type=\"button\" data-stimeo--pagination-target=\"next\"\n * data-action=\"stimeo--pagination#next\">Next</button>\n * </nav>\n *\n * There is no dedicated APG pattern; this uses a navigation landmark plus\n * `aria-current=\"page\"`. The controller owns current-page state, the\n * `aria-current` sync, boundary disabling of prev/next, and the change event.\n * Generating/eliding the page buttons and fetching data stay with the consumer.\n *\n * @remarks\n * Behavior only — each control is in the natural Tab order (no roving). When a\n * boundary disables the button that currently has focus, focus is moved first so\n * it is never lost to a `disabled` element.\n *\n * Behavior provided:\n * - `select` reads the clicked button's `data-page` and makes it current.\n * - `prev`/`next` step by one, clamped to `[1, total]`.\n * - The current page button gets `aria-current=\"page\"` (removed from the rest);\n * `prev` is `disabled` at page 1 and `next` at `total`.\n * - Every change dispatches `stimeo--pagination:change`.\n */\nexport class PaginationController extends Controller<HTMLElement> {\n static override targets = [\"page\", \"prev\", \"next\"];\n static override values = {\n page: { type: Number, default: 1 },\n total: { type: Number, default: 1 },\n };\n static actions = [\"next\", \"prev\", \"select\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly pageTargets: HTMLElement[];\n declare readonly prevTarget: HTMLButtonElement;\n declare readonly nextTarget: HTMLButtonElement;\n declare readonly hasPrevTarget: boolean;\n declare readonly hasNextTarget: boolean;\n declare pageValue: number;\n declare totalValue: number;\n\n /** Normalizes out-of-range initial values and renders the initial state. */\n override connect(): void {\n this.totalValue = Math.max(1, Math.trunc(this.totalValue));\n this.pageValue = Math.min(this.totalValue, Math.max(1, Math.trunc(this.pageValue)));\n this.#render();\n }\n\n /** Makes the clicked page button (its `data-page`) current. */\n select(event: Event): void {\n const button = event.currentTarget as HTMLElement;\n const page = Number(button.dataset.page);\n if (!Number.isFinite(page)) return;\n this.#goto(page);\n }\n\n /** Steps to the previous page. */\n prev(): void {\n this.#goto(this.pageValue - 1);\n }\n\n /** Steps to the next page. */\n next(): void {\n this.#goto(this.pageValue + 1);\n }\n\n /** Moves to `page` (clamped to `[1, total]`), re-renders, and dispatches `change`. */\n #goto(page: number): void {\n // Normalize total first so a stray total<=0 can't clamp the 1-based page to 0.\n const total = Math.max(1, Math.trunc(this.totalValue));\n const target = Math.min(total, Math.max(1, Math.trunc(page)));\n if (target === this.pageValue) return;\n const previous = this.pageValue;\n this.pageValue = target;\n this.#render();\n this.dispatch(\"change\", {\n detail: { page: target, total: this.totalValue, previous },\n });\n }\n\n /** Syncs `aria-current` on the page buttons and the prev/next `disabled` state. */\n #render(): void {\n for (const button of this.pageTargets) {\n if (Number(button.dataset.page) === this.pageValue) {\n button.setAttribute(\"aria-current\", \"page\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n }\n this.#setDisabled(this.hasPrevTarget ? this.prevTarget : null, this.pageValue <= 1);\n this.#setDisabled(\n this.hasNextTarget ? this.nextTarget : null,\n this.pageValue >= this.totalValue,\n );\n }\n\n /**\n * Sets `disabled` on a boundary button, first moving focus off it if it is the\n * active element so disabling never strands focus.\n */\n #setDisabled(button: HTMLButtonElement | null, disabled: boolean): void {\n if (!button) return;\n if (disabled && button === document.activeElement) {\n this.#moveFocusAwayFrom(button);\n }\n button.disabled = disabled;\n }\n\n /** Moves focus to the opposite enabled control, falling back to the current page. */\n #moveFocusAwayFrom(button: HTMLButtonElement): void {\n const opposite =\n button === (this.hasPrevTarget ? this.prevTarget : null)\n ? this.hasNextTarget\n ? this.nextTarget\n : null\n : this.hasPrevTarget\n ? this.prevTarget\n : null;\n const currentPage = this.pageTargets.find(\n (candidate) => Number(candidate.dataset.page) === this.pageValue,\n );\n const destination = opposite && !opposite.disabled ? opposite : currentPage;\n if (destination) {\n destination.focus();\n return;\n }\n // Degenerate config (e.g. a lone boundary button, no page buttons): keep focus\n // inside the pagination landmark instead of letting the browser drop it to\n // <body> when the button disables. The root is made programmatically focusable\n // just-in-time with `tabindex=\"-1\"`, which is not a Tab stop.\n if (!this.element.hasAttribute(\"tabindex\")) this.element.setAttribute(\"tabindex\", \"-1\");\n this.element.focus();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/focus_candidate.ts","../../src/utils/tabindex_loan.ts","../../src/controllers/pagination_controller.ts"],"names":[],"mappings":"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","file":"pagination_controller.js","sourcesContent":["/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported for the consumer that needs this rule *without* the rest of\n * {@link canTakeFocus}: `toolbar` bounds its own `hidden` walk at the toolbar\n * root, which is a different rule, so it composes the two itself.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the three conditions that make the platform refuse\n * focus are checked: `hidden` (an ancestor's counts), the native `disabled`\n * property, and `disabled` inherited from an ancestor `fieldset`. CSS-only\n * invisibility is not detectable here and stays the consumer's problem.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden]\")) return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n","/**\n * Shared bookkeeping for a `tabindex` a controller lends an element temporarily.\n *\n * A controller that must move focus somewhere the author never made focusable\n * (a landmark root, a scroll destination) reaches for the same trick: add a\n * `tabindex` just-in-time and hand it back once it is no longer needed. The\n * borrow is the easy half; the return is what the two conditions below are for.\n *\n * **Returning needs two conditions, not one.** Owning the borrow is not enough:\n * the attribute must also still hold the value this instance wrote. A consumer\n * that changed it afterwards — `tabindex=\"0\"` to make the root its own Tab stop\n * — owns it now, and removing it there silently discards authored markup. The\n * bookkeeping is dropped either way, since the loan is over regardless of who\n * ends up owning the value.\n *\n * **Never borrow over an existing value.** An element that already carries a\n * `tabindex` is the author's to control, so there is nothing to lend and nothing\n * to return.\n *\n * The registry is keyed by element, so a controller borrowing on a single\n * element (`this.element`) and one borrowing across a changing set of targets\n * use the same API — the single-element case is a set of one. It holds no\n * opinion about *when* to borrow or where focus goes next; that stays in the\n * controller.\n *\n * **The API is deliberately two methods.** This file's own doc block is dropped\n * from `dist`, but every member comment is inlined into **each** consumer entry\n * (`tsup` builds with `splitting: false`), so rationale belongs here, only the\n * contract belongs on the members, and every method no consumer calls is still\n * paid for once per consumer entry.\n *\n * @example\n * ```ts\n * readonly #tabindex = new TabindexLoan();\n *\n * #rescueFocus() {\n * this.#tabindex.lend(this.element);\n * this.element.focus();\n * }\n *\n * disconnect() {\n * this.#tabindex.returnAll();\n * }\n * ```\n */\nexport class TabindexLoan<T extends HTMLElement = HTMLElement> {\n readonly #value: string;\n readonly #lent = new Set<T>();\n\n /**\n * @param value - the `tabindex` to lend. `\"-1\"` (the default) is\n * programmatically focusable but not a Tab stop; `\"0\"` is a real Tab stop,\n * which a scroll region with no focusable content of its own needs.\n */\n constructor(value: string = \"-1\") {\n this.#value = value;\n }\n\n /** Lends `element` the value; no-ops when it already carries a `tabindex`. */\n lend(element: T): void {\n if (element.hasAttribute(\"tabindex\")) return;\n element.setAttribute(\"tabindex\", this.#value);\n this.#lent.add(element);\n }\n\n /** Takes back every loan whose value is still the one that was lent. */\n returnAll(): void {\n for (const element of this.#lent) {\n if (element.getAttribute(\"tabindex\") === this.#value) element.removeAttribute(\"tabindex\");\n }\n this.#lent.clear();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { canTakeFocus } from \"../utils/focus_candidate\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\n\n/**\n * Headless, accessible pagination behavior.\n *\n * Markup contract (identifier: `stimeo--pagination`):\n * <nav data-controller=\"stimeo--pagination\" aria-label=\"Pagination\"\n * data-stimeo--pagination-page-value=\"1\"\n * data-stimeo--pagination-total-value=\"5\">\n * <button type=\"button\" data-stimeo--pagination-target=\"prev\"\n * data-action=\"click->stimeo--pagination#prev\">Prev</button>\n * <button type=\"button\" data-page=\"1\" aria-current=\"page\"\n * data-stimeo--pagination-target=\"page\"\n * data-action=\"click->stimeo--pagination#select\">1</button>\n * <!-- more page buttons -->\n * <button type=\"button\" data-stimeo--pagination-target=\"next\"\n * data-action=\"click->stimeo--pagination#next\">Next</button>\n * </nav>\n *\n * There is no dedicated APG pattern; this uses a navigation landmark plus\n * `aria-current=\"page\"`. The controller owns current-page state, the\n * `aria-current` sync, boundary disabling of prev/next, and the change event.\n * Generating/eliding the page buttons and fetching data stay with the consumer.\n *\n * `prev`/`next` must be real `<button>` elements: the boundary state is applied\n * through the native `disabled` property, which no other element honors.\n *\n * @remarks\n * Behavior only — each control is in the natural Tab order (no roving). When a\n * boundary disables the button that currently has focus, focus is moved first so\n * it is never lost to a `disabled` element.\n *\n * Behavior provided:\n * - `select` reads the clicked button's `data-page` and makes it current. The\n * value must parse as an integer; blank/fractional/non-numeric ones are ignored.\n * - `prev`/`next` step by one, clamped to `[1, total]`.\n * - The current page button gets `aria-current=\"page\"` (removed from the rest);\n * `prev` is `disabled` at page 1 and `next` at `total`.\n * - `page`/`total` are normalized (`total` to a finite integer >= 1, `page` into\n * `[1, total]`) on connect **and** on every runtime Value change, so JS-driven\n * updates re-render. Normalization never dispatches `change`: it is a display\n * correction, not a user navigation, and the initial render is consumer-owned.\n * - Page/boundary buttons swapped in at runtime re-render through the Stimulus\n * target callbacks, so a consumer-regenerated button list stays in sync.\n * - Every navigation dispatches `stimeo--pagination:change`, whose `detail.total`\n * is the same normalized total the boundary state is derived from.\n *\n * The boundary `disabled` is **owned**: the controller marks what it disabled\n * with `data-stimeo--pagination-boundary-disabled` and releases only that. A\n * button already disabled by the consumer is never marked when it overlaps a\n * boundary, so loading / permission state survives both entering and leaving the\n * boundary. The marker lives in the DOM rather than in a field so ownership\n * survives a Turbo cache restore, where a fresh instance would otherwise either\n * strand or steal the flag.\n */\nexport class PaginationController extends Controller<HTMLElement> {\n static override targets = [\"page\", \"prev\", \"next\"];\n static override values = {\n page: { type: Number, default: 1 },\n total: { type: Number, default: 1 },\n };\n static actions = [\"next\", \"prev\", \"select\"] as const;\n static events = [\"change\"] as const;\n\n /** Marks a `disabled` this controller applied at a boundary (ownership flag). */\n static readonly #BOUNDARY_ATTR = \"data-stimeo--pagination-boundary-disabled\";\n\n declare readonly pageTargets: HTMLElement[];\n declare readonly prevTarget: HTMLButtonElement;\n declare readonly nextTarget: HTMLButtonElement;\n declare readonly hasPrevTarget: boolean;\n declare readonly hasNextTarget: boolean;\n declare pageValue: number;\n declare totalValue: number;\n\n #isConnected = false;\n /** The `tabindex` this instance lends the root for the focus fallback. */\n readonly #tabindex = new TabindexLoan();\n\n /** Normalizes out-of-range initial values and renders the initial state. */\n override connect(): void {\n this.#isConnected = true;\n this.#normalizeAndRender();\n }\n\n /** Reverts the one attribute the controller adds outside its state hooks. */\n override disconnect(): void {\n this.#isConnected = false;\n this.#tabindex.returnAll();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `page` at runtime. */\n pageValueChanged(): void {\n if (!this.#isConnected) return;\n this.#normalizeAndRender();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `total` at runtime. */\n totalValueChanged(): void {\n if (!this.#isConnected) return;\n this.#normalizeAndRender();\n }\n\n /** Syncs a page button appended/replaced at runtime (the consumer owns the list). */\n pageTargetConnected(): void {\n if (!this.#isConnected) return;\n this.#render();\n }\n\n /** Syncs a `prev` button appended/replaced at runtime. */\n prevTargetConnected(): void {\n if (!this.#isConnected) return;\n this.#render();\n }\n\n /** Syncs a `next` button appended/replaced at runtime. */\n nextTargetConnected(): void {\n if (!this.#isConnected) return;\n this.#render();\n }\n\n /** Makes the clicked page button (its `data-page`) current. */\n select(event: Event): void {\n const button = event.currentTarget as HTMLElement;\n const raw = button.dataset.page;\n if (raw === undefined || raw.trim() === \"\") return;\n const page = Number(raw);\n // Integer-only, matching `stimeo--stepper`: `Number(\"\")` is 0 and `Number(\"2.7\")`\n // truncates, so a bare finite check would accept meaningless page numbers.\n if (!Number.isInteger(page)) return;\n this.#goto(page);\n }\n\n /** Steps to the previous page. */\n prev(): void {\n this.#goto(this.#page - 1);\n }\n\n /** Steps to the next page. */\n next(): void {\n this.#goto(this.#page + 1);\n }\n\n /** Moves to `page` (clamped to `[1, total]`), re-renders, and dispatches `change`. */\n #goto(page: number): void {\n if (!Number.isFinite(page)) return;\n const previous = this.#page;\n const target = this.#clamp(page);\n if (target === previous) return;\n this.pageValue = target;\n this.#render();\n this.dispatch(\"change\", {\n detail: { page: target, total: this.#total, previous },\n });\n }\n\n /** Writes the normalized Values back (only when they differ) and renders. */\n #normalizeAndRender(): void {\n // Stimulus setters rewrite the data attribute unconditionally, so guard the\n // no-op case to avoid waking consumer MutationObservers for nothing.\n const total = this.#total;\n if (!Object.is(total, this.totalValue)) this.totalValue = total;\n const page = this.#page;\n if (!Object.is(page, this.pageValue)) this.pageValue = page;\n this.#render();\n }\n\n /** Syncs `aria-current` on the page buttons and the prev/next `disabled` state. */\n #render(): void {\n const page = this.#page;\n for (const button of this.pageTargets) {\n if (Number(button.dataset.page) === page) {\n button.setAttribute(\"aria-current\", \"page\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n }\n\n const prev = this.hasPrevTarget ? this.prevTarget : null;\n const next = this.hasNextTarget ? this.nextTarget : null;\n const atStart = page <= 1;\n const atEnd = page >= this.#total;\n // Resolve both post-render states up front: a focus hand-off must target the\n // opposite button as it will be *after* this pass, not as it is now (moving\n // between the two boundaries of a 2-page set re-enables it in the same pass).\n const prevStaysDisabled = this.#disabledAfter(prev, atStart);\n const nextStaysDisabled = this.#disabledAfter(next, atEnd);\n // Release before disabling, so the hand-off can land on a button this same\n // pass re-enables (focusing a still-`disabled` button is a no-op).\n this.#release(prev, atStart);\n this.#release(next, atEnd);\n this.#disable(prev, atStart, nextStaysDisabled ? null : next);\n this.#disable(next, atEnd, prevStaysDisabled ? null : prev);\n }\n\n /** Whether a boundary button will still be `disabled` once this render applies. */\n #disabledAfter(button: HTMLButtonElement | null, atBoundary: boolean): boolean {\n if (!button) return true;\n if (atBoundary) return true;\n // Away from a boundary, only the controller's own `disabled` is released.\n return button.disabled && !this.#owns(button);\n }\n\n /** Releases the boundary `disabled` this controller owns, once away from it. */\n #release(button: HTMLButtonElement | null, atBoundary: boolean): void {\n if (!button || atBoundary || !this.#owns(button)) return;\n button.disabled = false;\n button.removeAttribute(PaginationController.#BOUNDARY_ATTR);\n }\n\n /**\n * Disables a boundary button, first moving focus off it when it is the active\n * element so disabling never strands focus.\n */\n #disable(\n button: HTMLButtonElement | null,\n atBoundary: boolean,\n opposite: HTMLButtonElement | null,\n ): void {\n if (!button || !atBoundary) return;\n if (button.disabled && !this.#owns(button)) return;\n if (button === document.activeElement) this.#moveFocusAwayFrom(opposite);\n button.disabled = true;\n button.setAttribute(PaginationController.#BOUNDARY_ATTR, \"\");\n }\n\n /**\n * Moves focus to `opposite` (already resolved to `null` when it will stay\n * disabled), else to the current page button, else to the landmark itself.\n */\n #moveFocusAwayFrom(opposite: HTMLButtonElement | null): void {\n const page = this.#page;\n const currentPage = this.pageTargets.find(\n (candidate) => Number(candidate.dataset.page) === page,\n );\n // Checked before the call, never after: `hidden` and natively disabled\n // elements swallow `focus()` silently, so an unchecked destination leaves the\n // caret in the subtree that is about to disable and drops it to <body> a frame\n // later — the outcome this rescue exists to prevent.\n // Narrowed to `HTMLElement`, not `HTMLButtonElement`: `pageTargets` is typed\n // `HTMLElement[]`, so a page control can legitimately be an `<a>`. The\n // destination is only ever focused, so the wider type is the honest one.\n const destination = [opposite, currentPage].find(\n (candidate): candidate is HTMLElement => candidate != null && canTakeFocus(candidate),\n );\n if (destination) {\n destination.focus();\n return;\n }\n // Nothing left to hand focus to (a lone boundary button, or every page button\n // disabled): keep it inside the pagination landmark instead of letting the\n // browser drop it to <body> when the button disables. The root is made\n // programmatically focusable just-in-time with `tabindex=\"-1\"`, which is not a\n // Tab stop; `disconnect()` removes it again when this controller added it.\n this.#tabindex.lend(this.element);\n this.element.focus();\n }\n\n /** Total pages, normalized to a finite integer >= 1. */\n get #total(): number {\n const total = this.totalValue;\n return Number.isFinite(total) ? Math.max(1, Math.trunc(total)) : 1;\n }\n\n /** The current page, normalized into `[1, total]`. */\n get #page(): number {\n return this.#clamp(this.pageValue);\n }\n\n /** Constrains `page` to `[1, total]`; a non-finite page falls back to page 1. */\n #clamp(page: number): number {\n if (!Number.isFinite(page)) return 1;\n return Math.min(this.#total, Math.max(1, Math.trunc(page)));\n }\n\n /** Whether the button's current `disabled` was applied by boundary control. */\n #owns(button: HTMLButtonElement): boolean {\n return button.hasAttribute(PaginationController.#BOUNDARY_ATTR);\n }\n}\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless password show/hide (unmask) toggle behavior.
|
|
5
|
+
*
|
|
6
|
+
* Markup contract (identifier: `stimeo--password-reveal`):
|
|
7
|
+
* <div data-controller="stimeo--password-reveal">
|
|
8
|
+
* <input type="password" aria-label="Password"
|
|
9
|
+
* data-stimeo--password-reveal-target="input">
|
|
10
|
+
* <button type="button" aria-pressed="false" aria-label="Show password"
|
|
11
|
+
* data-stimeo--password-reveal-target="toggle"
|
|
12
|
+
* data-action="stimeo--password-reveal#toggle"></button>
|
|
13
|
+
* </div>
|
|
14
|
+
*
|
|
15
|
+
* No dedicated APG pattern; this follows the toggle **Button** practice. The
|
|
16
|
+
* accessible name stays state-independent ("Show password") while the pressed
|
|
17
|
+
* state is conveyed by `aria-pressed`.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Behavior only — icon rendering is the consumer's, keyed off `aria-pressed` /
|
|
21
|
+
* `data-state` (`hidden` / `visible`). Flipping `input.type` can drop focus and
|
|
22
|
+
* the caret, so when (and only when) the input was the focused element its focus
|
|
23
|
+
* and selection are restored afterward; when the toggle button holds focus
|
|
24
|
+
* (keyboard use) focus is left on the button. An optional `autoHide` re-masks
|
|
25
|
+
* after a delay, and that timer is torn down on disconnect (Turbo).
|
|
26
|
+
*/
|
|
27
|
+
declare class PasswordRevealController extends Controller<HTMLElement> {
|
|
28
|
+
#private;
|
|
29
|
+
static targets: string[];
|
|
30
|
+
static values: {
|
|
31
|
+
autoHide: {
|
|
32
|
+
type: NumberConstructor;
|
|
33
|
+
default: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
static actions: readonly ["toggle"];
|
|
37
|
+
static events: readonly ["toggle"];
|
|
38
|
+
readonly inputTarget: HTMLInputElement;
|
|
39
|
+
readonly toggleTarget: HTMLElement;
|
|
40
|
+
readonly hasInputTarget: boolean;
|
|
41
|
+
readonly hasToggleTarget: boolean;
|
|
42
|
+
autoHideValue: number;
|
|
43
|
+
connect(): void;
|
|
44
|
+
disconnect(): void;
|
|
45
|
+
/** Toggles the input between masked and revealed. Bound via `data-action`. */
|
|
46
|
+
toggle(): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { PasswordRevealController };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/password_reveal_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/safe_timeout.ts
|
|
6
|
+
var TimerRegistry = class {
|
|
7
|
+
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
8
|
+
ids = /* @__PURE__ */ new Set();
|
|
9
|
+
/**
|
|
10
|
+
* Cancels a single tracked timer.
|
|
11
|
+
*
|
|
12
|
+
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
13
|
+
* registry), so callers can clear defensively without guarding.
|
|
14
|
+
*/
|
|
15
|
+
clear(id) {
|
|
16
|
+
if (this.ids.delete(id)) {
|
|
17
|
+
this.cancel(id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
22
|
+
* to guarantee no timer outlives the element.
|
|
23
|
+
*/
|
|
24
|
+
clearAll() {
|
|
25
|
+
for (const id of this.ids) {
|
|
26
|
+
this.cancel(id);
|
|
27
|
+
}
|
|
28
|
+
this.ids.clear();
|
|
29
|
+
}
|
|
30
|
+
/** Number of timers currently tracked (pending). */
|
|
31
|
+
get size() {
|
|
32
|
+
return this.ids.size;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var SafeTimeout = class extends TimerRegistry {
|
|
36
|
+
/**
|
|
37
|
+
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
38
|
+
*
|
|
39
|
+
* The id is removed from the registry automatically when the timeout fires,
|
|
40
|
+
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
41
|
+
*/
|
|
42
|
+
set(callback, delay) {
|
|
43
|
+
const id = this.schedule(() => {
|
|
44
|
+
this.ids.delete(id);
|
|
45
|
+
callback();
|
|
46
|
+
}, delay);
|
|
47
|
+
this.ids.add(id);
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
schedule(callback, delay) {
|
|
51
|
+
return window.setTimeout(callback, delay);
|
|
52
|
+
}
|
|
53
|
+
cancel(id) {
|
|
54
|
+
window.clearTimeout(id);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/controllers/password_reveal_controller.ts
|
|
59
|
+
var PasswordRevealController = class extends Controller {
|
|
60
|
+
static targets = ["input", "toggle"];
|
|
61
|
+
static values = {
|
|
62
|
+
autoHide: { type: Number, default: 0 }
|
|
63
|
+
};
|
|
64
|
+
static actions = ["toggle"];
|
|
65
|
+
static events = ["toggle"];
|
|
66
|
+
/** Auto re-mask timer; torn down on disconnect. */
|
|
67
|
+
#timers = new SafeTimeout();
|
|
68
|
+
connect() {
|
|
69
|
+
this.#reflect(this.#isVisible);
|
|
70
|
+
}
|
|
71
|
+
disconnect() {
|
|
72
|
+
this.#timers.clearAll();
|
|
73
|
+
}
|
|
74
|
+
/** Toggles the input between masked and revealed. Bound via `data-action`. */
|
|
75
|
+
toggle() {
|
|
76
|
+
this.#setVisible(!this.#isVisible);
|
|
77
|
+
}
|
|
78
|
+
/** Whether the input is currently revealed (`type="text"`). */
|
|
79
|
+
get #isVisible() {
|
|
80
|
+
return this.hasInputTarget && this.inputTarget.type === "text";
|
|
81
|
+
}
|
|
82
|
+
/** Switches the masked/revealed state, preserving focus and caret. */
|
|
83
|
+
#setVisible(visible) {
|
|
84
|
+
if (!this.hasInputTarget) return;
|
|
85
|
+
const input = this.inputTarget;
|
|
86
|
+
const restoreInputFocus = document.activeElement === input;
|
|
87
|
+
const selectionStart = input.selectionStart;
|
|
88
|
+
const selectionEnd = input.selectionEnd;
|
|
89
|
+
input.type = visible ? "text" : "password";
|
|
90
|
+
if (restoreInputFocus) {
|
|
91
|
+
input.focus();
|
|
92
|
+
if (selectionStart !== null && selectionEnd !== null) {
|
|
93
|
+
try {
|
|
94
|
+
input.setSelectionRange(selectionStart, selectionEnd);
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.#reflect(visible);
|
|
100
|
+
this.dispatch("toggle", { detail: { visible } });
|
|
101
|
+
this.#timers.clearAll();
|
|
102
|
+
if (visible && this.autoHideValue > 0) {
|
|
103
|
+
this.#timers.set(() => this.#setVisible(false), this.autoHideValue);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Reflects the visible state onto `aria-pressed` and `data-state`. */
|
|
107
|
+
#reflect(visible) {
|
|
108
|
+
if (this.hasToggleTarget) {
|
|
109
|
+
this.toggleTarget.setAttribute("aria-pressed", visible ? "true" : "false");
|
|
110
|
+
}
|
|
111
|
+
this.element.setAttribute("data-state", visible ? "visible" : "hidden");
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export { PasswordRevealController };
|
|
116
|
+
//# sourceMappingURL=password_reveal_controller.js.map
|
|
117
|
+
//# sourceMappingURL=password_reveal_controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/password_reveal_controller.ts"],"names":[],"mappings":"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","file":"password_reveal_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 { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless password show/hide (unmask) toggle behavior.\n *\n * Markup contract (identifier: `stimeo--password-reveal`):\n * <div data-controller=\"stimeo--password-reveal\">\n * <input type=\"password\" aria-label=\"Password\"\n * data-stimeo--password-reveal-target=\"input\">\n * <button type=\"button\" aria-pressed=\"false\" aria-label=\"Show password\"\n * data-stimeo--password-reveal-target=\"toggle\"\n * data-action=\"stimeo--password-reveal#toggle\"></button>\n * </div>\n *\n * No dedicated APG pattern; this follows the toggle **Button** practice. The\n * accessible name stays state-independent (\"Show password\") while the pressed\n * state is conveyed by `aria-pressed`.\n *\n * @remarks\n * Behavior only — icon rendering is the consumer's, keyed off `aria-pressed` /\n * `data-state` (`hidden` / `visible`). Flipping `input.type` can drop focus and\n * the caret, so when (and only when) the input was the focused element its focus\n * and selection are restored afterward; when the toggle button holds focus\n * (keyboard use) focus is left on the button. An optional `autoHide` re-masks\n * after a delay, and that timer is torn down on disconnect (Turbo).\n */\nexport class PasswordRevealController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"toggle\"];\n static override values = {\n autoHide: { type: Number, default: 0 },\n };\n static actions = [\"toggle\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly toggleTarget: HTMLElement;\n declare readonly hasInputTarget: boolean;\n declare readonly hasToggleTarget: boolean;\n\n declare autoHideValue: number;\n\n /** Auto re-mask timer; torn down on disconnect. */\n #timers = new SafeTimeout();\n\n override connect(): void {\n this.#reflect(this.#isVisible);\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n }\n\n /** Toggles the input between masked and revealed. Bound via `data-action`. */\n toggle(): void {\n this.#setVisible(!this.#isVisible);\n }\n\n /** Whether the input is currently revealed (`type=\"text\"`). */\n get #isVisible(): boolean {\n return this.hasInputTarget && this.inputTarget.type === \"text\";\n }\n\n /** Switches the masked/revealed state, preserving focus and caret. */\n #setVisible(visible: boolean): void {\n if (!this.hasInputTarget) return;\n const input = this.inputTarget;\n\n // Only the input's *own* focus is restored across the type change; if the\n // toggle button (keyboard) holds focus, it is left untouched.\n const restoreInputFocus = document.activeElement === input;\n const selectionStart = input.selectionStart;\n const selectionEnd = input.selectionEnd;\n\n input.type = visible ? \"text\" : \"password\";\n\n if (restoreInputFocus) {\n input.focus();\n // `selectionStart` / `selectionEnd` are `number | null` (null for input\n // types that don't expose a selection); only restore when both are present.\n if (selectionStart !== null && selectionEnd !== null) {\n try {\n input.setSelectionRange(selectionStart, selectionEnd);\n } catch {\n // Some input types reject selection access; focus alone is enough.\n }\n }\n }\n\n this.#reflect(visible);\n this.dispatch(\"toggle\", { detail: { visible } });\n\n this.#timers.clearAll();\n if (visible && this.autoHideValue > 0) {\n this.#timers.set(() => this.#setVisible(false), this.autoHideValue);\n }\n }\n\n /** Reflects the visible state onto `aria-pressed` and `data-state`. */\n #reflect(visible: boolean): void {\n if (this.hasToggleTarget) {\n this.toggleTarget.setAttribute(\"aria-pressed\", visible ? \"true\" : \"false\");\n }\n this.element.setAttribute(\"data-state\", visible ? \"visible\" : \"hidden\");\n }\n}\n"]}
|
|
@@ -182,12 +182,12 @@ var PersistController = class extends Controller {
|
|
|
182
182
|
}
|
|
183
183
|
/**
|
|
184
184
|
* Persistable fields paired with a stable storage key. Uniquely-named fields key
|
|
185
|
-
* by their name
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
185
|
+
* by their name. Repeated same-name fields (e.g. a `tags[]` checkbox group or
|
|
186
|
+
* array text inputs) are disambiguated by DOM-order occurrence — the first keeps
|
|
187
|
+
* its plain `name`, later ones get a NUL-separated index suffix — so each is
|
|
188
|
+
* stored and restored individually instead of the last one clobbering the rest.
|
|
189
|
+
* Radios are the exception: a group intentionally shares one key (one value per
|
|
190
|
+
* name).
|
|
191
191
|
*/
|
|
192
192
|
#fieldEntries() {
|
|
193
193
|
const entries = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/persist_controller.ts"],"names":[],"mappings":"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","file":"persist_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 { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Field controls this controller can persist. */\ntype PersistField = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;\n\n/** Input types that carry no meaningful, restorable value. */\nconst NON_VALUE_TYPES = new Set([\"file\", \"submit\", \"reset\", \"button\", \"image\"]);\n/** localStorage key prefix so drafts never clobber unrelated app storage. */\nconst STORAGE_PREFIX = \"stimeo--persist:\";\n/** Separator for disambiguating repeated same-name fields (cannot occur in a real field name). */\nconst OCCURRENCE_SEP = \"\\u0000\";\n\n/**\n * Headless draft-autosave behavior: persists a form's field values to\n * `localStorage` and restores them across Turbo navigations and reloads (no APG\n * pattern; a state-holding utility). The Alpine `persist` equivalent.\n *\n * Markup contract (identifier: `stimeo--persist`):\n * <form data-controller=\"stimeo--persist\"\n * data-stimeo--persist-key-value=\"contact-draft\">\n * <input name=\"name\">\n * <textarea name=\"message\"></textarea>\n * </form>\n *\n * On connect it restores any saved values under `key` (falling back to the\n * element's `id`), then debounce-saves on every input/change. Password fields (and\n * anything in `exclude`) are never written. A `clear()` action — or the `clearOn`\n * event (e.g. `submit`) — drops the draft. Restoring never moves focus.\n *\n * @remarks\n * Behavior only. State lives entirely in `localStorage` (no module-scope state), so\n * instances never interfere. The input listener and debounce timer are removed on\n * `disconnect()` (Turbo navigation included), where a pending save is flushed first\n * so an in-flight edit is not lost.\n */\nexport class PersistController extends Controller<HTMLElement> {\n static override targets = [\"field\"];\n static override values = {\n key: { type: String, default: \"\" },\n debounce: { type: Number, default: 400 },\n exclude: { type: Array, default: [\"password\"] },\n clearOn: { type: String, default: \"\" },\n };\n static actions = [\"clear\"] as const;\n static events = [\"restore\", \"save\", \"clear\"] as const;\n\n declare readonly fieldTargets: PersistField[];\n declare readonly hasFieldTarget: boolean;\n\n declare keyValue: string;\n declare debounceValue: number;\n declare excludeValue: string[];\n declare clearOnValue: string;\n\n readonly #timeouts = new SafeTimeout();\n #saveId: number | null = null;\n\n readonly #onInput = (): void => {\n this.#scheduleSave();\n };\n\n readonly #onClearEvent = (): void => {\n this.clear();\n };\n\n override connect(): void {\n if (this.#storageKey === null) return;\n this.#restore();\n this.element.addEventListener(\"input\", this.#onInput);\n this.element.addEventListener(\"change\", this.#onInput);\n if (this.clearOnValue.length > 0) {\n this.element.addEventListener(this.clearOnValue, this.#onClearEvent);\n }\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"input\", this.#onInput);\n this.element.removeEventListener(\"change\", this.#onInput);\n if (this.clearOnValue.length > 0) {\n this.element.removeEventListener(this.clearOnValue, this.#onClearEvent);\n }\n // Flush a pending save so an in-flight edit survives a Turbo navigation.\n if (this.#saveId !== null) {\n this.#timeouts.clear(this.#saveId);\n this.#saveId = null;\n this.#write();\n }\n this.#timeouts.clearAll();\n }\n\n /** Drops the saved draft and clears the restored marker. */\n clear(): void {\n const key = this.#storageKey;\n if (key === null) return;\n if (this.#saveId !== null) {\n this.#timeouts.clear(this.#saveId);\n this.#saveId = null;\n }\n this.#removeItem(key);\n this.element.removeAttribute(\"data-persist-restored\");\n this.dispatch(\"clear\", { detail: { key: this.#logicalKey } });\n }\n\n /** Schedules a debounced save. */\n #scheduleSave(): void {\n if (this.#saveId !== null) this.#timeouts.clear(this.#saveId);\n this.#saveId = this.#timeouts.set(() => {\n this.#saveId = null;\n this.#save();\n }, this.debounceValue);\n }\n\n /** Writes the current values and emits `save`. */\n #save(): void {\n this.#write();\n this.dispatch(\"save\", { detail: { key: this.#logicalKey } });\n }\n\n /** Serializes persistable fields and stores them under the storage key. */\n #write(): void {\n const key = this.#storageKey;\n if (key === null) return;\n const data: Record<string, unknown> = {};\n for (const { field, key: fieldKey } of this.#fieldEntries()) {\n if (field instanceof HTMLInputElement && field.type === \"checkbox\") {\n data[fieldKey] = field.checked;\n } else if (field instanceof HTMLInputElement && field.type === \"radio\") {\n if (field.checked) data[fieldKey] = field.value;\n } else if (field instanceof HTMLSelectElement && field.multiple) {\n data[fieldKey] = Array.from(field.selectedOptions).map((o) => o.value);\n } else {\n data[fieldKey] = field.value;\n }\n }\n this.#setItem(key, JSON.stringify(data));\n }\n\n /** Applies any saved values to the fields, without moving focus. */\n #restore(): void {\n const key = this.#storageKey;\n if (key === null) return;\n const raw = this.#getItem(key);\n if (raw === null) return;\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return;\n }\n let restoredAny = false;\n for (const { field, key: fieldKey } of this.#fieldEntries()) {\n if (!Object.hasOwn(data, fieldKey)) continue;\n this.#applyValue(field, data[fieldKey]);\n restoredAny = true;\n }\n if (restoredAny) {\n this.element.setAttribute(\"data-persist-restored\", \"true\");\n this.dispatch(\"restore\", { detail: { key: this.#logicalKey } });\n }\n }\n\n /** Sets a single field's value from a stored entry. */\n #applyValue(field: PersistField, value: unknown): void {\n if (field instanceof HTMLInputElement && field.type === \"checkbox\") {\n field.checked = Boolean(value);\n } else if (field instanceof HTMLInputElement && field.type === \"radio\") {\n field.checked = field.value === value;\n } else if (field instanceof HTMLSelectElement && field.multiple) {\n const selected = new Set(Array.isArray(value) ? value.map(String) : []);\n for (const option of Array.from(field.options)) {\n option.selected = selected.has(option.value);\n }\n } else {\n field.value = String(value);\n }\n }\n\n /**\n * Persistable fields paired with a stable storage key. Uniquely-named fields key\n * by their name (unchanged, backward-compatible). Repeated same-name fields\n * (e.g. a `tags[]` checkbox group or array text inputs) are disambiguated by\n * DOM-order occurrence — the first keeps its plain `name` (backward-\n * compatible), later ones get a NUL-separated index suffix — so each is stored and\n * restored individually instead of the last one clobbering the rest. Radios are\n * the exception: a group intentionally shares one key (one value per name).\n */\n #fieldEntries(): Array<{ field: PersistField; key: string }> {\n const entries: Array<{ field: PersistField; key: string }> = [];\n const occurrence = new Map<string, number>();\n for (const field of this.#fields()) {\n const name = this.#keyOf(field);\n if (name === null) continue;\n if (field instanceof HTMLInputElement && field.type === \"radio\") {\n entries.push({ field, key: name });\n continue;\n }\n const seen = occurrence.get(name) ?? 0;\n occurrence.set(name, seen + 1);\n entries.push({ field, key: seen === 0 ? name : `${name}${OCCURRENCE_SEP}${seen}` });\n }\n return entries;\n }\n\n /** The fields to persist: `field` targets, or the element's named controls. */\n #fields(): PersistField[] {\n const candidates = this.hasFieldTarget\n ? this.fieldTargets\n : Array.from(this.element.querySelectorAll<PersistField>(\"input, textarea, select\"));\n return candidates.filter((field) => this.#persistable(field));\n }\n\n /** Whether a field carries a restorable, non-excluded value. */\n #persistable(field: PersistField): boolean {\n if (this.#keyOf(field) === null) return false;\n const type = field instanceof HTMLInputElement ? field.type : \"\";\n if (NON_VALUE_TYPES.has(type)) return false;\n if (this.excludeValue.includes(type)) return false;\n if (field.name.length > 0 && this.excludeValue.includes(field.name)) return false;\n return true;\n }\n\n /** A stable storage sub-key for a field (its name, else id). */\n #keyOf(field: PersistField): string | null {\n return field.name || field.id || null;\n }\n\n /** The logical key (key Value or element id), or null when neither is set. */\n get #logicalKey(): string | null {\n const key = this.keyValue || this.element.id;\n return key.length > 0 ? key : null;\n }\n\n /** The prefixed localStorage key, or null when persistence is disabled. */\n get #storageKey(): string | null {\n const logical = this.#logicalKey;\n return logical === null ? null : `${STORAGE_PREFIX}${logical}`;\n }\n\n #getItem(key: string): string | null {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n }\n\n #setItem(key: string, value: string): void {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Storage full or unavailable (private mode): persistence is best-effort.\n }\n }\n\n #removeItem(key: string): void {\n try {\n window.localStorage.removeItem(key);\n } catch {\n // Ignore: nothing to clear if storage is unavailable.\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/persist_controller.ts"],"names":[],"mappings":"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","file":"persist_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 { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Field controls this controller can persist. */\ntype PersistField = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;\n\n/** Input types that carry no meaningful, restorable value. */\nconst NON_VALUE_TYPES = new Set([\"file\", \"submit\", \"reset\", \"button\", \"image\"]);\n/** localStorage key prefix so drafts never clobber unrelated app storage. */\nconst STORAGE_PREFIX = \"stimeo--persist:\";\n/** Separator for disambiguating repeated same-name fields (cannot occur in a real field name). */\nconst OCCURRENCE_SEP = \"\\u0000\";\n\n/**\n * Headless draft-autosave behavior: persists a form's field values to\n * `localStorage` and restores them across Turbo navigations and reloads (no APG\n * pattern; a state-holding utility). The Alpine `persist` equivalent.\n *\n * Markup contract (identifier: `stimeo--persist`):\n * <form data-controller=\"stimeo--persist\"\n * data-stimeo--persist-key-value=\"contact-draft\">\n * <input name=\"name\">\n * <textarea name=\"message\"></textarea>\n * </form>\n *\n * On connect it restores any saved values under `key` (falling back to the\n * element's `id`), then debounce-saves on every input/change. Password fields (and\n * anything in `exclude`) are never written. A `clear()` action — or the `clearOn`\n * event (e.g. `submit`) — drops the draft. Restoring never moves focus.\n *\n * @remarks\n * Behavior only. State lives entirely in `localStorage` (no module-scope state), so\n * instances never interfere. The input listener and debounce timer are removed on\n * `disconnect()` (Turbo navigation included), where a pending save is flushed first\n * so an in-flight edit is not lost.\n */\nexport class PersistController extends Controller<HTMLElement> {\n static override targets = [\"field\"];\n static override values = {\n key: { type: String, default: \"\" },\n debounce: { type: Number, default: 400 },\n exclude: { type: Array, default: [\"password\"] },\n clearOn: { type: String, default: \"\" },\n };\n static actions = [\"clear\"] as const;\n static events = [\"restore\", \"save\", \"clear\"] as const;\n\n declare readonly fieldTargets: PersistField[];\n declare readonly hasFieldTarget: boolean;\n\n declare keyValue: string;\n declare debounceValue: number;\n declare excludeValue: string[];\n declare clearOnValue: string;\n\n readonly #timeouts = new SafeTimeout();\n #saveId: number | null = null;\n\n readonly #onInput = (): void => {\n this.#scheduleSave();\n };\n\n readonly #onClearEvent = (): void => {\n this.clear();\n };\n\n override connect(): void {\n if (this.#storageKey === null) return;\n this.#restore();\n this.element.addEventListener(\"input\", this.#onInput);\n this.element.addEventListener(\"change\", this.#onInput);\n if (this.clearOnValue.length > 0) {\n this.element.addEventListener(this.clearOnValue, this.#onClearEvent);\n }\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"input\", this.#onInput);\n this.element.removeEventListener(\"change\", this.#onInput);\n if (this.clearOnValue.length > 0) {\n this.element.removeEventListener(this.clearOnValue, this.#onClearEvent);\n }\n // Flush a pending save so an in-flight edit survives a Turbo navigation.\n if (this.#saveId !== null) {\n this.#timeouts.clear(this.#saveId);\n this.#saveId = null;\n this.#write();\n }\n this.#timeouts.clearAll();\n }\n\n /** Drops the saved draft and clears the restored marker. */\n clear(): void {\n const key = this.#storageKey;\n if (key === null) return;\n if (this.#saveId !== null) {\n this.#timeouts.clear(this.#saveId);\n this.#saveId = null;\n }\n this.#removeItem(key);\n this.element.removeAttribute(\"data-persist-restored\");\n this.dispatch(\"clear\", { detail: { key: this.#logicalKey } });\n }\n\n /** Schedules a debounced save. */\n #scheduleSave(): void {\n if (this.#saveId !== null) this.#timeouts.clear(this.#saveId);\n this.#saveId = this.#timeouts.set(() => {\n this.#saveId = null;\n this.#save();\n }, this.debounceValue);\n }\n\n /** Writes the current values and emits `save`. */\n #save(): void {\n this.#write();\n this.dispatch(\"save\", { detail: { key: this.#logicalKey } });\n }\n\n /** Serializes persistable fields and stores them under the storage key. */\n #write(): void {\n const key = this.#storageKey;\n if (key === null) return;\n const data: Record<string, unknown> = {};\n for (const { field, key: fieldKey } of this.#fieldEntries()) {\n if (field instanceof HTMLInputElement && field.type === \"checkbox\") {\n data[fieldKey] = field.checked;\n } else if (field instanceof HTMLInputElement && field.type === \"radio\") {\n if (field.checked) data[fieldKey] = field.value;\n } else if (field instanceof HTMLSelectElement && field.multiple) {\n data[fieldKey] = Array.from(field.selectedOptions).map((o) => o.value);\n } else {\n data[fieldKey] = field.value;\n }\n }\n this.#setItem(key, JSON.stringify(data));\n }\n\n /** Applies any saved values to the fields, without moving focus. */\n #restore(): void {\n const key = this.#storageKey;\n if (key === null) return;\n const raw = this.#getItem(key);\n if (raw === null) return;\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return;\n }\n let restoredAny = false;\n for (const { field, key: fieldKey } of this.#fieldEntries()) {\n if (!Object.hasOwn(data, fieldKey)) continue;\n this.#applyValue(field, data[fieldKey]);\n restoredAny = true;\n }\n if (restoredAny) {\n this.element.setAttribute(\"data-persist-restored\", \"true\");\n this.dispatch(\"restore\", { detail: { key: this.#logicalKey } });\n }\n }\n\n /** Sets a single field's value from a stored entry. */\n #applyValue(field: PersistField, value: unknown): void {\n if (field instanceof HTMLInputElement && field.type === \"checkbox\") {\n field.checked = Boolean(value);\n } else if (field instanceof HTMLInputElement && field.type === \"radio\") {\n field.checked = field.value === value;\n } else if (field instanceof HTMLSelectElement && field.multiple) {\n const selected = new Set(Array.isArray(value) ? value.map(String) : []);\n for (const option of Array.from(field.options)) {\n option.selected = selected.has(option.value);\n }\n } else {\n field.value = String(value);\n }\n }\n\n /**\n * Persistable fields paired with a stable storage key. Uniquely-named fields key\n * by their name. Repeated same-name fields (e.g. a `tags[]` checkbox group or\n * array text inputs) are disambiguated by DOM-order occurrence — the first keeps\n * its plain `name`, later ones get a NUL-separated index suffix — so each is\n * stored and restored individually instead of the last one clobbering the rest.\n * Radios are the exception: a group intentionally shares one key (one value per\n * name).\n */\n #fieldEntries(): Array<{ field: PersistField; key: string }> {\n const entries: Array<{ field: PersistField; key: string }> = [];\n const occurrence = new Map<string, number>();\n for (const field of this.#fields()) {\n const name = this.#keyOf(field);\n if (name === null) continue;\n if (field instanceof HTMLInputElement && field.type === \"radio\") {\n entries.push({ field, key: name });\n continue;\n }\n const seen = occurrence.get(name) ?? 0;\n occurrence.set(name, seen + 1);\n entries.push({ field, key: seen === 0 ? name : `${name}${OCCURRENCE_SEP}${seen}` });\n }\n return entries;\n }\n\n /** The fields to persist: `field` targets, or the element's named controls. */\n #fields(): PersistField[] {\n const candidates = this.hasFieldTarget\n ? this.fieldTargets\n : Array.from(this.element.querySelectorAll<PersistField>(\"input, textarea, select\"));\n return candidates.filter((field) => this.#persistable(field));\n }\n\n /** Whether a field carries a restorable, non-excluded value. */\n #persistable(field: PersistField): boolean {\n if (this.#keyOf(field) === null) return false;\n const type = field instanceof HTMLInputElement ? field.type : \"\";\n if (NON_VALUE_TYPES.has(type)) return false;\n if (this.excludeValue.includes(type)) return false;\n if (field.name.length > 0 && this.excludeValue.includes(field.name)) return false;\n return true;\n }\n\n /** A stable storage sub-key for a field (its name, else id). */\n #keyOf(field: PersistField): string | null {\n return field.name || field.id || null;\n }\n\n /** The logical key (key Value or element id), or null when neither is set. */\n get #logicalKey(): string | null {\n const key = this.keyValue || this.element.id;\n return key.length > 0 ? key : null;\n }\n\n /** The prefixed localStorage key, or null when persistence is disabled. */\n get #storageKey(): string | null {\n const logical = this.#logicalKey;\n return logical === null ? null : `${STORAGE_PREFIX}${logical}`;\n }\n\n #getItem(key: string): string | null {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n }\n\n #setItem(key: string, value: string): void {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Storage full or unavailable (private mode): persistence is best-effort.\n }\n }\n\n #removeItem(key: string): void {\n try {\n window.localStorage.removeItem(key);\n } catch {\n // Ignore: nothing to clear if storage is unavailable.\n }\n }\n}\n"]}
|
|
@@ -34,9 +34,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
34
34
|
* survive), commits the offset on drop and restores it on cancel — the exact
|
|
35
35
|
* boilerplate every simple consumer would otherwise re-write. With `follow` the
|
|
36
36
|
* element's inline `translate` belongs to this controller (`connect()` re-reads
|
|
37
|
-
* a
|
|
38
|
-
*
|
|
39
|
-
* 1.1 deprecated `aria-grabbed`/`aria-dropeffect`, so the grabbed state is
|
|
37
|
+
* a committed `<x>px <y>px` offset, so Turbo restores stay consistent). ARIA 1.1
|
|
38
|
+
* deprecated `aria-grabbed`/`aria-dropeffect`, so the grabbed state is
|
|
40
39
|
* published as `data-grabbed` and the *meaning* of a drag must be announced by
|
|
41
40
|
* the consumer (pair with `stimeo--announcer`). `touch-action` on the handles is
|
|
42
41
|
* derived from `axis` (marker-guarded, authored values win) so the page does not
|
|
@@ -2,6 +2,12 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/pointer_drag_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/detach_gate.ts
|
|
6
12
|
var DetachGate = class _DetachGate {
|
|
7
13
|
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
@@ -186,8 +192,10 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
186
192
|
if (this.disabledValue) return;
|
|
187
193
|
const handle = this.#handleFor(event.target);
|
|
188
194
|
if (!handle) return;
|
|
195
|
+
if (event.defaultPrevented) return;
|
|
196
|
+
if (isReservedArrowChord(event)) return;
|
|
189
197
|
if (event.key === "Escape") {
|
|
190
|
-
if (event.
|
|
198
|
+
if (event.isComposing) return;
|
|
191
199
|
if (this.#pointer?.started) {
|
|
192
200
|
const { pointerType } = this.#pointer;
|
|
193
201
|
this.#teardownSessions();
|