stimeo-ui 0.7.0 → 0.9.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/dist/controllers/auto_submit_controller.d.ts +14 -3
  3. package/dist/controllers/auto_submit_controller.js +94 -16
  4. package/dist/controllers/auto_submit_controller.js.map +1 -1
  5. package/dist/controllers/bulk_select_controller.d.ts +62 -15
  6. package/dist/controllers/bulk_select_controller.js +139 -28
  7. package/dist/controllers/bulk_select_controller.js.map +1 -1
  8. package/dist/controllers/carousel_controller.d.ts +81 -47
  9. package/dist/controllers/carousel_controller.js +451 -100
  10. package/dist/controllers/carousel_controller.js.map +1 -1
  11. package/dist/controllers/clipboard_controller.d.ts +48 -15
  12. package/dist/controllers/clipboard_controller.js +102 -20
  13. package/dist/controllers/clipboard_controller.js.map +1 -1
  14. package/dist/controllers/color_picker_controller.d.ts +37 -6
  15. package/dist/controllers/color_picker_controller.js +180 -43
  16. package/dist/controllers/color_picker_controller.js.map +1 -1
  17. package/dist/controllers/currency_input_controller.d.ts +39 -5
  18. package/dist/controllers/currency_input_controller.js +305 -74
  19. package/dist/controllers/currency_input_controller.js.map +1 -1
  20. package/dist/controllers/data_grid_controller.d.ts +25 -9
  21. package/dist/controllers/data_grid_controller.js +150 -24
  22. package/dist/controllers/data_grid_controller.js.map +1 -1
  23. package/dist/controllers/direct_upload_controller.d.ts +3 -1
  24. package/dist/controllers/direct_upload_controller.js +12 -2
  25. package/dist/controllers/direct_upload_controller.js.map +1 -1
  26. package/dist/controllers/editable_controller.d.ts +34 -9
  27. package/dist/controllers/editable_controller.js +83 -30
  28. package/dist/controllers/editable_controller.js.map +1 -1
  29. package/dist/controllers/file_dropzone_controller.d.ts +123 -29
  30. package/dist/controllers/file_dropzone_controller.js +386 -63
  31. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  32. package/dist/controllers/filter_controller.d.ts +15 -3
  33. package/dist/controllers/filter_controller.js +32 -1
  34. package/dist/controllers/filter_controller.js.map +1 -1
  35. package/dist/controllers/flash_controller.d.ts +3 -1
  36. package/dist/controllers/flash_controller.js +3 -1
  37. package/dist/controllers/flash_controller.js.map +1 -1
  38. package/dist/controllers/frame_loading_controller.d.ts +4 -2
  39. package/dist/controllers/frame_loading_controller.js +2 -1
  40. package/dist/controllers/frame_loading_controller.js.map +1 -1
  41. package/dist/controllers/input_mask_controller.d.ts +42 -16
  42. package/dist/controllers/input_mask_controller.js +251 -76
  43. package/dist/controllers/input_mask_controller.js.map +1 -1
  44. package/dist/controllers/masonry_controller.d.ts +34 -5
  45. package/dist/controllers/masonry_controller.js +129 -17
  46. package/dist/controllers/masonry_controller.js.map +1 -1
  47. package/dist/controllers/nested_form_controller.d.ts +51 -14
  48. package/dist/controllers/nested_form_controller.js +450 -42
  49. package/dist/controllers/nested_form_controller.js.map +1 -1
  50. package/dist/controllers/otp_controller.d.ts +64 -25
  51. package/dist/controllers/otp_controller.js +485 -114
  52. package/dist/controllers/otp_controller.js.map +1 -1
  53. package/dist/controllers/reset_before_cache_controller.d.ts +25 -2
  54. package/dist/controllers/reset_before_cache_controller.js +51 -5
  55. package/dist/controllers/reset_before_cache_controller.js.map +1 -1
  56. package/dist/controllers/resizable_controller.d.ts +23 -7
  57. package/dist/controllers/resizable_controller.js +128 -55
  58. package/dist/controllers/resizable_controller.js.map +1 -1
  59. package/dist/controllers/spinner_controller.d.ts +3 -2
  60. package/dist/controllers/spinner_controller.js +7 -5
  61. package/dist/controllers/spinner_controller.js.map +1 -1
  62. package/dist/controllers/submit_once_controller.d.ts +4 -1
  63. package/dist/controllers/submit_once_controller.js +3 -1
  64. package/dist/controllers/submit_once_controller.js.map +1 -1
  65. package/dist/controllers/textarea_autosize_controller.d.ts +30 -10
  66. package/dist/controllers/textarea_autosize_controller.js +131 -3
  67. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  68. package/dist/index.js +2764 -923
  69. package/dist/index.js.map +1 -1
  70. package/dist/inspector/cli.d.ts +8 -1
  71. package/dist/inspector/cli.js +5 -1
  72. package/dist/inspector/cli.js.map +1 -1
  73. package/dist/inspector/cli_bin.js +5 -1
  74. package/dist/inspector/cli_bin.js.map +1 -1
  75. package/dist/inspector/examples.json +24 -24
  76. package/dist/inspector/manifest.json +193 -30
  77. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/default_attribute.ts","../../src/utils/detach_gate.ts","../../src/utils/min_duration_floor.ts","../../src/utils/safe_timeout.ts","../../src/controllers/spinner_controller.ts"],"names":[],"mappings":"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","file":"spinner_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 * 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 * 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 { setDefaultAttribute } from \"../utils/default_attribute\";\nimport { DetachGate } from \"../utils/detach_gate\";\nimport { MinDurationFloor } from \"../utils/min_duration_floor\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless loading-indicator behavior built on the `aria-busy` practice (no\n * dedicated APG pattern).\n *\n * Markup contract (identifier: `stimeo--spinner`):\n * <div data-controller=\"stimeo--spinner\"\n * data-stimeo--spinner-delay-value=\"150\"\n * data-stimeo--spinner-min-duration-value=\"500\"\n * data-stimeo--spinner-timeout-value=\"0\"\n * data-stimeo--spinner-announce-text-value=\"Loading…\"\n * data-stimeo--spinner-announce-ready-text-value=\"Loading finished.\"\n * data-action=\"loading:start->stimeo--spinner#start\n * loading:stop->stimeo--spinner#stop\">\n * <div hidden data-stimeo--spinner-target=\"indicator\">\n * <span data-stimeo--spinner-target=\"message\">Loading…</span>\n * </div>\n * <div aria-busy=\"false\" data-stimeo--spinner-target=\"region\"></div>\n * </div>\n *\n * The indicator is the visual half — its text plus a spinner the consumer marks\n * `aria-hidden=\"true\"` — and the controlled `region` mirrors the busy state via\n * `aria-busy`. Assistive tech hears the transition through `announceText` /\n * `announceReadyText`, which go to the page's announcer: an indicator that only\n * becomes visible at the moment of the change is not reliably read, and giving it\n * live-region semantics on top of the announcement would say it twice. Two timers\n * tame flicker: `delay` suppresses the spinner for fast operations, and\n * `minDuration` keeps it visible long enough to be perceived once shown.\n * `timeout` is the opt-in safety net for the case the consumer's `stop` never\n * arrives.\n *\n * Events (all with an empty `detail`):\n * - `stimeo--spinner:show` — the indicator became visible, after `delay`.\n * - `stimeo--spinner:hide` — the indicator went away, after `minDuration`.\n * - `stimeo--spinner:timeout` — `timeout` elapsed with the load still running;\n * the controller then ends it exactly as `stop` would, so a `hide` follows.\n *\n * `hide`, `show` and `timeout` dispatch `{}`.\n *\n * @remarks\n * Behavior only — the visual spinner is the consumer's, alongside the text and\n * `aria-hidden=\"true\"`. Both timers are owned by {@link SafeTimeout}, kept across\n * an in-page move and dropped on a real detach via {@link DetachGate}, while the\n * loading state a cached page would freeze is rewound by {@link BeforeCacheReset}.\n */\nexport class SpinnerController extends Controller<HTMLElement> {\n static override targets = [\"indicator\", \"region\", \"message\"];\n static override values = {\n announceText: { type: String, default: \"\" },\n announceReadyText: { type: String, default: \"\" },\n delay: { type: Number, default: 0 },\n minDuration: { type: Number, default: 0 },\n timeout: { type: Number, default: 0 },\n };\n static actions = [\"start\", \"stop\"] as const;\n static events = [\"hide\", \"show\", \"timeout\"] as const;\n\n declare readonly indicatorTarget: HTMLElement;\n declare readonly regionTarget: HTMLElement;\n declare readonly messageTarget: HTMLElement;\n declare readonly hasIndicatorTarget: boolean;\n declare readonly hasRegionTarget: boolean;\n declare readonly hasMessageTarget: boolean;\n\n declare delayValue: number;\n declare minDurationValue: number;\n declare timeoutValue: number;\n declare announceTextValue: string;\n declare announceReadyTextValue: string;\n\n readonly #timers = new SafeTimeout();\n readonly #floor = new MinDurationFloor(this.#timers);\n readonly #gate = new DetachGate();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /** Pending show-delay timer id, or `null` when no start is awaiting its delay. */\n #delayTimerId: number | null = null;\n /** Pending safety-net timer id, or `null` when `timeout` is off or not armed. */\n #timeoutTimerId: number | null = null;\n\n override connect(): void {\n this.#gate.cancel();\n this.#beforeCache.activate();\n if (this.#state === \"pending\" && this.#delayTimerId === null) {\n // `pending` lives exactly as long as its show-delay timer. Reading it back with\n // no timer of this instance behind it means the markup outlived the timer — a\n // restored snapshot, or an element re-attached too late to count as a move — so\n // nothing is left to advance it and `start()` would refuse every later load.\n // Fall back to idle, dropping the busy flag the interrupted `start()` set.\n this.#setBusy(false);\n this.element.setAttribute(\"data-state\", \"idle\");\n return;\n }\n setDefaultAttribute(this.element, \"data-state\", \"idle\");\n }\n\n /**\n * Re-applies the current phase to an indicator that arrived after `connect()`.\n *\n * A Turbo Stream can swap the indicator for a fresh node mid-load, and that node\n * carries the markup contract's `hidden`. Without this the spinner would vanish\n * while `data-state` still says `loading`, and nothing but the next cycle would\n * bring it back.\n */\n indicatorTargetConnected(target: HTMLElement): void {\n target.hidden = this.#state !== \"loading\";\n }\n\n override disconnect(): void {\n // Symmetric with `connect()` regardless of why the disconnect came: an in-page\n // move re-subscribes, and only the timers are held back for the reconnect.\n this.#beforeCache.deactivate();\n this.#gate.disconnected(this, () => this.#teardown());\n }\n\n /** Begins loading. Honors `delay` before the spinner actually appears. */\n start(): void {\n if (this.#state === \"loading\") {\n // Already shown (possibly waiting out `minDuration` after a stop): a quick\n // stop→start within that window must keep the spinner visible. Restore the\n // busy state and cancel the pending hide instead of returning a no-op, which\n // would let the stale hide fire and flicker the spinner away mid-load.\n this.#setBusy(true);\n this.#floor.cancel();\n this.#armTimeout();\n return;\n }\n if (this.#state !== \"idle\") return;\n this.#setBusy(true);\n // A hide held back from a previous cycle is now stale.\n this.#floor.cancel();\n this.#armTimeout();\n if (this.delayValue > 0) {\n this.element.setAttribute(\"data-state\", \"pending\");\n this.#delayTimerId = this.#timers.set(() => {\n this.#delayTimerId = null;\n this.#show();\n }, this.delayValue);\n } else {\n this.#show();\n }\n }\n\n /** Ends loading. Honors `minDuration` so a shown spinner does not flicker. */\n stop(): void {\n const state = this.#state;\n this.#cancelTimeout();\n if (state === \"pending\") {\n // The delay never elapsed — the spinner never appeared, so just cancel.\n this.#cancelDelay();\n this.#setBusy(false);\n this.element.setAttribute(\"data-state\", \"idle\");\n return;\n }\n if (state !== \"loading\") return;\n\n this.#setBusy(false);\n this.#floor.schedule(this.minDurationValue, () => this.#hide());\n }\n\n /** Reveals the indicator, marks the moment shown, and announces via the live region. */\n #show(): void {\n this.#floor.begin();\n // A visible spinner always means a busy region. Re-asserting it costs nothing on\n // the direct path and closes the one where a snapshot rewind cleared the flag\n // while the show-delay timer it deliberately spared was still armed.\n this.#setBusy(true);\n if (this.hasIndicatorTarget) this.indicatorTarget.hidden = false;\n this.element.setAttribute(\"data-state\", \"loading\");\n this.dispatch(\"show\", { detail: {} });\n // loading ↔ ready is the transition worth reading; the spinner itself is\n // visual and carries no words.\n announce(fillTemplate(this.announceTextValue, {}));\n }\n\n /** Hides the indicator and returns to the idle state. */\n #hide(): void {\n if (this.hasIndicatorTarget) this.indicatorTarget.hidden = true;\n this.element.setAttribute(\"data-state\", \"idle\");\n this.dispatch(\"hide\", { detail: {} });\n announce(fillTemplate(this.announceReadyTextValue, {}));\n }\n\n /**\n * Drops both timers on a real detach. The markup keeps whatever it last held: an\n * element on its way out of the document has no reader left, and one whose\n * `data-controller` dropped the identifier no longer resolves its own targets, so\n * the rollback could only ever be partial. The snapshot is rewound where it is\n * still whole, on `turbo:before-cache`.\n */\n #teardown(): void {\n this.#gate.cancel();\n this.#timers.clearAll();\n this.#delayTimerId = null;\n this.#timeoutTimerId = null;\n this.#floor.cancel();\n }\n\n /**\n * Returns the loading state to idle for the snapshot Turbo is about to take,\n * so a page reached with the Back button is not restored mid-load with a\n * spinner nothing can stop. State only: `data-state`, the indicator's `hidden`,\n * and `aria-busy`. No `hide` is dispatched — the load was never observed to\n * finish, and a snapshot rewind is not a lifecycle event the consumer can act\n * on. The live page keeps its timers, so a navigation that never completes\n * leaves the running cycle intact.\n */\n #rewindForCache(): void {\n this.#cancelTimeout();\n this.#setBusy(false);\n if (this.hasIndicatorTarget) this.indicatorTarget.hidden = true;\n this.element.setAttribute(\"data-state\", \"idle\");\n }\n\n /** Reflects busy state onto the controlled region (if present). */\n #setBusy(busy: boolean): void {\n if (this.hasRegionTarget) {\n this.regionTarget.setAttribute(\"aria-busy\", String(busy));\n }\n }\n\n /**\n * Arms the safety net so a `stop` that never arrives cannot strand the spinner.\n * Off by default: the consumer owns the async work, so only it knows whether a\n * ceiling makes sense. Re-arming on a restart measures from the newest start.\n */\n #armTimeout(): void {\n this.#cancelTimeout();\n if (this.timeoutValue <= 0) return;\n this.#timeoutTimerId = this.#timers.set(() => {\n this.#timeoutTimerId = null;\n this.dispatch(\"timeout\", { detail: {} });\n this.stop();\n }, this.timeoutValue);\n }\n\n #cancelTimeout(): void {\n if (this.#timeoutTimerId !== null) {\n this.#timers.clear(this.#timeoutTimerId);\n this.#timeoutTimerId = null;\n }\n }\n\n #cancelDelay(): void {\n if (this.#delayTimerId !== null) {\n this.#timers.clear(this.#delayTimerId);\n this.#delayTimerId = null;\n }\n }\n\n /** Current lifecycle phase as reflected on `data-state`. */\n get #state(): string {\n return this.element.getAttribute(\"data-state\") ?? \"idle\";\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/default_attribute.ts","../../src/utils/detach_gate.ts","../../src/utils/min_duration_floor.ts","../../src/utils/safe_timeout.ts","../../src/controllers/spinner_controller.ts"],"names":[],"mappings":"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","file":"spinner_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 * 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 * 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 { setDefaultAttribute } from \"../utils/default_attribute\";\nimport { DetachGate } from \"../utils/detach_gate\";\nimport { MinDurationFloor } from \"../utils/min_duration_floor\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless loading-indicator behavior built on the `aria-busy` practice (no\n * dedicated APG pattern).\n *\n * Markup contract (identifier: `stimeo--spinner`):\n * <div data-controller=\"stimeo--spinner\"\n * data-stimeo--spinner-delay-value=\"150\"\n * data-stimeo--spinner-min-duration-value=\"500\"\n * data-stimeo--spinner-timeout-value=\"0\"\n * data-stimeo--spinner-announce-text-value=\"Loading…\"\n * data-stimeo--spinner-announce-ready-text-value=\"Loading finished.\"\n * data-action=\"loading:start->stimeo--spinner#start\n * loading:stop->stimeo--spinner#stop\">\n * <div hidden data-stimeo--spinner-target=\"indicator\">\n * <span data-stimeo--spinner-target=\"message\">Loading…</span>\n * </div>\n * <div aria-busy=\"false\" data-stimeo--spinner-target=\"region\"></div>\n * </div>\n *\n * The indicator is the visual half — its text plus a spinner the consumer marks\n * `aria-hidden=\"true\"` — and the controlled `region` mirrors the busy state via\n * `aria-busy`. Assistive tech hears the transition through `announceText` /\n * `announceReadyText`, which go to the page's announcer: an indicator that only\n * becomes visible at the moment of the change is not reliably read, and giving it\n * live-region semantics on top of the announcement would say it twice. Two timers\n * tame flicker: `delay` suppresses the spinner for fast operations, and\n * `minDuration` keeps it visible long enough to be perceived once shown.\n * `timeout` is the opt-in safety net for the case the consumer's `stop` never\n * arrives.\n *\n * Events (all with an empty `detail`):\n * - `stimeo--spinner:show` — the indicator became visible, after `delay`.\n * - `stimeo--spinner:hide` — the indicator went away, after `minDuration`.\n * - `stimeo--spinner:timeout` — `timeout` elapsed with the load still running;\n * the controller then ends it exactly as `stop` would, so a `hide` follows.\n *\n * `hide`, `show`, `timeout`, and `reconcile` dispatch `{}`. The last of those\n * reports that the Turbo cache rewind returned a running cycle to idle.\n *\n * @remarks\n * Behavior only — the visual spinner is the consumer's, alongside the text and\n * `aria-hidden=\"true\"`. Both timers are owned by {@link SafeTimeout}, kept across\n * an in-page move and dropped on a real detach via {@link DetachGate}, while the\n * loading state a cached page would freeze is rewound by {@link BeforeCacheReset}.\n */\nexport class SpinnerController extends Controller<HTMLElement> {\n static override targets = [\"indicator\", \"region\", \"message\"];\n static override values = {\n announceText: { type: String, default: \"\" },\n announceReadyText: { type: String, default: \"\" },\n delay: { type: Number, default: 0 },\n minDuration: { type: Number, default: 0 },\n timeout: { type: Number, default: 0 },\n };\n static actions = [\"start\", \"stop\"] as const;\n static events = [\"hide\", \"show\", \"timeout\", \"reconcile\"] as const;\n\n declare readonly indicatorTarget: HTMLElement;\n declare readonly regionTarget: HTMLElement;\n declare readonly messageTarget: HTMLElement;\n declare readonly hasIndicatorTarget: boolean;\n declare readonly hasRegionTarget: boolean;\n declare readonly hasMessageTarget: boolean;\n\n declare delayValue: number;\n declare minDurationValue: number;\n declare timeoutValue: number;\n declare announceTextValue: string;\n declare announceReadyTextValue: string;\n\n readonly #timers = new SafeTimeout();\n readonly #floor = new MinDurationFloor(this.#timers);\n readonly #gate = new DetachGate();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /** Pending show-delay timer id, or `null` when no start is awaiting its delay. */\n #delayTimerId: number | null = null;\n /** Pending safety-net timer id, or `null` when `timeout` is off or not armed. */\n #timeoutTimerId: number | null = null;\n\n override connect(): void {\n this.#gate.cancel();\n this.#beforeCache.activate();\n if (this.#state === \"pending\" && this.#delayTimerId === null) {\n // `pending` lives exactly as long as its show-delay timer. Reading it back with\n // no timer of this instance behind it means the markup outlived the timer — a\n // restored snapshot, or an element re-attached too late to count as a move — so\n // nothing is left to advance it and `start()` would refuse every later load.\n // Fall back to idle, dropping the busy flag the interrupted `start()` set.\n this.#setBusy(false);\n this.element.setAttribute(\"data-state\", \"idle\");\n return;\n }\n setDefaultAttribute(this.element, \"data-state\", \"idle\");\n }\n\n /**\n * Re-applies the current phase to an indicator that arrived after `connect()`.\n *\n * A Turbo Stream can swap the indicator for a fresh node mid-load, and that node\n * carries the markup contract's `hidden`. Without this the spinner would vanish\n * while `data-state` still says `loading`, and nothing but the next cycle would\n * bring it back.\n */\n indicatorTargetConnected(target: HTMLElement): void {\n target.hidden = this.#state !== \"loading\";\n }\n\n override disconnect(): void {\n // Symmetric with `connect()` regardless of why the disconnect came: an in-page\n // move re-subscribes, and only the timers are held back for the reconnect.\n this.#beforeCache.deactivate();\n this.#gate.disconnected(this, () => this.#teardown());\n }\n\n /** Begins loading. Honors `delay` before the spinner actually appears. */\n start(): void {\n if (this.#state === \"loading\") {\n // Already shown (possibly waiting out `minDuration` after a stop): a quick\n // stop→start within that window must keep the spinner visible. Restore the\n // busy state and cancel the pending hide instead of returning a no-op, which\n // would let the stale hide fire and flicker the spinner away mid-load.\n this.#setBusy(true);\n this.#floor.cancel();\n this.#armTimeout();\n return;\n }\n if (this.#state !== \"idle\") return;\n this.#setBusy(true);\n // A hide held back from a previous cycle is now stale.\n this.#floor.cancel();\n this.#armTimeout();\n if (this.delayValue > 0) {\n this.element.setAttribute(\"data-state\", \"pending\");\n this.#delayTimerId = this.#timers.set(() => {\n this.#delayTimerId = null;\n this.#show();\n }, this.delayValue);\n } else {\n this.#show();\n }\n }\n\n /** Ends loading. Honors `minDuration` so a shown spinner does not flicker. */\n stop(): void {\n const state = this.#state;\n this.#cancelTimeout();\n if (state === \"pending\") {\n // The delay never elapsed — the spinner never appeared, so just cancel.\n this.#cancelDelay();\n this.#setBusy(false);\n this.element.setAttribute(\"data-state\", \"idle\");\n return;\n }\n if (state !== \"loading\") return;\n\n this.#setBusy(false);\n this.#floor.schedule(this.minDurationValue, () => this.#hide());\n }\n\n /** Reveals the indicator, marks the moment shown, and announces via the live region. */\n #show(): void {\n this.#floor.begin();\n // A visible spinner always means a busy region. Re-asserting it costs nothing on\n // the direct path and closes the one where a snapshot rewind cleared the flag\n // while the show-delay timer it deliberately spared was still armed.\n this.#setBusy(true);\n if (this.hasIndicatorTarget) this.indicatorTarget.hidden = false;\n this.element.setAttribute(\"data-state\", \"loading\");\n this.dispatch(\"show\", { detail: {} });\n // loading ↔ ready is the transition worth reading; the spinner itself is\n // visual and carries no words.\n announce(fillTemplate(this.announceTextValue, {}));\n }\n\n /** Hides the indicator and returns to the idle state. */\n #hide(): void {\n if (this.hasIndicatorTarget) this.indicatorTarget.hidden = true;\n this.element.setAttribute(\"data-state\", \"idle\");\n this.dispatch(\"hide\", { detail: {} });\n announce(fillTemplate(this.announceReadyTextValue, {}));\n }\n\n /**\n * Drops both timers on a real detach. The markup keeps whatever it last held: an\n * element on its way out of the document has no reader left, and one whose\n * `data-controller` dropped the identifier no longer resolves its own targets, so\n * the rollback could only ever be partial. The snapshot is rewound where it is\n * still whole, on `turbo:before-cache`.\n */\n #teardown(): void {\n this.#gate.cancel();\n this.#timers.clearAll();\n this.#delayTimerId = null;\n this.#timeoutTimerId = null;\n this.#floor.cancel();\n }\n\n /**\n * Returns the loading state to idle for the snapshot Turbo is about to take,\n * so a page reached with the Back button is not restored mid-load with a\n * spinner nothing can stop. State only: `data-state`, the indicator's `hidden`,\n * and `aria-busy`. No `hide` is dispatched — that would claim the load finished.\n * The rewind reports itself as `reconcile` instead, so a consumer painting from\n * `show` can drop it before the snapshot is taken. The live page keeps its\n * timers, so a navigation that never completes leaves the running cycle intact.\n */\n #rewindForCache(): void {\n const running = this.element.getAttribute(\"data-state\") !== \"idle\";\n this.#cancelTimeout();\n this.#setBusy(false);\n if (this.hasIndicatorTarget) this.indicatorTarget.hidden = true;\n this.element.setAttribute(\"data-state\", \"idle\");\n if (running) this.dispatch(\"reconcile\", { detail: {} });\n }\n\n /** Reflects busy state onto the controlled region (if present). */\n #setBusy(busy: boolean): void {\n if (this.hasRegionTarget) {\n this.regionTarget.setAttribute(\"aria-busy\", String(busy));\n }\n }\n\n /**\n * Arms the safety net so a `stop` that never arrives cannot strand the spinner.\n * Off by default: the consumer owns the async work, so only it knows whether a\n * ceiling makes sense. Re-arming on a restart measures from the newest start.\n */\n #armTimeout(): void {\n this.#cancelTimeout();\n if (this.timeoutValue <= 0) return;\n this.#timeoutTimerId = this.#timers.set(() => {\n this.#timeoutTimerId = null;\n this.dispatch(\"timeout\", { detail: {} });\n this.stop();\n }, this.timeoutValue);\n }\n\n #cancelTimeout(): void {\n if (this.#timeoutTimerId !== null) {\n this.#timers.clear(this.#timeoutTimerId);\n this.#timeoutTimerId = null;\n }\n }\n\n #cancelDelay(): void {\n if (this.#delayTimerId !== null) {\n this.#timers.clear(this.#delayTimerId);\n this.#delayTimerId = null;\n }\n }\n\n /** Current lifecycle phase as reflected on `data-state`. */\n get #state(): string {\n return this.element.getAttribute(\"data-state\") ?? \"idle\";\n }\n}\n"]}
@@ -48,6 +48,9 @@ type SubmitControl = HTMLButtonElement | HTMLInputElement;
48
48
  *
49
49
  * Events:
50
50
  * - `stimeo--submit-once:start` — `{ form, submitter }`.
51
+ * - `stimeo--submit-once:reconcile` — `{ forms: HTMLFormElement[] }`, the forms
52
+ * whose in-flight submission the Turbo cache rewind abandoned. `end` would
53
+ * claim the submission resolved, so the rewind reports itself instead.
51
54
  * - `stimeo--submit-once:end` — `{ form, submitter, reason, success? }`, where
52
55
  * `reason` is `"turbo"`, `"timeout"`, `"manual"`, or `"canceled"`. Only
53
56
  * `"canceled"` skips the completion announcement, because no operation ran.
@@ -88,7 +91,7 @@ declare class SubmitOnceController extends Controller<HTMLElement> {
88
91
  };
89
92
  };
90
93
  static actions: readonly ["cancel", "finish", "start"];
91
- static events: readonly ["start", "end"];
94
+ static events: readonly ["start", "end", "reconcile"];
92
95
  readonly submitTargets: SubmitControl[];
93
96
  readonly idleTargets: HTMLElement[];
94
97
  readonly busyTargets: HTMLElement[];
@@ -203,7 +203,7 @@ var SubmitOnceController = class extends Controller {
203
203
  restoreFocus: { type: Boolean, default: false }
204
204
  };
205
205
  static actions = ["cancel", "finish", "start"];
206
- static events = ["start", "end"];
206
+ static events = ["start", "end", "reconcile"];
207
207
  #timers = new SafeTimeout();
208
208
  #gate = new DetachGate();
209
209
  #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
@@ -419,7 +419,9 @@ var SubmitOnceController = class extends Controller {
419
419
  }
420
420
  /** Keeps Turbo's cached snapshot idle, without events, announcements, or focus moves. */
421
421
  #rewindForCache() {
422
+ const forms = [...this.#sessions.keys()];
422
423
  this.#abandonSessions();
424
+ if (forms.length > 0) this.dispatch("reconcile", { detail: { forms } });
423
425
  }
424
426
  /** Tears down a true detach; an in-page move is canceled by the next `connect()`. */
425
427
  #teardown() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/attribute_lease.ts","../../src/utils/before_cache_reset.ts","../../src/utils/detach_gate.ts","../../src/utils/safe_timeout.ts","../../src/controllers/submit_once_controller.ts"],"names":[],"mappings":"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","file":"submit_once_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/** One temporarily controlled attribute value and the authored value it displaced. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n written: string | null;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events.\n * A consumer that must not let a derived value become the authored baseline of a\n * cached page returns its leases from its own `turbo:before-cache` rewind, where the\n * matching `disconnect()` also releases the subscription.\n */\nexport class AttributeLease<T extends Element = Element> {\n readonly #attribute: string;\n readonly #records = new Map<T, AttributeLeaseRecord>();\n\n /** @param attribute - The attribute whose temporary values this lease owns. */\n constructor(attribute: string) {\n this.#attribute = attribute;\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const existing = this.#records.get(element);\n if (existing) {\n existing.written = value;\n } else {\n this.#records.set(element, {\n original: element.getAttribute(this.#attribute),\n written: value,\n });\n }\n\n this.#reflect(element, value);\n }\n\n /** Returns one lease without overwriting a value subsequently authored by a consumer. */\n return(element: T): void {\n const record = this.#records.get(element);\n if (!record) return;\n this.#records.delete(element);\n const stillOwned = element.getAttribute(this.#attribute) === record.written;\n if (stillOwned) this.#reflect(element, record.original);\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n if (element.getAttribute(this.#attribute) === value) return;\n if (value === null) element.removeAttribute(this.#attribute);\n else element.setAttribute(this.#attribute, value);\n }\n\n /** Returns every outstanding lease using the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#records.keys())) this.return(element);\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (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","/**\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 } from \"../utils/announce\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { DetachGate } from \"../utils/detach_gate\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Native controls that can submit a form. */\ntype SubmitControl = HTMLButtonElement | HTMLInputElement;\n\n/** Why an active submission returned to idle. */\ntype CompletionReason = \"canceled\" | \"manual\" | \"timeout\" | \"turbo\";\n\n/** A text/value write that cannot be represented by an {@link AttributeLease}. */\ntype LabelWrite =\n | {\n readonly channel: \"text\";\n readonly control: HTMLButtonElement;\n readonly original: string;\n readonly written: string;\n }\n | {\n readonly channel: \"value\";\n readonly control: HTMLInputElement;\n readonly original: string;\n readonly written: string;\n };\n\n/** One form's independent in-flight submission. */\ninterface SubmissionSession {\n readonly form: HTMLFormElement;\n readonly submitter: SubmitControl | null;\n readonly submitterHadFocus: boolean;\n readonly controls: Set<SubmitControl>;\n timeoutId: number | null;\n labelWrite: LabelWrite | null;\n idlePart: HTMLElement | null;\n busyPart: HTMLElement | null;\n}\n\n/** Per-button override for the controller's default busy label. */\nconst BUTTON_BUSY_LABEL = \"data-submit-once-busy-label\";\n\n/**\n * Headless form-scoped double-submit guard (identifier:\n * `stimeo--submit-once`). It disables every native submit control associated\n * with the submitting form, publishes busy state, and restores only the state\n * it still owns when the operation finishes.\n *\n * Turbo forms need no `data-action`: the controller observes\n * `turbo:submit-start` and `turbo:submit-end` itself.\n *\n * A non-Turbo async form owns its own lifecycle and declares each transition:\n * `submit->stimeo--submit-once#start` applies the busy state synchronously,\n * before the request is issued, and `#finish` / `#cancel` end it. Cancelling the\n * event's default is how such a form suppresses the native navigation, so it\n * never means the submission died — only `#cancel` does. A submit already\n * cancelled by an earlier listener never starts a session at all.\n *\n * ```html\n * <form data-controller=\"stimeo--submit-once\"\n * data-action=\"submit->stimeo--submit-once#start\n * save:complete->stimeo--submit-once#finish\n * save:error->stimeo--submit-once#cancel\">\n * <button type=\"submit\">Save</button>\n * </form>\n * ```\n *\n * A form whose submit a later listener vetoes needs `#cancel` wired to that\n * veto, or a non-zero `timeout`, to leave the busy state.\n *\n * Plain text buttons can use `busyLabel` (overridden by\n * `data-submit-once-busy-label`). A button with authored structure instead uses\n * an `idle` / `busy` target pair inside that button, so icons and descendants\n * are never replaced with `textContent`:\n *\n * ```html\n * <form data-controller=\"stimeo--submit-once\"\n * data-stimeo--submit-once-announce-text-value=\"Saving…\"\n * data-stimeo--submit-once-announce-ready-text-value=\"Saved.\">\n * <button type=\"submit\" data-stimeo--submit-once-target=\"submit\">\n * <span data-stimeo--submit-once-target=\"idle\">Save</span>\n * <span data-stimeo--submit-once-target=\"busy\" hidden>Saving…</span>\n * </button>\n * </form>\n * ```\n *\n * Events:\n * - `stimeo--submit-once:start` — `{ form, submitter }`.\n * - `stimeo--submit-once:end` — `{ form, submitter, reason, success? }`, where\n * `reason` is `\"turbo\"`, `\"timeout\"`, `\"manual\"`, or `\"canceled\"`. Only\n * `\"canceled\"` skips the completion announcement, because no operation ran.\n *\n * @remarks\n * Behavior only: the consumer owns every visual, label, and announcement\n * string. `announceText` / `announceReadyText` are empty by default and speak\n * through the page's shared `stimeo--announcer` only on real state transitions.\n * Attribute writes use {@link AttributeLease}, so a consumer mutation made\n * while busy wins over restoration. {@link DetachGate} preserves an in-flight\n * session across an in-page move, while {@link BeforeCacheReset} rewinds the\n * snapshot before Turbo caches it. Cache/detach rewinds are silent and never\n * move focus.\n */\nexport class SubmitOnceController extends Controller<HTMLElement> {\n static override targets = [\"submit\", \"idle\", \"busy\"];\n static override values = {\n announceText: { type: String, default: \"\" },\n announceReadyText: { type: String, default: \"\" },\n busyLabel: { type: String, default: \"\" },\n timeout: { type: Number, default: 0 },\n restoreFocus: { type: Boolean, default: false },\n };\n static actions = [\"cancel\", \"finish\", \"start\"] as const;\n static events = [\"start\", \"end\"] as const;\n\n declare readonly submitTargets: SubmitControl[];\n declare readonly idleTargets: HTMLElement[];\n declare readonly busyTargets: HTMLElement[];\n\n declare announceTextValue: string;\n declare announceReadyTextValue: string;\n declare busyLabelValue: string;\n declare timeoutValue: number;\n declare restoreFocusValue: boolean;\n\n readonly #timers = new SafeTimeout();\n readonly #gate = new DetachGate();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n readonly #sessions = new Map<HTMLFormElement, SubmissionSession>();\n\n readonly #disabled = new AttributeLease<SubmitControl>(\"disabled\");\n readonly #controlBusy = new AttributeLease<SubmitControl>(\"aria-busy\");\n readonly #formBusy = new AttributeLease<HTMLFormElement>(\"aria-busy\");\n readonly #submitting = new AttributeLease<HTMLFormElement>(\"data-submitting\");\n readonly #ariaLabel = new AttributeLease<HTMLButtonElement>(\"aria-label\");\n readonly #hidden = new AttributeLease<HTMLElement>(\"hidden\");\n\n readonly #onSubmitStart = (event: Event): void => {\n this.start(event);\n };\n\n readonly #onSubmitEnd = (event: Event): void => {\n this.#complete(this.#eventForm(event), \"turbo\", this.#eventSuccess(event));\n };\n\n /** Blocks a later native submit while one for the same form is still active. */\n readonly #onNativeSubmit = (event: Event): void => {\n // The submit event's target is its form. This listener runs in capture so it\n // always observes that first event before a public `start` action creates the\n // session, while a later event sees the session and is canceled here.\n const session = this.#sessionFor(event.target);\n if (!session) return;\n this.#syncControls(session);\n event.preventDefault();\n };\n\n override connect(): void {\n this.#gate.cancel();\n this.#beforeCache.activate();\n this.element.addEventListener(\"submit\", this.#onNativeSubmit, true);\n this.element.addEventListener(\"turbo:submit-start\", this.#onSubmitStart);\n this.element.addEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"submit\", this.#onNativeSubmit, true);\n this.element.removeEventListener(\"turbo:submit-start\", this.#onSubmitStart);\n this.element.removeEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n this.#beforeCache.deactivate();\n this.#gate.disconnected(this, () => this.#teardown());\n }\n\n /**\n * Begins the submission represented by `event`.\n *\n * Native `submit` events are intended for non-Turbo async forms. Turbo calls\n * this path through its auto-subscribed `turbo:submit-start` event. An event\n * whose default an earlier listener already cancelled carries no submission,\n * so it starts nothing.\n */\n start(event: Event): void {\n if (event.defaultPrevented) return;\n const form = this.#eventForm(event);\n if (!form || this.#sessions.has(form)) return;\n\n const controls = this.#submitControls(form);\n const submitter = this.#resolveSubmitter(event, controls);\n const session: SubmissionSession = {\n form,\n submitter,\n submitterHadFocus: submitter !== null && document.activeElement === submitter,\n controls: new Set(),\n timeoutId: null,\n labelWrite: null,\n idlePart: null,\n busyPart: null,\n };\n this.#sessions.set(form, session);\n\n this.#formBusy.write(form, \"true\");\n this.#submitting.write(form, \"true\");\n this.#syncControls(session, controls);\n this.#enterLabel(session);\n this.dispatch(\"start\", { detail: { form, submitter } });\n\n if (this.timeoutValue > 0) {\n session.timeoutId = this.#timers.set(\n () => this.#complete(form, \"timeout\", false),\n this.timeoutValue,\n );\n }\n\n announce(this.announceTextValue);\n }\n\n /**\n * Completes a non-Turbo async submission for the form associated with\n * `event`. When called directly with no event, it completes the controller's\n * sole active form (or the controller element itself when it is a form).\n */\n finish(event?: Event): void {\n const form = event ? this.#eventForm(event) : this.#defaultForm();\n this.#complete(form, \"manual\", event ? this.#eventSuccess(event) : undefined);\n }\n\n /**\n * Abandons the submission for the form associated with `event` because it\n * never reached the network — a veto from validation, or a request the\n * consumer gave up before sending. With no event it targets the controller's\n * sole active form, exactly like {@link finish}.\n *\n * The distinction from `finish` is observable: a request that ran and failed\n * is `finish` with `success: false` and still announces completion, while a\n * submission that never happened announces nothing.\n */\n cancel(event?: Event): void {\n const form = event ? this.#eventForm(event) : this.#defaultForm();\n this.#complete(form, \"canceled\", false);\n }\n\n /** Disables a newly connected explicit submit target during its form's session. */\n submitTargetConnected(target: SubmitControl): void {\n const session = this.#sessionFor(target.form);\n if (!session || !this.#isNativeSubmitControl(target)) return;\n this.#enterControl(session, target);\n }\n\n /** Ends one session, restores owned state, and reports the observable completion. */\n #complete(form: HTMLFormElement | null, reason: CompletionReason, success?: boolean): void {\n const session = this.#sessionFor(form);\n if (!session) return;\n this.#sessions.delete(session.form);\n this.#returnSessionState(session);\n\n const detail =\n success === undefined\n ? { form: session.form, submitter: session.submitter, reason }\n : { form: session.form, submitter: session.submitter, reason, success };\n this.dispatch(\"end\", { detail });\n this.#restoreFocus(session);\n if (reason !== \"canceled\") announce(this.announceReadyTextValue);\n }\n\n /** Applies the busy state to every current native/explicit control exactly once. */\n #syncControls(session: SubmissionSession, controls = this.#submitControls(session.form)): void {\n for (const control of controls) this.#enterControl(session, control);\n }\n\n /** Temporarily disables one enabled submit control while retaining authored state. */\n #enterControl(session: SubmissionSession, control: SubmitControl): void {\n if (session.controls.has(control) || control.matches(\":disabled\")) return;\n session.controls.add(control);\n this.#disabled.write(control, \"\");\n this.#controlBusy.write(control, \"true\");\n }\n\n /** Swaps only the triggering control's safe label channel. */\n #enterLabel(session: SubmissionSession): void {\n const control = session.submitter;\n if (!control) return;\n\n const idlePart = this.#partInside(control, this.idleTargets);\n const busyPart = this.#partInside(control, this.busyTargets);\n if (idlePart && busyPart) {\n session.idlePart = idlePart;\n session.busyPart = busyPart;\n this.#hidden.write(idlePart, \"\");\n this.#hidden.write(busyPart, null);\n }\n\n const label = control.getAttribute(BUTTON_BUSY_LABEL) ?? this.busyLabelValue;\n if (label.length === 0) return;\n if (control instanceof HTMLInputElement) {\n // `input[type=image]#value` is submitted data, not a visible label.\n if (control.type !== \"submit\") return;\n session.labelWrite = {\n channel: \"value\",\n control,\n original: control.value,\n written: label,\n };\n control.value = label;\n return;\n }\n if (control.hasAttribute(\"aria-label\")) {\n this.#ariaLabel.write(control, label);\n return;\n }\n // A structured button needs the explicit idle/busy pair above. Replacing its\n // textContent would destroy icons, counters, and application-owned listeners.\n if (control.childElementCount > 0) return;\n session.labelWrite = {\n channel: \"text\",\n control,\n original: control.textContent ?? \"\",\n written: label,\n };\n control.textContent = label;\n }\n\n /** Returns labels, controls, and form hooks, respecting intervening consumer writes. */\n #returnSessionState(session: SubmissionSession): void {\n if (session.timeoutId !== null) {\n this.#timers.clear(session.timeoutId);\n session.timeoutId = null;\n }\n\n if (session.labelWrite?.channel === \"value\") {\n const { control, original, written } = session.labelWrite;\n if (control.value === written) control.value = original;\n } else if (session.labelWrite?.channel === \"text\") {\n const { control, original, written } = session.labelWrite;\n if (control.childElementCount === 0 && control.textContent === written) {\n control.textContent = original;\n }\n }\n session.labelWrite = null;\n\n if (session.submitter instanceof HTMLButtonElement) {\n this.#ariaLabel.return(session.submitter);\n }\n if (session.idlePart) this.#hidden.return(session.idlePart);\n if (session.busyPart) this.#hidden.return(session.busyPart);\n session.idlePart = null;\n session.busyPart = null;\n\n for (const control of session.controls) {\n this.#controlBusy.return(control);\n this.#disabled.return(control);\n }\n session.controls.clear();\n this.#submitting.return(session.form);\n this.#formBusy.return(session.form);\n }\n\n /** Restores focus only when disabling the focused submitter actually stranded it. */\n #restoreFocus(session: SubmissionSession): void {\n const submitter = session.submitter;\n if (!this.restoreFocusValue || !session.submitterHadFocus || !submitter?.isConnected) return;\n const active = document.activeElement;\n if (active === null || active === document.body || active === document.documentElement) {\n submitter.focus();\n }\n }\n\n /** Silently abandons every session before caching or on a real detach. */\n #abandonSessions(): void {\n this.#timers.clearAll();\n for (const session of this.#sessions.values()) {\n session.timeoutId = null;\n this.#returnSessionState(session);\n }\n this.#sessions.clear();\n }\n\n /** Keeps Turbo's cached snapshot idle, without events, announcements, or focus moves. */\n #rewindForCache(): void {\n this.#abandonSessions();\n }\n\n /** Tears down a true detach; an in-page move is canceled by the next `connect()`. */\n #teardown(): void {\n this.#gate.cancel();\n this.#abandonSessions();\n }\n\n /** All native controls associated with `form`, plus valid explicit targets. */\n #submitControls(form: HTMLFormElement): SubmitControl[] {\n const controls = new Set<SubmitControl>();\n for (const element of Array.from(form.elements)) {\n if (this.#isNativeSubmitControl(element)) controls.add(element);\n }\n // Browsers historically omit input[type=image] from form.elements. Search\n // the form's tree root as well so descendant and external `form=` image\n // submitters receive the same guard without broadening into sibling forms.\n const root = form.getRootNode() as ParentNode;\n for (const image of root.querySelectorAll<HTMLInputElement>('input[type=\"image\"]')) {\n if (image.form === form) controls.add(image);\n }\n return Array.from(controls);\n }\n\n /** Resolves the real submitter, falling back to the first enabled submit control. */\n #resolveSubmitter(event: Event, controls: SubmitControl[]): SubmitControl | null {\n const submitter = this.#eventSubmitter(event);\n if (submitter && controls.includes(submitter)) return submitter;\n return controls.find((control) => !control.matches(\":disabled\")) ?? controls[0] ?? null;\n }\n\n /** Reads a native or Turbo submitter without assuming `SubmitEvent` exists. */\n #eventSubmitter(event: Event): SubmitControl | null {\n if (typeof SubmitEvent !== \"undefined\" && event instanceof SubmitEvent) {\n return this.#isNativeSubmitControl(event.submitter) ? event.submitter : null;\n }\n const detail = (event as CustomEvent<{ formSubmission?: { submitter?: HTMLElement | null } }>)\n .detail;\n const submitter = detail?.formSubmission?.submitter;\n return this.#isNativeSubmitControl(submitter) ? submitter : null;\n }\n\n /** Finds the form represented by an action/Turbo event and verifies this scope owns it. */\n #eventForm(event: Event): HTMLFormElement | null {\n const detail = (\n event as CustomEvent<{\n form?: HTMLFormElement;\n formSubmission?: { formElement?: HTMLFormElement; submitter?: HTMLElement | null };\n }>\n ).detail;\n const target = event.target;\n const candidate =\n (target instanceof HTMLFormElement ? target : null) ??\n (target instanceof Element ? target.closest(\"form\") : null) ??\n detail?.formSubmission?.formElement ??\n detail?.form ??\n this.#eventSubmitter(event)?.form ??\n (this.element instanceof HTMLFormElement ? this.element : null);\n return candidate && this.#ownsForm(candidate) ? candidate : null;\n }\n\n /** The only unambiguous form for a direct `finish()` call. */\n #defaultForm(): HTMLFormElement | null {\n if (this.element instanceof HTMLFormElement && this.#ownsForm(this.element))\n return this.element;\n if (this.#sessions.size === 1) return this.#sessions.keys().next().value ?? null;\n return null;\n }\n\n /** Prevents an ancestor controller from also taking over a nested instance's form. */\n #ownsForm(form: HTMLFormElement): boolean {\n let candidate: Element | null = form;\n while (candidate) {\n const identifiers = (candidate.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n if (identifiers.includes(this.identifier)) return candidate === this.element;\n candidate = candidate.parentElement;\n }\n return false;\n }\n\n /** Returns the active session for a form-shaped external event value. */\n #sessionFor(candidate: unknown): SubmissionSession | undefined {\n if (!(candidate instanceof HTMLFormElement)) return undefined;\n return this.#sessions.get(candidate);\n }\n\n /** Effective native submit controls, including implicit buttons and image inputs. */\n #isNativeSubmitControl(element: unknown): element is SubmitControl {\n if (element instanceof HTMLButtonElement) return element.type === \"submit\";\n return (\n element instanceof HTMLInputElement && (element.type === \"submit\" || element.type === \"image\")\n );\n }\n\n /** Finds a structured label part belonging to this button, not a nested control. */\n #partInside(control: SubmitControl, parts: HTMLElement[]): HTMLElement | null {\n return (\n parts.find(\n (part) =>\n control.contains(part) &&\n part.closest(\"button, input[type=submit], input[type=image]\") === control,\n ) ?? null\n );\n }\n\n /** Reads an optional boolean `success` from Turbo or a manual completion event. */\n #eventSuccess(event: Event): boolean | undefined {\n const success = (event as CustomEvent<{ success?: unknown }>).detail?.success;\n return typeof success === \"boolean\" ? success : undefined;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/attribute_lease.ts","../../src/utils/before_cache_reset.ts","../../src/utils/detach_gate.ts","../../src/utils/safe_timeout.ts","../../src/controllers/submit_once_controller.ts"],"names":[],"mappings":"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","file":"submit_once_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/** One temporarily controlled attribute value and the authored value it displaced. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n written: string | null;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events.\n * A consumer that must not let a derived value become the authored baseline of a\n * cached page returns its leases from its own `turbo:before-cache` rewind, where the\n * matching `disconnect()` also releases the subscription.\n */\nexport class AttributeLease<T extends Element = Element> {\n readonly #attribute: string;\n readonly #records = new Map<T, AttributeLeaseRecord>();\n\n /** @param attribute - The attribute whose temporary values this lease owns. */\n constructor(attribute: string) {\n this.#attribute = attribute;\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const existing = this.#records.get(element);\n if (existing) {\n existing.written = value;\n } else {\n this.#records.set(element, {\n original: element.getAttribute(this.#attribute),\n written: value,\n });\n }\n\n this.#reflect(element, value);\n }\n\n /** Returns one lease without overwriting a value subsequently authored by a consumer. */\n return(element: T): void {\n const record = this.#records.get(element);\n if (!record) return;\n this.#records.delete(element);\n const stillOwned = element.getAttribute(this.#attribute) === record.written;\n if (stillOwned) this.#reflect(element, record.original);\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n if (element.getAttribute(this.#attribute) === value) return;\n if (value === null) element.removeAttribute(this.#attribute);\n else element.setAttribute(this.#attribute, value);\n }\n\n /** Returns every outstanding lease using the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#records.keys())) this.return(element);\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (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","/**\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 } from \"../utils/announce\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { DetachGate } from \"../utils/detach_gate\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Native controls that can submit a form. */\ntype SubmitControl = HTMLButtonElement | HTMLInputElement;\n\n/** Why an active submission returned to idle. */\ntype CompletionReason = \"canceled\" | \"manual\" | \"timeout\" | \"turbo\";\n\n/** A text/value write that cannot be represented by an {@link AttributeLease}. */\ntype LabelWrite =\n | {\n readonly channel: \"text\";\n readonly control: HTMLButtonElement;\n readonly original: string;\n readonly written: string;\n }\n | {\n readonly channel: \"value\";\n readonly control: HTMLInputElement;\n readonly original: string;\n readonly written: string;\n };\n\n/** One form's independent in-flight submission. */\ninterface SubmissionSession {\n readonly form: HTMLFormElement;\n readonly submitter: SubmitControl | null;\n readonly submitterHadFocus: boolean;\n readonly controls: Set<SubmitControl>;\n timeoutId: number | null;\n labelWrite: LabelWrite | null;\n idlePart: HTMLElement | null;\n busyPart: HTMLElement | null;\n}\n\n/** Per-button override for the controller's default busy label. */\nconst BUTTON_BUSY_LABEL = \"data-submit-once-busy-label\";\n\n/**\n * Headless form-scoped double-submit guard (identifier:\n * `stimeo--submit-once`). It disables every native submit control associated\n * with the submitting form, publishes busy state, and restores only the state\n * it still owns when the operation finishes.\n *\n * Turbo forms need no `data-action`: the controller observes\n * `turbo:submit-start` and `turbo:submit-end` itself.\n *\n * A non-Turbo async form owns its own lifecycle and declares each transition:\n * `submit->stimeo--submit-once#start` applies the busy state synchronously,\n * before the request is issued, and `#finish` / `#cancel` end it. Cancelling the\n * event's default is how such a form suppresses the native navigation, so it\n * never means the submission died — only `#cancel` does. A submit already\n * cancelled by an earlier listener never starts a session at all.\n *\n * ```html\n * <form data-controller=\"stimeo--submit-once\"\n * data-action=\"submit->stimeo--submit-once#start\n * save:complete->stimeo--submit-once#finish\n * save:error->stimeo--submit-once#cancel\">\n * <button type=\"submit\">Save</button>\n * </form>\n * ```\n *\n * A form whose submit a later listener vetoes needs `#cancel` wired to that\n * veto, or a non-zero `timeout`, to leave the busy state.\n *\n * Plain text buttons can use `busyLabel` (overridden by\n * `data-submit-once-busy-label`). A button with authored structure instead uses\n * an `idle` / `busy` target pair inside that button, so icons and descendants\n * are never replaced with `textContent`:\n *\n * ```html\n * <form data-controller=\"stimeo--submit-once\"\n * data-stimeo--submit-once-announce-text-value=\"Saving…\"\n * data-stimeo--submit-once-announce-ready-text-value=\"Saved.\">\n * <button type=\"submit\" data-stimeo--submit-once-target=\"submit\">\n * <span data-stimeo--submit-once-target=\"idle\">Save</span>\n * <span data-stimeo--submit-once-target=\"busy\" hidden>Saving…</span>\n * </button>\n * </form>\n * ```\n *\n * Events:\n * - `stimeo--submit-once:start` — `{ form, submitter }`.\n * - `stimeo--submit-once:reconcile` — `{ forms: HTMLFormElement[] }`, the forms\n * whose in-flight submission the Turbo cache rewind abandoned. `end` would\n * claim the submission resolved, so the rewind reports itself instead.\n * - `stimeo--submit-once:end` — `{ form, submitter, reason, success? }`, where\n * `reason` is `\"turbo\"`, `\"timeout\"`, `\"manual\"`, or `\"canceled\"`. Only\n * `\"canceled\"` skips the completion announcement, because no operation ran.\n *\n * @remarks\n * Behavior only: the consumer owns every visual, label, and announcement\n * string. `announceText` / `announceReadyText` are empty by default and speak\n * through the page's shared `stimeo--announcer` only on real state transitions.\n * Attribute writes use {@link AttributeLease}, so a consumer mutation made\n * while busy wins over restoration. {@link DetachGate} preserves an in-flight\n * session across an in-page move, while {@link BeforeCacheReset} rewinds the\n * snapshot before Turbo caches it. Cache/detach rewinds are silent and never\n * move focus.\n */\nexport class SubmitOnceController extends Controller<HTMLElement> {\n static override targets = [\"submit\", \"idle\", \"busy\"];\n static override values = {\n announceText: { type: String, default: \"\" },\n announceReadyText: { type: String, default: \"\" },\n busyLabel: { type: String, default: \"\" },\n timeout: { type: Number, default: 0 },\n restoreFocus: { type: Boolean, default: false },\n };\n static actions = [\"cancel\", \"finish\", \"start\"] as const;\n static events = [\"start\", \"end\", \"reconcile\"] as const;\n\n declare readonly submitTargets: SubmitControl[];\n declare readonly idleTargets: HTMLElement[];\n declare readonly busyTargets: HTMLElement[];\n\n declare announceTextValue: string;\n declare announceReadyTextValue: string;\n declare busyLabelValue: string;\n declare timeoutValue: number;\n declare restoreFocusValue: boolean;\n\n readonly #timers = new SafeTimeout();\n readonly #gate = new DetachGate();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n readonly #sessions = new Map<HTMLFormElement, SubmissionSession>();\n\n readonly #disabled = new AttributeLease<SubmitControl>(\"disabled\");\n readonly #controlBusy = new AttributeLease<SubmitControl>(\"aria-busy\");\n readonly #formBusy = new AttributeLease<HTMLFormElement>(\"aria-busy\");\n readonly #submitting = new AttributeLease<HTMLFormElement>(\"data-submitting\");\n readonly #ariaLabel = new AttributeLease<HTMLButtonElement>(\"aria-label\");\n readonly #hidden = new AttributeLease<HTMLElement>(\"hidden\");\n\n readonly #onSubmitStart = (event: Event): void => {\n this.start(event);\n };\n\n readonly #onSubmitEnd = (event: Event): void => {\n this.#complete(this.#eventForm(event), \"turbo\", this.#eventSuccess(event));\n };\n\n /** Blocks a later native submit while one for the same form is still active. */\n readonly #onNativeSubmit = (event: Event): void => {\n // The submit event's target is its form. This listener runs in capture so it\n // always observes that first event before a public `start` action creates the\n // session, while a later event sees the session and is canceled here.\n const session = this.#sessionFor(event.target);\n if (!session) return;\n this.#syncControls(session);\n event.preventDefault();\n };\n\n override connect(): void {\n this.#gate.cancel();\n this.#beforeCache.activate();\n this.element.addEventListener(\"submit\", this.#onNativeSubmit, true);\n this.element.addEventListener(\"turbo:submit-start\", this.#onSubmitStart);\n this.element.addEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"submit\", this.#onNativeSubmit, true);\n this.element.removeEventListener(\"turbo:submit-start\", this.#onSubmitStart);\n this.element.removeEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n this.#beforeCache.deactivate();\n this.#gate.disconnected(this, () => this.#teardown());\n }\n\n /**\n * Begins the submission represented by `event`.\n *\n * Native `submit` events are intended for non-Turbo async forms. Turbo calls\n * this path through its auto-subscribed `turbo:submit-start` event. An event\n * whose default an earlier listener already cancelled carries no submission,\n * so it starts nothing.\n */\n start(event: Event): void {\n if (event.defaultPrevented) return;\n const form = this.#eventForm(event);\n if (!form || this.#sessions.has(form)) return;\n\n const controls = this.#submitControls(form);\n const submitter = this.#resolveSubmitter(event, controls);\n const session: SubmissionSession = {\n form,\n submitter,\n submitterHadFocus: submitter !== null && document.activeElement === submitter,\n controls: new Set(),\n timeoutId: null,\n labelWrite: null,\n idlePart: null,\n busyPart: null,\n };\n this.#sessions.set(form, session);\n\n this.#formBusy.write(form, \"true\");\n this.#submitting.write(form, \"true\");\n this.#syncControls(session, controls);\n this.#enterLabel(session);\n this.dispatch(\"start\", { detail: { form, submitter } });\n\n if (this.timeoutValue > 0) {\n session.timeoutId = this.#timers.set(\n () => this.#complete(form, \"timeout\", false),\n this.timeoutValue,\n );\n }\n\n announce(this.announceTextValue);\n }\n\n /**\n * Completes a non-Turbo async submission for the form associated with\n * `event`. When called directly with no event, it completes the controller's\n * sole active form (or the controller element itself when it is a form).\n */\n finish(event?: Event): void {\n const form = event ? this.#eventForm(event) : this.#defaultForm();\n this.#complete(form, \"manual\", event ? this.#eventSuccess(event) : undefined);\n }\n\n /**\n * Abandons the submission for the form associated with `event` because it\n * never reached the network — a veto from validation, or a request the\n * consumer gave up before sending. With no event it targets the controller's\n * sole active form, exactly like {@link finish}.\n *\n * The distinction from `finish` is observable: a request that ran and failed\n * is `finish` with `success: false` and still announces completion, while a\n * submission that never happened announces nothing.\n */\n cancel(event?: Event): void {\n const form = event ? this.#eventForm(event) : this.#defaultForm();\n this.#complete(form, \"canceled\", false);\n }\n\n /** Disables a newly connected explicit submit target during its form's session. */\n submitTargetConnected(target: SubmitControl): void {\n const session = this.#sessionFor(target.form);\n if (!session || !this.#isNativeSubmitControl(target)) return;\n this.#enterControl(session, target);\n }\n\n /** Ends one session, restores owned state, and reports the observable completion. */\n #complete(form: HTMLFormElement | null, reason: CompletionReason, success?: boolean): void {\n const session = this.#sessionFor(form);\n if (!session) return;\n this.#sessions.delete(session.form);\n this.#returnSessionState(session);\n\n const detail =\n success === undefined\n ? { form: session.form, submitter: session.submitter, reason }\n : { form: session.form, submitter: session.submitter, reason, success };\n this.dispatch(\"end\", { detail });\n this.#restoreFocus(session);\n if (reason !== \"canceled\") announce(this.announceReadyTextValue);\n }\n\n /** Applies the busy state to every current native/explicit control exactly once. */\n #syncControls(session: SubmissionSession, controls = this.#submitControls(session.form)): void {\n for (const control of controls) this.#enterControl(session, control);\n }\n\n /** Temporarily disables one enabled submit control while retaining authored state. */\n #enterControl(session: SubmissionSession, control: SubmitControl): void {\n if (session.controls.has(control) || control.matches(\":disabled\")) return;\n session.controls.add(control);\n this.#disabled.write(control, \"\");\n this.#controlBusy.write(control, \"true\");\n }\n\n /** Swaps only the triggering control's safe label channel. */\n #enterLabel(session: SubmissionSession): void {\n const control = session.submitter;\n if (!control) return;\n\n const idlePart = this.#partInside(control, this.idleTargets);\n const busyPart = this.#partInside(control, this.busyTargets);\n if (idlePart && busyPart) {\n session.idlePart = idlePart;\n session.busyPart = busyPart;\n this.#hidden.write(idlePart, \"\");\n this.#hidden.write(busyPart, null);\n }\n\n const label = control.getAttribute(BUTTON_BUSY_LABEL) ?? this.busyLabelValue;\n if (label.length === 0) return;\n if (control instanceof HTMLInputElement) {\n // `input[type=image]#value` is submitted data, not a visible label.\n if (control.type !== \"submit\") return;\n session.labelWrite = {\n channel: \"value\",\n control,\n original: control.value,\n written: label,\n };\n control.value = label;\n return;\n }\n if (control.hasAttribute(\"aria-label\")) {\n this.#ariaLabel.write(control, label);\n return;\n }\n // A structured button needs the explicit idle/busy pair above. Replacing its\n // textContent would destroy icons, counters, and application-owned listeners.\n if (control.childElementCount > 0) return;\n session.labelWrite = {\n channel: \"text\",\n control,\n original: control.textContent ?? \"\",\n written: label,\n };\n control.textContent = label;\n }\n\n /** Returns labels, controls, and form hooks, respecting intervening consumer writes. */\n #returnSessionState(session: SubmissionSession): void {\n if (session.timeoutId !== null) {\n this.#timers.clear(session.timeoutId);\n session.timeoutId = null;\n }\n\n if (session.labelWrite?.channel === \"value\") {\n const { control, original, written } = session.labelWrite;\n if (control.value === written) control.value = original;\n } else if (session.labelWrite?.channel === \"text\") {\n const { control, original, written } = session.labelWrite;\n if (control.childElementCount === 0 && control.textContent === written) {\n control.textContent = original;\n }\n }\n session.labelWrite = null;\n\n if (session.submitter instanceof HTMLButtonElement) {\n this.#ariaLabel.return(session.submitter);\n }\n if (session.idlePart) this.#hidden.return(session.idlePart);\n if (session.busyPart) this.#hidden.return(session.busyPart);\n session.idlePart = null;\n session.busyPart = null;\n\n for (const control of session.controls) {\n this.#controlBusy.return(control);\n this.#disabled.return(control);\n }\n session.controls.clear();\n this.#submitting.return(session.form);\n this.#formBusy.return(session.form);\n }\n\n /** Restores focus only when disabling the focused submitter actually stranded it. */\n #restoreFocus(session: SubmissionSession): void {\n const submitter = session.submitter;\n if (!this.restoreFocusValue || !session.submitterHadFocus || !submitter?.isConnected) return;\n const active = document.activeElement;\n if (active === null || active === document.body || active === document.documentElement) {\n submitter.focus();\n }\n }\n\n /** Silently abandons every session before caching or on a real detach. */\n #abandonSessions(): void {\n this.#timers.clearAll();\n for (const session of this.#sessions.values()) {\n session.timeoutId = null;\n this.#returnSessionState(session);\n }\n this.#sessions.clear();\n }\n\n /** Keeps Turbo's cached snapshot idle, without events, announcements, or focus moves. */\n #rewindForCache(): void {\n const forms = [...this.#sessions.keys()];\n this.#abandonSessions();\n // A submission in flight dies with the navigation, so a consumer still\n // painting \"submitting\" from `start` would never be released.\n if (forms.length > 0) this.dispatch(\"reconcile\", { detail: { forms } });\n }\n\n /** Tears down a true detach; an in-page move is canceled by the next `connect()`. */\n #teardown(): void {\n this.#gate.cancel();\n this.#abandonSessions();\n }\n\n /** All native controls associated with `form`, plus valid explicit targets. */\n #submitControls(form: HTMLFormElement): SubmitControl[] {\n const controls = new Set<SubmitControl>();\n for (const element of Array.from(form.elements)) {\n if (this.#isNativeSubmitControl(element)) controls.add(element);\n }\n // Browsers historically omit input[type=image] from form.elements. Search\n // the form's tree root as well so descendant and external `form=` image\n // submitters receive the same guard without broadening into sibling forms.\n const root = form.getRootNode() as ParentNode;\n for (const image of root.querySelectorAll<HTMLInputElement>('input[type=\"image\"]')) {\n if (image.form === form) controls.add(image);\n }\n return Array.from(controls);\n }\n\n /** Resolves the real submitter, falling back to the first enabled submit control. */\n #resolveSubmitter(event: Event, controls: SubmitControl[]): SubmitControl | null {\n const submitter = this.#eventSubmitter(event);\n if (submitter && controls.includes(submitter)) return submitter;\n return controls.find((control) => !control.matches(\":disabled\")) ?? controls[0] ?? null;\n }\n\n /** Reads a native or Turbo submitter without assuming `SubmitEvent` exists. */\n #eventSubmitter(event: Event): SubmitControl | null {\n if (typeof SubmitEvent !== \"undefined\" && event instanceof SubmitEvent) {\n return this.#isNativeSubmitControl(event.submitter) ? event.submitter : null;\n }\n const detail = (event as CustomEvent<{ formSubmission?: { submitter?: HTMLElement | null } }>)\n .detail;\n const submitter = detail?.formSubmission?.submitter;\n return this.#isNativeSubmitControl(submitter) ? submitter : null;\n }\n\n /** Finds the form represented by an action/Turbo event and verifies this scope owns it. */\n #eventForm(event: Event): HTMLFormElement | null {\n const detail = (\n event as CustomEvent<{\n form?: HTMLFormElement;\n formSubmission?: { formElement?: HTMLFormElement; submitter?: HTMLElement | null };\n }>\n ).detail;\n const target = event.target;\n const candidate =\n (target instanceof HTMLFormElement ? target : null) ??\n (target instanceof Element ? target.closest(\"form\") : null) ??\n detail?.formSubmission?.formElement ??\n detail?.form ??\n this.#eventSubmitter(event)?.form ??\n (this.element instanceof HTMLFormElement ? this.element : null);\n return candidate && this.#ownsForm(candidate) ? candidate : null;\n }\n\n /** The only unambiguous form for a direct `finish()` call. */\n #defaultForm(): HTMLFormElement | null {\n if (this.element instanceof HTMLFormElement && this.#ownsForm(this.element))\n return this.element;\n if (this.#sessions.size === 1) return this.#sessions.keys().next().value ?? null;\n return null;\n }\n\n /** Prevents an ancestor controller from also taking over a nested instance's form. */\n #ownsForm(form: HTMLFormElement): boolean {\n let candidate: Element | null = form;\n while (candidate) {\n const identifiers = (candidate.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n if (identifiers.includes(this.identifier)) return candidate === this.element;\n candidate = candidate.parentElement;\n }\n return false;\n }\n\n /** Returns the active session for a form-shaped external event value. */\n #sessionFor(candidate: unknown): SubmissionSession | undefined {\n if (!(candidate instanceof HTMLFormElement)) return undefined;\n return this.#sessions.get(candidate);\n }\n\n /** Effective native submit controls, including implicit buttons and image inputs. */\n #isNativeSubmitControl(element: unknown): element is SubmitControl {\n if (element instanceof HTMLButtonElement) return element.type === \"submit\";\n return (\n element instanceof HTMLInputElement && (element.type === \"submit\" || element.type === \"image\")\n );\n }\n\n /** Finds a structured label part belonging to this button, not a nested control. */\n #partInside(control: SubmitControl, parts: HTMLElement[]): HTMLElement | null {\n return (\n parts.find(\n (part) =>\n control.contains(part) &&\n part.closest(\"button, input[type=submit], input[type=image]\") === control,\n ) ?? null\n );\n }\n\n /** Reads an optional boolean `success` from Turbo or a manual completion event. */\n #eventSuccess(event: Event): boolean | undefined {\n const success = (event as CustomEvent<{ success?: unknown }>).detail?.success;\n return typeof success === \"boolean\" ? success : undefined;\n }\n}\n"]}
@@ -10,19 +10,35 @@ import { Controller } from '@hotwired/stimulus';
10
10
  * data-controller="stimeo--textarea-autosize"
11
11
  * data-stimeo--textarea-autosize-max-rows-value="10"></textarea>
12
12
  *
13
- * On connect and on every input it collapses the element (`height:auto`), reads
14
- * `scrollHeight`, and sets an explicit pixel height clamped between `minRows` and
15
- * `maxRows` (in line-height units), toggling internal scrolling and the
16
- * `data-at-max-rows` hook at the cap. Height-only changes preserve focus and caret.
13
+ * Each re-measure collapses the element (`height:auto`), reads `scrollHeight`,
14
+ * and sets an explicit pixel height clamped between `minRows` and `maxRows` (in
15
+ * line-height units), toggling internal scrolling and the `data-at-max-rows`
16
+ * hook at the cap. Height-only changes preserve focus and caret.
17
17
  *
18
- * `resize` dispatches `{ height, rows }`.
18
+ * Re-measure triggers: `connect()`; `input` and `change` on the element
19
+ * (`change` covers programmatic writers that announce the write with a bubbling
20
+ * event, the convention Stimeo's own field-writing controllers follow); element
21
+ * size reports via the shared {@link LayoutObserver} — width changes
22
+ * (re-wrapping) and the applied inline height being stripped (a Turbo morph
23
+ * syncing attributes); font loading settling on `document.fonts`; runtime
24
+ * `minRows` / `maxRows` changes; and the explicit `resize` action. A silent
25
+ * `.value` assignment produces no DOM signal, so callers follow it with
26
+ * `resize` or dispatch `input` / `change`.
27
+ *
28
+ * `resize` dispatches `{ height, rows }` only when the applied height differs
29
+ * from the height this instance last applied. `connect()` adopts a height left
30
+ * on the element by a previous connection (Turbo restore / morph), so restoring
31
+ * an unchanged element stays silent.
19
32
  *
20
33
  * @remarks
21
- * Behavior only — the height is written to the element's own inline style, never a
22
- * CSS class. State lives on the element (no module-scope state), so `connect()`
23
- * re-measures after a Turbo navigation / morph. The input listener is removed on
24
- * `disconnect()` (Turbo navigation included). Measurement is synchronous, so there
25
- * is no pending animation frame to tear down.
34
+ * Behavior only — the height is written to the element's own inline style, never
35
+ * a CSS class. State lives on the element (no module-scope state), so `connect()`
36
+ * re-measures after a Turbo navigation / morph. Every subscription (`input` /
37
+ * `change` listeners, the layout observer, the font listeners) is released on
38
+ * `disconnect()` (Turbo navigation included). Measurement is synchronous, so
39
+ * there is no pending animation frame to tear down. The layout callback
40
+ * re-measures only when the element's width changed or the applied inline
41
+ * height vanished, so the controller's own height writes do not re-enter it.
26
42
  */
27
43
  declare class TextareaAutosizeController extends Controller<HTMLTextAreaElement> {
28
44
  #private;
@@ -42,6 +58,10 @@ declare class TextareaAutosizeController extends Controller<HTMLTextAreaElement>
42
58
  maxRowsValue: number;
43
59
  connect(): void;
44
60
  disconnect(): void;
61
+ /** Re-clamps when application code or a Turbo morph changes `minRows`. */
62
+ minRowsValueChanged(): void;
63
+ /** Re-clamps when application code or a Turbo morph changes `maxRows`. */
64
+ maxRowsValueChanged(): void;
45
65
  /** Re-measures the content and applies the clamped height. */
46
66
  resize(): void;
47
67
  }