stimeo-ui 0.9.0 → 0.11.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 +149 -0
- package/dist/cable/index.d.ts +79 -14
- package/dist/cable/index.js +226 -43
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.js +17 -7
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.js +1 -1
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.js +2 -2
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +1 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +1 -1
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js +15 -5
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +15 -5
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +5 -4
- 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.js +1 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.js +1 -1
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +15 -5
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +2 -2
- package/dist/controllers/direct_upload_controller.js +1 -5
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +2 -2
- package/dist/controllers/drawer_controller.js +17 -7
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +5 -0
- package/dist/controllers/focus_controller.js +45 -7
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +1 -1
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +4 -3
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +2 -2
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +3 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +1 -1
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +2 -2
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +1 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.js +1 -1
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +2 -2
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +1 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +27 -2
- package/dist/controllers/password_reveal_controller.js +102 -3
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +71 -17
- package/dist/controllers/password_strength_controller.js +287 -42
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +23 -5
- package/dist/controllers/pointer_drag_controller.js +64 -18
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +17 -4
- package/dist/controllers/portal_controller.js +32 -11
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +22 -14
- package/dist/controllers/preview_guard_controller.js +180 -20
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_before_cache_controller.d.ts +1 -2
- package/dist/controllers/reset_before_cache_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +1 -1
- package/dist/controllers/resizable_controller.js +1 -1
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +17 -4
- package/dist/controllers/roving_controller.js +127 -14
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +33 -11
- package/dist/controllers/scroll_restore_controller.js +127 -27
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +42 -10
- package/dist/controllers/scroll_visibility_controller.js +167 -14
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.js +15 -5
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.js +0 -5
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +26 -9
- package/dist/controllers/stick_to_bottom_controller.js +149 -28
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +32 -6
- package/dist/controllers/theme_controller.js +151 -40
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +2 -2
- package/dist/controllers/toast_controller.js +2 -2
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +11 -7
- package/dist/controllers/transition_controller.js +79 -15
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +1188 -318
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +3 -4
- package/dist/inspector/cli.js +1 -0
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +6 -5
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +6 -6
- package/dist/inspector/manifest.json +15 -32
- package/dist/positioning/index.d.ts +18 -4
- package/dist/positioning/index.js +84 -25
- package/dist/positioning/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/default_attribute.ts","../../src/utils/focus_candidate.ts","../../src/controllers/form_validation_controller.ts"],"names":[],"mappings":"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","file":"form_validation_controller.js","sourcesContent":["/**\n * Adds an attribute default without displacing an authored value.\n *\n * Attribute presence — including an authored empty string — is the ownership\n * boundary. The return value lets a caller remember that it supplied the\n * default when that caller must later restore the authored state.\n *\n * @param element - Element that owns the attribute\n * @param name - Attribute name\n * @param value - Value to write only when the attribute is absent\n * @returns Whether this call added the attribute\n */\nexport function setDefaultAttribute(element: Element, name: string, value: string): boolean {\n if (element.hasAttribute(name)) return false;\n element.setAttribute(name, value);\n return true;\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported for the consumer that needs this rule *without* the rest of\n * {@link canTakeFocus}: `toolbar` bounds its own `hidden` walk at the toolbar\n * root, which is a different rule, so it composes the two itself.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the three conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { setDefaultAttribute } from \"../utils/default_attribute\";\nimport { canTakeFocus, firstTabStop, isTabStop } from \"../utils/focus_candidate\";\nimport type { FormFieldController } from \"./form_field_controller\";\n\n/** Native form controls that participate in constraint validation. */\ntype ValidatableControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;\n\n/**\n * Maps each `ValidityState` flag (in priority order) to the kebab-case suffix of\n * its per-constraint message-override attribute. The first flag that is `true`\n * wins, so e.g. `valueMissing` is reported before a stale `patternMismatch`.\n */\nconst CONSTRAINT_MESSAGE_KEYS: ReadonlyArray<readonly [keyof ValidityState, string]> = [\n [\"valueMissing\", \"value-missing\"],\n [\"typeMismatch\", \"type-mismatch\"],\n [\"patternMismatch\", \"pattern-mismatch\"],\n [\"tooShort\", \"too-short\"],\n [\"tooLong\", \"too-long\"],\n [\"rangeUnderflow\", \"range-underflow\"],\n [\"rangeOverflow\", \"range-overflow\"],\n [\"stepMismatch\", \"step-mismatch\"],\n [\"badInput\", \"bad-input\"],\n];\n\n/** Attribute prefix for a per-constraint message override, authored on the control. */\nconst MESSAGE_ATTR_PREFIX = \"data-stimeo--form-validation-message-\";\n/** Attribute for a generic message override applied to any failing constraint. */\nconst MESSAGE_ATTR_GENERIC = \"data-stimeo--form-validation-message\";\n/** Attribute opting a control into a declarative custom rule (`\"whitespace\"`). */\nconst DISALLOW_ATTR = \"data-stimeo--form-validation-disallow\";\n/** Message override dedicated to the declarative whitespace rule. */\nconst DISALLOW_WHITESPACE_MESSAGE = `${MESSAGE_ATTR_PREFIX}whitespace`;\n/** Default message when `disallow=\"whitespace\"` fails and no override is given. */\nconst DISALLOW_WHITESPACE_DEFAULT = \"Please enter a value that is not only whitespace.\";\n\n/** Public invalid-state hook owned by `stimeo--form-field`. */\nconst FORM_FIELD_INVALID_ATTR = \"data-stimeo--form-field-invalid\";\n\n/** Identity used to collect controls into one validation field. */\ntype FieldGroupKey = FormFieldController | ValidatableControl | string;\n\n/** A field's validatable controls, grouped so the whole field validates together. */\ninterface FieldGroup {\n readonly key: FieldGroupKey;\n readonly field: FormFieldController | undefined;\n readonly controls: ValidatableControl[];\n}\n\n/** Live form grouping resolved once for a validation operation. */\ninterface ValidationSnapshot {\n readonly groups: Map<FieldGroupKey, FieldGroup>;\n /** `null` is a deliberate miss key so delegated non-control events need one guard. */\n readonly groupByControl: Map<ValidatableControl | null, FieldGroup>;\n}\n\n/**\n * Headless, accessible **form-validation orchestration**.\n *\n * Markup contract (identifier: `stimeo--form-validation`):\n * <form data-controller=\"stimeo--form-validation\"\n * data-stimeo--form-validation-stimeo--form-field-outlet=\"[data-controller~='stimeo--form-field']\">\n * <div data-controller=\"stimeo--form-field\">\n * <label for=\"email\">Email</label>\n * <input id=\"email\" type=\"email\" required\n * data-stimeo--form-field-target=\"control\" />\n * <p hidden data-stimeo--form-field-target=\"error\"></p>\n * </div>\n * <button type=\"submit\">Save</button>\n * </form>\n *\n * Not an APG widget pattern — this is the *timing* layer for **error\n * identification** (WCAG 3.3.1) and **error suggestion** (3.3.3): it decides\n * *when* each control is checked and routes the browser's native\n * `validationMessage` into the field's {@link FormFieldController} error region.\n * The per-field ARIA wiring (`aria-invalid` / `aria-errormessage` /\n * `aria-describedby`) therefore lives in exactly one place — `stimeo--form-field`,\n * reached through a Stimulus **outlet** — and is never re-implemented here.\n *\n * `valid` dispatches `{}`; `invalid` dispatches `{ invalid: HTMLElement[] }`.\n *\n * @remarks\n * Behavior only — validation **rules** stay in the markup (native HTML\n * constraints: `required`, `type`, `pattern`, `min`/`max`, …) or in the consumer's\n * own `setCustomValidity()` calls, which `checkValidity()` surfaces transparently.\n * It sets the form's `novalidate` so it can replace the browser's default error\n * bubbles with accessible in-page visual errors plus the shared Announcer, and\n * restores the attribute on disconnect.\n *\n * Two declarative escape hatches let a field **exceed** native validation with no\n * consumer JS (author them on the control):\n * - **Per-constraint messages** — `data-stimeo--form-validation-message-<constraint>`\n * (`value-missing`, `too-short`, `too-long`, `pattern-mismatch`, `type-mismatch`,\n * `range-overflow`, `range-underflow`, `step-mismatch`, `bad-input`), or a generic\n * `data-stimeo--form-validation-message` fallback, override the shown text per failing\n * `ValidityState` flag — controlled, localizable wording that also fixes headless\n * browsers returning an empty native `validationMessage`. Falls back to native.\n * - **`data-stimeo--form-validation-disallow=\"whitespace\"`** — a built-in custom rule\n * rejecting a value that is blank after trimming (which slips past `required` /\n * `minlength`), wired through `setCustomValidity` so it blocks submit like any\n * native constraint.\n *\n * Behavior provided:\n * - On connect, suppresses native bubbles (`novalidate`, restored on disconnect)\n * and intercepts the form's `submit` in the **capture phase** so an invalid form\n * is cancelled before any other submit handler (e.g. `stimeo--submit-once`)\n * reacts to a submission that will never happen.\n * - On submit, validates every control; if any is invalid it blocks submission,\n * moves focus to the first invalid control (unless `focusInvalid` is `false`),\n * and dispatches `stimeo--form-validation:invalid`. An all-valid form dispatches\n * `:valid` and submits normally.\n * - Validates a field on blur once it has been interacted with (`validateOnBlur`),\n * and re-validates it on input while it is already touched\n * (`revalidateOnInput`) so a shown message clears the moment the value becomes\n * valid — but a pristine field is never eagerly flagged mid-typing.\n *\n * A control with no owning `stimeo--form-field` outlet is still validated (it can\n * block submit and receive focus) but renders no message.\n *\n * Radio groups work unchanged: point the field's `control` target at the\n * `role=\"radiogroup\"` container so the invalid state lands on the group, and the\n * group is reported as a single invalid entry (not one per radio).\n *\n * Rich widgets (listbox, time-picker, …) that keep their committed value in a\n * hidden holder participate by making that holder a **validatable mirror**:\n * `<input type=\"text\" hidden required>` — the `hidden` *attribute*, not\n * `type=\"hidden\"`, which is barred from constraint validation. Native\n * constraints then govern the widget's value with no extra JavaScript. The\n * widget dispatches a bubbling `change` on the mirror when a value is committed\n * (a completed interaction, so it validates immediately), and focus for an\n * invalid mirror is delegated to the field's visible `control` target — the\n * target itself when focusable, else its first focusable descendant.\n *\n * No-JS caveat: a `required` mirror also gates the browser's own pre-Stimulus\n * validation, which cannot surface UI on an invisible control. When the no-JS\n * fallback matters, author `novalidate` on the form (this controller preserves\n * an author-set attribute) so the submission reaches the server's validation.\n */\nexport class FormValidationController extends Controller<HTMLFormElement> {\n static override outlets = [\"stimeo--form-field\"];\n static override values = {\n validateOnBlur: { type: Boolean, default: true },\n validateOnChange: { type: Boolean, default: true },\n revalidateOnInput: { type: Boolean, default: true },\n focusInvalid: { type: Boolean, default: true },\n };\n static actions = [\"validate\"] as const;\n static events = [\"valid\", \"invalid\"] as const;\n\n declare readonly stimeoFormFieldOutlets: FormFieldController[];\n declare readonly stimeoFormFieldOutletElements: HTMLElement[];\n\n declare validateOnBlurValue: boolean;\n declare validateOnChangeValue: boolean;\n declare revalidateOnInputValue: boolean;\n declare focusInvalidValue: boolean;\n\n /** Marker recording that we added `novalidate`, so we only remove our own. */\n static readonly #NOVALIDATE_MARKER = \"data-stimeo--form-validation-novalidate\";\n\n /** Object-backed groups already interacted with — the input revalidation gate. */\n readonly #touchedGroups = new WeakSet<FormFieldController | ValidatableControl>();\n\n /** String-backed fallback radio groups already interacted with. */\n readonly #touchedRadioGroups = new Set<string>();\n\n /**\n * Last message this controller wrote through `setCustomValidity`, kept as a\n * two-layer ownership ledger: a value is cleared only while the live message\n * still equals the recorded write. Iterable so disconnect can release every\n * surviving loan without touching a consumer's later custom error.\n */\n readonly #ownedCustomErrors = new Map<ValidatableControl, string>();\n\n /** Last invalid message routed to each field, suppressing duplicate reports. */\n readonly #reportedErrors = new WeakMap<FormFieldController, string>();\n\n /** Exact document that owns the delegated listeners for this connection. */\n #listenerDocument: Document | null = null;\n\n readonly #onSubmit = (event: SubmitEvent): void => {\n if (event.target !== this.element) return;\n const invalid = this.#validateAll();\n if (invalid.length === 0) {\n this.dispatch(\"valid\", { detail: {} });\n return;\n }\n // Cancel the whole submit: preventDefault stops native/Turbo navigation;\n // stopImmediatePropagation keeps later submit handlers (e.g. submit-once's\n // busy state) from acting on a submission that will never happen.\n event.preventDefault();\n event.stopImmediatePropagation();\n const first = invalid[0];\n if (this.focusInvalidValue && first) this.#focusTargetFor(first)?.focus();\n this.dispatch(\"invalid\", { detail: { invalid } });\n };\n\n readonly #onFocusOut = (event: FocusEvent): void => {\n if (!this.validateOnBlurValue) return;\n const snapshot = this.#snapshot();\n const group = snapshot.groupByControl.get(this.#controlFrom(event.target));\n if (!group) return;\n // Focus moving *within* the same field (e.g. between members of a radio\n // group) is not leaving it — defer validation until focus actually exits.\n const related = event.relatedTarget;\n if (group.field && related instanceof Node && group.field.element.contains(related)) return;\n if (snapshot.groupByControl.get(this.#controlFrom(related))?.key === group.key) return;\n this.#markTouched(group.key);\n this.#applyGroup(group);\n };\n\n readonly #onInput = (event: Event): void => {\n if (!this.revalidateOnInputValue) return;\n const group = this.#snapshot().groupByControl.get(this.#controlFrom(event.target));\n // Only re-validate a field the user has already left once, so the first\n // keystroke never eagerly flags a control they are still filling in.\n if (!group || !this.#isTouched(group.key)) return;\n this.#applyGroup(group);\n };\n\n readonly #onChange = (event: Event): void => {\n if (!this.validateOnChangeValue) return;\n const group = this.#snapshot().groupByControl.get(this.#controlFrom(event.target));\n if (!group) return;\n // change marks a *committed* interaction (a picked option, a toggled box, a\n // widget writing its mirror), so unlike input it both touches and validates.\n this.#markTouched(group.key);\n this.#applyGroup(group);\n };\n\n /** Suppresses native bubbles and binds the submit / blur / input listeners. */\n override connect(): void {\n if (setDefaultAttribute(this.element, \"novalidate\", \"\")) {\n this.element.setAttribute(FormValidationController.#NOVALIDATE_MARKER, \"\");\n }\n // Capture submit before form-bound actions. The other bubbling events live on\n // the same document so controls associated through `form=\"id\"` participate too;\n // #controlFrom rejects every control owned by a different form.\n this.#listenerDocument = this.element.ownerDocument;\n this.#listenerDocument.addEventListener(\"submit\", this.#onSubmit, true);\n this.#listenerDocument.addEventListener(\"focusout\", this.#onFocusOut);\n this.#listenerDocument.addEventListener(\"input\", this.#onInput);\n this.#listenerDocument.addEventListener(\"change\", this.#onChange);\n }\n\n /** Tears down listeners and restores `novalidate` if we added it. */\n override disconnect(): void {\n this.#listenerDocument?.removeEventListener(\"submit\", this.#onSubmit, true);\n this.#listenerDocument?.removeEventListener(\"focusout\", this.#onFocusOut);\n this.#listenerDocument?.removeEventListener(\"input\", this.#onInput);\n this.#listenerDocument?.removeEventListener(\"change\", this.#onChange);\n this.#listenerDocument = null;\n for (const [control, message] of this.#ownedCustomErrors) {\n if (control.validationMessage === message) control.setCustomValidity(\"\");\n }\n this.#ownedCustomErrors.clear();\n this.#touchedRadioGroups.clear();\n if (this.element.hasAttribute(FormValidationController.#NOVALIDATE_MARKER)) {\n this.element.removeAttribute(\"novalidate\");\n this.element.removeAttribute(FormValidationController.#NOVALIDATE_MARKER);\n }\n }\n\n /**\n * Validates every control now, rendering or clearing each field's message, and\n * returns whether the whole form is valid. Marks every field/group touched so\n * a later input from any sibling re-validates it. Bound via `data-action`\n * (`#validate`) or callable directly (e.g. before a programmatic submit).\n */\n validate(): boolean {\n return this.#validateAll().length === 0;\n }\n\n /**\n * Validates every control and returns one invalid control per field. Controls\n * are grouped by field first (see {@link #keyFor}) so a field with several\n * controls — a radio group, or a mirror plus its visible control — reflects\n * *all* of them: a valid sibling must never clear an invalid one's message.\n * Each group's first invalid control supplies the message and the focus target.\n */\n #validateAll(): ValidatableControl[] {\n const invalid: ValidatableControl[] = [];\n for (const group of this.#snapshot().groups.values()) {\n this.#markTouched(group.key);\n const firstInvalid = this.#applyGroup(group);\n if (firstInvalid) invalid.push(firstInvalid);\n }\n return invalid;\n }\n\n /** Builds the current field table in one pass over form controls and DOM depth. */\n #snapshot(): ValidationSnapshot {\n const fields = this.#fieldOutlets();\n const groups = new Map<FieldGroupKey, FieldGroup>();\n const groupByControl = new Map<ValidatableControl | null, FieldGroup>();\n\n for (const control of this.#controls) {\n const field = this.#fieldFor(control, fields);\n const key = this.#keyFor(control, field);\n let group = groups.get(key);\n if (!group) {\n group = { key, field, controls: [] };\n groups.set(key, group);\n }\n group.controls.push(control);\n groupByControl.set(control, group);\n }\n\n return { groups, groupByControl };\n }\n\n /** Records that a whole field/group, rather than one sibling control, was visited. */\n #markTouched(key: FieldGroupKey): void {\n if (typeof key === \"string\") {\n this.#touchedRadioGroups.add(key);\n } else {\n this.#touchedGroups.add(key);\n }\n }\n\n #isTouched(key: FieldGroupKey): boolean {\n return typeof key === \"string\"\n ? this.#touchedRadioGroups.has(key)\n : this.#touchedGroups.has(key);\n }\n\n /**\n * Runs native constraint validation across a field's controls and routes the\n * result to its `stimeo--form-field` outlet: the first invalid control's\n * `validationMessage` is shown, an all-valid field is cleared. Returns the\n * first invalid control (for the invalid list / focus), or `null` when valid.\n * Routing goes through the outlet, so the ARIA wiring is never duplicated here.\n */\n #applyGroup(group: FieldGroup): ValidatableControl | null {\n // Apply declarative custom rules (e.g. disallow=\"whitespace\") before reading\n // validity so they participate in checkValidity() like a native constraint.\n for (const control of group.controls) this.#syncCustomValidity(control);\n const firstInvalid = group.controls.find((control) => !control.checkValidity()) ?? null;\n if (group.field) {\n if (firstInvalid) {\n const message = this.#messageFor(firstInvalid);\n const alreadyReported =\n group.field.element.hasAttribute(FORM_FIELD_INVALID_ATTR) &&\n this.#reportedErrors.get(group.field) === message;\n if (!alreadyReported) group.field.setError(message, { focus: false });\n this.#reportedErrors.set(group.field, message);\n } else if (\n group.field.element.hasAttribute(FORM_FIELD_INVALID_ATTR) ||\n this.#reportedErrors.has(group.field)\n ) {\n group.field.clearError();\n this.#reportedErrors.delete(group.field);\n }\n }\n return firstInvalid;\n }\n\n /**\n * Resolves the message to show for an invalid control: a per-constraint\n * override (`data-stimeo--form-validation-message-<constraint>`) for the first failing\n * `ValidityState` flag, then a generic `data-stimeo--form-validation-message`\n * override, then the browser's native `validationMessage`. Authoring an override\n * gives controlled, localizable, theme-able wording with **no consumer JS** —\n * and sidesteps headless browsers that return an empty native message.\n */\n #messageFor(control: ValidatableControl): string {\n // `setCustomValidity()` owns its wording. In particular, a consumer's custom\n // error must not be replaced by a simultaneous native-constraint override.\n if (control.validity.customError) return control.validationMessage;\n for (const [flag, key] of CONSTRAINT_MESSAGE_KEYS) {\n if (control.validity[flag]) {\n return (\n this.#authoredMessage(control, `${MESSAGE_ATTR_PREFIX}${key}`) ??\n this.#authoredMessage(control, MESSAGE_ATTR_GENERIC) ??\n control.validationMessage\n );\n }\n }\n return control.validationMessage || this.#authoredMessage(control, MESSAGE_ATTR_GENERIC) || \"\";\n }\n\n /**\n * Applies (or clears) a declarative custom constraint via `setCustomValidity`,\n * for controls that opt in with `data-stimeo--form-validation-disallow`. The supported\n * rule is `\"whitespace\"` — a value that is non-empty but blank\n * after trimming (which slips past `required` / `minlength`); its message follows\n * the whitespace-specific → generic → default chain.\n *\n * Don't-clobber-authored-state: an unknown/absent rule is never written, an\n * existing consumer custom error wins, and a controller error is only cleared\n * while the live message still equals the recorded controller write.\n */\n #syncCustomValidity(control: ValidatableControl): void {\n const recorded = this.#ownedCustomErrors.get(control);\n const ownsCurrent = recorded !== undefined && control.validationMessage === recorded;\n\n const violates =\n control.getAttribute(DISALLOW_ATTR) === \"whitespace\" &&\n control.value.length > 0 &&\n control.value.trim() === \"\";\n if (!violates) {\n if (ownsCurrent) control.setCustomValidity(\"\");\n this.#ownedCustomErrors.delete(control);\n return;\n }\n\n // One custom-validity slot exists per control. Preserve a consumer's live\n // entry; once they clear it, the next validation applies the declarative rule.\n if (control.validity.customError && !ownsCurrent) return;\n\n const message =\n this.#authoredMessage(control, DISALLOW_WHITESPACE_MESSAGE) ??\n this.#authoredMessage(control, MESSAGE_ATTR_GENERIC) ??\n DISALLOW_WHITESPACE_DEFAULT;\n if (!ownsCurrent || recorded !== message) control.setCustomValidity(message);\n this.#ownedCustomErrors.set(control, message);\n }\n\n /** Returns a non-blank authored message, otherwise falls through to a fallback. */\n #authoredMessage(control: ValidatableControl, attribute: string): string | null {\n const message = control.getAttribute(attribute);\n return message && message.trim().length > 0 ? message : null;\n }\n\n /**\n * A grouping key that collects controls belonging to the same field: the owning\n * `stimeo--form-field` when present, else a radio group's shared `name`, else\n * the control itself (always distinct).\n */\n #keyFor(control: ValidatableControl, field: FormFieldController | undefined): FieldGroupKey {\n if (field) return field;\n if (control instanceof HTMLInputElement && control.type === \"radio\" && control.name) {\n return `radio:${control.name}`;\n }\n return control;\n }\n\n /**\n * Where focus should land for an invalid control. A visible control is focused\n * directly — the case for native fields and radios. A validatable mirror\n * (the `hidden` attribute) cannot receive focus, so focus is delegated to the\n * visible widget: the owning field's `control` target when it is itself\n * focusable, else its first focusable descendant (e.g. a roving-tabindex\n * member). Resolved structurally — never by probing `focus()` — so behavior\n * is deterministic and CSS-independent.\n */\n #focusTargetFor(control: ValidatableControl): HTMLElement | null {\n if (canTakeFocus(control)) return control;\n const field = this.#fieldFor(control, this.#fieldOutlets());\n if (!field?.hasControlTarget) return null;\n const root = field.controlTarget;\n if (isTabStop(root)) return root;\n return firstTabStop(root);\n }\n\n /** Pairs each live outlet element with its controller once per operation. */\n #fieldOutlets(): Map<HTMLElement, FormFieldController> {\n const fields = new Map<HTMLElement, FormFieldController>();\n const elements = this.stimeoFormFieldOutletElements;\n const outlets = this.stimeoFormFieldOutlets;\n for (let index = 0; index < elements.length; index++) {\n const element = elements[index];\n const outlet = outlets[index];\n if (element && outlet) fields.set(element, outlet);\n }\n return fields;\n }\n\n /** The nearest configured field ancestor of `control`, if any. */\n #fieldFor(\n control: ValidatableControl,\n fields: ReadonlyMap<HTMLElement, FormFieldController>,\n ): FormFieldController | undefined {\n let ancestor: HTMLElement | null = control;\n while (ancestor) {\n const field = fields.get(ancestor);\n if (field) return field;\n ancestor = ancestor.parentElement;\n }\n return undefined;\n }\n\n /** This form's native controls that participate in constraint validation. */\n get #controls(): ValidatableControl[] {\n const controls: ValidatableControl[] = [];\n for (const element of Array.from(this.element.elements)) {\n if (this.#isValidatable(element)) controls.push(element);\n }\n return controls;\n }\n\n /** Narrows an event target to a validatable control. */\n #controlFrom(target: EventTarget | null): ValidatableControl | null {\n return target instanceof Element && this.#isValidatable(target) && target.form === this.element\n ? target\n : null;\n }\n\n #isValidatable(element: Element): element is ValidatableControl {\n return (\n (element instanceof HTMLInputElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement) &&\n // `willValidate` already excludes disabled, read-only, input[type=hidden],\n // and button-type controls. The `hidden` attribute intentionally does not\n // bar a text mirror from constraint validation.\n element.willValidate\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/default_attribute.ts","../../src/utils/focus_candidate.ts","../../src/controllers/form_validation_controller.ts"],"names":[],"mappings":"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","file":"form_validation_controller.js","sourcesContent":["/**\n * Adds an attribute default without displacing an authored value.\n *\n * Attribute presence — including an authored empty string — is the ownership\n * boundary. The return value lets a caller remember that it supplied the\n * default when that caller must later restore the authored state.\n *\n * @param element - Element that owns the attribute\n * @param name - Attribute name\n * @param value - Value to write only when the attribute is absent\n * @returns Whether this call added the attribute\n */\nexport function setDefaultAttribute(element: Element, name: string, value: string): boolean {\n if (element.hasAttribute(name)) return false;\n element.setAttribute(name, value);\n return true;\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the three conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { setDefaultAttribute } from \"../utils/default_attribute\";\nimport { canTakeFocus, firstTabStop, isTabStop } from \"../utils/focus_candidate\";\nimport type { FormFieldController } from \"./form_field_controller\";\n\n/** Native form controls that participate in constraint validation. */\ntype ValidatableControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;\n\n/**\n * Maps each `ValidityState` flag (in priority order) to the kebab-case suffix of\n * its per-constraint message-override attribute. The first flag that is `true`\n * wins, so e.g. `valueMissing` is reported before a stale `patternMismatch`.\n */\nconst CONSTRAINT_MESSAGE_KEYS: ReadonlyArray<readonly [keyof ValidityState, string]> = [\n [\"valueMissing\", \"value-missing\"],\n [\"typeMismatch\", \"type-mismatch\"],\n [\"patternMismatch\", \"pattern-mismatch\"],\n [\"tooShort\", \"too-short\"],\n [\"tooLong\", \"too-long\"],\n [\"rangeUnderflow\", \"range-underflow\"],\n [\"rangeOverflow\", \"range-overflow\"],\n [\"stepMismatch\", \"step-mismatch\"],\n [\"badInput\", \"bad-input\"],\n];\n\n/** Attribute prefix for a per-constraint message override, authored on the control. */\nconst MESSAGE_ATTR_PREFIX = \"data-stimeo--form-validation-message-\";\n/** Attribute for a generic message override applied to any failing constraint. */\nconst MESSAGE_ATTR_GENERIC = \"data-stimeo--form-validation-message\";\n/** Attribute opting a control into a declarative custom rule (`\"whitespace\"`). */\nconst DISALLOW_ATTR = \"data-stimeo--form-validation-disallow\";\n/** Message override dedicated to the declarative whitespace rule. */\nconst DISALLOW_WHITESPACE_MESSAGE = `${MESSAGE_ATTR_PREFIX}whitespace`;\n/** Default message when `disallow=\"whitespace\"` fails and no override is given. */\nconst DISALLOW_WHITESPACE_DEFAULT = \"Please enter a value that is not only whitespace.\";\n\n/** Public invalid-state hook owned by `stimeo--form-field`. */\nconst FORM_FIELD_INVALID_ATTR = \"data-stimeo--form-field-invalid\";\n\n/** Identity used to collect controls into one validation field. */\ntype FieldGroupKey = FormFieldController | ValidatableControl | string;\n\n/** A field's validatable controls, grouped so the whole field validates together. */\ninterface FieldGroup {\n readonly key: FieldGroupKey;\n readonly field: FormFieldController | undefined;\n readonly controls: ValidatableControl[];\n}\n\n/** Live form grouping resolved once for a validation operation. */\ninterface ValidationSnapshot {\n readonly groups: Map<FieldGroupKey, FieldGroup>;\n /** `null` is a deliberate miss key so delegated non-control events need one guard. */\n readonly groupByControl: Map<ValidatableControl | null, FieldGroup>;\n}\n\n/**\n * Headless, accessible **form-validation orchestration**.\n *\n * Markup contract (identifier: `stimeo--form-validation`):\n * <form data-controller=\"stimeo--form-validation\"\n * data-stimeo--form-validation-stimeo--form-field-outlet=\"[data-controller~='stimeo--form-field']\">\n * <div data-controller=\"stimeo--form-field\">\n * <label for=\"email\">Email</label>\n * <input id=\"email\" type=\"email\" required\n * data-stimeo--form-field-target=\"control\" />\n * <p hidden data-stimeo--form-field-target=\"error\"></p>\n * </div>\n * <button type=\"submit\">Save</button>\n * </form>\n *\n * Not an APG widget pattern — this is the *timing* layer for **error\n * identification** (WCAG 3.3.1) and **error suggestion** (3.3.3): it decides\n * *when* each control is checked and routes the browser's native\n * `validationMessage` into the field's {@link FormFieldController} error region.\n * The per-field ARIA wiring (`aria-invalid` / `aria-errormessage` /\n * `aria-describedby`) therefore lives in exactly one place — `stimeo--form-field`,\n * reached through a Stimulus **outlet** — and is never re-implemented here.\n *\n * `valid` dispatches `{}`; `invalid` dispatches `{ invalid: HTMLElement[] }`.\n *\n * @remarks\n * Behavior only — validation **rules** stay in the markup (native HTML\n * constraints: `required`, `type`, `pattern`, `min`/`max`, …) or in the consumer's\n * own `setCustomValidity()` calls, which `checkValidity()` surfaces transparently.\n * It sets the form's `novalidate` so it can replace the browser's default error\n * bubbles with accessible in-page visual errors plus the shared Announcer, and\n * restores the attribute on disconnect.\n *\n * Two declarative escape hatches let a field **exceed** native validation with no\n * consumer JS (author them on the control):\n * - **Per-constraint messages** — `data-stimeo--form-validation-message-<constraint>`\n * (`value-missing`, `too-short`, `too-long`, `pattern-mismatch`, `type-mismatch`,\n * `range-overflow`, `range-underflow`, `step-mismatch`, `bad-input`), or a generic\n * `data-stimeo--form-validation-message` fallback, override the shown text per failing\n * `ValidityState` flag — controlled, localizable wording that also fixes headless\n * browsers returning an empty native `validationMessage`. Falls back to native.\n * - **`data-stimeo--form-validation-disallow=\"whitespace\"`** — a built-in custom rule\n * rejecting a value that is blank after trimming (which slips past `required` /\n * `minlength`), wired through `setCustomValidity` so it blocks submit like any\n * native constraint.\n *\n * Behavior provided:\n * - On connect, suppresses native bubbles (`novalidate`, restored on disconnect)\n * and intercepts the form's `submit` in the **capture phase** so an invalid form\n * is cancelled before any other submit handler (e.g. `stimeo--submit-once`)\n * reacts to a submission that will never happen.\n * - On submit, validates every control; if any is invalid it blocks submission,\n * moves focus to the first invalid control (unless `focusInvalid` is `false`),\n * and dispatches `stimeo--form-validation:invalid`. An all-valid form dispatches\n * `:valid` and submits normally.\n * - Validates a field on blur once it has been interacted with (`validateOnBlur`),\n * and re-validates it on input while it is already touched\n * (`revalidateOnInput`) so a shown message clears the moment the value becomes\n * valid — but a pristine field is never eagerly flagged mid-typing.\n *\n * A control with no owning `stimeo--form-field` outlet is still validated (it can\n * block submit and receive focus) but renders no message.\n *\n * Radio groups work unchanged: point the field's `control` target at the\n * `role=\"radiogroup\"` container so the invalid state lands on the group, and the\n * group is reported as a single invalid entry (not one per radio).\n *\n * Rich widgets (listbox, time-picker, …) that keep their committed value in a\n * hidden holder participate by making that holder a **validatable mirror**:\n * `<input type=\"text\" hidden required>` — the `hidden` *attribute*, not\n * `type=\"hidden\"`, which is barred from constraint validation. Native\n * constraints then govern the widget's value with no extra JavaScript. The\n * widget dispatches a bubbling `change` on the mirror when a value is committed\n * (a completed interaction, so it validates immediately), and focus for an\n * invalid mirror is delegated to the field's visible `control` target — the\n * target itself when focusable, else its first focusable descendant.\n *\n * No-JS caveat: a `required` mirror also gates the browser's own pre-Stimulus\n * validation, which cannot surface UI on an invisible control. When the no-JS\n * fallback matters, author `novalidate` on the form (this controller preserves\n * an author-set attribute) so the submission reaches the server's validation.\n */\nexport class FormValidationController extends Controller<HTMLFormElement> {\n static override outlets = [\"stimeo--form-field\"];\n static override values = {\n validateOnBlur: { type: Boolean, default: true },\n validateOnChange: { type: Boolean, default: true },\n revalidateOnInput: { type: Boolean, default: true },\n focusInvalid: { type: Boolean, default: true },\n };\n static actions = [\"validate\"] as const;\n static events = [\"valid\", \"invalid\"] as const;\n\n declare readonly stimeoFormFieldOutlets: FormFieldController[];\n declare readonly stimeoFormFieldOutletElements: HTMLElement[];\n\n declare validateOnBlurValue: boolean;\n declare validateOnChangeValue: boolean;\n declare revalidateOnInputValue: boolean;\n declare focusInvalidValue: boolean;\n\n /** Marker recording that we added `novalidate`, so we only remove our own. */\n static readonly #NOVALIDATE_MARKER = \"data-stimeo--form-validation-novalidate\";\n\n /** Object-backed groups already interacted with — the input revalidation gate. */\n readonly #touchedGroups = new WeakSet<FormFieldController | ValidatableControl>();\n\n /** String-backed fallback radio groups already interacted with. */\n readonly #touchedRadioGroups = new Set<string>();\n\n /**\n * Last message this controller wrote through `setCustomValidity`, kept as a\n * two-layer ownership ledger: a value is cleared only while the live message\n * still equals the recorded write. Iterable so disconnect can release every\n * surviving loan without touching a consumer's later custom error.\n */\n readonly #ownedCustomErrors = new Map<ValidatableControl, string>();\n\n /** Last invalid message routed to each field, suppressing duplicate reports. */\n readonly #reportedErrors = new WeakMap<FormFieldController, string>();\n\n /** Exact document that owns the delegated listeners for this connection. */\n #listenerDocument: Document | null = null;\n\n readonly #onSubmit = (event: SubmitEvent): void => {\n if (event.target !== this.element) return;\n const invalid = this.#validateAll();\n if (invalid.length === 0) {\n this.dispatch(\"valid\", { detail: {} });\n return;\n }\n // Cancel the whole submit: preventDefault stops native/Turbo navigation;\n // stopImmediatePropagation keeps later submit handlers (e.g. submit-once's\n // busy state) from acting on a submission that will never happen.\n event.preventDefault();\n event.stopImmediatePropagation();\n const first = invalid[0];\n if (this.focusInvalidValue && first) this.#focusTargetFor(first)?.focus();\n this.dispatch(\"invalid\", { detail: { invalid } });\n };\n\n readonly #onFocusOut = (event: FocusEvent): void => {\n if (!this.validateOnBlurValue) return;\n const snapshot = this.#snapshot();\n const group = snapshot.groupByControl.get(this.#controlFrom(event.target));\n if (!group) return;\n // Focus moving *within* the same field (e.g. between members of a radio\n // group) is not leaving it — defer validation until focus actually exits.\n const related = event.relatedTarget;\n if (group.field && related instanceof Node && group.field.element.contains(related)) return;\n if (snapshot.groupByControl.get(this.#controlFrom(related))?.key === group.key) return;\n this.#markTouched(group.key);\n this.#applyGroup(group);\n };\n\n readonly #onInput = (event: Event): void => {\n if (!this.revalidateOnInputValue) return;\n const group = this.#snapshot().groupByControl.get(this.#controlFrom(event.target));\n // Only re-validate a field the user has already left once, so the first\n // keystroke never eagerly flags a control they are still filling in.\n if (!group || !this.#isTouched(group.key)) return;\n this.#applyGroup(group);\n };\n\n readonly #onChange = (event: Event): void => {\n if (!this.validateOnChangeValue) return;\n const group = this.#snapshot().groupByControl.get(this.#controlFrom(event.target));\n if (!group) return;\n // change marks a *committed* interaction (a picked option, a toggled box, a\n // widget writing its mirror), so unlike input it both touches and validates.\n this.#markTouched(group.key);\n this.#applyGroup(group);\n };\n\n /** Suppresses native bubbles and binds the submit / blur / input listeners. */\n override connect(): void {\n if (setDefaultAttribute(this.element, \"novalidate\", \"\")) {\n this.element.setAttribute(FormValidationController.#NOVALIDATE_MARKER, \"\");\n }\n // Capture submit before form-bound actions. The other bubbling events live on\n // the same document so controls associated through `form=\"id\"` participate too;\n // #controlFrom rejects every control owned by a different form.\n this.#listenerDocument = this.element.ownerDocument;\n this.#listenerDocument.addEventListener(\"submit\", this.#onSubmit, true);\n this.#listenerDocument.addEventListener(\"focusout\", this.#onFocusOut);\n this.#listenerDocument.addEventListener(\"input\", this.#onInput);\n this.#listenerDocument.addEventListener(\"change\", this.#onChange);\n }\n\n /** Tears down listeners and restores `novalidate` if we added it. */\n override disconnect(): void {\n this.#listenerDocument?.removeEventListener(\"submit\", this.#onSubmit, true);\n this.#listenerDocument?.removeEventListener(\"focusout\", this.#onFocusOut);\n this.#listenerDocument?.removeEventListener(\"input\", this.#onInput);\n this.#listenerDocument?.removeEventListener(\"change\", this.#onChange);\n this.#listenerDocument = null;\n for (const [control, message] of this.#ownedCustomErrors) {\n if (control.validationMessage === message) control.setCustomValidity(\"\");\n }\n this.#ownedCustomErrors.clear();\n this.#touchedRadioGroups.clear();\n if (this.element.hasAttribute(FormValidationController.#NOVALIDATE_MARKER)) {\n this.element.removeAttribute(\"novalidate\");\n this.element.removeAttribute(FormValidationController.#NOVALIDATE_MARKER);\n }\n }\n\n /**\n * Validates every control now, rendering or clearing each field's message, and\n * returns whether the whole form is valid. Marks every field/group touched so\n * a later input from any sibling re-validates it. Bound via `data-action`\n * (`#validate`) or callable directly (e.g. before a programmatic submit).\n */\n validate(): boolean {\n return this.#validateAll().length === 0;\n }\n\n /**\n * Validates every control and returns one invalid control per field. Controls\n * are grouped by field first (see {@link #keyFor}) so a field with several\n * controls — a radio group, or a mirror plus its visible control — reflects\n * *all* of them: a valid sibling must never clear an invalid one's message.\n * Each group's first invalid control supplies the message and the focus target.\n */\n #validateAll(): ValidatableControl[] {\n const invalid: ValidatableControl[] = [];\n for (const group of this.#snapshot().groups.values()) {\n this.#markTouched(group.key);\n const firstInvalid = this.#applyGroup(group);\n if (firstInvalid) invalid.push(firstInvalid);\n }\n return invalid;\n }\n\n /** Builds the current field table in one pass over form controls and DOM depth. */\n #snapshot(): ValidationSnapshot {\n const fields = this.#fieldOutlets();\n const groups = new Map<FieldGroupKey, FieldGroup>();\n const groupByControl = new Map<ValidatableControl | null, FieldGroup>();\n\n for (const control of this.#controls) {\n const field = this.#fieldFor(control, fields);\n const key = this.#keyFor(control, field);\n let group = groups.get(key);\n if (!group) {\n group = { key, field, controls: [] };\n groups.set(key, group);\n }\n group.controls.push(control);\n groupByControl.set(control, group);\n }\n\n return { groups, groupByControl };\n }\n\n /** Records that a whole field/group, rather than one sibling control, was visited. */\n #markTouched(key: FieldGroupKey): void {\n if (typeof key === \"string\") {\n this.#touchedRadioGroups.add(key);\n } else {\n this.#touchedGroups.add(key);\n }\n }\n\n #isTouched(key: FieldGroupKey): boolean {\n return typeof key === \"string\"\n ? this.#touchedRadioGroups.has(key)\n : this.#touchedGroups.has(key);\n }\n\n /**\n * Runs native constraint validation across a field's controls and routes the\n * result to its `stimeo--form-field` outlet: the first invalid control's\n * `validationMessage` is shown, an all-valid field is cleared. Returns the\n * first invalid control (for the invalid list / focus), or `null` when valid.\n * Routing goes through the outlet, so the ARIA wiring is never duplicated here.\n */\n #applyGroup(group: FieldGroup): ValidatableControl | null {\n // Apply declarative custom rules (e.g. disallow=\"whitespace\") before reading\n // validity so they participate in checkValidity() like a native constraint.\n for (const control of group.controls) this.#syncCustomValidity(control);\n const firstInvalid = group.controls.find((control) => !control.checkValidity()) ?? null;\n if (group.field) {\n if (firstInvalid) {\n const message = this.#messageFor(firstInvalid);\n const alreadyReported =\n group.field.element.hasAttribute(FORM_FIELD_INVALID_ATTR) &&\n this.#reportedErrors.get(group.field) === message;\n if (!alreadyReported) group.field.setError(message, { focus: false });\n this.#reportedErrors.set(group.field, message);\n } else if (\n group.field.element.hasAttribute(FORM_FIELD_INVALID_ATTR) ||\n this.#reportedErrors.has(group.field)\n ) {\n group.field.clearError();\n this.#reportedErrors.delete(group.field);\n }\n }\n return firstInvalid;\n }\n\n /**\n * Resolves the message to show for an invalid control: a per-constraint\n * override (`data-stimeo--form-validation-message-<constraint>`) for the first failing\n * `ValidityState` flag, then a generic `data-stimeo--form-validation-message`\n * override, then the browser's native `validationMessage`. Authoring an override\n * gives controlled, localizable, theme-able wording with **no consumer JS** —\n * and sidesteps headless browsers that return an empty native message.\n */\n #messageFor(control: ValidatableControl): string {\n // `setCustomValidity()` owns its wording. In particular, a consumer's custom\n // error must not be replaced by a simultaneous native-constraint override.\n if (control.validity.customError) return control.validationMessage;\n for (const [flag, key] of CONSTRAINT_MESSAGE_KEYS) {\n if (control.validity[flag]) {\n return (\n this.#authoredMessage(control, `${MESSAGE_ATTR_PREFIX}${key}`) ??\n this.#authoredMessage(control, MESSAGE_ATTR_GENERIC) ??\n control.validationMessage\n );\n }\n }\n return control.validationMessage || this.#authoredMessage(control, MESSAGE_ATTR_GENERIC) || \"\";\n }\n\n /**\n * Applies (or clears) a declarative custom constraint via `setCustomValidity`,\n * for controls that opt in with `data-stimeo--form-validation-disallow`. The supported\n * rule is `\"whitespace\"` — a value that is non-empty but blank\n * after trimming (which slips past `required` / `minlength`); its message follows\n * the whitespace-specific → generic → default chain.\n *\n * Don't-clobber-authored-state: an unknown/absent rule is never written, an\n * existing consumer custom error wins, and a controller error is only cleared\n * while the live message still equals the recorded controller write.\n */\n #syncCustomValidity(control: ValidatableControl): void {\n const recorded = this.#ownedCustomErrors.get(control);\n const ownsCurrent = recorded !== undefined && control.validationMessage === recorded;\n\n const violates =\n control.getAttribute(DISALLOW_ATTR) === \"whitespace\" &&\n control.value.length > 0 &&\n control.value.trim() === \"\";\n if (!violates) {\n if (ownsCurrent) control.setCustomValidity(\"\");\n this.#ownedCustomErrors.delete(control);\n return;\n }\n\n // One custom-validity slot exists per control. Preserve a consumer's live\n // entry; once they clear it, the next validation applies the declarative rule.\n if (control.validity.customError && !ownsCurrent) return;\n\n const message =\n this.#authoredMessage(control, DISALLOW_WHITESPACE_MESSAGE) ??\n this.#authoredMessage(control, MESSAGE_ATTR_GENERIC) ??\n DISALLOW_WHITESPACE_DEFAULT;\n if (!ownsCurrent || recorded !== message) control.setCustomValidity(message);\n this.#ownedCustomErrors.set(control, message);\n }\n\n /** Returns a non-blank authored message, otherwise falls through to a fallback. */\n #authoredMessage(control: ValidatableControl, attribute: string): string | null {\n const message = control.getAttribute(attribute);\n return message && message.trim().length > 0 ? message : null;\n }\n\n /**\n * A grouping key that collects controls belonging to the same field: the owning\n * `stimeo--form-field` when present, else a radio group's shared `name`, else\n * the control itself (always distinct).\n */\n #keyFor(control: ValidatableControl, field: FormFieldController | undefined): FieldGroupKey {\n if (field) return field;\n if (control instanceof HTMLInputElement && control.type === \"radio\" && control.name) {\n return `radio:${control.name}`;\n }\n return control;\n }\n\n /**\n * Where focus should land for an invalid control. A visible control is focused\n * directly — the case for native fields and radios. A validatable mirror\n * (the `hidden` attribute) cannot receive focus, so focus is delegated to the\n * visible widget: the owning field's `control` target when it is itself\n * focusable, else its first focusable descendant (e.g. a roving-tabindex\n * member). Resolved structurally — never by probing `focus()` — so behavior\n * is deterministic and CSS-independent.\n */\n #focusTargetFor(control: ValidatableControl): HTMLElement | null {\n if (canTakeFocus(control)) return control;\n const field = this.#fieldFor(control, this.#fieldOutlets());\n if (!field?.hasControlTarget) return null;\n const root = field.controlTarget;\n if (isTabStop(root)) return root;\n return firstTabStop(root);\n }\n\n /** Pairs each live outlet element with its controller once per operation. */\n #fieldOutlets(): Map<HTMLElement, FormFieldController> {\n const fields = new Map<HTMLElement, FormFieldController>();\n const elements = this.stimeoFormFieldOutletElements;\n const outlets = this.stimeoFormFieldOutlets;\n for (let index = 0; index < elements.length; index++) {\n const element = elements[index];\n const outlet = outlets[index];\n if (element && outlet) fields.set(element, outlet);\n }\n return fields;\n }\n\n /** The nearest configured field ancestor of `control`, if any. */\n #fieldFor(\n control: ValidatableControl,\n fields: ReadonlyMap<HTMLElement, FormFieldController>,\n ): FormFieldController | undefined {\n let ancestor: HTMLElement | null = control;\n while (ancestor) {\n const field = fields.get(ancestor);\n if (field) return field;\n ancestor = ancestor.parentElement;\n }\n return undefined;\n }\n\n /** This form's native controls that participate in constraint validation. */\n get #controls(): ValidatableControl[] {\n const controls: ValidatableControl[] = [];\n for (const element of Array.from(this.element.elements)) {\n if (this.#isValidatable(element)) controls.push(element);\n }\n return controls;\n }\n\n /** Narrows an event target to a validatable control. */\n #controlFrom(target: EventTarget | null): ValidatableControl | null {\n return target instanceof Element && this.#isValidatable(target) && target.form === this.element\n ? target\n : null;\n }\n\n #isValidatable(element: Element): element is ValidatableControl {\n return (\n (element instanceof HTMLInputElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement) &&\n // `willValidate` already excludes disabled, read-only, input[type=hidden],\n // and button-type controls. The `hidden` attribute intentionally does not\n // bar a text mirror from constraint validation.\n element.willValidate\n );\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/detach_gate.ts","../../src/utils/min_duration_floor.ts","../../src/utils/safe_timeout.ts","../../src/controllers/frame_loading_controller.ts"],"names":[],"mappings":"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","file":"frame_loading_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","/**\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 (the `MicrotaskCoalescer` split).\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 (the `SafeTimeout` / `FocusTrap` lineage —\n * 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","import type { SafeTimeout } from \"./safe_timeout\";\n\n/**\n * Holds a transient state visible for a minimum time before letting it end.\n *\n * A loading indicator that appears and disappears within a few frames reads as a\n * flicker rather than as feedback, so several controllers keep one up for a floor\n * of `minDuration` milliseconds measured from the moment it became visible. The\n * shape is always the same: note the moment, and when the end signal arrives\n * either run it now, because the floor has already passed, or hold it on a timer\n * for whatever is left.\n *\n * The floor **borrows** a {@link SafeTimeout} instead of owning one, so a\n * controller that also schedules other work (a show delay) keeps every timer in a\n * single registry and one `clearAll()` in its teardown still drops them all.\n * Cancelling after such a bulk clear is safe: the registry no-ops on an id it no\n * longer holds.\n *\n * Scope is the floor itself. **When an end signal is allowed to arrive at all\n * stays with the controller**, because the answers differ — one keeps the first\n * queued finish and ignores repeats, another lets the newest replace it — and\n * flattening that into a shared default would silently change what the consumers\n * already promise.\n *\n * @example\n * ```ts\n * readonly #timers = new SafeTimeout();\n * readonly #floor = new MinDurationFloor(this.#timers);\n *\n * #show() { this.#floor.begin(); … }\n * stop() { this.#floor.schedule(this.minDurationValue, () => this.#hide()); }\n * ```\n */\nexport class MinDurationFloor {\n readonly #timers: SafeTimeout;\n\n /** Pending finish timer id, or `null` when nothing is held back. */\n #timerId: number | null = null;\n\n /** Epoch ms the floor is measured from. */\n #since = 0;\n\n /** @param timers - the controller's registry; the floor schedules into it. */\n constructor(timers: SafeTimeout) {\n this.#timers = timers;\n }\n\n /** Starts the floor: call when the state being held becomes visible. */\n begin(): void {\n this.#since = Date.now();\n }\n\n /** True while a finish is held back waiting for the floor to elapse. */\n get pending(): boolean {\n return this.#timerId !== null;\n }\n\n /**\n * Runs `finish` once the floor has elapsed, immediately when it already has.\n *\n * A held-back finish is **replaced**, never stacked: only the most recently\n * queued id is cancellable, so a second timer would outlive every cancel and\n * end a state that has since restarted. Controllers that want the first signal\n * to win guard on {@link pending} before calling.\n */\n schedule(minDuration: number, finish: () => void): void {\n this.cancel();\n const remaining = minDuration - (Date.now() - this.#since);\n if (remaining > 0) {\n this.#timerId = this.#timers.set(() => {\n this.#timerId = null;\n finish();\n }, remaining);\n } else {\n finish();\n }\n }\n\n /** Drops a held-back finish. Safe when none is queued, or after a bulk clear. */\n cancel(): void {\n if (this.#timerId !== null) {\n this.#timers.clear(this.#timerId);\n this.#timerId = null;\n }\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { DetachGate } from \"../utils/detach_gate\";\nimport { MinDurationFloor } from \"../utils/min_duration_floor\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless `<turbo-frame>` loading-state behavior: while the frame is fetching it\n * sets `aria-busy` + `data-frame-loading`, reveals an optional skeleton / overlay,\n * suppresses interaction with the stale content, and retreats focus, restoring it on\n * completion (no dedicated APG pattern; supports WCAG 2.2 **4.1.3 Status Messages**\n * via `aria-busy` and 2.4.3 focus order via the retreat/restore).\n *\n * Markup contract (identifier: `stimeo--frame-loading`):\n * <turbo-frame id=\"panel\" data-controller=\"stimeo--frame-loading\">\n * <div data-stimeo--frame-loading-target=\"skeleton\" hidden>…</div>\n * <div data-stimeo--frame-loading-target=\"content\">…</div>\n * </turbo-frame>\n *\n * It subscribes on the frame to Turbo's own fetch lifecycle: `turbo:before-fetch-request`\n * (which bubbles from the frame's links/forms or the frame itself) starts the loading\n * state, and `turbo:frame-load` ends it (with `turbo:fetch-request-error` as a safety\n * net so the state never sticks). `minDuration` keeps the skeleton up long enough to\n * avoid a flicker.\n *\n * `start`, `end`, and `reconcile` dispatch `{}`. The last of those reports that\n * the Turbo cache rewind ended a load the frame never finished — `end` would\n * claim the frame arrived.\n *\n * @remarks\n * Behavior only — it ships no skeleton markup or styling (pair with Skeleton/CSS);\n * loading is held purely in `aria-busy` / `data-frame-loading` and the optional\n * targets' `hidden`. The `content` target is marked `inert` while loading to block\n * double-submits, and focus inside the frame is explicitly blurred then restored\n * (when `restoreFocus`) so it is testable without relying on emergent `inert`\n * focus behavior. Listeners are torn down on `disconnect()` (Turbo navigation\n * included) along with the {@link MinDurationFloor} holding the finish back, kept\n * across an in-page move by {@link DetachGate}. A detach that keeps the element\n * returns the frame to its idle form, as does {@link BeforeCacheReset} for the\n * snapshot a cached page freezes; a frame render that swaps the targets mid-load\n * re-arms them.\n */\nexport class FrameLoadingController extends Controller<HTMLElement> {\n static override targets = [\"content\", \"skeleton\", \"overlay\"];\n static override values = {\n announceText: { type: String, default: \"\" },\n announceReadyText: { type: String, default: \"\" },\n minDuration: { type: Number, default: 0 },\n restoreFocus: { type: Boolean, default: true },\n };\n static events = [\"start\", \"end\", \"reconcile\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly skeletonTarget: HTMLElement;\n declare readonly overlayTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n declare readonly hasSkeletonTarget: boolean;\n declare readonly hasOverlayTarget: boolean;\n\n declare minDurationValue: number;\n declare restoreFocusValue: boolean;\n declare announceTextValue: string;\n declare announceReadyTextValue: string;\n\n readonly #timeouts = new SafeTimeout();\n readonly #floor = new MinDurationFloor(this.#timeouts);\n readonly #gate = new DetachGate();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n #loading = false;\n /**\n * The optional targets this controller revealed, and the content it marked inert.\n * Held as references rather than re-resolved on the way out: a detach that keeps\n * the element takes the identifier off `data-controller` first, and a scope\n * without its identifier stops resolving targets — the elements to tidy would be\n * unreachable exactly when the tidying matters. They double as the ownership\n * marker, so a `hidden` or an `inert` the consumer wrote is never taken over.\n */\n #revealedSkeleton: HTMLElement | null = null;\n #revealedOverlay: HTMLElement | null = null;\n #inertTarget: HTMLElement | null = null;\n #previousFocus: HTMLElement | null = null;\n /** The id of the retreated element, used to re-find it if the load replaced it. */\n #previousFocusId = \"\";\n\n readonly #onStart = (): void => {\n // A (possibly new) fetch began: drop a finish the floor is still holding so the\n // loading state is not torn down mid-load, then begin if not already loading.\n this.#floor.cancel();\n if (!this.#loading) this.#begin();\n };\n\n readonly #onEnd = (): void => {\n if (!this.#loading) return;\n // The newest end signal owns the finish: the floor replaces whatever it was\n // holding rather than letting a second wait stack behind the first.\n this.#floor.schedule(this.minDurationValue, () => this.#finish());\n };\n\n override connect(): void {\n this.#gate.cancel();\n this.#beforeCache.activate();\n this.element.addEventListener(\"turbo:before-fetch-request\", this.#onStart);\n this.element.addEventListener(\"turbo:frame-load\", this.#onEnd);\n this.element.addEventListener(\"turbo:fetch-request-error\", this.#onEnd);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"turbo:before-fetch-request\", this.#onStart);\n this.element.removeEventListener(\"turbo:frame-load\", this.#onEnd);\n this.element.removeEventListener(\"turbo:fetch-request-error\", this.#onEnd);\n this.#beforeCache.deactivate();\n this.#gate.disconnected(this, () => this.#teardown());\n }\n\n /**\n * Drops the held finish and the loading bookkeeping on a real detach, returning\n * the frame to its idle form. No reconnect is coming, so nothing is left that\n * could finish the load and clear the hooks — a detach that keeps the element\n * (a morph dropping the identifier, an exit from a scoped observed root) would\n * otherwise strand it busy and inert. Focus is left where it is: the element is\n * leaving this controller's care, and moving it now would be an unexplained jump.\n */\n #teardown(): void {\n this.#gate.cancel();\n this.#timeouts.clearAll();\n this.#floor.cancel();\n if (this.#loading) this.#rewindHooks();\n this.#loading = false;\n this.#previousFocus = null;\n }\n\n /**\n * Returns the frame to its idle form for the snapshot Turbo is about to take, so\n * a page reached with the Back button does not restore a frame that is busy and\n * inert with nothing left to finish it. State only — no `end` event and no focus\n * move, because the load did not actually complete.\n *\n * The load is abandoned rather than paused, so the flag and any finish the floor\n * still holds drop along with the hooks. A kept finish would surface after the\n * rewind as exactly the three things this pass exists to avoid — an `end`, a\n * completion announcement, and a focus move — and a kept flag would leave the\n * next fetch on a page that survives a cancelled visit skipping the loading\n * state, its idempotence guard already satisfied.\n */\n #rewindForCache(): void {\n if (!this.#loading) return;\n this.#loading = false;\n this.#floor.cancel();\n this.#rewindHooks();\n // The fetch does not survive the navigation, so a consumer still painting\n // \"loading\" from `start` would never be released by `end`.\n this.dispatch(\"reconcile\", { detail: {} });\n }\n\n /**\n * Clears every hook the loading state writes. Shared by the three ways a load can\n * stop — completion, detach, snapshot — so none of them can drift into tidying\n * only part of it.\n */\n #rewindHooks(): void {\n this.element.removeAttribute(\"aria-busy\");\n this.element.removeAttribute(\"data-frame-loading\");\n if (this.#revealedSkeleton) this.#revealedSkeleton.hidden = true;\n if (this.#revealedOverlay) this.#revealedOverlay.hidden = true;\n this.#revealedSkeleton = null;\n this.#revealedOverlay = null;\n this.#clearInert();\n }\n\n /**\n * Re-shows a `skeleton` that arrived mid-load. Turbo's frame renderer empties the\n * frame and re-inserts the response's children, so a response's authored (hidden)\n * skeleton can land while a later fetch is still running, and only the controller\n * knows the frame is still busy.\n */\n skeletonTargetConnected(): void {\n if (this.#loading) this.#revealSkeleton();\n }\n\n /** Re-shows an `overlay` that arrived mid-load — the same swap as the skeleton. */\n overlayTargetConnected(): void {\n if (this.#loading) this.#revealOverlay();\n }\n\n /**\n * Re-blocks a `content` that arrived mid-load, so the stale copy stays unusable.\n * The element that left is released first and ownership is then decided afresh, so\n * an `inert` the replacement authored stays the consumer's.\n */\n contentTargetConnected(): void {\n if (!this.#loading) return;\n this.#clearInert();\n this.#applyInert();\n }\n\n /** Reveals the optional `skeleton`, noting it as this controller's to hide again. */\n #revealSkeleton(): void {\n if (!this.hasSkeletonTarget) return;\n this.#revealedSkeleton = this.skeletonTarget;\n this.skeletonTarget.hidden = false;\n }\n\n /** Reveals the optional `overlay`, noting it as this controller's to hide again. */\n #revealOverlay(): void {\n if (!this.hasOverlayTarget) return;\n this.#revealedOverlay = this.overlayTarget;\n this.overlayTarget.hidden = false;\n }\n\n /** Enters the loading state: hooks, skeleton/overlay, inert content, focus retreat. */\n #begin(): void {\n this.#loading = true;\n this.#floor.begin();\n this.element.setAttribute(\"aria-busy\", \"true\");\n this.element.setAttribute(\"data-frame-loading\", \"true\");\n this.#revealSkeleton();\n this.#revealOverlay();\n this.#applyInert();\n this.#retreatFocus();\n this.dispatch(\"start\", { detail: {} });\n announce(fillTemplate(this.announceTextValue, {}));\n }\n\n /** Leaves the loading state: restore hooks, hide skeleton/overlay, restore focus. */\n #finish(): void {\n this.#loading = false;\n this.#rewindHooks();\n this.#restoreFocus();\n this.dispatch(\"end\", { detail: {} });\n announce(fillTemplate(this.announceReadyTextValue, {}));\n }\n\n /** Marks the content inert to block double-submits while stale (if we own it). */\n #applyInert(): void {\n if (!this.hasContentTarget || this.contentTarget.hasAttribute(\"inert\")) return;\n this.contentTarget.setAttribute(\"inert\", \"\");\n this.#inertTarget = this.contentTarget;\n }\n\n #clearInert(): void {\n this.#inertTarget?.removeAttribute(\"inert\");\n this.#inertTarget = null;\n }\n\n /** Saves and blurs focus if it sits inside the frame about to go stale. */\n #retreatFocus(): void {\n this.#previousFocus = null;\n this.#previousFocusId = \"\";\n if (!this.restoreFocusValue) return;\n const active = document.activeElement;\n if (active instanceof HTMLElement && active !== this.element && this.element.contains(active)) {\n this.#previousFocus = active;\n this.#previousFocusId = active.id;\n active.blur();\n }\n }\n\n /**\n * Restores focus after the load. The same node when it survived (e.g. a\n * non-replacing update), else the element re-rendered with the same id inside the\n * frame — Turbo frames typically re-emit the same controls. When neither is present\n * (an anonymous control was replaced) focus is left where the browser put it, to\n * avoid an unexpected jump (WCAG 3.2.x).\n */\n #restoreFocus(): void {\n const target = this.#previousFocus;\n const id = this.#previousFocusId;\n this.#previousFocus = null;\n this.#previousFocusId = \"\";\n if (!this.restoreFocusValue) return;\n if (target?.isConnected) {\n target.focus();\n return;\n }\n if (id) {\n const replacement = document.getElementById(id);\n if (replacement && this.element.contains(replacement)) replacement.focus();\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/detach_gate.ts","../../src/utils/min_duration_floor.ts","../../src/utils/safe_timeout.ts","../../src/controllers/frame_loading_controller.ts"],"names":[],"mappings":";;;;;AAoBO,SAAS,QAAA,CAAS,OAAA,EAAiB,OAAA,GAAmC,EAAC,EAAS;AACrF,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,EAAK;AAC1B,EAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACvB,EAAA,MAAA,CAAO,aAAA;AAAA,IACL,IAAI,YAAY,4BAAA,EAA8B;AAAA,MAC5C,QAAQ,EAAE,OAAA,EAAS,MAAM,SAAA,EAAW,OAAA,CAAQ,cAAc,IAAA;AAAK,KAChE;AAAA,GACH;AACF;AAUO,SAAS,YAAA,CAAa,UAAkB,MAAA,EAAiD;AAC9F,EAAA,OAAO,QAAA,CAAS,OAAA,CAAQ,6BAAA,EAA+B,CAAC,OAAO,IAAA,KAAiB;AAC9E,IAAA,MAAM,WAAA,GAAc,OAAO,IAAI,CAAA;AAC/B,IAAA,OAAO,WAAA,KAAgB,MAAA,GAAY,KAAA,GAAQ,MAAA,CAAO,WAAW,CAAA;AAAA,EAC/D,CAAC,CAAA;AACH;;;ACAO,IAAM,gBAAA,GAAN,MAAM,iBAAA,CAAiB;AAAA;AAAA,EAE5B,OAAgB,YAAA,mBAAe,IAAI,GAAA,EAAsB;AAAA;AAAA,EAGzD,OAAgB,iBAAiB,MAAY;AAC3C,IAAA,KAAA,MAAW,UAAA,IAAc,iBAAA,CAAiB,YAAA,EAAc,UAAA,CAAW,OAAA,EAAQ;AAAA,EAC7E,CAAA;AAAA,EAES,OAAA;AAAA;AAAA,EAGT,YAAY,MAAA,EAAoB;AAC9B,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AAAA,EACjB;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,MAAM,KAAA,GAAQ,iBAAA,CAAiB,YAAA,CAAa,IAAA,KAAS,CAAA;AACrD,IAAA,iBAAA,CAAiB,YAAA,CAAa,IAAI,IAAI,CAAA;AACtC,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAA,EAAsB,iBAAA,CAAiB,cAAc,CAAA;AAAA,IACjF;AAAA,EACF;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,iBAAA,CAAiB,YAAA,CAAa,OAAO,IAAI,CAAA;AACzC,IAAA,IAAI,iBAAA,CAAiB,YAAA,CAAa,IAAA,GAAO,CAAA,EAAG;AAC5C,IAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAA,EAAsB,iBAAA,CAAiB,cAAc,CAAA;AAAA,EACpF;AACF,CAAA;;;ACCO,IAAM,UAAA,GAAN,MAAM,WAAA,CAAW;AAAA;AAAA,EAEtB,QAAA,GAAW,KAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASX,IAAI,OAAA,GAAmB;AACrB,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,WAAW,IAAA,EAA+B;AAC/C,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAa,OAAO,IAAA;AACtC,IAAA,MAAM,MAAA,GAAA,CAAU,KAAK,OAAA,CAAQ,YAAA,CAAa,iBAAiB,CAAA,IAAK,EAAA,EAAI,MAAM,KAAK,CAAA;AAC/E,IAAA,OAAO,CAAC,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,UAAU,CAAA;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,YAAA,CAAa,MAAsB,QAAA,EAA4B;AAC7D,IAAA,IAAI,WAAA,CAAW,UAAA,CAAW,IAAI,CAAA,EAAG;AAG/B,MAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAChB,MAAA,QAAA,EAAS;AACT,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,IAAA,cAAA,CAAe,MAAM;AACnB,MAAA,IAAI,CAAC,KAAK,QAAA,EAAU;AACpB,MAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAChB,MAAA,QAAA,EAAS;AAAA,IACX,CAAC,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAAA,EAClB;AACF,CAAA;;;ACpGO,IAAM,mBAAN,MAAuB;AAAA,EACnB,OAAA;AAAA;AAAA,EAGT,QAAA,GAA0B,IAAA;AAAA;AAAA,EAG1B,MAAA,GAAS,CAAA;AAAA;AAAA,EAGT,YAAY,MAAA,EAAqB;AAC/B,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AAAA,EACjB;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,KAAK,GAAA,EAAI;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,OAAA,GAAmB;AACrB,IAAA,OAAO,KAAK,QAAA,KAAa,IAAA;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,QAAA,CAAS,aAAqB,MAAA,EAA0B;AACtD,IAAA,IAAA,CAAK,MAAA,EAAO;AACZ,IAAA,MAAM,SAAA,GAAY,WAAA,IAAe,IAAA,CAAK,GAAA,KAAQ,IAAA,CAAK,MAAA,CAAA;AACnD,IAAA,IAAI,YAAY,CAAA,EAAG;AACjB,MAAA,IAAA,CAAK,QAAA,GAAW,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,MAAM;AACrC,QAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,QAAA,MAAA,EAAO;AAAA,MACT,GAAG,SAAS,CAAA;AAAA,IACd,CAAA,MAAO;AACL,MAAA,MAAA,EAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAI,IAAA,CAAK,aAAa,IAAA,EAAM;AAC1B,MAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAA;AAChC,MAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAAA,IAClB;AAAA,EACF;AACF,CAAA;;;AC7DA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AAmBO,IAAM,WAAA,GAAN,cAA0B,aAAA,CAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,MAAM;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAClB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EACxB;AACF,CAAA;;;AC5DO,IAAM,sBAAA,GAAN,cAAqC,UAAA,CAAwB;AAAA,EAClE,OAAgB,OAAA,GAAU,CAAC,SAAA,EAAW,YAAY,SAAS,CAAA;AAAA,EAC3D,OAAgB,MAAA,GAAS;AAAA,IACvB,YAAA,EAAc,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IAC1C,iBAAA,EAAmB,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IAC/C,WAAA,EAAa,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA,EAAE;AAAA,IACxC,YAAA,EAAc,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA;AAAK,GAC/C;AAAA,EACA,OAAO,MAAA,GAAS,CAAC,OAAA,EAAS,OAAO,WAAW,CAAA;AAAA,EAcnC,SAAA,GAAY,IAAI,WAAA,EAAY;AAAA,EAC5B,MAAA,GAAS,IAAI,gBAAA,CAAiB,IAAA,CAAK,SAAS,CAAA;AAAA,EAC5C,KAAA,GAAQ,IAAI,UAAA,EAAW;AAAA,EACvB,eAAe,IAAI,gBAAA,CAAiB,MAAM,IAAA,CAAK,iBAAiB,CAAA;AAAA,EACzE,QAAA,GAAW,KAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASX,iBAAA,GAAwC,IAAA;AAAA,EACxC,gBAAA,GAAuC,IAAA;AAAA,EACvC,YAAA,GAAmC,IAAA;AAAA,EACnC,cAAA,GAAqC,IAAA;AAAA;AAAA,EAErC,gBAAA,GAAmB,EAAA;AAAA,EAEV,WAAW,MAAY;AAG9B,IAAA,IAAA,CAAK,OAAO,MAAA,EAAO;AACnB,IAAA,IAAI,CAAC,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,MAAA,EAAO;AAAA,EAClC,CAAA;AAAA,EAES,SAAS,MAAY;AAC5B,IAAA,IAAI,CAAC,KAAK,QAAA,EAAU;AAGpB,IAAA,IAAA,CAAK,OAAO,QAAA,CAAS,IAAA,CAAK,kBAAkB,MAAM,IAAA,CAAK,SAAS,CAAA;AAAA,EAClE,CAAA;AAAA,EAES,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,MAAM,MAAA,EAAO;AAClB,IAAA,IAAA,CAAK,aAAa,QAAA,EAAS;AAC3B,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,4BAAA,EAA8B,IAAA,CAAK,QAAQ,CAAA;AACzE,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,kBAAA,EAAoB,IAAA,CAAK,MAAM,CAAA;AAC7D,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,2BAAA,EAA6B,IAAA,CAAK,MAAM,CAAA;AAAA,EACxE;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,4BAAA,EAA8B,IAAA,CAAK,QAAQ,CAAA;AAC5E,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,kBAAA,EAAoB,IAAA,CAAK,MAAM,CAAA;AAChE,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,2BAAA,EAA6B,IAAA,CAAK,MAAM,CAAA;AACzE,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,IAAA,CAAK,MAAM,YAAA,CAAa,IAAA,EAAM,MAAM,IAAA,CAAK,WAAW,CAAA;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,SAAA,GAAkB;AAChB,IAAA,IAAA,CAAK,MAAM,MAAA,EAAO;AAClB,IAAA,IAAA,CAAK,UAAU,QAAA,EAAS;AACxB,IAAA,IAAA,CAAK,OAAO,MAAA,EAAO;AACnB,IAAA,IAAI,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,YAAA,EAAa;AACrC,IAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAChB,IAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,eAAA,GAAwB;AACtB,IAAA,IAAI,CAAC,KAAK,QAAA,EAAU;AACpB,IAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAChB,IAAA,IAAA,CAAK,OAAO,MAAA,EAAO;AACnB,IAAA,IAAA,CAAK,YAAA,EAAa;AAGlB,IAAA,IAAA,CAAK,SAAS,WAAA,EAAa,EAAE,MAAA,EAAQ,IAAI,CAAA;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,YAAA,GAAqB;AACnB,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,WAAW,CAAA;AACxC,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,oBAAoB,CAAA;AACjD,IAAA,IAAI,IAAA,CAAK,iBAAA,EAAmB,IAAA,CAAK,iBAAA,CAAkB,MAAA,GAAS,IAAA;AAC5D,IAAA,IAAI,IAAA,CAAK,gBAAA,EAAkB,IAAA,CAAK,gBAAA,CAAiB,MAAA,GAAS,IAAA;AAC1D,IAAA,IAAA,CAAK,iBAAA,GAAoB,IAAA;AACzB,IAAA,IAAA,CAAK,gBAAA,GAAmB,IAAA;AACxB,IAAA,IAAA,CAAK,WAAA,EAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,uBAAA,GAAgC;AAC9B,IAAA,IAAI,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,eAAA,EAAgB;AAAA,EAC1C;AAAA;AAAA,EAGA,sBAAA,GAA+B;AAC7B,IAAA,IAAI,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,cAAA,EAAe;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,sBAAA,GAA+B;AAC7B,IAAA,IAAI,CAAC,KAAK,QAAA,EAAU;AACpB,IAAA,IAAA,CAAK,WAAA,EAAY;AACjB,IAAA,IAAA,CAAK,WAAA,EAAY;AAAA,EACnB;AAAA;AAAA,EAGA,eAAA,GAAwB;AACtB,IAAA,IAAI,CAAC,KAAK,iBAAA,EAAmB;AAC7B,IAAA,IAAA,CAAK,oBAAoB,IAAA,CAAK,cAAA;AAC9B,IAAA,IAAA,CAAK,eAAe,MAAA,GAAS,KAAA;AAAA,EAC/B;AAAA;AAAA,EAGA,cAAA,GAAuB;AACrB,IAAA,IAAI,CAAC,KAAK,gBAAA,EAAkB;AAC5B,IAAA,IAAA,CAAK,mBAAmB,IAAA,CAAK,aAAA;AAC7B,IAAA,IAAA,CAAK,cAAc,MAAA,GAAS,KAAA;AAAA,EAC9B;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,IAAA,IAAA,CAAK,OAAO,KAAA,EAAM;AAClB,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,WAAA,EAAa,MAAM,CAAA;AAC7C,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,oBAAA,EAAsB,MAAM,CAAA;AACtD,IAAA,IAAA,CAAK,eAAA,EAAgB;AACrB,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,IAAA,CAAK,WAAA,EAAY;AACjB,IAAA,IAAA,CAAK,aAAA,EAAc;AACnB,IAAA,IAAA,CAAK,SAAS,OAAA,EAAS,EAAE,MAAA,EAAQ,IAAI,CAAA;AACrC,IAAA,QAAA,CAAS,YAAA,CAAa,IAAA,CAAK,iBAAA,EAAmB,EAAE,CAAC,CAAA;AAAA,EACnD;AAAA;AAAA,EAGA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAChB,IAAA,IAAA,CAAK,YAAA,EAAa;AAClB,IAAA,IAAA,CAAK,aAAA,EAAc;AACnB,IAAA,IAAA,CAAK,SAAS,KAAA,EAAO,EAAE,MAAA,EAAQ,IAAI,CAAA;AACnC,IAAA,QAAA,CAAS,YAAA,CAAa,IAAA,CAAK,sBAAA,EAAwB,EAAE,CAAC,CAAA;AAAA,EACxD;AAAA;AAAA,EAGA,WAAA,GAAoB;AAClB,IAAA,IAAI,CAAC,IAAA,CAAK,gBAAA,IAAoB,KAAK,aAAA,CAAc,YAAA,CAAa,OAAO,CAAA,EAAG;AACxE,IAAA,IAAA,CAAK,aAAA,CAAc,YAAA,CAAa,OAAA,EAAS,EAAE,CAAA;AAC3C,IAAA,IAAA,CAAK,eAAe,IAAA,CAAK,aAAA;AAAA,EAC3B;AAAA,EAEA,WAAA,GAAoB;AAClB,IAAA,IAAA,CAAK,YAAA,EAAc,gBAAgB,OAAO,CAAA;AAC1C,IAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AAAA,EACtB;AAAA;AAAA,EAGA,aAAA,GAAsB;AACpB,IAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AACtB,IAAA,IAAA,CAAK,gBAAA,GAAmB,EAAA;AACxB,IAAA,IAAI,CAAC,KAAK,iBAAA,EAAmB;AAC7B,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AACxB,IAAA,IAAI,MAAA,YAAkB,eAAe,MAAA,KAAW,IAAA,CAAK,WAAW,IAAA,CAAK,OAAA,CAAQ,QAAA,CAAS,MAAM,CAAA,EAAG;AAC7F,MAAA,IAAA,CAAK,cAAA,GAAiB,MAAA;AACtB,MAAA,IAAA,CAAK,mBAAmB,MAAA,CAAO,EAAA;AAC/B,MAAA,MAAA,CAAO,IAAA,EAAK;AAAA,IACd;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,aAAA,GAAsB;AACpB,IAAA,MAAM,SAAS,IAAA,CAAK,cAAA;AACpB,IAAA,MAAM,KAAK,IAAA,CAAK,gBAAA;AAChB,IAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AACtB,IAAA,IAAA,CAAK,gBAAA,GAAmB,EAAA;AACxB,IAAA,IAAI,CAAC,KAAK,iBAAA,EAAmB;AAC7B,IAAA,IAAI,QAAQ,WAAA,EAAa;AACvB,MAAA,MAAA,CAAO,KAAA,EAAM;AACb,MAAA;AAAA,IACF;AACA,IAAA,IAAI,EAAA,EAAI;AACN,MAAA,MAAM,WAAA,GAAc,QAAA,CAAS,cAAA,CAAe,EAAE,CAAA;AAC9C,MAAA,IAAI,eAAe,IAAA,CAAK,OAAA,CAAQ,SAAS,WAAW,CAAA,cAAe,KAAA,EAAM;AAAA,IAC3E;AAAA,EACF;AACF","file":"frame_loading_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","/**\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","import type { SafeTimeout } from \"./safe_timeout\";\n\n/**\n * Holds a transient state visible for a minimum time before letting it end.\n *\n * A loading indicator that appears and disappears within a few frames reads as a\n * flicker rather than as feedback, so several controllers keep one up for a floor\n * of `minDuration` milliseconds measured from the moment it became visible. The\n * shape is always the same: note the moment, and when the end signal arrives\n * either run it now, because the floor has already passed, or hold it on a timer\n * for whatever is left.\n *\n * The floor **borrows** a {@link SafeTimeout} instead of owning one, so a\n * controller that also schedules other work (a show delay) keeps every timer in a\n * single registry and one `clearAll()` in its teardown still drops them all.\n * Cancelling after such a bulk clear is safe: the registry no-ops on an id it no\n * longer holds.\n *\n * Scope is the floor itself. **When an end signal is allowed to arrive at all\n * stays with the controller**, because the answers differ — one keeps the first\n * queued finish and ignores repeats, another lets the newest replace it — and\n * flattening that into a shared default would silently change what the consumers\n * already promise.\n *\n * @example\n * ```ts\n * readonly #timers = new SafeTimeout();\n * readonly #floor = new MinDurationFloor(this.#timers);\n *\n * #show() { this.#floor.begin(); … }\n * stop() { this.#floor.schedule(this.minDurationValue, () => this.#hide()); }\n * ```\n */\nexport class MinDurationFloor {\n readonly #timers: SafeTimeout;\n\n /** Pending finish timer id, or `null` when nothing is held back. */\n #timerId: number | null = null;\n\n /** Epoch ms the floor is measured from. */\n #since = 0;\n\n /** @param timers - the controller's registry; the floor schedules into it. */\n constructor(timers: SafeTimeout) {\n this.#timers = timers;\n }\n\n /** Starts the floor: call when the state being held becomes visible. */\n begin(): void {\n this.#since = Date.now();\n }\n\n /** True while a finish is held back waiting for the floor to elapse. */\n get pending(): boolean {\n return this.#timerId !== null;\n }\n\n /**\n * Runs `finish` once the floor has elapsed, immediately when it already has.\n *\n * A held-back finish is **replaced**, never stacked: only the most recently\n * queued id is cancellable, so a second timer would outlive every cancel and\n * end a state that has since restarted. Controllers that want the first signal\n * to win guard on {@link pending} before calling.\n */\n schedule(minDuration: number, finish: () => void): void {\n this.cancel();\n const remaining = minDuration - (Date.now() - this.#since);\n if (remaining > 0) {\n this.#timerId = this.#timers.set(() => {\n this.#timerId = null;\n finish();\n }, remaining);\n } else {\n finish();\n }\n }\n\n /** Drops a held-back finish. Safe when none is queued, or after a bulk clear. */\n cancel(): void {\n if (this.#timerId !== null) {\n this.#timers.clear(this.#timerId);\n this.#timerId = null;\n }\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { DetachGate } from \"../utils/detach_gate\";\nimport { MinDurationFloor } from \"../utils/min_duration_floor\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless `<turbo-frame>` loading-state behavior: while the frame is fetching it\n * sets `aria-busy` + `data-frame-loading`, reveals an optional skeleton / overlay,\n * suppresses interaction with the stale content, and retreats focus, restoring it on\n * completion (no dedicated APG pattern; supports WCAG 2.2 **4.1.3 Status Messages**\n * via `aria-busy` and 2.4.3 focus order via the retreat/restore).\n *\n * Markup contract (identifier: `stimeo--frame-loading`):\n * <turbo-frame id=\"panel\" data-controller=\"stimeo--frame-loading\">\n * <div data-stimeo--frame-loading-target=\"skeleton\" hidden>…</div>\n * <div data-stimeo--frame-loading-target=\"content\">…</div>\n * </turbo-frame>\n *\n * It subscribes on the frame to Turbo's own fetch lifecycle: `turbo:before-fetch-request`\n * (which bubbles from the frame's links/forms or the frame itself) starts the loading\n * state, and `turbo:frame-load` ends it (with `turbo:fetch-request-error` as a safety\n * net so the state never sticks). `minDuration` keeps the skeleton up long enough to\n * avoid a flicker.\n *\n * `start`, `end`, and `reconcile` dispatch `{}`. The last of those reports that\n * the Turbo cache rewind ended a load the frame never finished — `end` would\n * claim the frame arrived.\n *\n * @remarks\n * Behavior only — it ships no skeleton markup or styling (pair with Skeleton/CSS);\n * loading is held purely in `aria-busy` / `data-frame-loading` and the optional\n * targets' `hidden`. The `content` target is marked `inert` while loading to block\n * double-submits, and focus inside the frame is explicitly blurred then restored\n * (when `restoreFocus`) so it is testable without relying on emergent `inert`\n * focus behavior. Listeners are torn down on `disconnect()` (Turbo navigation\n * included) along with the {@link MinDurationFloor} holding the finish back, kept\n * across an in-page move by {@link DetachGate}. A detach that keeps the element\n * returns the frame to its idle form, as does {@link BeforeCacheReset} for the\n * snapshot a cached page freezes; a frame render that swaps the targets mid-load\n * re-arms them.\n */\nexport class FrameLoadingController extends Controller<HTMLElement> {\n static override targets = [\"content\", \"skeleton\", \"overlay\"];\n static override values = {\n announceText: { type: String, default: \"\" },\n announceReadyText: { type: String, default: \"\" },\n minDuration: { type: Number, default: 0 },\n restoreFocus: { type: Boolean, default: true },\n };\n static events = [\"start\", \"end\", \"reconcile\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly skeletonTarget: HTMLElement;\n declare readonly overlayTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n declare readonly hasSkeletonTarget: boolean;\n declare readonly hasOverlayTarget: boolean;\n\n declare minDurationValue: number;\n declare restoreFocusValue: boolean;\n declare announceTextValue: string;\n declare announceReadyTextValue: string;\n\n readonly #timeouts = new SafeTimeout();\n readonly #floor = new MinDurationFloor(this.#timeouts);\n readonly #gate = new DetachGate();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n #loading = false;\n /**\n * The optional targets this controller revealed, and the content it marked inert.\n * Held as references rather than re-resolved on the way out: a detach that keeps\n * the element takes the identifier off `data-controller` first, and a scope\n * without its identifier stops resolving targets — the elements to tidy would be\n * unreachable exactly when the tidying matters. They double as the ownership\n * marker, so a `hidden` or an `inert` the consumer wrote is never taken over.\n */\n #revealedSkeleton: HTMLElement | null = null;\n #revealedOverlay: HTMLElement | null = null;\n #inertTarget: HTMLElement | null = null;\n #previousFocus: HTMLElement | null = null;\n /** The id of the retreated element, used to re-find it if the load replaced it. */\n #previousFocusId = \"\";\n\n readonly #onStart = (): void => {\n // A (possibly new) fetch began: drop a finish the floor is still holding so the\n // loading state is not torn down mid-load, then begin if not already loading.\n this.#floor.cancel();\n if (!this.#loading) this.#begin();\n };\n\n readonly #onEnd = (): void => {\n if (!this.#loading) return;\n // The newest end signal owns the finish: the floor replaces whatever it was\n // holding rather than letting a second wait stack behind the first.\n this.#floor.schedule(this.minDurationValue, () => this.#finish());\n };\n\n override connect(): void {\n this.#gate.cancel();\n this.#beforeCache.activate();\n this.element.addEventListener(\"turbo:before-fetch-request\", this.#onStart);\n this.element.addEventListener(\"turbo:frame-load\", this.#onEnd);\n this.element.addEventListener(\"turbo:fetch-request-error\", this.#onEnd);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"turbo:before-fetch-request\", this.#onStart);\n this.element.removeEventListener(\"turbo:frame-load\", this.#onEnd);\n this.element.removeEventListener(\"turbo:fetch-request-error\", this.#onEnd);\n this.#beforeCache.deactivate();\n this.#gate.disconnected(this, () => this.#teardown());\n }\n\n /**\n * Drops the held finish and the loading bookkeeping on a real detach, returning\n * the frame to its idle form. No reconnect is coming, so nothing is left that\n * could finish the load and clear the hooks — a detach that keeps the element\n * (a morph dropping the identifier, an exit from a scoped observed root) would\n * otherwise strand it busy and inert. Focus is left where it is: the element is\n * leaving this controller's care, and moving it now would be an unexplained jump.\n */\n #teardown(): void {\n this.#gate.cancel();\n this.#timeouts.clearAll();\n this.#floor.cancel();\n if (this.#loading) this.#rewindHooks();\n this.#loading = false;\n this.#previousFocus = null;\n }\n\n /**\n * Returns the frame to its idle form for the snapshot Turbo is about to take, so\n * a page reached with the Back button does not restore a frame that is busy and\n * inert with nothing left to finish it. State only — no `end` event and no focus\n * move, because the load did not actually complete.\n *\n * The load is abandoned rather than paused, so the flag and any finish the floor\n * still holds drop along with the hooks. A kept finish would surface after the\n * rewind as exactly the three things this pass exists to avoid — an `end`, a\n * completion announcement, and a focus move — and a kept flag would leave the\n * next fetch on a page that survives a cancelled visit skipping the loading\n * state, its idempotence guard already satisfied.\n */\n #rewindForCache(): void {\n if (!this.#loading) return;\n this.#loading = false;\n this.#floor.cancel();\n this.#rewindHooks();\n // The fetch does not survive the navigation, so a consumer still painting\n // \"loading\" from `start` would never be released by `end`.\n this.dispatch(\"reconcile\", { detail: {} });\n }\n\n /**\n * Clears every hook the loading state writes. Shared by the three ways a load can\n * stop — completion, detach, snapshot — so none of them can drift into tidying\n * only part of it.\n */\n #rewindHooks(): void {\n this.element.removeAttribute(\"aria-busy\");\n this.element.removeAttribute(\"data-frame-loading\");\n if (this.#revealedSkeleton) this.#revealedSkeleton.hidden = true;\n if (this.#revealedOverlay) this.#revealedOverlay.hidden = true;\n this.#revealedSkeleton = null;\n this.#revealedOverlay = null;\n this.#clearInert();\n }\n\n /**\n * Re-shows a `skeleton` that arrived mid-load. Turbo's frame renderer empties the\n * frame and re-inserts the response's children, so a response's authored (hidden)\n * skeleton can land while a later fetch is still running, and only the controller\n * knows the frame is still busy.\n */\n skeletonTargetConnected(): void {\n if (this.#loading) this.#revealSkeleton();\n }\n\n /** Re-shows an `overlay` that arrived mid-load — the same swap as the skeleton. */\n overlayTargetConnected(): void {\n if (this.#loading) this.#revealOverlay();\n }\n\n /**\n * Re-blocks a `content` that arrived mid-load, so the stale copy stays unusable.\n * The element that left is released first and ownership is then decided afresh, so\n * an `inert` the replacement authored stays the consumer's.\n */\n contentTargetConnected(): void {\n if (!this.#loading) return;\n this.#clearInert();\n this.#applyInert();\n }\n\n /** Reveals the optional `skeleton`, noting it as this controller's to hide again. */\n #revealSkeleton(): void {\n if (!this.hasSkeletonTarget) return;\n this.#revealedSkeleton = this.skeletonTarget;\n this.skeletonTarget.hidden = false;\n }\n\n /** Reveals the optional `overlay`, noting it as this controller's to hide again. */\n #revealOverlay(): void {\n if (!this.hasOverlayTarget) return;\n this.#revealedOverlay = this.overlayTarget;\n this.overlayTarget.hidden = false;\n }\n\n /** Enters the loading state: hooks, skeleton/overlay, inert content, focus retreat. */\n #begin(): void {\n this.#loading = true;\n this.#floor.begin();\n this.element.setAttribute(\"aria-busy\", \"true\");\n this.element.setAttribute(\"data-frame-loading\", \"true\");\n this.#revealSkeleton();\n this.#revealOverlay();\n this.#applyInert();\n this.#retreatFocus();\n this.dispatch(\"start\", { detail: {} });\n announce(fillTemplate(this.announceTextValue, {}));\n }\n\n /** Leaves the loading state: restore hooks, hide skeleton/overlay, restore focus. */\n #finish(): void {\n this.#loading = false;\n this.#rewindHooks();\n this.#restoreFocus();\n this.dispatch(\"end\", { detail: {} });\n announce(fillTemplate(this.announceReadyTextValue, {}));\n }\n\n /** Marks the content inert to block double-submits while stale (if we own it). */\n #applyInert(): void {\n if (!this.hasContentTarget || this.contentTarget.hasAttribute(\"inert\")) return;\n this.contentTarget.setAttribute(\"inert\", \"\");\n this.#inertTarget = this.contentTarget;\n }\n\n #clearInert(): void {\n this.#inertTarget?.removeAttribute(\"inert\");\n this.#inertTarget = null;\n }\n\n /** Saves and blurs focus if it sits inside the frame about to go stale. */\n #retreatFocus(): void {\n this.#previousFocus = null;\n this.#previousFocusId = \"\";\n if (!this.restoreFocusValue) return;\n const active = document.activeElement;\n if (active instanceof HTMLElement && active !== this.element && this.element.contains(active)) {\n this.#previousFocus = active;\n this.#previousFocusId = active.id;\n active.blur();\n }\n }\n\n /**\n * Restores focus after the load. The same node when it survived (e.g. a\n * non-replacing update), else the element re-rendered with the same id inside the\n * frame — Turbo frames typically re-emit the same controls. When neither is present\n * (an anonymous control was replaced) focus is left where the browser put it, to\n * avoid an unexpected jump (WCAG 3.2.x).\n */\n #restoreFocus(): void {\n const target = this.#previousFocus;\n const id = this.#previousFocusId;\n this.#previousFocus = null;\n this.#previousFocusId = \"\";\n if (!this.restoreFocusValue) return;\n if (target?.isConnected) {\n target.focus();\n return;\n }\n if (id) {\n const replacement = document.getElementById(id);\n if (replacement && this.element.contains(replacement)) replacement.focus();\n }\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/reduced_motion.ts","../../src/utils/safe_timeout.ts","../../src/controllers/highlight_controller.ts"],"names":[],"mappings":"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","file":"highlight_controller.js","sourcesContent":["/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers\n * (count-up, highlight, overflow-indicator, scroll-visibility, stick-to-bottom,\n * transition).\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","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * The connection whose removal timer currently owns an element's `data-highlight`\n * hook. Shared by every instance, so a row moved from one watched container into\n * another still has exactly one timer deciding when its emphasis ends.\n *\n * Only the keys are weak: an entry keeps its controller — and through it that\n * controller's element — reachable for as long as the row is in the DOM. So an entry\n * is dropped as soon as the timer that owns it is released or fires, and `connect()`\n * drops the claims the connection before it left behind.\n */\nconst hookOwners = new WeakMap<Element, HighlightController>();\n\n/**\n * Headless \"highlight on insert\" behavior: briefly flags a freshly inserted element\n * with `data-highlight` so CSS can flash / fade it in, then removes the flag after\n * `duration` ms (no dedicated APG pattern; a purely visual emphasis that honors the\n * WCAG \"animation from interactions\" practice via `prefers-reduced-motion`).\n *\n * Markup contract (identifier: `stimeo--highlight`):\n * <!-- self-highlight: put it on the inserted row itself -->\n * <li data-controller=\"stimeo--highlight\">New item</li>\n *\n * <!-- container mode: watch for added children and highlight each -->\n * <ul data-controller=\"stimeo--highlight\" data-stimeo--highlight-observe-value=\"true\"></ul>\n *\n * In the default (self) mode the controller element is highlighted once on\n * `connect()`. With `observe`, a `MutationObserver` instead highlights every element\n * child added to the container (e.g. Turbo Stream appends). Each highlight sets\n * `data-highlight=\"true\"`, dispatches `start`, and after `duration` removes it and\n * dispatches `end` — both carry `detail.element`.\n *\n * `start` and `end` dispatch `{ element }`.\n *\n * @remarks\n * Behavior only — it ships no color or animation, just the hook (pair with CSS, and\n * with Announcer / Flash for non-visual notice). Under `prefers-reduced-motion: reduce`\n * the emphasis is suppressed entirely (the element simply appears), so no hook or\n * event is emitted. A hook never outlives the connection that set it: the observer and\n * pending timers are torn down on `disconnect()` (Turbo navigation included), and\n * `connect()` clears any hook that arrived with the DOM — a restored `turbo:before-cache`\n * snapshot, an in-page move — because the timer that would have removed it is gone.\n * An element carries at most one emphasis at a time: highlighting it again — a reorder\n * inside one container, or a move into another watched one — takes the hook over and\n * releases the timer that held it, so `duration` is measured from the latest highlight\n * and `end` fires once per emphasis.\n */\nexport class HighlightController extends Controller<HTMLElement> {\n static override values = {\n duration: { type: Number, default: 1500 },\n observe: { type: Boolean, default: false },\n };\n static events = [\"start\", \"end\"] as const;\n\n declare durationValue: number;\n declare observeValue: boolean;\n\n readonly #timeouts = new SafeTimeout();\n /**\n * The removal timer this connection has outstanding for an element. Held weakly so\n * a row that leaves the DOM is not retained, and dropped wholesale on `disconnect()`\n * so a cleared id can never be matched against a recycled one. Which connection owns\n * an element's hook is answered by the shared owner registry above.\n */\n #pending = new WeakMap<HTMLElement, number>();\n #observer: MutationObserver | null = null;\n\n override connect(): void {\n // A hook that arrived with the DOM was written by an earlier connection, whose\n // removal timer is gone; this connection owns no timer for it either, so nothing\n // would ever take it off. Drop it before anything else — in self mode\n // `#highlight()` re-adds it with a fresh timer below, except under reduced\n // motion, where the element must carry no hook at all.\n this.#clearArrivedHook(this.element);\n if (this.observeValue) {\n // A container highlights its children, never itself, so the same reasoning\n // applies to every child present before the observer starts watching.\n for (const child of this.element.children) this.#clearArrivedHook(child);\n if (typeof MutationObserver !== \"undefined\") {\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(this.element, { childList: true });\n }\n return;\n }\n this.#highlight(this.element);\n }\n\n override disconnect(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n this.#timeouts.clearAll();\n this.#pending = new WeakMap();\n }\n\n /** Drops a hook that arrived with the DOM, along with this connection's claim on it. */\n #clearArrivedHook(el: Element): void {\n // Another connection may still hold a live timer for this element, and that timer\n // has to stay reachable so the next highlight can release it. Only our own claim,\n // whose timer went down with the previous disconnect, is dropped here.\n if (hookOwners.get(el) === this) hookOwners.delete(el);\n el.removeAttribute(\"data-highlight\");\n }\n\n /** Highlights every element child added by a childList mutation. */\n #onMutations(mutations: MutationRecord[]): void {\n for (const mutation of mutations) {\n for (const node of mutation.addedNodes) {\n if (node instanceof HTMLElement) this.#highlight(node);\n }\n }\n }\n\n /** Flags `el` with `data-highlight` and schedules its removal (unless reduced-motion). */\n #highlight(el: HTMLElement): void {\n // Reduced motion: suppress the emphasis so the element just appears — no hook,\n // no timer, no events, nothing to transition.\n if (prefersReducedMotion()) return;\n\n // A second highlight of the same element — a re-insert, which is the shape a\n // reorder takes — owns the hook from here on. Release the first timer, or it\n // ends the new emphasis early and then reports an `end` for a hook already gone.\n this.#releasePending(el);\n el.setAttribute(\"data-highlight\", \"true\");\n this.dispatch(\"start\", { target: el, detail: { element: el } });\n const id = this.#timeouts.set(() => {\n this.#pending.delete(el);\n hookOwners.delete(el);\n el.removeAttribute(\"data-highlight\");\n this.dispatch(\"end\", { target: el, detail: { element: el } });\n }, this.durationValue);\n this.#pending.set(el, id);\n hookOwners.set(el, this);\n }\n\n /**\n * Releases whichever removal timer holds `el`'s hook. The row may have been\n * highlighted inside a different watched container before it moved here, and that\n * container's timer is reachable only through the shared owner registry.\n */\n #releasePending(el: HTMLElement): void {\n const owner = hookOwners.get(el);\n if (owner !== undefined && owner !== this) owner.#cancelPending(el);\n this.#cancelPending(el);\n }\n\n /** Releases `el`'s pending removal timer, if it has one. */\n #cancelPending(el: HTMLElement): void {\n // `SafeTimeout.clear` ignores an id it does not own, so the \"no pending timer\"\n // case needs no branch of its own.\n this.#timeouts.clear(this.#pending.get(el) ?? -1);\n this.#pending.delete(el);\n hookOwners.delete(el);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/reduced_motion.ts","../../src/utils/safe_timeout.ts","../../src/controllers/highlight_controller.ts"],"names":[],"mappings":"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","file":"highlight_controller.js","sourcesContent":["/**\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","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * The connection whose removal timer currently owns an element's `data-highlight`\n * hook. Shared by every instance, so a row moved from one watched container into\n * another still has exactly one timer deciding when its emphasis ends.\n *\n * Only the keys are weak: an entry keeps its controller — and through it that\n * controller's element — reachable for as long as the row is in the DOM. So an entry\n * is dropped as soon as the timer that owns it is released or fires, and `connect()`\n * drops the claims the connection before it left behind.\n */\nconst hookOwners = new WeakMap<Element, HighlightController>();\n\n/**\n * Headless \"highlight on insert\" behavior: briefly flags a freshly inserted element\n * with `data-highlight` so CSS can flash / fade it in, then removes the flag after\n * `duration` ms (no dedicated APG pattern; a purely visual emphasis that honors the\n * WCAG \"animation from interactions\" practice via `prefers-reduced-motion`).\n *\n * Markup contract (identifier: `stimeo--highlight`):\n * <!-- self-highlight: put it on the inserted row itself -->\n * <li data-controller=\"stimeo--highlight\">New item</li>\n *\n * <!-- container mode: watch for added children and highlight each -->\n * <ul data-controller=\"stimeo--highlight\" data-stimeo--highlight-observe-value=\"true\"></ul>\n *\n * In the default (self) mode the controller element is highlighted once on\n * `connect()`. With `observe`, a `MutationObserver` instead highlights every element\n * child added to the container (e.g. Turbo Stream appends). Each highlight sets\n * `data-highlight=\"true\"`, dispatches `start`, and after `duration` removes it and\n * dispatches `end` — both carry `detail.element`.\n *\n * `start` and `end` dispatch `{ element }`.\n *\n * @remarks\n * Behavior only — it ships no color or animation, just the hook (pair with CSS, and\n * with Announcer / Flash for non-visual notice). Under `prefers-reduced-motion: reduce`\n * the emphasis is suppressed entirely (the element simply appears), so no hook or\n * event is emitted. A hook never outlives the connection that set it: the observer and\n * pending timers are torn down on `disconnect()` (Turbo navigation included), and\n * `connect()` clears any hook that arrived with the DOM — a restored `turbo:before-cache`\n * snapshot, an in-page move — because the timer that would have removed it is gone.\n * An element carries at most one emphasis at a time: highlighting it again — a reorder\n * inside one container, or a move into another watched one — takes the hook over and\n * releases the timer that held it, so `duration` is measured from the latest highlight\n * and `end` fires once per emphasis.\n */\nexport class HighlightController extends Controller<HTMLElement> {\n static override values = {\n duration: { type: Number, default: 1500 },\n observe: { type: Boolean, default: false },\n };\n static events = [\"start\", \"end\"] as const;\n\n declare durationValue: number;\n declare observeValue: boolean;\n\n readonly #timeouts = new SafeTimeout();\n /**\n * The removal timer this connection has outstanding for an element. Held weakly so\n * a row that leaves the DOM is not retained, and dropped wholesale on `disconnect()`\n * so a cleared id can never be matched against a recycled one. Which connection owns\n * an element's hook is answered by the shared owner registry above.\n */\n #pending = new WeakMap<HTMLElement, number>();\n #observer: MutationObserver | null = null;\n\n override connect(): void {\n // A hook that arrived with the DOM was written by an earlier connection, whose\n // removal timer is gone; this connection owns no timer for it either, so nothing\n // would ever take it off. Drop it before anything else — in self mode\n // `#highlight()` re-adds it with a fresh timer below, except under reduced\n // motion, where the element must carry no hook at all.\n this.#clearArrivedHook(this.element);\n if (this.observeValue) {\n // A container highlights its children, never itself, so the same reasoning\n // applies to every child present before the observer starts watching.\n for (const child of this.element.children) this.#clearArrivedHook(child);\n if (typeof MutationObserver !== \"undefined\") {\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(this.element, { childList: true });\n }\n return;\n }\n this.#highlight(this.element);\n }\n\n override disconnect(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n this.#timeouts.clearAll();\n this.#pending = new WeakMap();\n }\n\n /** Drops a hook that arrived with the DOM, along with this connection's claim on it. */\n #clearArrivedHook(el: Element): void {\n // Another connection may still hold a live timer for this element, and that timer\n // has to stay reachable so the next highlight can release it. Only our own claim,\n // whose timer went down with the previous disconnect, is dropped here.\n if (hookOwners.get(el) === this) hookOwners.delete(el);\n el.removeAttribute(\"data-highlight\");\n }\n\n /** Highlights every element child added by a childList mutation. */\n #onMutations(mutations: MutationRecord[]): void {\n for (const mutation of mutations) {\n for (const node of mutation.addedNodes) {\n if (node instanceof HTMLElement) this.#highlight(node);\n }\n }\n }\n\n /** Flags `el` with `data-highlight` and schedules its removal (unless reduced-motion). */\n #highlight(el: HTMLElement): void {\n // Reduced motion: suppress the emphasis so the element just appears — no hook,\n // no timer, no events, nothing to transition.\n if (prefersReducedMotion()) return;\n\n // A second highlight of the same element — a re-insert, which is the shape a\n // reorder takes — owns the hook from here on. Release the first timer, or it\n // ends the new emphasis early and then reports an `end` for a hook already gone.\n this.#releasePending(el);\n el.setAttribute(\"data-highlight\", \"true\");\n this.dispatch(\"start\", { target: el, detail: { element: el } });\n const id = this.#timeouts.set(() => {\n this.#pending.delete(el);\n hookOwners.delete(el);\n el.removeAttribute(\"data-highlight\");\n this.dispatch(\"end\", { target: el, detail: { element: el } });\n }, this.durationValue);\n this.#pending.set(el, id);\n hookOwners.set(el, this);\n }\n\n /**\n * Releases whichever removal timer holds `el`'s hook. The row may have been\n * highlighted inside a different watched container before it moved here, and that\n * container's timer is reachable only through the shared owner registry.\n */\n #releasePending(el: HTMLElement): void {\n const owner = hookOwners.get(el);\n if (owner !== undefined && owner !== this) owner.#cancelPending(el);\n this.#cancelPending(el);\n }\n\n /** Releases `el`'s pending removal timer, if it has one. */\n #cancelPending(el: HTMLElement): void {\n // `SafeTimeout.clear` ignores an id it does not own, so the \"no pending timer\"\n // case needs no branch of its own.\n this.#timeouts.clear(this.#pending.get(el) ?? -1);\n this.#pending.delete(el);\n hookOwners.delete(el);\n }\n}\n"]}
|