stimeo-ui 0.14.0 → 0.16.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 +212 -0
- package/README.md +120 -0
- package/dist/cable/index.js +123 -29
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +30 -3
- package/dist/controllers/accordion_controller.js +98 -9
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +8 -7
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +3 -2
- package/dist/controllers/announcer_controller.js +96 -62
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js +83 -7
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +1 -1
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +10 -7
- package/dist/controllers/breadcrumb_controller.js +38 -11
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +4 -4
- package/dist/controllers/bulk_select_controller.js +7 -6
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +101 -23
- package/dist/controllers/calendar_controller.js +340 -123
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +67 -22
- package/dist/controllers/carousel_controller.js +263 -38
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +1 -1
- package/dist/controllers/character_counter_controller.js +40 -2
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.js +81 -12
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +28 -2
- package/dist/controllers/clipboard_controller.js +63 -5
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +25 -1
- package/dist/controllers/collapsible_controller.js +99 -14
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +29 -8
- package/dist/controllers/color_picker_controller.js +80 -34
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +10 -1
- package/dist/controllers/combobox_controller.js +106 -16
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +3 -3
- package/dist/controllers/command_palette_controller.js +35 -3
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js +85 -17
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +3 -0
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +6 -0
- package/dist/controllers/context_menu_controller.js +32 -12
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +30 -1
- package/dist/controllers/countdown_controller.js +129 -26
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +77 -16
- package/dist/controllers/currency_input_controller.js +221 -67
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +63 -18
- package/dist/controllers/data_grid_controller.js +195 -29
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +41 -6
- package/dist/controllers/date_range_picker_controller.js +151 -30
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +10 -3
- package/dist/controllers/dialog_controller.js +35 -8
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.js +22 -4
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +2 -2
- package/dist/controllers/dirty_form_controller.js +46 -13
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js +1 -0
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +18 -10
- package/dist/controllers/drawer_controller.js +54 -19
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +9 -3
- package/dist/controllers/dropdown_controller.js +36 -9
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.js +34 -0
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js +144 -51
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +10 -4
- package/dist/controllers/filter_controller.js +20 -6
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +26 -6
- package/dist/controllers/flash_controller.js +432 -71
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.js +1 -0
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +7 -5
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +19 -13
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.js +45 -8
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +82 -25
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +10 -2
- package/dist/controllers/hover_card_controller.js +40 -14
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +16 -3
- package/dist/controllers/idle_controller.js +90 -5
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +5 -2
- package/dist/controllers/input_mask_controller.js +65 -9
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +3 -0
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +11 -2
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +50 -8
- package/dist/controllers/listbox_controller.js +203 -45
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +10 -5
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +3 -3
- package/dist/controllers/masonry_controller.js +31 -15
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +9 -3
- package/dist/controllers/menu_controller.js +45 -16
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +11 -0
- package/dist/controllers/menubar_controller.js +58 -24
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +9 -5
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +18 -3
- package/dist/controllers/multi_select_controller.js +278 -104
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +11 -0
- package/dist/controllers/navigation_menu_controller.js +48 -15
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js +37 -8
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.js +9 -1
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +36 -8
- package/dist/controllers/number_input_controller.js +124 -21
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +42 -5
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +22 -7
- package/dist/controllers/otp_controller.js +198 -55
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
- package/dist/controllers/overflow_indicator_controller.js +115 -21
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +26 -6
- package/dist/controllers/overflow_menu_controller.js +141 -44
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +19 -11
- package/dist/controllers/pagination_controller.js +74 -28
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +15 -1
- package/dist/controllers/password_reveal_controller.js +59 -2
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js +30 -8
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +131 -52
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +9 -3
- package/dist/controllers/popover_controller.js +45 -11
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -1
- package/dist/controllers/portal_controller.js +6 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.js +16 -1
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js +8 -4
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +6 -4
- package/dist/controllers/radio_group_controller.js +42 -17
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +49 -1
- package/dist/controllers/range_slider_controller.js +88 -42
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +14 -3
- package/dist/controllers/rating_controller.js +39 -15
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +24 -2
- package/dist/controllers/read_more_controller.js +100 -7
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.js +65 -19
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +10 -5
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +16 -2
- package/dist/controllers/resizable_controller.js +82 -22
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +75 -27
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +37 -16
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +49 -30
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +3 -2
- package/dist/controllers/scrollspy_controller.js +71 -26
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +41 -14
- package/dist/controllers/separator_controller.js +66 -37
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +20 -3
- package/dist/controllers/sidebar_controller.js +77 -18
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js +6 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +45 -7
- package/dist/controllers/slider_controller.js +82 -47
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.js +60 -26
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +17 -2
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +10 -2
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +19 -17
- package/dist/controllers/step_indicator_controller.js +18 -17
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +34 -9
- package/dist/controllers/stepper_controller.js +101 -19
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
- package/dist/controllers/stick_to_bottom_controller.js +104 -8
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +3 -2
- package/dist/controllers/submit_once_controller.js +45 -9
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +29 -7
- package/dist/controllers/switch_controller.js +101 -10
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +12 -0
- package/dist/controllers/tabs_controller.js +21 -2
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +15 -3
- package/dist/controllers/tags_input_controller.js +209 -59
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +29 -3
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +20 -3
- package/dist/controllers/theme_controller.js +64 -14
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js +23 -8
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +55 -15
- package/dist/controllers/toast_controller.js +451 -105
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +49 -7
- package/dist/controllers/toggle_group_controller.js +159 -23
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +32 -0
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +8 -0
- package/dist/controllers/tooltip_controller.js +39 -13
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.js +4 -0
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +39 -8
- package/dist/controllers/tree_view_controller.js +169 -16
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +5002 -1911
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +72 -6
- package/dist/inspector/cli.js +262 -51
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +309 -51
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +40 -40
- package/dist/inspector/manifest.json +529 -61
- package/dist/positioning/index.js +2 -0
- package/dist/positioning/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/layout_observer.ts","../../src/utils/reduced_motion.ts","../../src/controllers/stick_to_bottom_controller.ts"],"names":[],"mappings":"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","file":"stick_to_bottom_controller.js","sourcesContent":["/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers\n *.\n *\n * This one-liner keeps the media query string and the environment guard\n * single-sourced across them. The preference is intentionally re-read on every\n * call — the controllers check it at each animation/scroll start (WCAG 2.2\n * **2.3.3**), so flipping the OS setting takes effect immediately without any\n * listener or cache bookkeeping here.\n */\n\n/**\n * Whether the user currently requests reduced motion.\n *\n * @returns `true` when `(prefers-reduced-motion: reduce)` matches; `false`\n * otherwise, including environments without `window.matchMedia` (treated as\n * \"no preference\").\n */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\n\n/** Distance from the bottom, in px, that still counts as pinned. */\nconst DEFAULT_THRESHOLD = 80;\n\n/** Counts the element nodes in an added-node list (text nodes are ignored). */\nconst countElements = (nodes: NodeList): number => {\n let n = 0;\n for (const node of nodes) if (node.nodeType === Node.ELEMENT_NODE) n += 1;\n return n;\n};\n\n/**\n * Headless **stick-to-bottom**: auto-follows new content to the bottom of a scroll\n * container (a chat log, a live console) — but only while the user is already near the\n * bottom; if they have scrolled up to read, it holds position and flags that new content\n * arrived instead (no APG pattern; honors WCAG 2.3.3 via `prefers-reduced-motion` and\n * never steals focus).\n *\n * Markup contract (identifier: `stimeo--stick-to-bottom`):\n * <div data-controller=\"stimeo--stick-to-bottom\"\n * data-stimeo--stick-to-bottom-threshold-value=\"80\"\n * data-stimeo--stick-to-bottom-pin-on-connect-value=\"true\" style=\"overflow:auto\">\n * <ul data-stimeo--stick-to-bottom-target=\"content\"><!-- Turbo Stream appends --></ul>\n * </div>\n *\n * The container is \"pinned\" while it has a box to measure and its distance from the\n * bottom is within `threshold`. A `MutationObserver` on `content` (or the element) reacts\n * to appended children: while pinned it scrolls to the bottom; while unpinned it sets\n * `data-has-new` and emits `new`. Scrolling recomputes pinned and reflects `data-pinned`,\n * emitting `pin` on change; the `scrollToBottom` action jumps back down (a \"new messages\"\n * button). A `threshold` changed at runtime re-derives the state at once.\n *\n * A container that already overflows renders at `scrollTop` 0 — further than\n * `threshold` from the bottom, so unpinned — and flags the first append rather\n * than following it. `pinOnConnect` opts into the other starting point:\n * `connect()` jumps to the bottom and reads the state back from where it landed. It runs\n * on every connect, so a container inserted or re-rendered later (a Turbo Stream, a panel\n * built on open) starts at the bottom too — which a once-per-document page event cannot\n * do. It is off by default, so a restored reading position (a Turbo cache restore, a user\n * who had scrolled up) is never yanked to the bottom unless the consumer asks for it.\n *\n * A container connected without a box — inside a closed panel — reports every metric as\n * 0, so it is held unpinned until layout arrives: appends meanwhile are flagged rather\n * than followed into a box that cannot move, and the decision is then made against the\n * layout the user actually sees.\n *\n * `pin` dispatches `{ pinned }`; `new` dispatches `{ count }`.\n *\n * @remarks\n * The `MutationObserver` watches `childList` only (not `subtree`), so it follows\n * direct appends to `content` (or the element). Appends made deeper inside a nested\n * wrapper are not detected — keep messages as direct children, or call the public\n * `scrollToBottom` action after such inserts. A `content` target that is swapped at\n * runtime moves the watch with it.\n *\n * Behavior only — it does not add content (Turbo Stream / the consumer does) and is the\n * minimal follow primitive, not a full chat UI (no virtualization / message input). State\n * is derived from the scroll position each pass (no module-scope state), so `connect()`\n * re-syncs after a Turbo Stream insert; reduced motion forces an instant jump\n * independently of consumer CSS; auto-scroll never moves focus; the observers and the\n * passive scroll listener are released on `disconnect()` (Turbo navigation included).\n */\nexport class StickToBottomController extends Controller<HTMLElement> {\n static override targets = [\"content\"];\n static override values = {\n threshold: { type: Number, default: DEFAULT_THRESHOLD },\n behavior: { type: String, default: \"auto\" },\n pinOnConnect: { type: Boolean, default: false },\n };\n static actions = [\"scrollToBottom\"] as const;\n static events = [\"pin\", \"new\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n\n declare thresholdValue: number;\n declare behaviorValue: string;\n declare pinOnConnectValue: boolean;\n\n #observer: MutationObserver | null = null;\n /** The element the append observer currently holds, so a swap can be detected. */\n #watched: HTMLElement | null = null;\n /** Watches for the box a container connected without one is still waiting on. */\n readonly #layout = new LayoutObserver(() => this.#onLaidOut());\n #awaitingLayout = false;\n #connected = false;\n #pinned = false;\n\n readonly #onScroll = (): void => this.#updatePinned();\n\n override connect(): void {\n this.#connected = true;\n // Instant whatever `behavior` says, overriding a consumer's `scroll-behavior: smooth`:\n // an animated jump emits scroll events on the way down, each recomputing pinned from a\n // position still far from the bottom, so the container would unpin (and re-pin)\n // mid-flight before it ever settles.\n if (this.pinOnConnectValue && this.#measurable()) this.#scrollToBottom(\"instant\");\n // Read the state back from where the container actually is, asked for a jump or not: a\n // request the engine does not honor must leave it unpinned, so the next append is\n // flagged rather than swallowed. Both hooks are re-derived here, so a stale data-pinned\n // or data-has-new a Turbo cache restore brought back is dropped: has-new records an\n // arrival this connection has not seen, and nothing in the DOM can attest to one.\n this.element.removeAttribute(\"data-has-new\");\n this.#pinned = this.#isPinned();\n this.#reflectPinned();\n\n this.element.addEventListener(\"scroll\", this.#onScroll, { passive: true });\n this.#syncWatched();\n // A container with no box yet reports every metric as 0; it stays unpinned until the\n // layout it will actually be read against arrives.\n if (!this.#measurable()) this.#waitForLayout();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.element.removeEventListener(\"scroll\", this.#onScroll);\n this.#stopWatching();\n this.#stopWaitingForLayout();\n }\n\n /** Moves the append watch onto a `content` target that arrived at runtime. */\n contentTargetConnected(): void {\n this.#syncWatched();\n }\n\n /** Moves the append watch off a `content` target that left, back onto the container. */\n contentTargetDisconnected(): void {\n this.#syncWatched();\n }\n\n /**\n * Re-derives pinned when the distance that counts as the bottom is changed at runtime\n * (a morph that swaps the attribute on a retained element).\n */\n thresholdValueChanged(): void {\n if (!this.#connected) return;\n this.#updatePinned();\n }\n\n /**\n * Jumps to the bottom and re-pins (wired to a \"new messages\" button).\n *\n * The has-new flag clears on request — the user has acknowledged the arrival — while\n * pinned is read back from where the scroll landed: a jump that arrives by the time\n * this returns pins immediately, an animated one settles from its own scroll events,\n * and a jump the engine cannot honor leaves the container unpinned, so the next append\n * flags it again instead of being swallowed by a pinned state that does not hold.\n *\n * Which of those happens is not this method's to decide — see {@link behaviorValue}.\n */\n scrollToBottom(): void {\n this.#scrollToBottom();\n this.element.removeAttribute(\"data-has-new\");\n this.#updatePinned();\n }\n\n /** Follows appended children while pinned; otherwise flags new content. */\n #onMutations(mutations: MutationRecord[]): void {\n let added = 0;\n for (const mutation of mutations) added += countElements(mutation.addedNodes);\n if (added === 0) return;\n\n if (this.#pinned) {\n this.#scrollToBottom();\n } else {\n this.element.setAttribute(\"data-has-new\", \"true\");\n this.dispatch(\"new\", { detail: { count: added } });\n }\n }\n\n /**\n * Recomputes pinned from the scroll position and reflects it on a transition.\n *\n * @stimeoRenderRoot\n */\n #updatePinned(): void {\n const pinned = this.#isPinned();\n if (pinned === this.#pinned) return;\n this.#pinned = pinned;\n this.#reflectPinned();\n this.dispatch(\"pin\", { detail: { pinned } });\n }\n\n /** Mirrors the current `#pinned` onto the state hooks (clearing has-new once pinned). */\n #reflectPinned(): void {\n if (this.#pinned) {\n this.element.setAttribute(\"data-pinned\", \"true\");\n this.element.removeAttribute(\"data-has-new\"); // caught up with the bottom\n } else {\n this.element.removeAttribute(\"data-pinned\");\n }\n }\n\n /** Whether the container currently sits within `threshold` of its bottom. */\n #isPinned(): boolean {\n if (!this.#measurable()) return false;\n const el = this.element;\n return el.scrollHeight - el.clientHeight - el.scrollTop <= this.#threshold;\n }\n\n /**\n * The distance from the bottom that counts as pinned: a finite, non-negative number of\n * pixels. Anything else names no distance the container can be at, and settles the\n * comparison the same way at every scroll position, so it falls back to the default.\n * `Number` reads `\"abc\"` as `NaN` and every comparison against it is false; a negative\n * distance sits below the closest the container ever gets; `Infinity` is never\n * exceeded. The first two stop following and flag every append as new, and the last\n * never stops following — it takes the reading position the flag exists to protect.\n * Zero is a real declaration: it pins at the exact bottom only.\n */\n get #threshold(): number {\n const declared = this.thresholdValue;\n return Number.isFinite(declared) && declared >= 0 ? declared : DEFAULT_THRESHOLD;\n }\n\n /**\n * Whether the container has a box to scroll and to measure. One that is not rendered\n * (inside a closed panel) reports every metric as 0, which reads as \"already at the\n * bottom\" — a position describing no layout the user will ever see.\n */\n #measurable(): boolean {\n return this.element.clientHeight > 0;\n }\n\n /**\n * Holds the pinned decision until the container is laid out — otherwise the panel opens\n * at the top while the state claims the bottom, and the appends that arrived meanwhile\n * were followed into a box that could not move rather than flagged.\n */\n #waitForLayout(): void {\n this.#awaitingLayout = true;\n this.#layout.observe(this.element);\n }\n\n /** Runs the held decision once the container has the box it was waiting for. */\n #onLaidOut(): void {\n if (!this.#awaitingLayout || !this.#measurable()) return;\n this.#stopWaitingForLayout();\n if (this.pinOnConnectValue) this.#scrollToBottom(\"instant\");\n this.#updatePinned();\n }\n\n /** Releases the layout watch, whether or not the held decision ever ran. */\n #stopWaitingForLayout(): void {\n this.#awaitingLayout = false;\n this.#layout.disconnect();\n }\n\n /**\n * Scrolls to the bottom, clamped by the engine to the maximum scroll offset — which is\n * 0 for a container tall enough to hold its whole content, so the jump moves nothing\n * there. `behavior` defaults to the configured follow behavior; pass `\"instant\"` for a\n * jump that must not animate.\n */\n #scrollToBottom(behavior: ScrollBehavior = this.#behavior()): void {\n const top = this.element.scrollHeight;\n if (typeof this.element.scrollTo === \"function\") {\n this.element.scrollTo({ top, behavior });\n } else {\n this.element.scrollTop = top;\n }\n }\n\n /**\n * Points the append watch at the current `content` target, or at the container when\n * there is none. Re-resolved whenever that target changes, so a swap does not leave the\n * observer holding a detached node whose appends nobody sees.\n *\n * Stimulus runs the target callbacks outside the connected window too — before\n * `connect()` for a target already in the DOM, and after `disconnect()` while the\n * element is torn down — where this would arm an observer nothing releases. Re-syncing\n * to the target already held is left alone, so an arrival still in flight is not\n * dropped with the observer that was about to deliver it.\n */\n #syncWatched(): void {\n if (!this.#connected) return;\n const next = this.hasContentTarget ? this.contentTarget : this.element;\n if (next === this.#watched) return;\n\n this.#stopWatching();\n this.#watched = next;\n if (typeof MutationObserver === \"undefined\") return;\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(next, { childList: true });\n }\n\n /** Releases the append watch and the element it held. */\n #stopWatching(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n this.#watched = null;\n }\n\n /**\n * The behavior a follow-scroll runs with. `\"auto\"` is **not** a request to arrive at\n * once: it hands the decision to the element's computed `scroll-behavior`, so a\n * consumer stylesheet saying `smooth` animates these scrolls too. Only `\"instant\"`\n * overrides that CSS, which is why reduced motion and the `pinOnConnect` jump name it.\n */\n #behavior(): ScrollBehavior {\n if (prefersReducedMotion()) return \"instant\";\n return this.behaviorValue === \"smooth\" ? \"smooth\" : \"auto\";\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/layout_observer.ts","../../src/utils/reduced_motion.ts","../../src/utils/state_regions.ts","../../src/controllers/stick_to_bottom_controller.ts"],"names":[],"mappings":"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","file":"stick_to_bottom_controller.js","sourcesContent":["/**\n * Unified layout observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — have three sources that move the layout under them: their\n * *own* box changing — via {@link ResizeObserver} — the *viewport* changing — via the\n * `window` `resize` event — and a descendant *resource settling*, because an image or\n * a frame reports a height of zero until it has loaded. Wiring those three by hand in\n * every controller risks leaked listeners on `disconnect()`. {@link LayoutObserver}\n * owns all three behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * this.#layout.observeDescendantLoads(this.panelTarget);\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n #loadContainer: Element | null = null;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */\n readonly #handleDescendantLoad = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Starts reporting a `load` from anywhere inside `container` — an image or a\n * frame settling changes the box it sits in, and it measures as zero high until\n * then. `load` does not bubble, so the subscription is a capture-phase listener\n * on the container itself and nothing the caller spells.\n *\n * **One container at a time.** A further call moves the observation, so a widget\n * whose content element is swapped at runtime releases the element it let go by\n * naming the new one — there is no second place for the release to drift from.\n */\n observeDescendantLoads(container: Element): void {\n this.unobserveDescendantLoads();\n this.#loadContainer = container;\n container.addEventListener(\"load\", this.#handleDescendantLoad, true);\n }\n\n /** Stops reporting descendant loads without affecting element or viewport observation. */\n unobserveDescendantLoads(): void {\n this.#loadContainer?.removeEventListener(\"load\", this.#handleDescendantLoad, true);\n this.#loadContainer = null;\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and removes\n * the viewport and descendant-load listeners. Safe to call multiple times. Call\n * this from a controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n this.unobserveDescendantLoads();\n }\n}\n","/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers.\n *\n * This one-liner keeps the media query string and the environment guard\n * single-sourced across them. The preference is intentionally re-read on every\n * call — the controllers check it at each animation/scroll start (WCAG 2.2\n * **2.3.3**), so flipping the OS setting takes effect immediately without any\n * listener or cache bookkeeping here.\n */\n\n/**\n * Whether the user currently requests reduced motion.\n *\n * @returns `true` when `(prefers-reduced-motion: reduce)` matches; `false`\n * otherwise, including environments without `window.matchMedia` (treated as\n * \"no preference\").\n */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n","/** What a consumer declares once about the regions one boolean state reveals. */\nexport interface StateRegionsOptions {\n /** Returns the declared regions shown while the state holds. */\n readonly whenTrue: () => readonly HTMLElement[];\n /**\n * Returns the declared regions shown while the state does not hold. Omitted by a\n * state whose other side has nothing of its own to show.\n */\n readonly whenFalse?: () => readonly HTMLElement[];\n}\n\n/**\n * Owns `hidden` on the regions a boolean state reveals.\n *\n * A region is a **declared** target, so the consumer decides whether one exists at\n * all; where it does, which side is shown is a pure function of the state and the\n * controller writes it unconditionally. An authored `hidden` is therefore not read\n * back — the first reflection after a connection settles it, and a restored DOM\n * cannot leave a region contradicting the state it belongs to.\n *\n * The regions arrive as the caller's own target arrays, which Stimulus has already\n * scoped to this instance: a nested controller registered under the same identifier\n * keeps its own regions. `host` narrows that set further, to the regions inside one\n * trigger or item, so a widget with several of them reflects each independently.\n *\n * **Two sides are a pair.** Where the state has a `whenFalse` side and only one half\n * sits inside `host`, the half that is there is left in view: hiding it would take\n * the only label with it, and the static check names the missing one instead. What\n * the author wrote on a lone half is theirs and stays — only a half this instance\n * took out of view is given back, which is what a pair losing one side at runtime\n * leaves behind.\n *\n * `hidden` moves only when it changes, so a consumer watching its own subtree for\n * attribute mutations sees one record per transition rather than one per reflection.\n *\n * @example\n * ```ts\n * readonly #labels = new StateRegions({\n * whenTrue: () => this.expandedLabelTargets,\n * whenFalse: () => this.collapsedLabelTargets,\n * });\n *\n * #reflect(): void {\n * this.#labels.reflect(this.triggerTarget, this.#expanded);\n * }\n * ```\n */\nexport class StateRegions {\n readonly #whenTrue: () => readonly HTMLElement[];\n readonly #whenFalse: (() => readonly HTMLElement[]) | null;\n readonly #taken = new WeakSet<HTMLElement>();\n\n constructor(options: StateRegionsOptions) {\n this.#whenTrue = options.whenTrue;\n this.#whenFalse = options.whenFalse ?? null;\n }\n\n /** Shows the regions inside `host` that belong to `isTrue` and hides the others. */\n reflect(host: Element, isTrue: boolean): void {\n const shown = this.#inside(host, this.#whenTrue());\n if (!this.#whenFalse) {\n for (const region of shown) this.#write(region, !isTrue);\n return;\n }\n const hiddenSide = this.#inside(host, this.#whenFalse());\n if (shown.length === 0 || hiddenSide.length === 0) {\n for (const region of shown) this.#give(region);\n for (const region of hiddenSide) this.#give(region);\n return;\n }\n for (const region of shown) this.#write(region, !isTrue);\n for (const region of hiddenSide) this.#write(region, isTrue);\n }\n\n /** Writes `hidden` where it moves, noting which regions it takes out of view. */\n #write(region: HTMLElement, hidden: boolean): void {\n if (hidden) this.#taken.add(region);\n else this.#taken.delete(region);\n if (region.hidden !== hidden) region.hidden = hidden;\n }\n\n /** Returns a region this instance hid; one it never hid keeps what it carries. */\n #give(region: HTMLElement): void {\n if (this.#taken.delete(region)) region.hidden = false;\n }\n\n /** The declared regions that sit within `host`, which may be the host itself. */\n #inside(host: Element, regions: readonly HTMLElement[]): HTMLElement[] {\n return regions.filter((region) => host.contains(region));\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\nimport { StateRegions } from \"../utils/state_regions\";\n\n/** Distance from the bottom, in px, that still counts as pinned. */\nconst DEFAULT_THRESHOLD = 80;\n\n/** Counts the element nodes in an added-node list (text nodes are ignored). */\nconst countElements = (nodes: NodeList): number => {\n let n = 0;\n for (const node of nodes) if (node.nodeType === Node.ELEMENT_NODE) n += 1;\n return n;\n};\n\n/**\n * Headless **stick-to-bottom**: auto-follows new content to the bottom of a scroll\n * container (a chat log, a live console) — but only while the user is already near the\n * bottom; if they have scrolled up to read, it holds position and flags that new content\n * arrived instead (no APG pattern; honors WCAG 2.3.3 via `prefers-reduced-motion` and\n * never steals focus).\n *\n * Markup contract (identifier: `stimeo--stick-to-bottom`):\n * <div data-controller=\"stimeo--stick-to-bottom\"\n * data-stimeo--stick-to-bottom-threshold-value=\"80\"\n * data-stimeo--stick-to-bottom-pin-on-connect-value=\"true\" style=\"overflow:auto\">\n * <ul data-stimeo--stick-to-bottom-target=\"content\"><!-- Turbo Stream appends --></ul>\n * <button type=\"button\" hidden data-stimeo--stick-to-bottom-target=\"hasNew\"\n * data-action=\"stimeo--stick-to-bottom#scrollToBottom\">New messages</button>\n * </div>\n *\n * The container is \"pinned\" while it has a box to measure and its distance from the\n * bottom is within `threshold`. A `MutationObserver` on `content` (or the element) reacts\n * to appended children: while pinned it scrolls to the bottom; while unpinned it sets\n * `data-has-new` and emits `new`. Scrolling recomputes pinned and reflects `data-pinned`,\n * emitting `pin` on change; the `scrollToBottom` action jumps back down (a \"new messages\"\n * button). A `threshold` changed at runtime re-derives the state at once.\n *\n * `data-has-new` says which state the container is in; the optional `hasNew` targets are\n * the regions that belong to the flagged side — the affordance offering the jump back\n * down. They are shown while the flag is up and hidden while it is down, written wherever\n * the flag moves, so an authored `hidden` settles at the first reflection and a restored\n * DOM cannot leave the region contradicting the flag.\n *\n * A container that already overflows renders at `scrollTop` 0 — further than\n * `threshold` from the bottom, so unpinned — and flags the first append rather\n * than following it. `pinOnConnect` opts into the other starting point:\n * `connect()` jumps to the bottom and reads the state back from where it landed. It runs\n * on every connect, so a container inserted or re-rendered later (a Turbo Stream, a panel\n * built on open) starts at the bottom too — which a once-per-document page event cannot\n * do. It is off by default, so a restored reading position (a Turbo cache restore, a user\n * who had scrolled up) is never yanked to the bottom unless the consumer asks for it.\n *\n * A container connected without a box — inside a closed panel — reports every metric as\n * 0, so it is held unpinned until layout arrives: appends meanwhile are flagged rather\n * than followed into a box that cannot move, and the decision is then made against the\n * layout the user actually sees.\n *\n * `pin` dispatches `{ pinned }`; `new` dispatches `{ count }`.\n *\n * @remarks\n * The `MutationObserver` watches `childList` only (not `subtree`), so it follows\n * direct appends to `content` (or the element). Appends made deeper inside a nested\n * wrapper are not detected — keep messages as direct children, or call the public\n * `scrollToBottom` action after such inserts. A `content` target that is swapped at\n * runtime moves the watch with it.\n *\n * Behavior only — it does not add content (Turbo Stream / the consumer does) and is the\n * minimal follow primitive, not a full chat UI (no virtualization / message input). State\n * is derived from the scroll position each pass (no module-scope state), so `connect()`\n * re-syncs after a Turbo Stream insert; reduced motion forces an instant jump\n * independently of consumer CSS; auto-scroll never moves focus; the observers and the\n * passive scroll listener are released on `disconnect()` (Turbo navigation included).\n */\nexport class StickToBottomController extends Controller<HTMLElement> {\n static override targets = [\"content\", \"hasNew\"];\n static override values = {\n threshold: { type: Number, default: DEFAULT_THRESHOLD },\n behavior: { type: String, default: \"auto\" },\n pinOnConnect: { type: Boolean, default: false },\n };\n static actions = [\"scrollToBottom\"] as const;\n static events = [\"pin\", \"new\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n declare readonly hasNewTargets: HTMLElement[];\n\n declare thresholdValue: number;\n declare behaviorValue: string;\n declare pinOnConnectValue: boolean;\n\n #observer: MutationObserver | null = null;\n /** The element the append observer currently holds, so a swap can be detected. */\n #watched: HTMLElement | null = null;\n /** Watches for the box a container connected without one is still waiting on. */\n readonly #layout = new LayoutObserver(() => this.#onLaidOut());\n #awaitingLayout = false;\n #connected = false;\n #pinned = false;\n #hasNew = false;\n /** Owns `hidden` on the regions declared for the has-new state. */\n readonly #hasNewRegion = new StateRegions({ whenTrue: () => this.hasNewTargets });\n\n readonly #onScroll = (): void => this.#updatePinned();\n\n override connect(): void {\n this.#connected = true;\n // Instant whatever `behavior` says, overriding a consumer's `scroll-behavior: smooth`:\n // an animated jump emits scroll events on the way down, each recomputing pinned from a\n // position still far from the bottom, so the container would unpin (and re-pin)\n // mid-flight before it ever settles.\n if (this.pinOnConnectValue && this.#measurable()) this.#scrollToBottom(\"instant\");\n // Read the state back from where the container actually is, asked for a jump or not: a\n // request the engine does not honor must leave it unpinned, so the next append is\n // flagged rather than swallowed. Both hooks are re-derived here, so a stale data-pinned\n // or data-has-new a Turbo cache restore brought back is dropped: has-new records an\n // arrival this connection has not seen, and nothing in the DOM can attest to one.\n this.#setHasNew(false);\n this.#pinned = this.#isPinned();\n this.#reflectPinned();\n\n this.element.addEventListener(\"scroll\", this.#onScroll, { passive: true });\n this.#syncWatched();\n // A container with no box yet reports every metric as 0; it stays unpinned until the\n // layout it will actually be read against arrives.\n if (!this.#measurable()) this.#waitForLayout();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.element.removeEventListener(\"scroll\", this.#onScroll);\n this.#stopWatching();\n this.#stopWaitingForLayout();\n }\n\n /** Moves the append watch onto a `content` target that arrived at runtime. */\n contentTargetConnected(): void {\n this.#syncWatched();\n }\n\n /** Moves the append watch off a `content` target that left, back onto the container. */\n contentTargetDisconnected(): void {\n this.#syncWatched();\n }\n\n /**\n * Settles a has-new region that arrived at runtime on the side the flag is currently\n * on, the way a region present at connect is settled by the connect-time normalization.\n *\n * Stimulus attaches targets before `connect()`, and a container that leaves the document\n * and comes back keeps its controller, so in that window the flag still reads the\n * arrival of the connection that ended. A region attached there is left to the\n * connect-time normalization, which drops the flag and settles the region in one write.\n */\n hasNewTargetConnected(): void {\n if (!this.#connected) return;\n this.#hasNewRegion.reflect(this.element, this.#hasNew);\n }\n\n /**\n * Re-derives pinned when the distance that counts as the bottom is changed at runtime\n * (a morph that swaps the attribute on a retained element).\n */\n thresholdValueChanged(): void {\n if (!this.#connected) return;\n this.#updatePinned();\n }\n\n /**\n * Jumps to the bottom and re-pins (wired to a \"new messages\" button).\n *\n * The has-new flag clears on request — the user has acknowledged the arrival — while\n * pinned is read back from where the scroll landed: a jump that arrives by the time\n * this returns pins immediately, an animated one settles from its own scroll events,\n * and a jump the engine cannot honor leaves the container unpinned, so the next append\n * flags it again instead of being swallowed by a pinned state that does not hold.\n *\n * Which of those happens is not this method's to decide — see {@link behaviorValue}.\n */\n scrollToBottom(): void {\n this.#scrollToBottom();\n this.#setHasNew(false);\n this.#updatePinned();\n }\n\n /** Follows appended children while pinned; otherwise flags new content. */\n #onMutations(mutations: MutationRecord[]): void {\n let added = 0;\n for (const mutation of mutations) added += countElements(mutation.addedNodes);\n if (added === 0) return;\n\n if (this.#pinned) {\n this.#scrollToBottom();\n } else {\n this.#setHasNew(true);\n this.dispatch(\"new\", { detail: { count: added } });\n }\n }\n\n /**\n * Recomputes pinned from the scroll position and reflects it on a transition.\n *\n * @stimeoRenderRoot\n */\n #updatePinned(): void {\n const pinned = this.#isPinned();\n if (pinned === this.#pinned) return;\n this.#pinned = pinned;\n this.#reflectPinned();\n this.dispatch(\"pin\", { detail: { pinned } });\n }\n\n /** Mirrors the current `#pinned` onto the state hooks (clearing has-new once pinned). */\n #reflectPinned(): void {\n if (this.#pinned) {\n this.element.setAttribute(\"data-pinned\", \"true\");\n this.#setHasNew(false); // caught up with the bottom\n } else {\n this.element.removeAttribute(\"data-pinned\");\n }\n }\n\n /**\n * Mirrors whether content has arrived that the container did not follow onto the state\n * hook and onto the regions declared for that state.\n */\n #setHasNew(hasNew: boolean): void {\n this.#hasNew = hasNew;\n if (hasNew) {\n this.element.setAttribute(\"data-has-new\", \"true\");\n } else {\n this.element.removeAttribute(\"data-has-new\");\n }\n this.#hasNewRegion.reflect(this.element, hasNew);\n }\n\n /** Whether the container currently sits within `threshold` of its bottom. */\n #isPinned(): boolean {\n if (!this.#measurable()) return false;\n const el = this.element;\n return el.scrollHeight - el.clientHeight - el.scrollTop <= this.#threshold;\n }\n\n /**\n * The distance from the bottom that counts as pinned: a finite, non-negative number of\n * pixels. Anything else names no distance the container can be at, and settles the\n * comparison the same way at every scroll position, so it falls back to the default.\n * `Number` reads `\"abc\"` as `NaN` and every comparison against it is false; a negative\n * distance sits below the closest the container ever gets; `Infinity` is never\n * exceeded. The first two stop following and flag every append as new, and the last\n * never stops following — it takes the reading position the flag exists to protect.\n * Zero is a real declaration: it pins at the exact bottom only.\n */\n get #threshold(): number {\n const declared = this.thresholdValue;\n return Number.isFinite(declared) && declared >= 0 ? declared : DEFAULT_THRESHOLD;\n }\n\n /**\n * Whether the container has a box to scroll and to measure. One that is not rendered\n * (inside a closed panel) reports every metric as 0, which reads as \"already at the\n * bottom\" — a position describing no layout the user will ever see.\n */\n #measurable(): boolean {\n return this.element.clientHeight > 0;\n }\n\n /**\n * Holds the pinned decision until the container is laid out — otherwise the panel opens\n * at the top while the state claims the bottom, and the appends that arrived meanwhile\n * were followed into a box that could not move rather than flagged.\n */\n #waitForLayout(): void {\n this.#awaitingLayout = true;\n this.#layout.observe(this.element);\n }\n\n /** Runs the held decision once the container has the box it was waiting for. */\n #onLaidOut(): void {\n if (!this.#awaitingLayout || !this.#measurable()) return;\n this.#stopWaitingForLayout();\n if (this.pinOnConnectValue) this.#scrollToBottom(\"instant\");\n this.#updatePinned();\n }\n\n /** Releases the layout watch, whether or not the held decision ever ran. */\n #stopWaitingForLayout(): void {\n this.#awaitingLayout = false;\n this.#layout.disconnect();\n }\n\n /**\n * Scrolls to the bottom, clamped by the engine to the maximum scroll offset — which is\n * 0 for a container tall enough to hold its whole content, so the jump moves nothing\n * there. `behavior` defaults to the configured follow behavior; pass `\"instant\"` for a\n * jump that must not animate.\n */\n #scrollToBottom(behavior: ScrollBehavior = this.#behavior()): void {\n const top = this.element.scrollHeight;\n if (typeof this.element.scrollTo === \"function\") {\n this.element.scrollTo({ top, behavior });\n } else {\n this.element.scrollTop = top;\n }\n }\n\n /**\n * Points the append watch at the current `content` target, or at the container when\n * there is none. Re-resolved whenever that target changes, so a swap does not leave the\n * observer holding a detached node whose appends nobody sees.\n *\n * Stimulus runs the target callbacks outside the connected window too — before\n * `connect()` for a target already in the DOM, and after `disconnect()` while the\n * element is torn down — where this would arm an observer nothing releases. Re-syncing\n * to the target already held is left alone, so an arrival still in flight is not\n * dropped with the observer that was about to deliver it.\n */\n #syncWatched(): void {\n if (!this.#connected) return;\n const next = this.hasContentTarget ? this.contentTarget : this.element;\n if (next === this.#watched) return;\n\n this.#stopWatching();\n this.#watched = next;\n if (typeof MutationObserver === \"undefined\") return;\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(next, { childList: true });\n }\n\n /** Releases the append watch and the element it held. */\n #stopWatching(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n this.#watched = null;\n }\n\n /**\n * The behavior a follow-scroll runs with. `\"auto\"` is **not** a request to arrive at\n * once: it hands the decision to the element's computed `scroll-behavior`, so a\n * consumer stylesheet saying `smooth` animates these scrolls too. Only `\"instant\"`\n * overrides that CSS, which is why reduced motion and the `pinOnConnect` jump name it.\n */\n #behavior(): ScrollBehavior {\n if (prefersReducedMotion()) return \"instant\";\n return this.behaviorValue === \"smooth\" ? \"smooth\" : \"auto\";\n }\n}\n"]}
|
|
@@ -62,8 +62,9 @@ type SubmitControl = HTMLButtonElement | HTMLInputElement;
|
|
|
62
62
|
* Attribute writes use `AttributeLease`, so a consumer mutation made
|
|
63
63
|
* while busy wins over restoration. `DetachGate` preserves an in-flight
|
|
64
64
|
* session across an in-page move, while `BeforeCacheReset` rewinds the
|
|
65
|
-
* snapshot before Turbo caches it. Cache/detach rewinds
|
|
66
|
-
* move focus
|
|
65
|
+
* snapshot before Turbo caches it. Cache/detach rewinds dispatch no `end`,
|
|
66
|
+
* announce nothing and never move focus; a cache rewind that abandons a
|
|
67
|
+
* submission reports it once as `reconcile`.
|
|
67
68
|
*/
|
|
68
69
|
declare class SubmitOnceController extends Controller<HTMLElement> {
|
|
69
70
|
#private;
|
|
@@ -138,6 +138,30 @@ var DetachGate = class _DetachGate {
|
|
|
138
138
|
}
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
+
// src/utils/listener_set.ts
|
|
142
|
+
var ListenerSet = class {
|
|
143
|
+
/** The generation every `add` joins until the next `dispose()`. */
|
|
144
|
+
#abort = new AbortController();
|
|
145
|
+
/**
|
|
146
|
+
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
147
|
+
*
|
|
148
|
+
* The set supplies the signal, so `options` carries everything else the DOM
|
|
149
|
+
* accepts — `capture` included, which has to match at release time and no
|
|
150
|
+
* longer has a second place to drift from.
|
|
151
|
+
*/
|
|
152
|
+
add(target, type, handler, options) {
|
|
153
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Releases every listener of the open generation, synchronously, and opens the
|
|
157
|
+
* next one. Idempotent, and safe before anything has been added.
|
|
158
|
+
*/
|
|
159
|
+
dispose() {
|
|
160
|
+
this.#abort.abort();
|
|
161
|
+
this.#abort = new AbortController();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
141
165
|
// src/utils/safe_timeout.ts
|
|
142
166
|
var TimerRegistry = class {
|
|
143
167
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -206,6 +230,7 @@ var SubmitOnceController = class extends Controller {
|
|
|
206
230
|
static events = ["start", "end", "reconcile"];
|
|
207
231
|
#timers = new SafeTimeout();
|
|
208
232
|
#gate = new DetachGate();
|
|
233
|
+
#listeners = new ListenerSet();
|
|
209
234
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
210
235
|
#sessions = /* @__PURE__ */ new Map();
|
|
211
236
|
#disabled = new AttributeLease("disabled");
|
|
@@ -230,14 +255,12 @@ var SubmitOnceController = class extends Controller {
|
|
|
230
255
|
connect() {
|
|
231
256
|
this.#gate.cancel();
|
|
232
257
|
this.#beforeCache.activate();
|
|
233
|
-
this.element
|
|
234
|
-
this.element
|
|
235
|
-
this.element
|
|
258
|
+
this.#listeners.add(this.element, "submit", this.#onNativeSubmit, { capture: true });
|
|
259
|
+
this.#listeners.add(this.element, "turbo:submit-start", this.#onSubmitStart);
|
|
260
|
+
this.#listeners.add(this.element, "turbo:submit-end", this.#onSubmitEnd);
|
|
236
261
|
}
|
|
237
262
|
disconnect() {
|
|
238
|
-
this.
|
|
239
|
-
this.element.removeEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
240
|
-
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
263
|
+
this.#listeners.dispose();
|
|
241
264
|
this.#beforeCache.deactivate();
|
|
242
265
|
this.#gate.disconnected(this, () => this.#teardown());
|
|
243
266
|
}
|
|
@@ -308,7 +331,12 @@ var SubmitOnceController = class extends Controller {
|
|
|
308
331
|
if (!session || !this.#isNativeSubmitControl(target)) return;
|
|
309
332
|
this.#enterControl(session, target);
|
|
310
333
|
}
|
|
311
|
-
/**
|
|
334
|
+
/**
|
|
335
|
+
* Ends one session, restores owned state, and reports the observable completion.
|
|
336
|
+
*
|
|
337
|
+
* @stimeoRuntimeOnly `announceReadyText` words the one announcement of this finished submission
|
|
338
|
+
* and `restoreFocus` decides where its focus returns.
|
|
339
|
+
*/
|
|
312
340
|
#complete(form, reason, success) {
|
|
313
341
|
const session = this.#sessionFor(form);
|
|
314
342
|
if (!session) return;
|
|
@@ -330,7 +358,12 @@ var SubmitOnceController = class extends Controller {
|
|
|
330
358
|
this.#disabled.write(control, "");
|
|
331
359
|
this.#controlBusy.write(control, "true");
|
|
332
360
|
}
|
|
333
|
-
/**
|
|
361
|
+
/**
|
|
362
|
+
* Swaps only the triggering control's safe label channel.
|
|
363
|
+
*
|
|
364
|
+
* @stimeoRuntimeOnly `busyLabel` is the label this one submission shows until it ends; the idle
|
|
365
|
+
* label comes back with the state it returns.
|
|
366
|
+
*/
|
|
334
367
|
#enterLabel(session) {
|
|
335
368
|
const control = session.submitter;
|
|
336
369
|
if (!control) return;
|
|
@@ -417,7 +450,10 @@ var SubmitOnceController = class extends Controller {
|
|
|
417
450
|
}
|
|
418
451
|
this.#sessions.clear();
|
|
419
452
|
}
|
|
420
|
-
/**
|
|
453
|
+
/**
|
|
454
|
+
* Keeps Turbo's cached snapshot idle without `end`, announcements, or focus
|
|
455
|
+
* moves; the abandoned forms are reported once as `reconcile`.
|
|
456
|
+
*/
|
|
421
457
|
#rewindForCache() {
|
|
422
458
|
const forms = [...this.#sessions.keys()];
|
|
423
459
|
this.#abandonSessions();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/attribute_lease.ts","../../src/utils/before_cache_reset.ts","../../src/utils/detach_gate.ts","../../src/utils/safe_timeout.ts","../../src/controllers/submit_once_controller.ts"],"names":[],"mappings":"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","file":"submit_once_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/** One temporarily controlled attribute value and the authored value it displaced. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n written: string | null;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events.\n * A consumer that must not let a derived value become the authored baseline of a\n * cached page returns its leases from its own `turbo:before-cache` rewind, where the\n * matching `disconnect()` also releases the subscription.\n */\nexport class AttributeLease<T extends Element = Element> {\n readonly #attribute: string;\n readonly #records = new Map<T, AttributeLeaseRecord>();\n\n /** @param attribute - The attribute whose temporary values this lease owns. */\n constructor(attribute: string) {\n this.#attribute = attribute;\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const existing = this.#records.get(element);\n if (existing) {\n existing.written = value;\n } else {\n this.#records.set(element, {\n original: element.getAttribute(this.#attribute),\n written: value,\n });\n }\n\n this.#reflect(element, value);\n }\n\n /** Returns one lease without overwriting a value subsequently authored by a consumer. */\n return(element: T): void {\n const record = this.#records.get(element);\n if (!record) return;\n this.#records.delete(element);\n const stillOwned = element.getAttribute(this.#attribute) === record.written;\n if (stillOwned) this.#reflect(element, record.original);\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n if (element.getAttribute(this.#attribute) === value) return;\n if (value === null) element.removeAttribute(this.#attribute);\n else element.setAttribute(this.#attribute, value);\n }\n\n /** Returns every outstanding lease using the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#records.keys())) this.return(element);\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Shared \"in-page move vs real detach\" discriminator for `disconnect()`.\n *\n * A mid-session `disconnect()` whose element is still in the document is\n * ambiguous. It can be:\n *\n * - an **in-page move** — a consumer (sortable, a teleport) re-inserted the\n * element; Stimulus reconnects the SAME instance in the same mutation batch\n * and the controller's runtime state must survive;\n * - a **detach that keeps the element** — `data-controller` no longer lists the\n * identifier (a Turbo 8 morph), or the element moved outside the observed\n * root (a scoped `Application.start(root)`, a shadow root); no reconnect will\n * ever come and the controller must tear down or it leaks its sessions,\n * document listeners, and moved nodes.\n *\n * Neither available signal decides this alone. A synchronous `data-controller`\n * token check catches the morph case but misses an observed-root exit (the\n * token stays, the reconnect never comes). Deferring teardown one microtask and\n * letting `connect()` cancel it catches the root exit, but leans on Stimulus\n * reconnecting moved elements within the same mutation batch — internal\n * behavior, not a public contract. {@link DetachGate} composes both: the token\n * check is the synchronous fast path (no probe, no reliance on batching), the\n * microtask probe is the fallback for the genuinely ambiguous remainder.\n *\n * Policy stays with the consumer; this util owns the discrimination only:\n *\n * - **Probe** ({@link disconnected}) when the teardown must eventually happen —\n * live drag sessions, document listeners, teleported *content*. Call\n * {@link cancel} from `connect()` (a reconnect means in-page move) and from\n * the head of any direct teardown path (see below).\n * - **Keep** (bare {@link DetachGate.isDetached}) when undoing the ambiguous\n * case would fight the controller's own effect: a controller that moves its\n * OWN element (portal's no-`content` form) exits a scoped observed root as\n * its normal job — a probe-driven teardown would restore the element into\n * the root, reconnect, re-teleport, and disconnect again, forever. Such\n * controllers keep their state on an ambiguous disconnect and tear down only\n * on {@link DetachGate.isDetached}.\n *\n * Event convention for probe consumers: teardown into a dead (detached) tree is\n * silent — consumers restore from their own `connect()`; a detach that keeps the\n * tree alive must end an in-flight session with a `cancel` event, or composing\n * consumers strand their session bookkeeping.\n */\n\n/** The slice of a Stimulus controller the gate inspects. */\nexport interface DetachGateHost {\n readonly element: Element;\n readonly identifier: string;\n}\n\n/**\n * Per-controller-instance gate deciding whether a `disconnect()` is a real\n * detach. Hold one per controller (`#gate = new DetachGate()`) — Stimulus\n * reuses the instance across reconnects, so the pending probe state carries\n * over exactly as the protocol needs.\n *\n * @example\n * ```ts\n * #gate = new DetachGate();\n *\n * connect(): void {\n * this.#gate.cancel(); // a reconnect: the element moved in-page\n * // …\n * }\n *\n * disconnect(): void {\n * this.#gate.disconnected(this, () => this.#teardown());\n * }\n *\n * #teardown(): void {\n * this.#gate.cancel(); // disarm a still-queued probe (double-run guard)\n * // …\n * }\n * ```\n */\nexport class DetachGate {\n /** Set while a probe is queued, waiting for a reconnect to cancel it. */\n #pending = false;\n\n /**\n * True while a probe is queued — the last disconnect was ambiguous and no\n * reconnect has cancelled it yet. Read it from `connect()` to tell the\n * reconnect half of an in-page move from a first connect: a controller whose\n * initialisation restarts a measurement (a min-duration floor, an elapsed\n * counter) must skip it for the move, where nothing actually restarted.\n */\n get pending(): boolean {\n return this.#pending;\n }\n\n /**\n * True when the disconnect is definitely a real detach — the element left\n * the document, or `data-controller` no longer lists the identifier. False\n * means ambiguous (in-page move or observed-root exit), NOT \"alive\".\n */\n static isDetached(host: DetachGateHost): boolean {\n if (!host.element.isConnected) return true;\n const tokens = (host.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return !tokens.includes(host.identifier);\n }\n\n /**\n * Call from `disconnect()`: runs `teardown` synchronously on a definite\n * detach (fast path), otherwise defers it one microtask — a reconnect\n * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.\n * One microtask is the whole probe window: Stimulus reconnects a moved\n * element within the same mutation batch, before the checkpoint drains.\n */\n disconnected(host: DetachGateHost, teardown: () => void): void {\n if (DetachGate.isDetached(host)) {\n // Also disarms a probe a previous ambiguous disconnect left queued\n // (defer, element removed, disconnect again) — exactly one teardown.\n this.#pending = false;\n teardown();\n return;\n }\n this.#pending = true;\n queueMicrotask(() => {\n if (!this.#pending) return;\n this.#pending = false;\n teardown();\n });\n }\n\n /**\n * Disarms a pending probe. Call from `connect()` (the reconnect that proves\n * an in-page move) and from the head of any teardown path not routed through\n * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can\n * never run the teardown a second time.\n */\n cancel(): void {\n this.#pending = false;\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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\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 { announce } from \"../utils/announce\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { DetachGate } from \"../utils/detach_gate\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Native controls that can submit a form. */\ntype SubmitControl = HTMLButtonElement | HTMLInputElement;\n\n/** Why an active submission returned to idle. */\ntype CompletionReason = \"canceled\" | \"manual\" | \"timeout\" | \"turbo\";\n\n/** A text/value write that cannot be represented by an `AttributeLease`. */\ntype LabelWrite =\n | {\n readonly channel: \"text\";\n readonly control: HTMLButtonElement;\n readonly original: string;\n readonly written: string;\n }\n | {\n readonly channel: \"value\";\n readonly control: HTMLInputElement;\n readonly original: string;\n readonly written: string;\n };\n\n/** One form's independent in-flight submission. */\ninterface SubmissionSession {\n readonly form: HTMLFormElement;\n readonly submitter: SubmitControl | null;\n readonly submitterHadFocus: boolean;\n readonly controls: Set<SubmitControl>;\n timeoutId: number | null;\n labelWrite: LabelWrite | null;\n idlePart: HTMLElement | null;\n busyPart: HTMLElement | null;\n}\n\n/** Per-button override for the controller's default busy label. */\nconst BUTTON_BUSY_LABEL = \"data-submit-once-busy-label\";\n\n/**\n * Headless form-scoped double-submit guard (identifier:\n * `stimeo--submit-once`). It disables every native submit control associated\n * with the submitting form, publishes busy state, and restores only the state\n * it still owns when the operation finishes.\n *\n * Turbo forms need no `data-action`: the controller observes\n * `turbo:submit-start` and `turbo:submit-end` itself.\n *\n * A non-Turbo async form owns its own lifecycle and declares each transition:\n * `submit->stimeo--submit-once#start` applies the busy state synchronously,\n * before the request is issued, and `#finish` / `#cancel` end it. Cancelling the\n * event's default is how such a form suppresses the native navigation, so it\n * never means the submission died — only `#cancel` does. A submit already\n * cancelled by an earlier listener never starts a session at all.\n *\n * ```html\n * <form data-controller=\"stimeo--submit-once\"\n * data-action=\"submit->stimeo--submit-once#start\n * save:complete->stimeo--submit-once#finish\n * save:error->stimeo--submit-once#cancel\">\n * <button type=\"submit\">Save</button>\n * </form>\n * ```\n *\n * A form whose submit a later listener vetoes needs `#cancel` wired to that\n * veto, or a non-zero `timeout`, to leave the busy state.\n *\n * Plain text buttons can use `busyLabel` (overridden by\n * `data-submit-once-busy-label`). A button with authored structure instead uses\n * an `idle` / `busy` target pair inside that button, so icons and descendants\n * are never replaced with `textContent`:\n *\n * ```html\n * <form data-controller=\"stimeo--submit-once\"\n * data-stimeo--submit-once-announce-text-value=\"Saving…\"\n * data-stimeo--submit-once-announce-ready-text-value=\"Saved.\">\n * <button type=\"submit\" data-stimeo--submit-once-target=\"submit\">\n * <span data-stimeo--submit-once-target=\"idle\">Save</span>\n * <span data-stimeo--submit-once-target=\"busy\" hidden>Saving…</span>\n * </button>\n * </form>\n * ```\n *\n * Events:\n * - `stimeo--submit-once:start` — `{ form, submitter }`.\n * - `stimeo--submit-once:reconcile` — `{ forms: HTMLFormElement[] }`, the forms\n * whose in-flight submission the Turbo cache rewind abandoned. `end` would\n * claim the submission resolved, so the rewind reports itself instead.\n * - `stimeo--submit-once:end` — `{ form, submitter, reason, success? }`, where\n * `reason` is `\"turbo\"`, `\"timeout\"`, `\"manual\"`, or `\"canceled\"`. Only\n * `\"canceled\"` skips the completion announcement, because no operation ran.\n *\n * @remarks\n * Behavior only: the consumer owns every visual, label, and announcement\n * string. `announceText` / `announceReadyText` are empty by default and speak\n * through the page's shared `stimeo--announcer` only on real state transitions.\n * Attribute writes use `AttributeLease`, so a consumer mutation made\n * while busy wins over restoration. `DetachGate` preserves an in-flight\n * session across an in-page move, while `BeforeCacheReset` rewinds the\n * snapshot before Turbo caches it. Cache/detach rewinds are silent and never\n * move focus.\n */\nexport class SubmitOnceController extends Controller<HTMLElement> {\n static override targets = [\"submit\", \"idle\", \"busy\"];\n static override values = {\n announceText: { type: String, default: \"\" },\n announceReadyText: { type: String, default: \"\" },\n busyLabel: { type: String, default: \"\" },\n timeout: { type: Number, default: 0 },\n restoreFocus: { type: Boolean, default: false },\n };\n static actions = [\"cancel\", \"finish\", \"start\"] as const;\n static events = [\"start\", \"end\", \"reconcile\"] as const;\n\n declare readonly submitTargets: SubmitControl[];\n declare readonly idleTargets: HTMLElement[];\n declare readonly busyTargets: HTMLElement[];\n\n declare announceTextValue: string;\n declare announceReadyTextValue: string;\n declare busyLabelValue: string;\n declare timeoutValue: number;\n declare restoreFocusValue: boolean;\n\n readonly #timers = new SafeTimeout();\n readonly #gate = new DetachGate();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n readonly #sessions = new Map<HTMLFormElement, SubmissionSession>();\n\n readonly #disabled = new AttributeLease<SubmitControl>(\"disabled\");\n readonly #controlBusy = new AttributeLease<SubmitControl>(\"aria-busy\");\n readonly #formBusy = new AttributeLease<HTMLFormElement>(\"aria-busy\");\n readonly #submitting = new AttributeLease<HTMLFormElement>(\"data-submitting\");\n readonly #ariaLabel = new AttributeLease<HTMLButtonElement>(\"aria-label\");\n readonly #hidden = new AttributeLease<HTMLElement>(\"hidden\");\n\n readonly #onSubmitStart = (event: Event): void => {\n this.start(event);\n };\n\n readonly #onSubmitEnd = (event: Event): void => {\n this.#complete(this.#eventForm(event), \"turbo\", this.#eventSuccess(event));\n };\n\n /** Blocks a later native submit while one for the same form is still active. */\n readonly #onNativeSubmit = (event: Event): void => {\n // The submit event's target is its form. This listener runs in capture so it\n // always observes that first event before a public `start` action creates the\n // session, while a later event sees the session and is canceled here.\n const session = this.#sessionFor(event.target);\n if (!session) return;\n this.#syncControls(session);\n event.preventDefault();\n };\n\n override connect(): void {\n this.#gate.cancel();\n this.#beforeCache.activate();\n this.element.addEventListener(\"submit\", this.#onNativeSubmit, true);\n this.element.addEventListener(\"turbo:submit-start\", this.#onSubmitStart);\n this.element.addEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"submit\", this.#onNativeSubmit, true);\n this.element.removeEventListener(\"turbo:submit-start\", this.#onSubmitStart);\n this.element.removeEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n this.#beforeCache.deactivate();\n this.#gate.disconnected(this, () => this.#teardown());\n }\n\n /**\n * Begins the submission represented by `event`.\n *\n * Native `submit` events are intended for non-Turbo async forms. Turbo calls\n * this path through its auto-subscribed `turbo:submit-start` event. An event\n * whose default an earlier listener already cancelled carries no submission,\n * so it starts nothing.\n */\n start(event: Event): void {\n if (event.defaultPrevented) return;\n const form = this.#eventForm(event);\n if (!form || this.#sessions.has(form)) return;\n\n const controls = this.#submitControls(form);\n const submitter = this.#resolveSubmitter(event, controls);\n const session: SubmissionSession = {\n form,\n submitter,\n submitterHadFocus: submitter !== null && document.activeElement === submitter,\n controls: new Set(),\n timeoutId: null,\n labelWrite: null,\n idlePart: null,\n busyPart: null,\n };\n this.#sessions.set(form, session);\n\n this.#formBusy.write(form, \"true\");\n this.#submitting.write(form, \"true\");\n this.#syncControls(session, controls);\n this.#enterLabel(session);\n this.dispatch(\"start\", { detail: { form, submitter } });\n\n if (this.timeoutValue > 0) {\n session.timeoutId = this.#timers.set(\n () => this.#complete(form, \"timeout\", false),\n this.timeoutValue,\n );\n }\n\n announce(this.announceTextValue);\n }\n\n /**\n * Completes a non-Turbo async submission for the form associated with\n * `event`. When called directly with no event, it completes the controller's\n * sole active form (or the controller element itself when it is a form).\n */\n finish(event?: Event): void {\n const form = event ? this.#eventForm(event) : this.#defaultForm();\n this.#complete(form, \"manual\", event ? this.#eventSuccess(event) : undefined);\n }\n\n /**\n * Abandons the submission for the form associated with `event` because it\n * never reached the network — a veto from validation, or a request the\n * consumer gave up before sending. With no event it targets the controller's\n * sole active form, exactly like {@link finish}.\n *\n * The distinction from `finish` is observable: a request that ran and failed\n * is `finish` with `success: false` and still announces completion, while a\n * submission that never happened announces nothing.\n */\n cancel(event?: Event): void {\n const form = event ? this.#eventForm(event) : this.#defaultForm();\n this.#complete(form, \"canceled\", false);\n }\n\n /** Disables a newly connected explicit submit target during its form's session. */\n submitTargetConnected(target: SubmitControl): void {\n const session = this.#sessionFor(target.form);\n if (!session || !this.#isNativeSubmitControl(target)) return;\n this.#enterControl(session, target);\n }\n\n /** Ends one session, restores owned state, and reports the observable completion. */\n #complete(form: HTMLFormElement | null, reason: CompletionReason, success?: boolean): void {\n const session = this.#sessionFor(form);\n if (!session) return;\n this.#sessions.delete(session.form);\n this.#returnSessionState(session);\n\n const detail =\n success === undefined\n ? { form: session.form, submitter: session.submitter, reason }\n : { form: session.form, submitter: session.submitter, reason, success };\n this.dispatch(\"end\", { detail });\n this.#restoreFocus(session);\n if (reason !== \"canceled\") announce(this.announceReadyTextValue);\n }\n\n /** Applies the busy state to every current native/explicit control exactly once. */\n #syncControls(session: SubmissionSession, controls = this.#submitControls(session.form)): void {\n for (const control of controls) this.#enterControl(session, control);\n }\n\n /** Temporarily disables one enabled submit control while retaining authored state. */\n #enterControl(session: SubmissionSession, control: SubmitControl): void {\n if (session.controls.has(control) || control.matches(\":disabled\")) return;\n session.controls.add(control);\n this.#disabled.write(control, \"\");\n this.#controlBusy.write(control, \"true\");\n }\n\n /** Swaps only the triggering control's safe label channel. */\n #enterLabel(session: SubmissionSession): void {\n const control = session.submitter;\n if (!control) return;\n\n const idlePart = this.#partInside(control, this.idleTargets);\n const busyPart = this.#partInside(control, this.busyTargets);\n if (idlePart && busyPart) {\n session.idlePart = idlePart;\n session.busyPart = busyPart;\n this.#hidden.write(idlePart, \"\");\n this.#hidden.write(busyPart, null);\n }\n\n const label = control.getAttribute(BUTTON_BUSY_LABEL) ?? this.busyLabelValue;\n if (label.length === 0) return;\n if (control instanceof HTMLInputElement) {\n // `input[type=image]#value` is submitted data, not a visible label.\n if (control.type !== \"submit\") return;\n session.labelWrite = {\n channel: \"value\",\n control,\n original: control.value,\n written: label,\n };\n control.value = label;\n return;\n }\n if (control.hasAttribute(\"aria-label\")) {\n this.#ariaLabel.write(control, label);\n return;\n }\n // A structured button needs the explicit idle/busy pair above. Replacing its\n // textContent would destroy icons, counters, and application-owned listeners.\n if (control.childElementCount > 0) return;\n session.labelWrite = {\n channel: \"text\",\n control,\n original: control.textContent ?? \"\",\n written: label,\n };\n control.textContent = label;\n }\n\n /** Returns labels, controls, and form hooks, respecting intervening consumer writes. */\n #returnSessionState(session: SubmissionSession): void {\n if (session.timeoutId !== null) {\n this.#timers.clear(session.timeoutId);\n session.timeoutId = null;\n }\n\n if (session.labelWrite?.channel === \"value\") {\n const { control, original, written } = session.labelWrite;\n if (control.value === written) control.value = original;\n } else if (session.labelWrite?.channel === \"text\") {\n const { control, original, written } = session.labelWrite;\n if (control.childElementCount === 0 && control.textContent === written) {\n control.textContent = original;\n }\n }\n session.labelWrite = null;\n\n if (session.submitter instanceof HTMLButtonElement) {\n this.#ariaLabel.return(session.submitter);\n }\n if (session.idlePart) this.#hidden.return(session.idlePart);\n if (session.busyPart) this.#hidden.return(session.busyPart);\n session.idlePart = null;\n session.busyPart = null;\n\n for (const control of session.controls) {\n this.#controlBusy.return(control);\n this.#disabled.return(control);\n }\n session.controls.clear();\n this.#submitting.return(session.form);\n this.#formBusy.return(session.form);\n }\n\n /** Restores focus only when disabling the focused submitter actually stranded it. */\n #restoreFocus(session: SubmissionSession): void {\n const submitter = session.submitter;\n if (!this.restoreFocusValue || !session.submitterHadFocus || !submitter?.isConnected) return;\n const active = document.activeElement;\n if (active === null || active === document.body || active === document.documentElement) {\n submitter.focus();\n }\n }\n\n /** Silently abandons every session before caching or on a real detach. */\n #abandonSessions(): void {\n this.#timers.clearAll();\n for (const session of this.#sessions.values()) {\n session.timeoutId = null;\n this.#returnSessionState(session);\n }\n this.#sessions.clear();\n }\n\n /** Keeps Turbo's cached snapshot idle, without events, announcements, or focus moves. */\n #rewindForCache(): void {\n const forms = [...this.#sessions.keys()];\n this.#abandonSessions();\n // A submission in flight dies with the navigation, so a consumer still\n // painting \"submitting\" from `start` would never be released.\n if (forms.length > 0) this.dispatch(\"reconcile\", { detail: { forms } });\n }\n\n /** Tears down a true detach; an in-page move is canceled by the next `connect()`. */\n #teardown(): void {\n this.#gate.cancel();\n this.#abandonSessions();\n }\n\n /** All native controls associated with `form`, plus valid explicit targets. */\n #submitControls(form: HTMLFormElement): SubmitControl[] {\n const controls = new Set<SubmitControl>();\n for (const element of Array.from(form.elements)) {\n if (this.#isNativeSubmitControl(element)) controls.add(element);\n }\n // Browsers omit input[type=image] from form.elements. Search\n // the form's tree root as well so descendant and external `form=` image\n // submitters receive the same guard without broadening into sibling forms.\n const root = form.getRootNode() as ParentNode;\n for (const image of root.querySelectorAll<HTMLInputElement>('input[type=\"image\"]')) {\n if (image.form === form) controls.add(image);\n }\n return Array.from(controls);\n }\n\n /** Resolves the real submitter, falling back to the first enabled submit control. */\n #resolveSubmitter(event: Event, controls: SubmitControl[]): SubmitControl | null {\n const submitter = this.#eventSubmitter(event);\n if (submitter && controls.includes(submitter)) return submitter;\n return controls.find((control) => !control.matches(\":disabled\")) ?? controls[0] ?? null;\n }\n\n /** Reads a native or Turbo submitter without assuming `SubmitEvent` exists. */\n #eventSubmitter(event: Event): SubmitControl | null {\n if (typeof SubmitEvent !== \"undefined\" && event instanceof SubmitEvent) {\n return this.#isNativeSubmitControl(event.submitter) ? event.submitter : null;\n }\n const detail = (event as CustomEvent<{ formSubmission?: { submitter?: HTMLElement | null } }>)\n .detail;\n const submitter = detail?.formSubmission?.submitter;\n return this.#isNativeSubmitControl(submitter) ? submitter : null;\n }\n\n /** Finds the form represented by an action/Turbo event and verifies this scope owns it. */\n #eventForm(event: Event): HTMLFormElement | null {\n const detail = (\n event as CustomEvent<{\n form?: HTMLFormElement;\n formSubmission?: { formElement?: HTMLFormElement; submitter?: HTMLElement | null };\n }>\n ).detail;\n const target = event.target;\n const candidate =\n (target instanceof HTMLFormElement ? target : null) ??\n (target instanceof Element ? target.closest(\"form\") : null) ??\n detail?.formSubmission?.formElement ??\n detail?.form ??\n this.#eventSubmitter(event)?.form ??\n (this.element instanceof HTMLFormElement ? this.element : null);\n return candidate && this.#ownsForm(candidate) ? candidate : null;\n }\n\n /** The only unambiguous form for a direct `finish()` call. */\n #defaultForm(): HTMLFormElement | null {\n if (this.element instanceof HTMLFormElement && this.#ownsForm(this.element))\n return this.element;\n if (this.#sessions.size === 1) return this.#sessions.keys().next().value ?? null;\n return null;\n }\n\n /** Prevents an ancestor controller from also taking over a nested instance's form. */\n #ownsForm(form: HTMLFormElement): boolean {\n let candidate: Element | null = form;\n while (candidate) {\n const identifiers = (candidate.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n if (identifiers.includes(this.identifier)) return candidate === this.element;\n candidate = candidate.parentElement;\n }\n return false;\n }\n\n /** Returns the active session for a form-shaped external event value. */\n #sessionFor(candidate: unknown): SubmissionSession | undefined {\n if (!(candidate instanceof HTMLFormElement)) return undefined;\n return this.#sessions.get(candidate);\n }\n\n /** Effective native submit controls, including implicit buttons and image inputs. */\n #isNativeSubmitControl(element: unknown): element is SubmitControl {\n if (element instanceof HTMLButtonElement) return element.type === \"submit\";\n return (\n element instanceof HTMLInputElement && (element.type === \"submit\" || element.type === \"image\")\n );\n }\n\n /** Finds a structured label part belonging to this button, not a nested control. */\n #partInside(control: SubmitControl, parts: HTMLElement[]): HTMLElement | null {\n return (\n parts.find(\n (part) =>\n control.contains(part) &&\n part.closest(\"button, input[type=submit], input[type=image]\") === control,\n ) ?? null\n );\n }\n\n /** Reads an optional boolean `success` from Turbo or a manual completion event. */\n #eventSuccess(event: Event): boolean | undefined {\n const success = (event as CustomEvent<{ success?: unknown }>).detail?.success;\n return typeof success === \"boolean\" ? success : undefined;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/attribute_lease.ts","../../src/utils/before_cache_reset.ts","../../src/utils/detach_gate.ts","../../src/utils/listener_set.ts","../../src/utils/safe_timeout.ts","../../src/controllers/submit_once_controller.ts"],"names":[],"mappings":"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","file":"submit_once_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/** One temporarily controlled attribute value and the authored value it displaced. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n written: string | null;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events.\n * A consumer that must not let a derived value become the authored baseline of a\n * cached page returns its leases from its own `turbo:before-cache` rewind, where the\n * matching `disconnect()` also releases the subscription.\n */\nexport class AttributeLease<T extends Element = Element> {\n readonly #attribute: string;\n readonly #records = new Map<T, AttributeLeaseRecord>();\n\n /** @param attribute - The attribute whose temporary values this lease owns. */\n constructor(attribute: string) {\n this.#attribute = attribute;\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const existing = this.#records.get(element);\n if (existing) {\n existing.written = value;\n } else {\n this.#records.set(element, {\n original: element.getAttribute(this.#attribute),\n written: value,\n });\n }\n\n this.#reflect(element, value);\n }\n\n /** Returns one lease without overwriting a value subsequently authored by a consumer. */\n return(element: T): void {\n const record = this.#records.get(element);\n if (!record) return;\n this.#records.delete(element);\n const stillOwned = element.getAttribute(this.#attribute) === record.written;\n if (stillOwned) this.#reflect(element, record.original);\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n if (element.getAttribute(this.#attribute) === value) return;\n if (value === null) element.removeAttribute(this.#attribute);\n else element.setAttribute(this.#attribute, value);\n }\n\n /** Returns every outstanding lease using the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#records.keys())) this.return(element);\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Shared \"in-page move vs real detach\" discriminator for `disconnect()`.\n *\n * A mid-session `disconnect()` whose element is still in the document is\n * ambiguous. It can be:\n *\n * - an **in-page move** — a consumer (sortable, a teleport) re-inserted the\n * element; Stimulus reconnects the SAME instance in the same mutation batch\n * and the controller's runtime state must survive;\n * - a **detach that keeps the element** — `data-controller` no longer lists the\n * identifier (a Turbo 8 morph), or the element moved outside the observed\n * root (a scoped `Application.start(root)`, a shadow root); no reconnect will\n * ever come and the controller must tear down or it leaks its sessions,\n * document listeners, and moved nodes.\n *\n * Neither available signal decides this alone. A synchronous `data-controller`\n * token check catches the morph case but misses an observed-root exit (the\n * token stays, the reconnect never comes). Deferring teardown one microtask and\n * letting `connect()` cancel it catches the root exit, but leans on Stimulus\n * reconnecting moved elements within the same mutation batch — internal\n * behavior, not a public contract. {@link DetachGate} composes both: the token\n * check is the synchronous fast path (no probe, no reliance on batching), the\n * microtask probe is the fallback for the genuinely ambiguous remainder.\n *\n * Policy stays with the consumer; this util owns the discrimination only:\n *\n * - **Probe** ({@link disconnected}) when the teardown must eventually happen —\n * live drag sessions, document listeners, teleported *content*. Call\n * {@link cancel} from `connect()` (a reconnect means in-page move) and from\n * the head of any direct teardown path (see below).\n * - **Keep** (bare {@link DetachGate.isDetached}) when undoing the ambiguous\n * case would fight the controller's own effect: a controller that moves its\n * OWN element (portal's no-`content` form) exits a scoped observed root as\n * its normal job — a probe-driven teardown would restore the element into\n * the root, reconnect, re-teleport, and disconnect again, forever. Such\n * controllers keep their state on an ambiguous disconnect and tear down only\n * on {@link DetachGate.isDetached}.\n *\n * Event convention for probe consumers: teardown into a dead (detached) tree is\n * silent — consumers restore from their own `connect()`; a detach that keeps the\n * tree alive must end an in-flight session with a `cancel` event, or composing\n * consumers strand their session bookkeeping.\n */\n\n/** The slice of a Stimulus controller the gate inspects. */\nexport interface DetachGateHost {\n readonly element: Element;\n readonly identifier: string;\n}\n\n/**\n * Per-controller-instance gate deciding whether a `disconnect()` is a real\n * detach. Hold one per controller (`#gate = new DetachGate()`) — Stimulus\n * reuses the instance across reconnects, so the pending probe state carries\n * over exactly as the protocol needs.\n *\n * @example\n * ```ts\n * #gate = new DetachGate();\n *\n * connect(): void {\n * this.#gate.cancel(); // a reconnect: the element moved in-page\n * // …\n * }\n *\n * disconnect(): void {\n * this.#gate.disconnected(this, () => this.#teardown());\n * }\n *\n * #teardown(): void {\n * this.#gate.cancel(); // disarm a still-queued probe (double-run guard)\n * // …\n * }\n * ```\n */\nexport class DetachGate {\n /** Set while a probe is queued, waiting for a reconnect to cancel it. */\n #pending = false;\n\n /**\n * True while a probe is queued — the last disconnect was ambiguous and no\n * reconnect has cancelled it yet. Read it from `connect()` to tell the\n * reconnect half of an in-page move from a first connect: a controller whose\n * initialisation restarts a measurement (a min-duration floor, an elapsed\n * counter) must skip it for the move, where nothing actually restarted.\n */\n get pending(): boolean {\n return this.#pending;\n }\n\n /**\n * True when the disconnect is definitely a real detach — the element left\n * the document, or `data-controller` no longer lists the identifier. False\n * means ambiguous (in-page move or observed-root exit), NOT \"alive\".\n */\n static isDetached(host: DetachGateHost): boolean {\n if (!host.element.isConnected) return true;\n const tokens = (host.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return !tokens.includes(host.identifier);\n }\n\n /**\n * Call from `disconnect()`: runs `teardown` synchronously on a definite\n * detach (fast path), otherwise defers it one microtask — a reconnect\n * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.\n * One microtask is the whole probe window: Stimulus reconnects a moved\n * element within the same mutation batch, before the checkpoint drains.\n */\n disconnected(host: DetachGateHost, teardown: () => void): void {\n if (DetachGate.isDetached(host)) {\n // Also disarms a probe a previous ambiguous disconnect left queued\n // (defer, element removed, disconnect again) — exactly one teardown.\n this.#pending = false;\n teardown();\n return;\n }\n this.#pending = true;\n queueMicrotask(() => {\n if (!this.#pending) return;\n this.#pending = false;\n teardown();\n });\n }\n\n /**\n * Disarms a pending probe. Call from `connect()` (the reconnect that proves\n * an in-page move) and from the head of any teardown path not routed through\n * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can\n * never run the teardown a second time.\n */\n cancel(): void {\n this.#pending = false;\n }\n}\n","/**\n * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt. {@link\n * ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, so `dispose()` belongs exactly where\n * the `removeEventListener` block stood. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, or one that\n * lives for less than the connection, keeps its own pair of calls.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included, which has to match at release time and no\n * longer has a second place to drift from.\n */\n add(\n target: EventTarget,\n type: string,\n handler: EventListener,\n options?: Omit<AddEventListenerOptions, \"signal\">,\n ): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\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 { announce } from \"../utils/announce\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { DetachGate } from \"../utils/detach_gate\";\nimport { ListenerSet } from \"../utils/listener_set\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Native controls that can submit a form. */\ntype SubmitControl = HTMLButtonElement | HTMLInputElement;\n\n/** Why an active submission returned to idle. */\ntype CompletionReason = \"canceled\" | \"manual\" | \"timeout\" | \"turbo\";\n\n/** A text/value write that cannot be represented by an `AttributeLease`. */\ntype LabelWrite =\n | {\n readonly channel: \"text\";\n readonly control: HTMLButtonElement;\n readonly original: string;\n readonly written: string;\n }\n | {\n readonly channel: \"value\";\n readonly control: HTMLInputElement;\n readonly original: string;\n readonly written: string;\n };\n\n/** One form's independent in-flight submission. */\ninterface SubmissionSession {\n readonly form: HTMLFormElement;\n readonly submitter: SubmitControl | null;\n readonly submitterHadFocus: boolean;\n readonly controls: Set<SubmitControl>;\n timeoutId: number | null;\n labelWrite: LabelWrite | null;\n idlePart: HTMLElement | null;\n busyPart: HTMLElement | null;\n}\n\n/** Per-button override for the controller's default busy label. */\nconst BUTTON_BUSY_LABEL = \"data-submit-once-busy-label\";\n\n/**\n * Headless form-scoped double-submit guard (identifier:\n * `stimeo--submit-once`). It disables every native submit control associated\n * with the submitting form, publishes busy state, and restores only the state\n * it still owns when the operation finishes.\n *\n * Turbo forms need no `data-action`: the controller observes\n * `turbo:submit-start` and `turbo:submit-end` itself.\n *\n * A non-Turbo async form owns its own lifecycle and declares each transition:\n * `submit->stimeo--submit-once#start` applies the busy state synchronously,\n * before the request is issued, and `#finish` / `#cancel` end it. Cancelling the\n * event's default is how such a form suppresses the native navigation, so it\n * never means the submission died — only `#cancel` does. A submit already\n * cancelled by an earlier listener never starts a session at all.\n *\n * ```html\n * <form data-controller=\"stimeo--submit-once\"\n * data-action=\"submit->stimeo--submit-once#start\n * save:complete->stimeo--submit-once#finish\n * save:error->stimeo--submit-once#cancel\">\n * <button type=\"submit\">Save</button>\n * </form>\n * ```\n *\n * A form whose submit a later listener vetoes needs `#cancel` wired to that\n * veto, or a non-zero `timeout`, to leave the busy state.\n *\n * Plain text buttons can use `busyLabel` (overridden by\n * `data-submit-once-busy-label`). A button with authored structure instead uses\n * an `idle` / `busy` target pair inside that button, so icons and descendants\n * are never replaced with `textContent`:\n *\n * ```html\n * <form data-controller=\"stimeo--submit-once\"\n * data-stimeo--submit-once-announce-text-value=\"Saving…\"\n * data-stimeo--submit-once-announce-ready-text-value=\"Saved.\">\n * <button type=\"submit\" data-stimeo--submit-once-target=\"submit\">\n * <span data-stimeo--submit-once-target=\"idle\">Save</span>\n * <span data-stimeo--submit-once-target=\"busy\" hidden>Saving…</span>\n * </button>\n * </form>\n * ```\n *\n * Events:\n * - `stimeo--submit-once:start` — `{ form, submitter }`.\n * - `stimeo--submit-once:reconcile` — `{ forms: HTMLFormElement[] }`, the forms\n * whose in-flight submission the Turbo cache rewind abandoned. `end` would\n * claim the submission resolved, so the rewind reports itself instead.\n * - `stimeo--submit-once:end` — `{ form, submitter, reason, success? }`, where\n * `reason` is `\"turbo\"`, `\"timeout\"`, `\"manual\"`, or `\"canceled\"`. Only\n * `\"canceled\"` skips the completion announcement, because no operation ran.\n *\n * @remarks\n * Behavior only: the consumer owns every visual, label, and announcement\n * string. `announceText` / `announceReadyText` are empty by default and speak\n * through the page's shared `stimeo--announcer` only on real state transitions.\n * Attribute writes use `AttributeLease`, so a consumer mutation made\n * while busy wins over restoration. `DetachGate` preserves an in-flight\n * session across an in-page move, while `BeforeCacheReset` rewinds the\n * snapshot before Turbo caches it. Cache/detach rewinds dispatch no `end`,\n * announce nothing and never move focus; a cache rewind that abandons a\n * submission reports it once as `reconcile`.\n */\nexport class SubmitOnceController extends Controller<HTMLElement> {\n static override targets = [\"submit\", \"idle\", \"busy\"];\n static override values = {\n announceText: { type: String, default: \"\" },\n announceReadyText: { type: String, default: \"\" },\n busyLabel: { type: String, default: \"\" },\n timeout: { type: Number, default: 0 },\n restoreFocus: { type: Boolean, default: false },\n };\n static actions = [\"cancel\", \"finish\", \"start\"] as const;\n static events = [\"start\", \"end\", \"reconcile\"] as const;\n\n declare readonly submitTargets: SubmitControl[];\n declare readonly idleTargets: HTMLElement[];\n declare readonly busyTargets: HTMLElement[];\n\n declare announceTextValue: string;\n declare announceReadyTextValue: string;\n declare busyLabelValue: string;\n declare timeoutValue: number;\n declare restoreFocusValue: boolean;\n\n readonly #timers = new SafeTimeout();\n readonly #gate = new DetachGate();\n readonly #listeners = new ListenerSet();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n readonly #sessions = new Map<HTMLFormElement, SubmissionSession>();\n\n readonly #disabled = new AttributeLease<SubmitControl>(\"disabled\");\n readonly #controlBusy = new AttributeLease<SubmitControl>(\"aria-busy\");\n readonly #formBusy = new AttributeLease<HTMLFormElement>(\"aria-busy\");\n readonly #submitting = new AttributeLease<HTMLFormElement>(\"data-submitting\");\n readonly #ariaLabel = new AttributeLease<HTMLButtonElement>(\"aria-label\");\n readonly #hidden = new AttributeLease<HTMLElement>(\"hidden\");\n\n readonly #onSubmitStart = (event: Event): void => {\n this.start(event);\n };\n\n readonly #onSubmitEnd = (event: Event): void => {\n this.#complete(this.#eventForm(event), \"turbo\", this.#eventSuccess(event));\n };\n\n /** Blocks a later native submit while one for the same form is still active. */\n readonly #onNativeSubmit = (event: Event): void => {\n // The submit event's target is its form. This listener runs in capture so it\n // always observes that first event before a public `start` action creates the\n // session, while a later event sees the session and is canceled here.\n const session = this.#sessionFor(event.target);\n if (!session) return;\n this.#syncControls(session);\n event.preventDefault();\n };\n\n override connect(): void {\n this.#gate.cancel();\n this.#beforeCache.activate();\n this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n }\n\n override disconnect(): void {\n this.#listeners.dispose();\n this.#beforeCache.deactivate();\n this.#gate.disconnected(this, () => this.#teardown());\n }\n\n /**\n * Begins the submission represented by `event`.\n *\n * Native `submit` events are intended for non-Turbo async forms. Turbo calls\n * this path through its auto-subscribed `turbo:submit-start` event. An event\n * whose default an earlier listener already cancelled carries no submission,\n * so it starts nothing.\n */\n start(event: Event): void {\n if (event.defaultPrevented) return;\n const form = this.#eventForm(event);\n if (!form || this.#sessions.has(form)) return;\n\n const controls = this.#submitControls(form);\n const submitter = this.#resolveSubmitter(event, controls);\n const session: SubmissionSession = {\n form,\n submitter,\n submitterHadFocus: submitter !== null && document.activeElement === submitter,\n controls: new Set(),\n timeoutId: null,\n labelWrite: null,\n idlePart: null,\n busyPart: null,\n };\n this.#sessions.set(form, session);\n\n this.#formBusy.write(form, \"true\");\n this.#submitting.write(form, \"true\");\n this.#syncControls(session, controls);\n this.#enterLabel(session);\n this.dispatch(\"start\", { detail: { form, submitter } });\n\n if (this.timeoutValue > 0) {\n session.timeoutId = this.#timers.set(\n () => this.#complete(form, \"timeout\", false),\n this.timeoutValue,\n );\n }\n\n announce(this.announceTextValue);\n }\n\n /**\n * Completes a non-Turbo async submission for the form associated with\n * `event`. When called directly with no event, it completes the controller's\n * sole active form (or the controller element itself when it is a form).\n */\n finish(event?: Event): void {\n const form = event ? this.#eventForm(event) : this.#defaultForm();\n this.#complete(form, \"manual\", event ? this.#eventSuccess(event) : undefined);\n }\n\n /**\n * Abandons the submission for the form associated with `event` because it\n * never reached the network — a veto from validation, or a request the\n * consumer gave up before sending. With no event it targets the controller's\n * sole active form, exactly like {@link finish}.\n *\n * The distinction from `finish` is observable: a request that ran and failed\n * is `finish` with `success: false` and still announces completion, while a\n * submission that never happened announces nothing.\n */\n cancel(event?: Event): void {\n const form = event ? this.#eventForm(event) : this.#defaultForm();\n this.#complete(form, \"canceled\", false);\n }\n\n /** Disables a newly connected explicit submit target during its form's session. */\n submitTargetConnected(target: SubmitControl): void {\n const session = this.#sessionFor(target.form);\n if (!session || !this.#isNativeSubmitControl(target)) return;\n this.#enterControl(session, target);\n }\n\n /**\n * Ends one session, restores owned state, and reports the observable completion.\n *\n * @stimeoRuntimeOnly `announceReadyText` words the one announcement of this finished submission\n * and `restoreFocus` decides where its focus returns.\n */\n #complete(form: HTMLFormElement | null, reason: CompletionReason, success?: boolean): void {\n const session = this.#sessionFor(form);\n if (!session) return;\n this.#sessions.delete(session.form);\n this.#returnSessionState(session);\n\n const detail =\n success === undefined\n ? { form: session.form, submitter: session.submitter, reason }\n : { form: session.form, submitter: session.submitter, reason, success };\n this.dispatch(\"end\", { detail });\n this.#restoreFocus(session);\n if (reason !== \"canceled\") announce(this.announceReadyTextValue);\n }\n\n /** Applies the busy state to every current native/explicit control exactly once. */\n #syncControls(session: SubmissionSession, controls = this.#submitControls(session.form)): void {\n for (const control of controls) this.#enterControl(session, control);\n }\n\n /** Temporarily disables one enabled submit control while retaining authored state. */\n #enterControl(session: SubmissionSession, control: SubmitControl): void {\n if (session.controls.has(control) || control.matches(\":disabled\")) return;\n session.controls.add(control);\n this.#disabled.write(control, \"\");\n this.#controlBusy.write(control, \"true\");\n }\n\n /**\n * Swaps only the triggering control's safe label channel.\n *\n * @stimeoRuntimeOnly `busyLabel` is the label this one submission shows until it ends; the idle\n * label comes back with the state it returns.\n */\n #enterLabel(session: SubmissionSession): void {\n const control = session.submitter;\n if (!control) return;\n\n const idlePart = this.#partInside(control, this.idleTargets);\n const busyPart = this.#partInside(control, this.busyTargets);\n if (idlePart && busyPart) {\n session.idlePart = idlePart;\n session.busyPart = busyPart;\n this.#hidden.write(idlePart, \"\");\n this.#hidden.write(busyPart, null);\n }\n\n const label = control.getAttribute(BUTTON_BUSY_LABEL) ?? this.busyLabelValue;\n if (label.length === 0) return;\n if (control instanceof HTMLInputElement) {\n // `input[type=image]#value` is submitted data, not a visible label.\n if (control.type !== \"submit\") return;\n session.labelWrite = {\n channel: \"value\",\n control,\n original: control.value,\n written: label,\n };\n control.value = label;\n return;\n }\n if (control.hasAttribute(\"aria-label\")) {\n this.#ariaLabel.write(control, label);\n return;\n }\n // A structured button needs the explicit idle/busy pair above. Replacing its\n // textContent would destroy icons, counters, and application-owned listeners.\n if (control.childElementCount > 0) return;\n session.labelWrite = {\n channel: \"text\",\n control,\n original: control.textContent ?? \"\",\n written: label,\n };\n control.textContent = label;\n }\n\n /** Returns labels, controls, and form hooks, respecting intervening consumer writes. */\n #returnSessionState(session: SubmissionSession): void {\n if (session.timeoutId !== null) {\n this.#timers.clear(session.timeoutId);\n session.timeoutId = null;\n }\n\n if (session.labelWrite?.channel === \"value\") {\n const { control, original, written } = session.labelWrite;\n if (control.value === written) control.value = original;\n } else if (session.labelWrite?.channel === \"text\") {\n const { control, original, written } = session.labelWrite;\n if (control.childElementCount === 0 && control.textContent === written) {\n control.textContent = original;\n }\n }\n session.labelWrite = null;\n\n if (session.submitter instanceof HTMLButtonElement) {\n this.#ariaLabel.return(session.submitter);\n }\n if (session.idlePart) this.#hidden.return(session.idlePart);\n if (session.busyPart) this.#hidden.return(session.busyPart);\n session.idlePart = null;\n session.busyPart = null;\n\n for (const control of session.controls) {\n this.#controlBusy.return(control);\n this.#disabled.return(control);\n }\n session.controls.clear();\n this.#submitting.return(session.form);\n this.#formBusy.return(session.form);\n }\n\n /** Restores focus only when disabling the focused submitter actually stranded it. */\n #restoreFocus(session: SubmissionSession): void {\n const submitter = session.submitter;\n if (!this.restoreFocusValue || !session.submitterHadFocus || !submitter?.isConnected) return;\n const active = document.activeElement;\n if (active === null || active === document.body || active === document.documentElement) {\n submitter.focus();\n }\n }\n\n /** Silently abandons every session before caching or on a real detach. */\n #abandonSessions(): void {\n this.#timers.clearAll();\n for (const session of this.#sessions.values()) {\n session.timeoutId = null;\n this.#returnSessionState(session);\n }\n this.#sessions.clear();\n }\n\n /**\n * Keeps Turbo's cached snapshot idle without `end`, announcements, or focus\n * moves; the abandoned forms are reported once as `reconcile`.\n */\n #rewindForCache(): void {\n const forms = [...this.#sessions.keys()];\n this.#abandonSessions();\n // A submission in flight dies with the navigation, so a consumer still\n // painting \"submitting\" from `start` would never be released.\n if (forms.length > 0) this.dispatch(\"reconcile\", { detail: { forms } });\n }\n\n /** Tears down a true detach; an in-page move is canceled by the next `connect()`. */\n #teardown(): void {\n this.#gate.cancel();\n this.#abandonSessions();\n }\n\n /** All native controls associated with `form`, plus valid explicit targets. */\n #submitControls(form: HTMLFormElement): SubmitControl[] {\n const controls = new Set<SubmitControl>();\n for (const element of Array.from(form.elements)) {\n if (this.#isNativeSubmitControl(element)) controls.add(element);\n }\n // Browsers omit input[type=image] from form.elements. Search\n // the form's tree root as well so descendant and external `form=` image\n // submitters receive the same guard without broadening into sibling forms.\n const root = form.getRootNode() as ParentNode;\n for (const image of root.querySelectorAll<HTMLInputElement>('input[type=\"image\"]')) {\n if (image.form === form) controls.add(image);\n }\n return Array.from(controls);\n }\n\n /** Resolves the real submitter, falling back to the first enabled submit control. */\n #resolveSubmitter(event: Event, controls: SubmitControl[]): SubmitControl | null {\n const submitter = this.#eventSubmitter(event);\n if (submitter && controls.includes(submitter)) return submitter;\n return controls.find((control) => !control.matches(\":disabled\")) ?? controls[0] ?? null;\n }\n\n /** Reads a native or Turbo submitter without assuming `SubmitEvent` exists. */\n #eventSubmitter(event: Event): SubmitControl | null {\n if (typeof SubmitEvent !== \"undefined\" && event instanceof SubmitEvent) {\n return this.#isNativeSubmitControl(event.submitter) ? event.submitter : null;\n }\n const detail = (event as CustomEvent<{ formSubmission?: { submitter?: HTMLElement | null } }>)\n .detail;\n const submitter = detail?.formSubmission?.submitter;\n return this.#isNativeSubmitControl(submitter) ? submitter : null;\n }\n\n /** Finds the form represented by an action/Turbo event and verifies this scope owns it. */\n #eventForm(event: Event): HTMLFormElement | null {\n const detail = (\n event as CustomEvent<{\n form?: HTMLFormElement;\n formSubmission?: { formElement?: HTMLFormElement; submitter?: HTMLElement | null };\n }>\n ).detail;\n const target = event.target;\n const candidate =\n (target instanceof HTMLFormElement ? target : null) ??\n (target instanceof Element ? target.closest(\"form\") : null) ??\n detail?.formSubmission?.formElement ??\n detail?.form ??\n this.#eventSubmitter(event)?.form ??\n (this.element instanceof HTMLFormElement ? this.element : null);\n return candidate && this.#ownsForm(candidate) ? candidate : null;\n }\n\n /** The only unambiguous form for a direct `finish()` call. */\n #defaultForm(): HTMLFormElement | null {\n if (this.element instanceof HTMLFormElement && this.#ownsForm(this.element))\n return this.element;\n if (this.#sessions.size === 1) return this.#sessions.keys().next().value ?? null;\n return null;\n }\n\n /** Prevents an ancestor controller from also taking over a nested instance's form. */\n #ownsForm(form: HTMLFormElement): boolean {\n let candidate: Element | null = form;\n while (candidate) {\n const identifiers = (candidate.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n if (identifiers.includes(this.identifier)) return candidate === this.element;\n candidate = candidate.parentElement;\n }\n return false;\n }\n\n /** Returns the active session for a form-shaped external event value. */\n #sessionFor(candidate: unknown): SubmissionSession | undefined {\n if (!(candidate instanceof HTMLFormElement)) return undefined;\n return this.#sessions.get(candidate);\n }\n\n /** Effective native submit controls, including implicit buttons and image inputs. */\n #isNativeSubmitControl(element: unknown): element is SubmitControl {\n if (element instanceof HTMLButtonElement) return element.type === \"submit\";\n return (\n element instanceof HTMLInputElement && (element.type === \"submit\" || element.type === \"image\")\n );\n }\n\n /** Finds a structured label part belonging to this button, not a nested control. */\n #partInside(control: SubmitControl, parts: HTMLElement[]): HTMLElement | null {\n return (\n parts.find(\n (part) =>\n control.contains(part) &&\n part.closest(\"button, input[type=submit], input[type=image]\") === control,\n ) ?? null\n );\n }\n\n /** Reads an optional boolean `success` from Turbo or a manual completion event. */\n #eventSuccess(event: Event): boolean | undefined {\n const success = (event as CustomEvent<{ success?: unknown }>).detail?.success;\n return typeof success === \"boolean\" ? success : undefined;\n }\n}\n"]}
|
|
@@ -6,12 +6,14 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
6
6
|
* Markup contract (identifier: `stimeo--switch`):
|
|
7
7
|
* <button type="button" data-controller="stimeo--switch"
|
|
8
8
|
* data-action="click->stimeo--switch#toggle keydown->stimeo--switch#onKeydown"
|
|
9
|
-
* role="switch" aria-checked="false"
|
|
9
|
+
* role="switch" aria-checked="false">
|
|
10
|
+
* <input type="hidden" name="notify" data-stimeo--switch-target="field" />
|
|
11
|
+
* </button>
|
|
10
12
|
*
|
|
11
13
|
* Implements the WAI-ARIA APG **Switch** pattern. The controller element is the
|
|
12
14
|
* switch itself; its on/off state is reflected solely through `aria-checked`.
|
|
13
15
|
*
|
|
14
|
-
* `
|
|
16
|
+
* `change` and `reconcile` dispatch `{ checked: boolean }`.
|
|
15
17
|
*
|
|
16
18
|
* @remarks
|
|
17
19
|
* Behavior only — the consumer owns all styling (typically keyed off the
|
|
@@ -24,16 +26,36 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
24
26
|
*
|
|
25
27
|
* Behavior provided:
|
|
26
28
|
* - Click (or Space/Enter) toggles `aria-checked` between `"true"` and `"false"`.
|
|
27
|
-
* -
|
|
28
|
-
*
|
|
29
|
+
* - `stimeo--switch:change` is dispatched for every toggle the user makes; its
|
|
30
|
+
* `detail.checked` carries the new boolean state.
|
|
31
|
+
* - `stimeo--switch:reconcile` reports a checked state the page moved instead —
|
|
32
|
+
* a retained-element morph that writes `aria-checked` or strips it (the state
|
|
33
|
+
* then falls back to the `"false"` default), or a host that stops supporting
|
|
34
|
+
* the switch and takes back the `aria-checked` this controller supplied. It
|
|
35
|
+
* carries the same detail, once per batch of changes, and never on connect.
|
|
36
|
+
* - The optional `field` target mirrors the state as `"true"` / `"false"` so the
|
|
37
|
+
* switch can be submitted and read server-side. A toggle the user made emits
|
|
38
|
+
* a native bubbling `change` from that field, the way a form control does, so
|
|
39
|
+
* `stimeo--auto-submit` and form-level validation hear it. The mirror is
|
|
40
|
+
* refreshed silently on connect, on a replacement field, and whenever the
|
|
41
|
+
* defaults are re-derived after an attribute change. A hidden input is not
|
|
42
|
+
* interactive content, so it is valid inside the `<button>` host.
|
|
29
43
|
*/
|
|
30
44
|
declare class SwitchController extends Controller<HTMLElement> {
|
|
31
45
|
#private;
|
|
46
|
+
static targets: string[];
|
|
32
47
|
static actions: readonly ["onKeydown", "toggle"];
|
|
33
|
-
static events: readonly ["
|
|
34
|
-
|
|
48
|
+
static events: readonly ["change", "reconcile"];
|
|
49
|
+
readonly fieldTarget: HTMLInputElement;
|
|
50
|
+
readonly hasFieldTarget: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Ensures the switch exposes a role and is keyboard-reachable, and takes the
|
|
53
|
+
* state it finds as the one a later move is measured from, reporting nothing.
|
|
54
|
+
*/
|
|
35
55
|
connect(): void;
|
|
36
|
-
/**
|
|
56
|
+
/** Fills a form field inserted or replaced at runtime with the current state. */
|
|
57
|
+
fieldTargetConnected(): void;
|
|
58
|
+
/** Releases the click guard, the attribute observer, and a pass still queued. */
|
|
37
59
|
disconnect(): void;
|
|
38
60
|
/** Toggles the checked state. Bound via `data-action` (click). */
|
|
39
61
|
toggle(): void;
|