stimeo-ui 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/dist/controllers/auto_submit_controller.d.ts +14 -3
- package/dist/controllers/auto_submit_controller.js +94 -16
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +81 -47
- package/dist/controllers/carousel_controller.js +451 -100
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +39 -5
- package/dist/controllers/currency_input_controller.js +305 -74
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +3 -1
- package/dist/controllers/direct_upload_controller.js +12 -2
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +123 -29
- package/dist/controllers/file_dropzone_controller.js +386 -63
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +3 -1
- package/dist/controllers/flash_controller.js +3 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +4 -2
- package/dist/controllers/frame_loading_controller.js +2 -1
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +42 -16
- package/dist/controllers/input_mask_controller.js +251 -76
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +51 -14
- package/dist/controllers/nested_form_controller.js +450 -42
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +61 -25
- package/dist/controllers/otp_controller.js +472 -114
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +3 -2
- package/dist/controllers/spinner_controller.js +7 -5
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +4 -1
- package/dist/controllers/submit_once_controller.js +3 -1
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +30 -10
- package/dist/controllers/textarea_autosize_controller.js +131 -3
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/index.js +1927 -607
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +8 -1
- package/dist/inspector/cli.js +3 -1
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +3 -1
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +4 -4
- package/dist/inspector/manifest.json +182 -23
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/file_dropzone_controller.ts"],"names":[],"mappings":"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","file":"file_dropzone_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless, accessible file drag-and-drop / upload field.\n *\n * Markup contract (identifier: `stimeo--file-dropzone`):\n * <div data-controller=\"stimeo--file-dropzone\"\n * data-stimeo--file-dropzone-max-size-value=\"5242880\">\n * <div data-stimeo--file-dropzone-target=\"zone\"\n * data-action=\"dragover->stimeo--file-dropzone#onDragOver\n * dragleave->stimeo--file-dropzone#onDragLeave\n * drop->stimeo--file-dropzone#onDrop\">\n * <button type=\"button\" data-stimeo--file-dropzone-target=\"trigger\"\n * data-action=\"click->stimeo--file-dropzone#openDialog\">Choose…</button>\n * <input type=\"file\" accept=\"image/*\" multiple class=\"visually-hidden\"\n * data-stimeo--file-dropzone-target=\"input\"\n * data-action=\"change->stimeo--file-dropzone#onChange\" />\n * </div>\n * <ul data-stimeo--file-dropzone-target=\"list\" aria-label=\"Selected files\"></ul>\n * <span role=\"status\" aria-live=\"polite\" class=\"visually-hidden\"\n * data-stimeo--file-dropzone-target=\"status\"></span>\n * <template data-stimeo--file-dropzone-target=\"itemTemplate\">…</template>\n * </div>\n *\n * There is no single APG pattern; the native `<input type=\"file\">` stays the\n * primary, keyboard-operable path and the drop zone is an enhancement, mapping to\n * WCAG 2.1.1, 2.4.7, 4.1.2, 4.1.3, and 1.4.1 (drag state is conveyed in words,\n * not color alone).\n *\n * Behavior provided:\n * - Click / keyboard via the `trigger` opens the native file dialog; drag-and-drop\n * over the `zone` adds files (with a `data-dragover` flag and a spoken hint).\n * - Each file is validated against `accept`, `maxSize`, and the file count\n * (`maxFiles`, or 1 when the input is not `multiple`); rejects fire\n * `stimeo--file-dropzone:reject` and set `data-…-invalid`.\n * - Accepted files render from `itemTemplate` with a `Remove {name}` button and an\n * image thumbnail (`objectURL`); every change dispatches\n * `stimeo--file-dropzone:change` with the current `File[]`.\n * - Removing a file revokes its `objectURL` and moves focus to the next (else\n * previous) remove button, falling back to the trigger; `disconnect()` revokes\n * every outstanding `objectURL`. Replacing the `list` target rebinds delegated\n * removal and moves the client-only previews into the replacement.\n * `reject` dispatches `{ file: File, reason: \"type\" | \"size\" | \"count\" }`.\n * `change` dispatches `{ files: File[] }`.\n */\nexport class FileDropzoneController extends Controller<HTMLElement> {\n static override targets = [\"zone\", \"trigger\", \"input\", \"list\", \"item\", \"itemTemplate\", \"status\"];\n static override values = {\n maxSize: { type: Number, default: 0 },\n maxFiles: { type: Number, default: 0 },\n dragLabel: { type: String, default: \"Drop files to add them\" },\n };\n static actions = [\"onChange\", \"onDragLeave\", \"onDragOver\", \"onDrop\", \"openDialog\"] as const;\n static events = [\"change\", \"reject\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly listTarget: HTMLElement;\n declare readonly listTargets: HTMLElement[];\n declare readonly triggerTarget: HTMLElement;\n declare readonly itemTemplateTarget: HTMLTemplateElement;\n declare readonly statusTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasItemTemplateTarget: boolean;\n declare readonly hasStatusTarget: boolean;\n declare readonly hasZoneTarget: boolean;\n declare readonly zoneTarget: HTMLElement;\n\n declare maxSizeValue: number;\n declare maxFilesValue: number;\n declare dragLabelValue: string;\n\n /** Selected files paired with their rendered item and any preview objectURL. */\n readonly #entries: Array<{ file: File; item: HTMLElement; url?: string }> = [];\n /** Prevents initial and teardown target callbacks from binding outside controller lifetime. */\n #connected = false;\n\n /** Wires file removal as a delegated listener on the list container. */\n override connect(): void {\n this.#connected = true;\n if (this.hasListTarget) this.#bindList(this.listTarget);\n }\n\n /** Revokes any outstanding preview URLs so none leaks across navigations. */\n override disconnect(): void {\n this.#connected = false;\n for (const list of this.listTargets) list.removeEventListener(\"click\", this.#onItemClick);\n for (const entry of this.#entries) {\n if (entry.url) URL.revokeObjectURL(entry.url);\n }\n this.#entries.length = 0;\n }\n\n /** Rebinds removal and restores client-only previews when Turbo replaces the list target. */\n listTargetConnected(list: HTMLElement): void {\n if (!this.#connected) return;\n this.#bindList(list);\n }\n\n /** Releases only the list target that actually disconnected. */\n listTargetDisconnected(list: HTMLElement): void {\n list.removeEventListener(\"click\", this.#onItemClick);\n }\n\n /** Binds delegated removal once and moves live preview items into the current list. */\n #bindList(list: HTMLElement): void {\n list.addEventListener(\"click\", this.#onItemClick);\n for (const entry of this.#entries) {\n if (!list.contains(entry.item)) list.appendChild(entry.item);\n }\n }\n\n /** Opens the native file dialog. Bound via `data-action` (trigger click). */\n openDialog(): void {\n this.inputTarget.click();\n }\n\n /** Adds the files chosen through the native dialog. */\n onChange(): void {\n if (this.inputTarget.files) this.#addFiles(this.inputTarget.files);\n // Clear so re-selecting the same file still fires `change`.\n this.inputTarget.value = \"\";\n }\n\n /** Marks the zone as a drop target and announces the affordance in words. */\n onDragOver(event: DragEvent): void {\n event.preventDefault();\n if (this.hasZoneTarget) this.zoneTarget.setAttribute(\"data-dragover\", \"\");\n this.#setStatus(this.dragLabelValue);\n }\n\n /** Clears the drag-over flag when the pointer leaves the zone. */\n onDragLeave(): void {\n if (this.hasZoneTarget) this.zoneTarget.removeAttribute(\"data-dragover\");\n }\n\n /** Accepts dropped files, clearing the drag-over state. */\n onDrop(event: DragEvent): void {\n event.preventDefault();\n if (this.hasZoneTarget) this.zoneTarget.removeAttribute(\"data-dragover\");\n if (event.dataTransfer?.files) this.#addFiles(event.dataTransfer.files);\n }\n\n /**\n * Removes the file whose remove button was clicked. Delegated on the list\n * container rather than bound per item via `data-action`, so it works the instant\n * an item is appended without waiting on Stimulus to wire a freshly created element.\n */\n readonly #onItemClick = (event: MouseEvent): void => {\n const list = event.currentTarget as HTMLElement | null;\n const button = (event.target as HTMLElement).closest(\"button\");\n if (\n !this.#connected ||\n !list ||\n !button ||\n !this.hasListTarget ||\n list !== this.listTarget ||\n !list.contains(button)\n ) {\n return;\n }\n const index = this.#entries.findIndex((entry) => entry.item.contains(button));\n if (index !== -1) this.#removeAt(index);\n };\n\n /** Validates each incoming file and renders the accepted ones. */\n #addFiles(files: FileList): void {\n let changed = false;\n if (this.hasZoneTarget) this.zoneTarget.removeAttribute(\"data-stimeo--file-dropzone-invalid\");\n for (const file of Array.from(files)) {\n const reason = this.#validate(file);\n if (reason) {\n if (this.hasZoneTarget) {\n this.zoneTarget.setAttribute(\"data-stimeo--file-dropzone-invalid\", \"\");\n }\n this.#setStatus(file.name);\n this.dispatch(\"reject\", { detail: { file, reason } });\n continue;\n }\n this.#appendFile(file);\n this.#setStatus(file.name);\n changed = true;\n }\n if (changed) this.dispatch(\"change\", { detail: { files: this.#files } });\n }\n\n /** Returns the rejection reason for `file`, or `null` when it is acceptable. */\n #validate(file: File): \"type\" | \"size\" | \"count\" | null {\n const limit = this.#effectiveMaxFiles;\n if (limit > 0 && this.#entries.length >= limit) return \"count\";\n if (!this.#matchesAccept(file)) return \"type\";\n if (this.maxSizeValue > 0 && file.size > this.maxSizeValue) return \"size\";\n return null;\n }\n\n /** Builds one preview item (name, optional thumbnail, remove button). */\n #appendFile(file: File): void {\n if (!this.hasItemTemplateTarget || !this.hasListTarget) return;\n const fragment = this.itemTemplateTarget.content.cloneNode(true) as DocumentFragment;\n const item = fragment.querySelector<HTMLElement>('[data-stimeo--file-dropzone-target=\"item\"]');\n const name = fragment.querySelector<HTMLElement>('[data-file-dropzone-slot=\"name\"]');\n const thumb = fragment.querySelector<HTMLImageElement>('[data-file-dropzone-slot=\"thumb\"]');\n const button = fragment.querySelector<HTMLButtonElement>(\"button\");\n if (!item) return;\n if (name) name.textContent = file.name;\n if (button) button.setAttribute(\"aria-label\", `Remove ${file.name}`);\n\n let url: string | undefined;\n if (thumb && file.type.startsWith(\"image/\")) {\n url = URL.createObjectURL(file);\n thumb.src = url;\n thumb.alt = file.name;\n thumb.hidden = false;\n } else if (thumb) {\n thumb.hidden = true;\n }\n\n this.listTarget.appendChild(fragment);\n this.#entries.push({ file, item, url });\n }\n\n /** Removes entry `index`, revokes its preview, and re-homes focus. */\n #removeAt(index: number): void {\n const entry = this.#entries[index];\n if (!entry) return;\n if (entry.url) URL.revokeObjectURL(entry.url);\n entry.item.remove();\n this.#entries.splice(index, 1);\n this.#setStatus(entry.file.name);\n this.dispatch(\"change\", { detail: { files: this.#files } });\n\n const buttons = this.#removeButtons;\n if (buttons.length === 0) {\n if (this.hasTriggerTarget) this.triggerTarget.focus();\n } else {\n (buttons[index] ?? buttons[buttons.length - 1])?.focus();\n }\n }\n\n /** Whether `file` satisfies the input's `accept` list (empty accepts all). */\n #matchesAccept(file: File): boolean {\n const accept = this.inputTarget.accept.trim();\n if (accept === \"\") return true;\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n return accept.split(\",\").some((raw) => {\n const token = raw.trim().toLowerCase();\n if (token === \"\") return false;\n if (token.startsWith(\".\")) return name.endsWith(token);\n if (token.endsWith(\"/*\")) return type.startsWith(token.slice(0, -1));\n return type === token;\n });\n }\n\n /** Updates the live region so assistive tech announces the change. */\n #setStatus(text: string): void {\n if (this.hasStatusTarget) this.statusTarget.textContent = text;\n }\n\n /** Effective file cap: `maxFiles`, or 1 when the input is single-select. */\n get #effectiveMaxFiles(): number {\n if (this.maxFilesValue > 0) return this.maxFilesValue;\n return this.inputTarget.multiple ? 0 : 1;\n }\n\n /** The remove buttons currently in the list, in order. */\n get #removeButtons(): HTMLButtonElement[] {\n return Array.from(this.listTarget.querySelectorAll<HTMLButtonElement>(\"button\"));\n }\n\n /** The accepted files in selection order. */\n get #files(): File[] {\n return this.#entries.map((entry) => entry.file);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/detach_gate.ts","../../src/utils/focus_candidate.ts","../../src/controllers/file_dropzone_controller.ts"],"names":[],"mappings":"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","file":"file_dropzone_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Shared \"in-page move vs real detach\" discriminator for `disconnect()`.\n *\n * A mid-session `disconnect()` whose element is still in the document is\n * ambiguous. It can be:\n *\n * - an **in-page move** — a consumer (sortable, a teleport) re-inserted the\n * element; Stimulus reconnects the SAME instance in the same mutation batch\n * and the controller's runtime state must survive;\n * - a **detach that keeps the element** — `data-controller` no longer lists the\n * identifier (a Turbo 8 morph), or the element moved outside the observed\n * root (a scoped `Application.start(root)`, a shadow root); no reconnect will\n * ever come and the controller must tear down or it leaks its sessions,\n * document listeners, and moved nodes.\n *\n * Neither available signal decides this alone. A synchronous `data-controller`\n * token check catches the morph case but misses an observed-root exit (the\n * token stays, the reconnect never comes). Deferring teardown one microtask and\n * letting `connect()` cancel it catches the root exit, but leans on Stimulus\n * reconnecting moved elements within the same mutation batch — internal\n * behavior, not a public contract. {@link DetachGate} composes both: the token\n * check is the synchronous fast path (no probe, no reliance on batching), the\n * microtask probe is the fallback for the genuinely ambiguous remainder.\n *\n * Policy stays with the consumer (the `SafeTimeout` / `FocusTrap` lineage —\n * this util owns the discrimination only):\n *\n * - **Probe** ({@link disconnected}) when the teardown must eventually happen —\n * live drag sessions, document listeners, teleported *content*. Call\n * {@link cancel} from `connect()` (a reconnect means in-page move) and from\n * the head of any direct teardown path (see below).\n * - **Keep** (bare {@link DetachGate.isDetached}) when undoing the ambiguous\n * case would fight the controller's own effect: a controller that moves its\n * OWN element (portal's no-`content` form) exits a scoped observed root as\n * its normal job — a probe-driven teardown would restore the element into\n * the root, reconnect, re-teleport, and disconnect again, forever. Such\n * controllers keep their state on an ambiguous disconnect and tear down only\n * on {@link DetachGate.isDetached}.\n *\n * Event convention for probe consumers: teardown into a dead (detached) tree is\n * silent — consumers restore from their own `connect()`; a detach that keeps the\n * tree alive must end an in-flight session with a `cancel` event, or composing\n * consumers strand their session bookkeeping.\n */\n\n/** The slice of a Stimulus controller the gate inspects. */\nexport interface DetachGateHost {\n readonly element: Element;\n readonly identifier: string;\n}\n\n/**\n * Per-controller-instance gate deciding whether a `disconnect()` is a real\n * detach. Hold one per controller (`#gate = new DetachGate()`) — Stimulus\n * reuses the instance across reconnects, so the pending probe state carries\n * over exactly as the protocol needs.\n *\n * @example\n * ```ts\n * #gate = new DetachGate();\n *\n * connect(): void {\n * this.#gate.cancel(); // a reconnect: the element moved in-page\n * // …\n * }\n *\n * disconnect(): void {\n * this.#gate.disconnected(this, () => this.#teardown());\n * }\n *\n * #teardown(): void {\n * this.#gate.cancel(); // disarm a still-queued probe (double-run guard)\n * // …\n * }\n * ```\n */\nexport class DetachGate {\n /** Set while a probe is queued, waiting for a reconnect to cancel it. */\n #pending = false;\n\n /**\n * True while a probe is queued — the last disconnect was ambiguous and no\n * reconnect has cancelled it yet. Read it from `connect()` to tell the\n * reconnect half of an in-page move from a first connect: a controller whose\n * initialisation restarts a measurement (a min-duration floor, an elapsed\n * counter) must skip it for the move, where nothing actually restarted.\n */\n get pending(): boolean {\n return this.#pending;\n }\n\n /**\n * True when the disconnect is definitely a real detach — the element left\n * the document, or `data-controller` no longer lists the identifier. False\n * means ambiguous (in-page move or observed-root exit), NOT \"alive\".\n */\n static isDetached(host: DetachGateHost): boolean {\n if (!host.element.isConnected) return true;\n const tokens = (host.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return !tokens.includes(host.identifier);\n }\n\n /**\n * Call from `disconnect()`: runs `teardown` synchronously on a definite\n * detach (fast path), otherwise defers it one microtask — a reconnect\n * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.\n * One microtask is the whole probe window: Stimulus reconnects a moved\n * element within the same mutation batch, before the checkpoint drains.\n */\n disconnected(host: DetachGateHost, teardown: () => void): void {\n if (DetachGate.isDetached(host)) {\n // Also disarms a probe a previous ambiguous disconnect left queued\n // (defer, element removed, disconnect again) — exactly one teardown.\n this.#pending = false;\n teardown();\n return;\n }\n this.#pending = true;\n queueMicrotask(() => {\n if (!this.#pending) return;\n this.#pending = false;\n teardown();\n });\n }\n\n /**\n * Disarms a pending probe. Call from `connect()` (the reconnect that proves\n * an in-page move) and from the head of any teardown path not routed through\n * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can\n * never run the teardown a second time.\n */\n cancel(): void {\n this.#pending = false;\n }\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported for the consumer that needs this rule *without* the rest of\n * {@link canTakeFocus}: `toolbar` bounds its own `hidden` walk at the toolbar\n * root, which is a different rule, so it composes the two itself.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the three conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { DetachGate } from \"../utils/detach_gate\";\nimport { inheritsFieldsetDisabled } from \"../utils/focus_candidate\";\n\n/** Why one file was turned away, in the order the checks run. */\ntype RejectReason = \"type\" | \"size\" | \"duplicate\" | \"count\";\n\n/** Present on the zone while a drag hovers it. */\nconst DRAGOVER_ATTRIBUTE = \"data-dragover\";\n\n/** Present on the zone from the first rejection of a batch until the next batch. */\nconst INVALID_ATTRIBUTE = \"data-stimeo--file-dropzone-invalid\";\n\n/** One selected file paired with its rendered item and any preview objectURL. */\ninterface Entry {\n readonly file: File;\n readonly item: HTMLElement;\n readonly url?: string;\n}\n\n/** One batch's rejections for a single reason, condensed into one announcement. */\ninterface RejectBatch {\n readonly name: string;\n count: number;\n}\n\n/**\n * Headless, accessible file drag-and-drop / upload field.\n *\n * Markup contract (identifier: `stimeo--file-dropzone`):\n * <div data-controller=\"stimeo--file-dropzone\"\n * data-stimeo--file-dropzone-max-size-value=\"5242880\"\n * data-stimeo--file-dropzone-announce-added-text-value=\"{name} added; {total} selected\">\n * <div data-stimeo--file-dropzone-target=\"zone\"\n * data-action=\"dragover->stimeo--file-dropzone#onDragOver\n * dragleave->stimeo--file-dropzone#onDragLeave\n * drop->stimeo--file-dropzone#onDrop\">\n * <button type=\"button\" data-stimeo--file-dropzone-target=\"trigger\"\n * data-action=\"click->stimeo--file-dropzone#openDialog\">Choose…</button>\n * <input type=\"file\" accept=\"image/*\" multiple class=\"visually-hidden\"\n * data-stimeo--file-dropzone-target=\"input\"\n * data-action=\"change->stimeo--file-dropzone#onChange\" />\n * </div>\n * <ul data-stimeo--file-dropzone-target=\"list\" aria-label=\"Selected files\"></ul>\n * <template data-stimeo--file-dropzone-target=\"itemTemplate\">\n * <li data-stimeo--file-dropzone-target=\"item\">\n * <img data-stimeo--file-dropzone-target=\"thumb\" alt=\"\" hidden />\n * <span data-stimeo--file-dropzone-target=\"name\"></span>\n * <button type=\"button\" aria-label=\"Remove {name}\"\n * data-stimeo--file-dropzone-target=\"remove\">×</button>\n * </li>\n * </template>\n * </div>\n *\n * There is no single APG pattern; the native `<input type=\"file\">` stays the\n * primary, keyboard-operable path and the drop zone is an enhancement, mapping to\n * WCAG 2.1.1, 2.4.7, 4.1.2, 4.1.3, and 1.4.1 (drag state is conveyed in words,\n * not color alone).\n *\n * Behavior provided:\n * - Click / keyboard via the `trigger` opens the native file dialog; drag-and-drop\n * over the `zone` adds files. The zone carries `data-dragover` for as long as the\n * pointer is anywhere inside it, descendants included — a `dragleave` whose\n * `relatedTarget` is still in the zone is the pointer crossing an inner element,\n * not leaving.\n * - Each file is validated in order against `accept`, `maxSize`, duplication, and\n * the file count (`maxFiles`, or 1 when the input is not `multiple`). The reason\n * is therefore the most specific one: a file the zone would refuse whatever the\n * count says reports its own defect rather than `count`. Rejects fire\n * `stimeo--file-dropzone:reject` and set `data-…-invalid`, which the next batch\n * clears.\n * - Two files count as the same when name, size, and last-modified time all match;\n * `allowDuplicates` turns the check off.\n * - Accepted files render from `itemTemplate`; the template's own remove-button\n * `aria-label` is kept and its `{name}` expanded, so the accessible name stays in\n * the consumer's language. A template missing a part renders nothing, changes no\n * state, and reports itself once per connection.\n * - The accepted set is mirrored onto the native input, so a plain form submit\n * carries the dropped files with no JavaScript from the consumer. Where\n * `DataTransfer` cannot be constructed the previews and events still work and the\n * input keeps whatever the native dialog last put there.\n * - Removing a file revokes its `objectURL` and moves focus to the next (else\n * previous) remove button, falling back to the trigger.\n * - Additions, rejections, removals, and the drag affordance are handed to the\n * page's shared `stimeo--announcer` as one polite message each, worded by the\n * consumer (`{name}` is the file this message is about, `{count}` how many files\n * it covers, `{total}` how many are selected afterwards). Empty templates stay\n * silent, keeping announcements opt-in and i18n-neutral. One batch produces at\n * most one message per outcome, so dropping twenty files never reads twenty lines.\n * - A drop into a disabled field is refused: with the input `disabled` or inside a\n * disabled `fieldset` the zone never becomes a drop target, matching what the\n * native click and keyboard paths already do.\n * - Replacing the `list` target rebinds delegated removal and moves the\n * client-only previews into the replacement; a morph that empties the list in\n * place moves them back on the next interaction.\n * `reject` dispatches `{ file: File, reason: \"type\" | \"size\" | \"duplicate\" | \"count\" }`.\n * `change` and `reconcile` dispatch `{ files: File[] }`.\n *\n * @remarks\n * Previews are client-only state that no restored snapshot can revive — the `File`\n * objects and their `blob:` URLs die with the page. Just before Turbo caches the\n * page the generated items are removed, every `objectURL` revoked, and both state\n * attributes withdrawn, so a restored snapshot starts pristine instead of showing\n * rows that cannot be removed and do not count towards `maxFiles`. A `disconnect()`\n * that turns out to be an in-page move keeps the selection intact.\n */\nexport class FileDropzoneController extends Controller<HTMLElement> {\n static override targets = [\n \"zone\",\n \"trigger\",\n \"input\",\n \"list\",\n \"item\",\n \"itemTemplate\",\n \"name\",\n \"thumb\",\n \"remove\",\n ];\n static override values = {\n maxSize: { type: Number, default: 0 },\n maxFiles: { type: Number, default: 0 },\n allowDuplicates: { type: Boolean, default: false },\n announceDragText: { type: String, default: \"\" },\n announceAddedText: { type: String, default: \"\" },\n announceRemovedText: { type: String, default: \"\" },\n announceRejectedTypeText: { type: String, default: \"\" },\n announceRejectedSizeText: { type: String, default: \"\" },\n announceRejectedDuplicateText: { type: String, default: \"\" },\n announceRejectedCountText: { type: String, default: \"\" },\n };\n static actions = [\"onChange\", \"onDragLeave\", \"onDragOver\", \"onDrop\", \"openDialog\"] as const;\n static events = [\"change\", \"reject\", \"reconcile\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly listTarget: HTMLElement;\n declare readonly listTargets: HTMLElement[];\n declare readonly triggerTarget: HTMLElement;\n declare readonly itemTemplateTarget: HTMLTemplateElement;\n declare readonly hasInputTarget: boolean;\n declare readonly hasListTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasItemTemplateTarget: boolean;\n declare readonly hasZoneTarget: boolean;\n declare readonly zoneTarget: HTMLElement;\n\n declare maxSizeValue: number;\n declare maxFilesValue: number;\n declare allowDuplicatesValue: boolean;\n declare announceDragTextValue: string;\n declare announceAddedTextValue: string;\n declare announceRemovedTextValue: string;\n declare announceRejectedTypeTextValue: string;\n declare announceRejectedSizeTextValue: string;\n declare announceRejectedDuplicateTextValue: string;\n declare announceRejectedCountTextValue: string;\n\n /** Selected files paired with their rendered item and any preview objectURL. */\n readonly #entries: Entry[] = [];\n /** Whether a drag is currently over the zone; the source for `data-dragover`. */\n #dragging = false;\n /** Whether this connection already reported its unusable item template. */\n #warnedTemplate = false;\n readonly #gate = new DetachGate();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /** Subscribes to the Turbo cache rewind and re-arms the template diagnostic. */\n override connect(): void {\n this.#gate.cancel();\n this.#warnedTemplate = false;\n this.#beforeCache.activate();\n }\n\n /**\n * Releases the delegated listeners. The selection itself survives an in-page\n * move and is released only once the gate proves a real detach — revoking a\n * preview URL on a move would leave a live item pointing at a dead `blob:`.\n */\n override disconnect(): void {\n for (const list of this.listTargets) list.removeEventListener(\"click\", this.#onItemClick);\n this.#beforeCache.deactivate();\n this.#gate.disconnected(this, () => this.#teardown());\n }\n\n /**\n * Binds removal and restores client-only previews for every list this controller\n * renders into — the one present at connect and any Turbo puts in its place.\n * This is the only place the listener is attached, so the pair with\n * {@link listTargetDisconnected} keeps it from outliving the element it is on.\n */\n listTargetConnected(list: HTMLElement): void {\n this.#bindList(list);\n }\n\n /** Releases only the list target that actually disconnected. */\n listTargetDisconnected(list: HTMLElement): void {\n list.removeEventListener(\"click\", this.#onItemClick);\n }\n\n /** Binds delegated removal once and moves live preview items into the current list. */\n #bindList(list: HTMLElement): void {\n list.addEventListener(\"click\", this.#onItemClick);\n for (const entry of this.#entries) {\n if (!list.contains(entry.item)) list.appendChild(entry.item);\n }\n }\n\n /** Opens the native file dialog. Bound via `data-action` (trigger click). */\n openDialog(): void {\n if (this.#isDisabled) return;\n this.inputTarget.click();\n }\n\n /**\n * Adds the files chosen through the native dialog. The input holds only what\n * the dialog just returned, so the accepted set is written back over it once\n * the batch is validated.\n */\n onChange(): void {\n const files = this.inputTarget.files;\n if (files) this.#addFiles(files);\n else this.#syncInput();\n }\n\n /**\n * Marks the zone as a drop target and announces the affordance once per drag.\n * Bound via `data-action` (dragover). Leaving the default alone is what refuses\n * the drop, so a disabled field and a drag an inner dropzone already claimed\n * both fall through untouched.\n */\n onDragOver(event: DragEvent): void {\n if (event.defaultPrevented || this.#isDisabled) return;\n event.preventDefault();\n if (this.#dragging) return;\n this.#dragging = true;\n if (this.hasZoneTarget) this.zoneTarget.setAttribute(DRAGOVER_ATTRIBUTE, \"\");\n // No file is involved yet, so only `{total}` resolves; the rest stay as authored.\n announce(fillTemplate(this.announceDragTextValue, { total: this.#entries.length }));\n }\n\n /**\n * Clears the drag-over flag when the pointer leaves the zone. Bound via\n * `data-action` (dragleave). `dragleave` bubbles from every descendant the\n * pointer crosses, so the flag only drops when the element being entered is\n * outside the zone (or there is none, the pointer having left the window).\n */\n onDragLeave(event: DragEvent): void {\n const next = event.relatedTarget;\n if (this.hasZoneTarget && next instanceof Node && this.zoneTarget.contains(next)) return;\n this.#endDrag();\n }\n\n /** Accepts dropped files, clearing the drag-over state. Bound via `data-action` (drop). */\n onDrop(event: DragEvent): void {\n if (event.defaultPrevented) return;\n this.#endDrag();\n if (this.#isDisabled) return;\n event.preventDefault();\n if (event.dataTransfer?.files) this.#addFiles(event.dataTransfer.files);\n }\n\n /**\n * Removes the file whose remove button was clicked. Delegated on the list\n * container rather than bound per item via `data-action`, so it works the instant\n * an item is appended without waiting on Stimulus to wire a freshly created element.\n * Only the item's declared `remove` target counts, so an authored second control\n * inside an item does what it says instead of silently discarding the file. The\n * button has to belong to a tracked item, which is what makes a list this\n * controller no longer renders into inert — its items moved out with it.\n */\n readonly #onItemClick = (event: MouseEvent): void => {\n const button = (event.target as HTMLElement).closest<HTMLButtonElement>(\n 'button[data-stimeo--file-dropzone-target~=\"remove\"]',\n );\n const index = this.#entries.findIndex((entry) => entry.item.contains(button));\n if (index !== -1) this.#removeAt(index);\n };\n\n /** Validates each incoming file, renders the accepted ones, and reports the batch. */\n #addFiles(files: FileList): void {\n this.#rehome();\n if (this.hasZoneTarget) this.zoneTarget.removeAttribute(INVALID_ATTRIBUTE);\n const rejected = new Map<RejectReason, RejectBatch>();\n const turnedAway: Array<{ file: File; reason: RejectReason }> = [];\n let addedName = \"\";\n let added = 0;\n for (const file of Array.from(files)) {\n const reason = this.#validate(file);\n if (reason !== null) {\n if (this.hasZoneTarget) this.zoneTarget.setAttribute(INVALID_ATTRIBUTE, \"\");\n const batch = rejected.get(reason);\n if (batch) batch.count += 1;\n else rejected.set(reason, { name: file.name, count: 1 });\n turnedAway.push({ file, reason });\n continue;\n }\n // Nothing is rendered when the template is unusable, so nothing was selected\n // either: the set must not report a change it did not make.\n if (!this.#appendFile(file)) continue;\n if (added === 0) addedName = file.name;\n added += 1;\n }\n this.#syncInput();\n // The batch reports what it took before what it turned away — the same order\n // the announcements use. A consumer that clears its rejection notice when a\n // file lands would otherwise wipe the notice for the very drop that raised it.\n if (added > 0) {\n this.#announce(this.announceAddedTextValue, addedName, added);\n this.dispatch(\"change\", { detail: { files: this.#files } });\n }\n for (const { file, reason } of turnedAway) {\n this.dispatch(\"reject\", { detail: { file, reason } });\n }\n for (const [reason, batch] of rejected) {\n this.#announce(this.#rejectText(reason), batch.name, batch.count);\n }\n }\n\n /**\n * Returns the rejection reason for `file`, or `null` when it is acceptable.\n * The file's own defects are decided first, so a full list still tells the user\n * which files it would never have taken.\n */\n #validate(file: File): RejectReason | null {\n if (!this.#matchesAccept(file)) return \"type\";\n if (this.maxSizeValue > 0 && file.size > this.maxSizeValue) return \"size\";\n if (\n !this.allowDuplicatesValue &&\n this.#entries.some((entry) => this.#isSame(entry.file, file))\n ) {\n return \"duplicate\";\n }\n const limit = this.#effectiveMaxFiles;\n if (limit > 0 && this.#entries.length >= limit) return \"count\";\n return null;\n }\n\n /**\n * Whether two files are the same selection. `File` objects from separate picks\n * are never the same reference, so identity is the triple the platform exposes.\n */\n #isSame(a: File, b: File): boolean {\n return a.name === b.name && a.size === b.size && a.lastModified === b.lastModified;\n }\n\n /**\n * Builds one preview item (name, optional thumbnail, remove button) and reports\n * whether it was rendered.\n */\n #appendFile(file: File): boolean {\n if (!this.hasListTarget) return this.#warnTemplate('a \"list\" target to render into');\n if (!this.hasItemTemplateTarget) return this.#warnTemplate('an \"itemTemplate\" target');\n const fragment = this.itemTemplateTarget.content.cloneNode(true) as DocumentFragment;\n const item = this.#slot(fragment, \"item\");\n const name = this.#slot(fragment, \"name\");\n const thumb = this.#slot<HTMLImageElement>(fragment, \"thumb\");\n const button = fragment.querySelector<HTMLButtonElement>(\n 'button[data-stimeo--file-dropzone-target~=\"remove\"]',\n );\n const removeName = button?.getAttribute(\"aria-label\")?.trim() ?? \"\";\n if (!item) return this.#warnTemplate('an \"item\" root');\n if (!name) return this.#warnTemplate('a \"name\" element');\n if (!button) return this.#warnTemplate('a \"remove\" target <button>');\n if (removeName === \"\") {\n return this.#warnTemplate('a non-empty aria-label on its \"remove\" target');\n }\n name.textContent = file.name;\n // The authored label owns the wording and the language; only `{name}` is filled.\n button.setAttribute(\"aria-label\", fillTemplate(removeName, { name: file.name }));\n\n let url: string | undefined;\n if (thumb && file.type.startsWith(\"image/\")) {\n url = URL.createObjectURL(file);\n thumb.src = url;\n thumb.alt = file.name;\n thumb.hidden = false;\n } else if (thumb) {\n thumb.hidden = true;\n }\n\n this.listTarget.appendChild(fragment);\n this.#entries.push({ file, item, url });\n return true;\n }\n\n /** Resolves one declared part inside a cloned item template. */\n #slot<T extends HTMLElement = HTMLElement>(fragment: DocumentFragment, name: string): T | null {\n return fragment.querySelector<T>(`[data-stimeo--file-dropzone-target~=\"${name}\"]`);\n }\n\n /**\n * Reports an unusable item template to the author, once per connection.\n *\n * The addition itself stays a no-op — nothing about the selection, the native\n * input, the announcements, or the events changes. Without this line the only\n * symptom is a picker that accepts no file at all, and the causes the Inspector\n * cannot see statically (a server-rendered template, a name that renders empty\n * from a missing translation) would have no diagnostic anywhere.\n */\n #warnTemplate(missing: string): false {\n if (!this.#warnedTemplate) {\n this.#warnedTemplate = true;\n console.warn(\n `Stimeo UI: \"${this.identifier}\" added no file because its item template lacks ${missing}.`,\n );\n }\n return false;\n }\n\n /** Removes entry `index`, revokes its preview, and re-homes focus. */\n #removeAt(index: number): void {\n const entry = this.#entries[index];\n if (!entry) return;\n if (entry.url) URL.revokeObjectURL(entry.url);\n entry.item.remove();\n this.#entries.splice(index, 1);\n this.#syncInput();\n this.#announce(this.announceRemovedTextValue, entry.file.name, 1);\n this.dispatch(\"change\", { detail: { files: this.#files } });\n\n const buttons = this.#removeButtons;\n if (buttons.length === 0) {\n if (this.hasTriggerTarget) this.triggerTarget.focus();\n } else {\n (buttons[index] ?? buttons[buttons.length - 1])?.focus();\n }\n }\n\n /** Whether `file` satisfies the input's `accept` list (empty accepts all). */\n #matchesAccept(file: File): boolean {\n const accept = this.inputTarget.accept.trim();\n if (accept === \"\") return true;\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n return accept.split(\",\").some((raw) => {\n const token = raw.trim().toLowerCase();\n if (token === \"\") return false;\n if (token.startsWith(\".\")) return name.endsWith(token);\n if (token.endsWith(\"/*\")) return type.startsWith(token.slice(0, -1));\n return type === token;\n });\n }\n\n /**\n * Sends one consumer-worded message to the page's shared announcer. `{name}` is\n * the file the message is about, `{count}` how many files it covers, and\n * `{total}` how many are selected once the batch has settled.\n */\n #announce(template: string, name: string, count: number): void {\n announce(fillTemplate(template, { name, count, total: this.#entries.length }));\n }\n\n /** The consumer's wording for one rejection reason. */\n #rejectText(reason: RejectReason): string {\n switch (reason) {\n case \"type\":\n return this.announceRejectedTypeTextValue;\n case \"size\":\n return this.announceRejectedSizeTextValue;\n case \"duplicate\":\n return this.announceRejectedDuplicateTextValue;\n case \"count\":\n return this.announceRejectedCountTextValue;\n }\n }\n\n /**\n * Mirrors the accepted set onto the native input, so a plain form submit carries\n * the dropped files. Skipped where `DataTransfer` cannot be constructed: the\n * widget keeps working and the consumer still receives every `File` on `change`.\n */\n #syncInput(): void {\n // The rewind runs from the shared before-cache pass, where a throw would rob\n // every later subscriber of its own rewind — and a morph can take the input\n // target away before that point.\n if (!this.hasInputTarget) return;\n const transfer = this.#newTransfer();\n if (!transfer) return;\n for (const entry of this.#entries) transfer.items.add(entry.file);\n this.inputTarget.files = transfer.files;\n }\n\n /** A usable empty `DataTransfer`, or `null` where the platform has none. */\n #newTransfer(): DataTransfer | null {\n try {\n return new DataTransfer();\n } catch {\n // No constructor at all, or one that refuses construction.\n return null;\n }\n }\n\n /**\n * Moves surviving preview items back under the current list. A morph that\n * empties the list in place leaves the selection with no rendering, and the\n * files it holds cannot be rebuilt from the DOM, so the items are re-homed\n * rather than forgotten.\n */\n #rehome(): void {\n if (!this.hasListTarget) return;\n for (const entry of this.#entries) {\n if (!this.listTarget.contains(entry.item)) this.listTarget.appendChild(entry.item);\n }\n }\n\n /** Drops the drag-over state, whether the drag ended in a drop or left the zone. */\n #endDrag(): void {\n this.#dragging = false;\n if (this.hasZoneTarget) this.zoneTarget.removeAttribute(DRAGOVER_ATTRIBUTE);\n }\n\n /**\n * Discards the selection and every state attribute this controller wrote, so\n * neither a cached snapshot nor a stranded subtree keeps items whose files are\n * gone. Silent: `change` means a selection the user changed, and the cache\n * rewind reports itself as `reconcile` instead.\n */\n #rewindForCache(): void {\n const had = this.#entries.length > 0;\n this.#reset();\n // The rewind decides the selection is gone, so consumers that painted from\n // `change` (or from `reject`) can drop what they drew before the snapshot is\n // taken. Nothing to report when there was nothing to discard.\n if (had) this.dispatch(\"reconcile\", { detail: { files: this.#files } });\n }\n\n #reset(): void {\n for (const entry of this.#entries) {\n if (entry.url) URL.revokeObjectURL(entry.url);\n entry.item.remove();\n }\n this.#entries.length = 0;\n this.#syncInput();\n this.#endDrag();\n if (this.hasZoneTarget) this.zoneTarget.removeAttribute(INVALID_ATTRIBUTE);\n }\n\n /** Releases the selection once the disconnect is known to be a real detach. */\n #teardown(): void {\n this.#gate.cancel();\n this.#reset();\n }\n\n /** Whether the field refuses input, natively or through an ancestor `fieldset`. */\n get #isDisabled(): boolean {\n return this.inputTarget.disabled || inheritsFieldsetDisabled(this.inputTarget);\n }\n\n /** Effective file cap: `maxFiles`, or 1 when the input is single-select. */\n get #effectiveMaxFiles(): number {\n if (this.maxFilesValue > 0) return this.maxFilesValue;\n return this.inputTarget.multiple ? 0 : 1;\n }\n\n /** The declared remove button of each rendered item, in selection order. */\n get #removeButtons(): HTMLButtonElement[] {\n const buttons: HTMLButtonElement[] = [];\n for (const entry of this.#entries) {\n const button = entry.item.querySelector<HTMLButtonElement>(\n 'button[data-stimeo--file-dropzone-target~=\"remove\"]',\n );\n if (button) buttons.push(button);\n }\n return buttons;\n }\n\n /** The accepted files in selection order. */\n get #files(): File[] {\n return this.#entries.map((entry) => entry.file);\n }\n}\n"]}
|
|
@@ -22,6 +22,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
22
22
|
* `dismiss` dispatches `{ element, reason }`.
|
|
23
23
|
*
|
|
24
24
|
* `show` dispatches `{ type, message }`.
|
|
25
|
+
* `reconcile` dispatches `{ removed: number }` — the messages the Turbo cache
|
|
26
|
+
* rewind took out, which no `dismiss` will report because nobody dismissed them.
|
|
25
27
|
*
|
|
26
28
|
* @remarks
|
|
27
29
|
* Reading is **delegated to the shared Announcer** — but only for the *initial*,
|
|
@@ -57,7 +59,7 @@ declare class FlashController extends Controller<HTMLElement> {
|
|
|
57
59
|
};
|
|
58
60
|
};
|
|
59
61
|
static actions: readonly ["dismiss"];
|
|
60
|
-
static events: readonly ["show", "dismiss"];
|
|
62
|
+
static events: readonly ["show", "dismiss", "reconcile"];
|
|
61
63
|
readonly regionTarget: HTMLElement;
|
|
62
64
|
readonly messageTargets: HTMLElement[];
|
|
63
65
|
readonly hasRegionTarget: boolean;
|
|
@@ -129,7 +129,7 @@ var FlashController = class extends Controller {
|
|
|
129
129
|
max: { type: Number, default: 0 }
|
|
130
130
|
};
|
|
131
131
|
static actions = ["dismiss"];
|
|
132
|
-
static events = ["show", "dismiss"];
|
|
132
|
+
static events = ["show", "dismiss", "reconcile"];
|
|
133
133
|
#timers = new SafeTimeout();
|
|
134
134
|
#observer = null;
|
|
135
135
|
/** Whether the controller is between `connect()` and `disconnect()`. */
|
|
@@ -175,12 +175,14 @@ var FlashController = class extends Controller {
|
|
|
175
175
|
* only: `dismiss` reports a dismissal, and freezing the page is not one.
|
|
176
176
|
*/
|
|
177
177
|
#rewindForCache() {
|
|
178
|
+
const removed = this.#order.length;
|
|
178
179
|
for (const message of [...this.#order]) {
|
|
179
180
|
message.remove();
|
|
180
181
|
this.#forget(message);
|
|
181
182
|
}
|
|
182
183
|
for (const message of this.#leaving) message.remove();
|
|
183
184
|
this.#leaving.clear();
|
|
185
|
+
if (removed > 0) this.dispatch("reconcile", { detail: { removed } });
|
|
184
186
|
}
|
|
185
187
|
/** Follows a `region` element swapped in — or arriving — at runtime (Turbo Stream). */
|
|
186
188
|
regionTargetConnected() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/safe_timeout.ts","../../src/utils/transition_completion.ts","../../src/controllers/flash_controller.ts"],"names":[],"mappings":"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","file":"flash_controller.js","sourcesContent":["/**\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 * 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 { SafeTimeout } from \"./safe_timeout\";\n\n/** Computed transition fields needed to determine when an element has settled. */\nexport type TransitionStyle = Pick<\n CSSStyleDeclaration,\n \"transitionProperty\" | \"transitionDuration\" | \"transitionDelay\"\n>;\n\n/** Parsed transition property paired with its effective duration and delay. */\ninterface TransitionTiming {\n property: string;\n totalMs: number;\n}\n\n/** Parses a CSS `<time>` into milliseconds, returning `0` for malformed values. */\nfunction timeMs(value: string): number {\n const trimmed = value.trim();\n const amount = Number.parseFloat(trimmed);\n if (!Number.isFinite(amount)) return 0;\n if (trimmed.endsWith(\"ms\")) return amount;\n if (trimmed.endsWith(\"s\")) return amount * 1000;\n return 0;\n}\n\n/** Splits a computed comma-separated CSS list into normalized, non-empty items. */\nfunction cssList(value: string): string[] {\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\n/**\n * Pairs transition properties, durations, and delays using CSS list repetition.\n *\n * The `transition-property` list determines the number of transitions. Shorter\n * duration/delay lists repeat, while extra values are ignored. A negative delay\n * shortens the remaining wall-clock time, and a non-positive total is settled.\n */\nfunction transitionTimings(style: TransitionStyle): TransitionTiming[] {\n const properties = cssList(style.transitionProperty);\n const durations = cssList(style.transitionDuration).map(timeMs);\n const delays = cssList(style.transitionDelay).map(timeMs);\n const effectiveProperties =\n properties.length > 0\n ? properties\n : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => \"all\");\n const effectiveDurations = durations.length > 0 ? durations : [0];\n const effectiveDelays = delays.length > 0 ? delays : [0];\n\n return effectiveProperties\n .filter((property) => property !== \"none\")\n .map((property, index) => ({\n property,\n totalMs: Math.max(\n 0,\n (effectiveDurations[index % effectiveDurations.length] ?? 0) +\n (effectiveDelays[index % effectiveDelays.length] ?? 0),\n ),\n }));\n}\n\n/** Maximum total over already-parsed timings (0 when the list is empty). */\nfunction maxTotalMs(timings: TransitionTiming[]): number {\n return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);\n}\n\n/** Maximum effective transition duration plus delay, in milliseconds. */\nexport function maxTransitionTotalMs(style: TransitionStyle): number {\n return maxTotalMs(transitionTimings(style));\n}\n\n/** Optional tuning for a single {@link TransitionCompletion.wait} call. */\nexport interface TransitionWaitOptions {\n /**\n * Positive wall-clock override (ms) for the bounded fallback, replacing the\n * auto-computed `max + 50ms` timer verbatim. It also keeps the wait armed for\n * a computed 0ms transition (no synchronous completion): the consumer asserts\n * a transition budget that computed styles cannot see. Non-positive and\n * non-finite (`NaN` / `Infinity`) values are ignored.\n */\n timeoutMs?: number;\n}\n\n/**\n * Owns one cancellable wait for an element's CSS transitions to settle.\n *\n * When available, active CSS transitions provide their expanded property names.\n * Tracking those names lets shorthand declarations settle from their longhand\n * terminal events. A terminal event from an interrupted phase is ignored while a\n * replacement transition for the same property is still active. Because that set\n * is sampled when the wait is armed, a property whose transition only starts a\n * frame later would otherwise be settled over; the still-running set is therefore\n * re-read before completing and adopted when it is not yet empty. Without Web\n * Animations data, explicit transition properties retain the declared-property\n * behavior and the ambiguous `transition-property: all` form waits until the\n * computed maximum wall-clock time. A `max + 50ms` timeout is always armed\n * because browsers emit no terminal event when a property never transitions, an\n * ancestor disappears, or rendering is otherwise interrupted; a consumer with\n * better knowledge can replace that fallback via\n * {@link TransitionWaitOptions.timeoutMs}.\n */\nexport class TransitionCompletion {\n readonly #timers = new SafeTimeout();\n #element: HTMLElement | null = null;\n #complete: (() => void) | null = null;\n #pendingProperties: Set<string> | null = null;\n #deadline = 0;\n\n /**\n * Replaces any prior wait and invokes `complete` synchronously for a 0ms\n * transition (including when `getComputedStyle` is unavailable).\n *\n * With a positive `options.timeoutMs` the synchronous fast-path is skipped and\n * the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).\n */\n wait(element: HTMLElement, complete: () => void, options: TransitionWaitOptions = {}): void {\n this.cancel();\n\n const requested = options.timeoutMs ?? 0;\n const override = Number.isFinite(requested) && requested > 0 ? requested : 0;\n // A non-browser / partial-DOM environment cannot report a computable\n // transition, which is indistinguishable from a 0ms one.\n const timings =\n typeof window.getComputedStyle === \"function\"\n ? transitionTimings(window.getComputedStyle(element))\n : [];\n const maximum = maxTotalMs(timings);\n if (maximum <= 0 && override <= 0) {\n complete();\n return;\n }\n\n this.#element = element;\n this.#complete = complete;\n this.#deadline = Date.now() + maximum;\n const activeProperties = this.#activeTransitionProperties(element);\n this.#pendingProperties =\n activeProperties.size > 0 ? activeProperties : this.#explicitPendingProperties(timings);\n element.addEventListener(\"transitionend\", this.#onTerminal);\n element.addEventListener(\"transitioncancel\", this.#onTerminal);\n this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);\n }\n\n /** Cancels the pending wait without invoking its completion callback. */\n cancel(): void {\n this.#complete = null;\n this.#teardown();\n }\n\n /**\n * Handles terminal events from the observed element only.\n *\n * For explicit property lists, every declared positive-time property must\n * settle. For `all`, no reliable property set exists, so an event can finish\n * only after the computed maximum time; the safety timeout owns the usual path.\n */\n readonly #onTerminal = (event: Event): void => {\n if (event.target !== this.#element) return;\n const transitionEvent = event as TransitionEvent;\n // A pseudo-element's transition is reported with its originating element\n // as `target`; it must not settle the element's own transition.\n if (transitionEvent.pseudoElement) return;\n\n if (this.#pendingProperties) {\n const propertyName = transitionEvent.propertyName;\n const active = this.#activeTransitionProperties(this.#element);\n // An interrupted transition queues its cancel event before the replacement\n // phase starts, but dispatch can happen after the new wait is armed. The\n // replacement is then the active transition for this property, so the stale\n // terminal event must not settle the new phase.\n if (active.has(propertyName)) return;\n if (!this.#pendingProperties.delete(propertyName)) return;\n if (this.#pendingProperties.size > 0) return;\n // The tracked set is an arm-time snapshot, so a transition the phase starts\n // one frame later is absent from it. Re-reading the still-running set before\n // settling adopts those stragglers instead of completing over them; the\n // fallback timer stays armed, so this cannot extend the wait unboundedly.\n if (active.size > 0) {\n this.#pendingProperties = active;\n return;\n }\n this.#finish();\n return;\n }\n\n if (Date.now() >= this.#deadline) this.#finish();\n };\n\n /**\n * Returns active CSS transition properties expanded to the names reported by\n * terminal events. CSS animations and pseudo-element effects are excluded.\n */\n #activeTransitionProperties(element: HTMLElement | null): Set<string> {\n if (!element || typeof element.getAnimations !== \"function\") return new Set();\n\n try {\n const properties = element.getAnimations().flatMap((animation) => {\n if (animation.playState === \"idle\" || animation.playState === \"finished\") return [];\n const effect = animation.effect as KeyframeEffect | null;\n // A pseudo-element effect names the originating element as its target, so\n // `target` alone cannot separate it. Its terminal events are ignored, so\n // tracking its properties would only strand the wait on the fallback.\n if (effect?.pseudoElement) return [];\n const target = effect?.target;\n if (target && target !== element) return [];\n const property = (animation as Partial<CSSTransition>).transitionProperty;\n return typeof property === \"string\" && property.length > 0 ? [property] : [];\n });\n return new Set(properties);\n } catch {\n // Partial DOM implementations can expose getAnimations without supporting\n // it. The declared-property tracker and bounded timer remain safe fallbacks.\n return new Set();\n }\n }\n\n /** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */\n #explicitPendingProperties(timings: TransitionTiming[]): Set<string> | null {\n if (timings.some(({ property }) => property === \"all\")) return null;\n const pending = new Set(\n timings.filter(({ totalMs }) => totalMs > 0).map(({ property }) => property),\n );\n // An empty set is only reachable through a timeout override (a 0ms transition\n // otherwise completes synchronously). With nothing to track, fall back to the\n // deadline rule so any terminal event can settle the wait, exactly like `all`.\n return pending.size > 0 ? pending : null;\n }\n\n /** Completes exactly once, releasing listeners and the fallback before the callback. */\n #finish(): void {\n const complete = this.#complete;\n if (!complete) return;\n this.#complete = null;\n this.#teardown();\n complete();\n }\n\n /** Releases the exact element listeners and timer owned by the current wait. */\n #teardown(): void {\n this.#timers.clearAll();\n this.#element?.removeEventListener(\"transitionend\", this.#onTerminal);\n this.#element?.removeEventListener(\"transitioncancel\", this.#onTerminal);\n this.#element = null;\n this.#pendingProperties = null;\n this.#deadline = 0;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { maxTransitionTotalMs } from \"../utils/transition_completion\";\n\n/** Flash types that map to an assertive `alert` (everything else is a polite `status`). */\nconst ASSERTIVE_TYPES = new Set([\"alert\", \"error\"]);\n\n/** Selector for message targets, used by the MutationObserver to spot dynamic inserts. */\nconst MESSAGE_SELECTOR = '[data-stimeo--flash-target=\"message\"]';\n\n/** Per-message auto-dismiss timer bookkeeping (id 0 means paused). */\ninterface FlashTimer {\n id: number;\n startedAt: number;\n remaining: number;\n /** Active pause reasons; the timer resumes only once every one is released. */\n paused: Set<\"focus\" | \"hover\">;\n}\n\n/**\n * Headless **Rails flash bridge**: turns server-rendered (and Turbo Stream-inserted)\n * `flash` elements into live-region announcements with auto-dismiss and a stacking\n * cap (no dedicated APG pattern; follows the WAI-ARIA status/alert guidance and WCAG\n * 2.2 **4.1.3 Status Messages**). The general-purpose sibling is Toast; this one is\n * specialized to the Rails `flash` convention.\n *\n * Markup contract (identifier: `stimeo--flash`):\n * <div data-controller=\"stimeo--flash\" data-stimeo--flash-target=\"region\">\n * <!-- server-rendered or Turbo Stream-inserted -->\n * <div data-stimeo--flash-target=\"message\" data-flash-type=\"notice\">Saved</div>\n * </div>\n *\n * Each message is mapped by `data-flash-type` to `role=\"status\"` (notice) or\n * `role=\"alert\"` (alert/error), flagged `data-flash-state=\"visible\"`, auto-dismissed\n * after `duration` (paused while hovered *or* focused when `pauseOnHover`, and\n * resumed only once both are released), and capped at `max` simultaneous messages.\n * A close button wired to the `dismiss` action removes one manually.\n *\n * `dismiss` dispatches `{ element, reason }`.\n *\n * `show` dispatches `{ type, message }`.\n *\n * @remarks\n * Reading is **delegated to the shared Announcer** — but only for the *initial*,\n * page-loaded messages: an in-place live region present at load is not announced by\n * assistive tech, so the controller bridges those via an `stimeo--announcer:announce`\n * event. Messages inserted *later* (Turbo Stream) are announced by their own freshly\n * inserted `role`, exactly like Toast, so they are not bridged again (no double\n * announcement). Behavior only — no styling; `data-flash-state=\"leaving\"` lets CSS\n * animate removal. Focus is never moved (WCAG 2.2 4.1.3): pausing never removes a\n * message, so the control under the pointer or holding focus stays put. The\n * observation follows a `region` element replaced at runtime; the managed set is the\n * current region's subtree, and a message that leaves it gives up its stacking slot,\n * its pending auto-dismiss, and any removal already scheduled. The observer, timers,\n * and per-message listeners are torn down on `disconnect()` (Turbo navigation\n * included), and the managed flashes leave the page before Turbo caches it so a\n * restored snapshot does not replay a notification the visitor already received.\n */\nexport class FlashController extends Controller<HTMLElement> {\n static override targets = [\"region\", \"message\"];\n static override values = {\n duration: { type: Number, default: 5000 },\n pauseOnHover: { type: Boolean, default: true },\n max: { type: Number, default: 0 },\n };\n static actions = [\"dismiss\"] as const;\n static events = [\"show\", \"dismiss\"] as const;\n\n declare readonly regionTarget: HTMLElement;\n declare readonly messageTargets: HTMLElement[];\n declare readonly hasRegionTarget: boolean;\n\n declare durationValue: number;\n declare pauseOnHoverValue: boolean;\n declare maxValue: number;\n\n readonly #timers = new SafeTimeout();\n #observer: MutationObserver | null = null;\n /** Whether the controller is between `connect()` and `disconnect()`. */\n #connected = false;\n /** Auto-dismiss timer state keyed by message element. */\n readonly #state = new Map<HTMLElement, FlashTimer>();\n /** Messages already processed, in insertion order, to enforce `max` and avoid double work. */\n readonly #order: HTMLElement[] = [];\n /**\n * Messages between `leaving` and their removal. {@link FlashController.#beginDismiss}\n * releases the bookkeeping above *before* the transition wait, so for that window the\n * element is in the DOM but in neither collection — without this set a re-scan would\n * read it as a brand-new flash and show it a second time.\n */\n readonly #leaving = new Set<HTMLElement>();\n\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n readonly #onEnter = (event: Event): void =>\n this.#pause(event.currentTarget as HTMLElement, event.type === \"focusin\" ? \"focus\" : \"hover\");\n readonly #onLeave = (event: Event): void =>\n this.#resume(event.currentTarget as HTMLElement, event.type === \"focusout\" ? \"focus\" : \"hover\");\n\n override connect(): void {\n this.#connected = true;\n // Initial, server-rendered flashes: bridge them to the Announcer because an\n // in-place live region present at page load is not announced on its own.\n for (const message of this.messageTargets) {\n if (this.#owns(message)) this.#process(message, true);\n }\n this.#syncObservation();\n this.#beforeCache.activate();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#beforeCache.deactivate();\n this.#stopObserving();\n this.#timers.clearAll();\n for (const message of this.#order) this.#unbindPause(message);\n this.#state.clear();\n this.#order.length = 0;\n // The pending finalizes died with the timers above, so a message still marked\n // `leaving` is free to be shown again by the next connect (a snapshot restore).\n this.#leaving.clear();\n }\n\n /**\n * Takes the managed flashes out of the page just before Turbo freezes it, so a\n * restored snapshot carries no notification the visitor has already received: the\n * fresh `connect()` there reads a leftover flash as a brand-new one and announces it\n * a second time. A message that never auto-dismisses (`duration: 0`) is one of these\n * too — that value governs the timer, not what belongs in a cached page. Removal\n * only: `dismiss` reports a dismissal, and freezing the page is not one.\n */\n #rewindForCache(): void {\n for (const message of [...this.#order]) {\n message.remove();\n this.#forget(message);\n }\n for (const message of this.#leaving) message.remove();\n this.#leaving.clear();\n }\n\n /** Follows a `region` element swapped in — or arriving — at runtime (Turbo Stream). */\n regionTargetConnected(): void {\n this.#resync();\n }\n\n /** Releases the observation when the `region` element leaves the target set. */\n regionTargetDisconnected(): void {\n this.#resync();\n }\n\n /**\n * Whether this controller owns `message`. Ownership is the current `region`'s\n * subtree: a message target anywhere else in the controller's scope is the\n * consumer's, and so is one in a region that has gone away. The initial scan, a\n * re-scan after a `region` swap, and a departure from the target set all resolve\n * ownership through this one test; the observation gets it structurally, by watching\n * that subtree and nothing else.\n */\n #owns(message: HTMLElement): boolean {\n return this.hasRegionTarget && this.regionTarget.contains(message);\n }\n\n /**\n * Releases a message that left the target set (a Turbo Stream `remove`, the consumer\n * detaching the node, or a morph that rewrote the target attribute in place): it\n * stops occupying a `max` slot, and both its pending auto-dismiss and an already\n * scheduled removal are cancelled. A move *within* the region keeps all of them —\n * which is why the element must still be a message to be treated as one: ownership\n * alone reads an in-place attribute rewrite as a move, and a node outside the target\n * set belongs to the consumer, so nothing here may dismiss it.\n */\n messageTargetDisconnected(message: HTMLElement): void {\n if (!this.#connected) return;\n const moved = this.#owns(message) && message.matches(MESSAGE_SELECTOR);\n if (moved) return;\n this.#forget(message);\n // Ownership is re-checked at both ends of the leaving transition: dropping the\n // claim here is what the pending finalize reads to leave the node alone.\n this.#leaving.delete(message);\n }\n\n /**\n * Re-points the observation after a `region` swap and picks up the messages the\n * new element brought with it (dynamic inserts, so their own `role` announces\n * them). The `#connected` guard is load-bearing: Stimulus runs target callbacks\n * for the initial markup *before* `connect()` and again during teardown *after*\n * `disconnect()`, and re-observing there would outlive the controller.\n */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncObservation();\n for (const message of this.messageTargets) {\n if (this.#owns(message)) this.#process(message, false);\n }\n }\n\n /**\n * Points the mutation observation at the current `region` target, re-resolved on\n * every sync rather than captured at connect, so an element swapped in at runtime\n * is observed instead of the detached original.\n */\n #syncObservation(): void {\n this.#stopObserving();\n if (!this.hasRegionTarget || typeof MutationObserver === \"undefined\") return;\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(this.regionTarget, { childList: true, subtree: true });\n }\n\n #stopObserving(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /**\n * Pause-on-hover/focus listeners, bound and unbound as a pair so the two sides\n * stay in sync. Binding is gated by `pauseOnHover`; unbinding is unconditional\n * and idempotent (a no-op when nothing was bound), which keeps teardown correct\n * even if `pauseOnHover` were ever toggled during a message's life.\n */\n #bindPause(message: HTMLElement): void {\n if (!this.pauseOnHoverValue) return;\n message.addEventListener(\"mouseenter\", this.#onEnter);\n message.addEventListener(\"mouseleave\", this.#onLeave);\n message.addEventListener(\"focusin\", this.#onEnter);\n message.addEventListener(\"focusout\", this.#onLeave);\n }\n\n #unbindPause(message: HTMLElement): void {\n message.removeEventListener(\"mouseenter\", this.#onEnter);\n message.removeEventListener(\"mouseleave\", this.#onLeave);\n message.removeEventListener(\"focusin\", this.#onEnter);\n message.removeEventListener(\"focusout\", this.#onLeave);\n }\n\n /** Dismisses the flash whose close control fired the event. */\n dismiss(event: Event): void {\n const target = (event.currentTarget || event.target) as HTMLElement | null;\n const message = target?.closest<HTMLElement>(MESSAGE_SELECTOR);\n if (message) this.#beginDismiss(message, \"user\");\n }\n\n /** Processes messages added after connect (Turbo Stream); their own role announces them. */\n #onMutations(mutations: MutationRecord[]): void {\n for (const mutation of mutations) {\n for (const node of mutation.addedNodes) {\n if (!(node instanceof HTMLElement)) continue;\n if (node.matches(MESSAGE_SELECTOR)) this.#process(node, false);\n for (const message of node.querySelectorAll<HTMLElement>(MESSAGE_SELECTOR)) {\n this.#process(message, false);\n }\n }\n }\n }\n\n /**\n * Applies role/state, wires pause listeners, schedules auto-dismiss, and either\n * bridges to the Announcer (`bridge`, for initial flashes) or leaves the message's\n * own role to do the announcing (dynamic inserts). Idempotent per message.\n */\n #process(message: HTMLElement, bridge: boolean): void {\n if (this.#state.has(message) || this.#order.includes(message)) return;\n if (this.#leaving.has(message)) return;\n\n const type = message.getAttribute(\"data-flash-type\") ?? \"\";\n const assertive = ASSERTIVE_TYPES.has(type);\n // Don't clobber an authored role; otherwise map the flash type to a live role.\n if (!message.hasAttribute(\"role\")) {\n message.setAttribute(\"role\", assertive ? \"alert\" : \"status\");\n }\n message.setAttribute(\"data-flash-state\", \"visible\");\n this.#order.push(message);\n\n this.#bindPause(message);\n\n const text = message.textContent?.trim() ?? \"\";\n this.dispatch(\"show\", { target: message, detail: { type, message: text } });\n if (bridge && text) {\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", { detail: { message: text, assertive } }),\n );\n }\n\n this.#startTimer(message);\n this.#enforceMax();\n }\n\n /** Removes the oldest visible flashes once the count exceeds `max` (0 = unlimited). */\n #enforceMax(): void {\n if (this.maxValue <= 0) return;\n while (this.#order.length > this.maxValue) {\n const oldest = this.#order[0];\n if (!oldest) break;\n this.#beginDismiss(oldest, \"limit\");\n }\n }\n\n #startTimer(message: HTMLElement, duration = this.durationValue): void {\n if (duration <= 0) return;\n const existing = this.#state.get(message);\n if (existing?.id) this.#timers.clear(existing.id);\n const id = this.#timers.set(() => this.#beginDismiss(message, \"timeout\"), duration);\n this.#state.set(message, {\n id,\n startedAt: Date.now(),\n remaining: duration,\n paused: existing?.paused ?? new Set(),\n });\n }\n\n /**\n * Pauses a message's auto-dismiss, banking the time left (hover/focus, WCAG 2.2.1).\n * Hover and focus are independent reasons: the remaining time is banked on the\n * first of them, and {@link FlashController.#resume} waits for the last one.\n */\n #pause(message: HTMLElement, reason: \"focus\" | \"hover\"): void {\n const timer = this.#state.get(message);\n if (!timer) return;\n timer.paused.add(reason);\n if (timer.id === 0) return;\n\n this.#timers.clear(timer.id);\n // The bank has a floor of 1ms, so a deadline already spent when the pause arrives\n // (the timer sat queued in a throttled tab or behind a long task) settles right\n // after the last reason is released rather than during the event that paused it.\n // Pausing therefore never removes a message — which is what keeps the pointer's\n // target under the pointer and the focused control focused (WCAG 2.2 4.1.3).\n // Banking zero instead would strand the message: nothing resumes a spent window.\n const remaining = Math.max(1, timer.remaining - (Date.now() - timer.startedAt));\n this.#state.set(message, { id: 0, startedAt: 0, remaining, paused: timer.paused });\n }\n\n /** Resumes a paused message's auto-dismiss with the banked time. */\n #resume(message: HTMLElement, reason: \"focus\" | \"hover\"): void {\n const timer = this.#state.get(message);\n if (!timer) return;\n timer.paused.delete(reason);\n // Still held by the other reason (the pointer left, focus stayed, or vice versa).\n if (timer.paused.size > 0) return;\n // Only resume a genuinely paused timer; a running one keeps its own deadline.\n if (timer.id !== 0) return;\n this.#startTimer(message, timer.remaining);\n }\n\n /** Releases every per-message resource: timer, stacking slot, pause listeners. */\n #forget(message: HTMLElement): void {\n const timer = this.#state.get(message);\n if (timer?.id) this.#timers.clear(timer.id);\n this.#state.delete(message);\n const index = this.#order.indexOf(message);\n if (index !== -1) this.#order.splice(index, 1);\n this.#unbindPause(message);\n }\n\n /** Marks a message leaving, then removes it after its CSS transition and emits dismiss. */\n #beginDismiss(message: HTMLElement, reason: \"timeout\" | \"user\" | \"limit\"): void {\n // One removal, one `dismiss`: a close control fired during the leaving\n // transition, or a repeated request, finds the message already released and\n // must not start a second finalize. Reading the bookkeeping rather than\n // `data-flash-state` also keeps `#enforceMax`'s loop terminating, since the\n // release below is what shrinks `#order`.\n if (!this.#state.has(message) && !this.#order.includes(message)) return;\n this.#forget(message);\n this.#leaving.add(message);\n\n message.setAttribute(\"data-flash-state\", \"leaving\");\n\n const finalize = (): void => {\n // The claim taken above is the license to remove: a message that lost the target\n // attribute during the transition belongs to the consumer by the time this runs.\n if (!this.#leaving.delete(message)) return;\n message.remove();\n this.dispatch(\"dismiss\", { detail: { element: message, reason } });\n };\n\n const transition = this.#transitionMs(message);\n if (transition > 0) {\n this.#timers.set(finalize, transition);\n } else {\n finalize();\n }\n }\n\n /** Maximum transition total (duration + delay) of `el` in ms (0 when none / unsupported). */\n #transitionMs(el: HTMLElement): number {\n if (typeof window.getComputedStyle !== \"function\") return 0;\n return maxTransitionTotalMs(window.getComputedStyle(el));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/safe_timeout.ts","../../src/utils/transition_completion.ts","../../src/controllers/flash_controller.ts"],"names":[],"mappings":";;;;;AA2CO,IAAM,gBAAA,GAAN,MAAM,iBAAA,CAAiB;AAAA;AAAA,EAE5B,OAAgB,YAAA,mBAAe,IAAI,GAAA,EAAsB;AAAA;AAAA,EAGzD,OAAgB,iBAAiB,MAAY;AAC3C,IAAA,KAAA,MAAW,UAAA,IAAc,iBAAA,CAAiB,YAAA,EAAc,UAAA,CAAW,OAAA,EAAQ;AAAA,EAC7E,CAAA;AAAA,EAES,OAAA;AAAA;AAAA,EAGT,YAAY,MAAA,EAAoB;AAC9B,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AAAA,EACjB;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,MAAM,KAAA,GAAQ,iBAAA,CAAiB,YAAA,CAAa,IAAA,KAAS,CAAA;AACrD,IAAA,iBAAA,CAAiB,YAAA,CAAa,IAAI,IAAI,CAAA;AACtC,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAA,EAAsB,iBAAA,CAAiB,cAAc,CAAA;AAAA,IACjF;AAAA,EACF;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,iBAAA,CAAiB,YAAA,CAAa,OAAO,IAAI,CAAA;AACzC,IAAA,IAAI,iBAAA,CAAiB,YAAA,CAAa,IAAA,GAAO,CAAA,EAAG;AAC5C,IAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAA,EAAsB,iBAAA,CAAiB,cAAc,CAAA;AAAA,EACpF;AACF,CAAA;;;AClDA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AAmBO,IAAM,WAAA,GAAN,cAA0B,aAAA,CAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,MAAM;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAClB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EACxB;AACF,CAAA;;;ACxFA,SAAS,OAAO,KAAA,EAAuB;AACrC,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,UAAA,CAAW,OAAO,CAAA;AACxC,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,MAAM,GAAG,OAAO,CAAA;AACrC,EAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,IAAI,CAAA,EAAG,OAAO,MAAA;AACnC,EAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,GAAG,CAAA,SAAU,MAAA,GAAS,GAAA;AAC3C,EAAA,OAAO,CAAA;AACT;AAGA,SAAS,QAAQ,KAAA,EAAyB;AACxC,EAAA,OAAO,KAAA,CACJ,KAAA,CAAM,GAAG,CAAA,CACT,GAAA,CAAI,CAAC,IAAA,KAAS,IAAA,CAAK,IAAA,EAAM,CAAA,CACzB,MAAA,CAAO,OAAO,CAAA;AACnB;AASA,SAAS,kBAAkB,KAAA,EAA4C;AACrE,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,KAAA,CAAM,kBAAkB,CAAA;AACnD,EAAA,MAAM,YAAY,OAAA,CAAQ,KAAA,CAAM,kBAAkB,CAAA,CAAE,IAAI,MAAM,CAAA;AAC9D,EAAA,MAAM,SAAS,OAAA,CAAQ,KAAA,CAAM,eAAe,CAAA,CAAE,IAAI,MAAM,CAAA;AACxD,EAAA,MAAM,sBACJ,UAAA,CAAW,MAAA,GAAS,IAChB,UAAA,GACA,KAAA,CAAM,KAAK,EAAE,MAAA,EAAQ,KAAK,GAAA,CAAI,SAAA,CAAU,QAAQ,MAAA,CAAO,MAAA,EAAQ,CAAC,CAAA,EAAE,EAAG,MAAM,KAAK,CAAA;AACtF,EAAA,MAAM,qBAAqB,SAAA,CAAU,MAAA,GAAS,CAAA,GAAI,SAAA,GAAY,CAAC,CAAC,CAAA;AAChE,EAAA,MAAM,kBAAkB,MAAA,CAAO,MAAA,GAAS,CAAA,GAAI,MAAA,GAAS,CAAC,CAAC,CAAA;AAEvD,EAAA,OAAO,mBAAA,CACJ,MAAA,CAAO,CAAC,QAAA,KAAa,QAAA,KAAa,MAAM,CAAA,CACxC,GAAA,CAAI,CAAC,QAAA,EAAU,KAAA,MAAW;AAAA,IACzB,QAAA;AAAA,IACA,SAAS,IAAA,CAAK,GAAA;AAAA,MACZ,CAAA;AAAA,MAAA,CACC,kBAAA,CAAmB,KAAA,GAAQ,kBAAA,CAAmB,MAAM,CAAA,IAAK,MACvD,eAAA,CAAgB,KAAA,GAAQ,eAAA,CAAgB,MAAM,CAAA,IAAK,CAAA;AAAA;AACxD,GACF,CAAE,CAAA;AACN;AAGA,SAAS,WAAW,OAAA,EAAqC;AACvD,EAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,CAAC,GAAA,EAAK,EAAE,OAAA,EAAQ,KAAM,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,OAAO,CAAA,EAAG,CAAC,CAAA;AACvE;AAGO,SAAS,qBAAqB,KAAA,EAAgC;AACnE,EAAA,OAAO,UAAA,CAAW,iBAAA,CAAkB,KAAK,CAAC,CAAA;AAC5C;;;AChEA,IAAM,kCAAkB,IAAI,GAAA,CAAI,CAAC,OAAA,EAAS,OAAO,CAAC,CAAA;AAGlD,IAAM,gBAAA,GAAmB,uCAAA;AAoDlB,IAAM,eAAA,GAAN,cAA8B,UAAA,CAAwB;AAAA,EAC3D,OAAgB,OAAA,GAAU,CAAC,QAAA,EAAU,SAAS,CAAA;AAAA,EAC9C,OAAgB,MAAA,GAAS;AAAA,IACvB,QAAA,EAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,GAAA,EAAK;AAAA,IACxC,YAAA,EAAc,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA,EAAK;AAAA,IAC7C,GAAA,EAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA;AAAE,GAClC;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,SAAS,CAAA;AAAA,EAC3B,OAAO,MAAA,GAAS,CAAC,MAAA,EAAQ,WAAW,WAAW,CAAA;AAAA,EAUtC,OAAA,GAAU,IAAI,WAAA,EAAY;AAAA,EACnC,SAAA,GAAqC,IAAA;AAAA;AAAA,EAErC,UAAA,GAAa,KAAA;AAAA;AAAA,EAEJ,MAAA,uBAAa,GAAA,EAA6B;AAAA;AAAA,EAE1C,SAAwB,EAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOzB,QAAA,uBAAe,GAAA,EAAiB;AAAA,EAEhC,eAAe,IAAI,gBAAA,CAAiB,MAAM,IAAA,CAAK,iBAAiB,CAAA;AAAA,EAEhE,QAAA,GAAW,CAAC,KAAA,KACnB,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,aAAA,EAA8B,KAAA,CAAM,IAAA,KAAS,SAAA,GAAY,OAAA,GAAU,OAAO,CAAA;AAAA,EACrF,QAAA,GAAW,CAAC,KAAA,KACnB,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,aAAA,EAA8B,KAAA,CAAM,IAAA,KAAS,UAAA,GAAa,OAAA,GAAU,OAAO,CAAA;AAAA,EAEvF,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAGlB,IAAA,KAAA,MAAW,OAAA,IAAW,KAAK,cAAA,EAAgB;AACzC,MAAA,IAAI,KAAK,KAAA,CAAM,OAAO,GAAG,IAAA,CAAK,QAAA,CAAS,SAAS,IAAI,CAAA;AAAA,IACtD;AACA,IAAA,IAAA,CAAK,gBAAA,EAAiB;AACtB,IAAA,IAAA,CAAK,aAAa,QAAA,EAAS;AAAA,EAC7B;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AACtB,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,MAAA,EAAQ,IAAA,CAAK,aAAa,OAAO,CAAA;AAC5D,IAAA,IAAA,CAAK,OAAO,KAAA,EAAM;AAClB,IAAA,IAAA,CAAK,OAAO,MAAA,GAAS,CAAA;AAGrB,IAAA,IAAA,CAAK,SAAS,KAAA,EAAM;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,eAAA,GAAwB;AACtB,IAAA,MAAM,OAAA,GAAU,KAAK,MAAA,CAAO,MAAA;AAC5B,IAAA,KAAA,MAAW,OAAA,IAAW,CAAC,GAAG,IAAA,CAAK,MAAM,CAAA,EAAG;AACtC,MAAA,OAAA,CAAQ,MAAA,EAAO;AACf,MAAA,IAAA,CAAK,QAAQ,OAAO,CAAA;AAAA,IACtB;AACA,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,QAAA,EAAU,OAAA,CAAQ,MAAA,EAAO;AACpD,IAAA,IAAA,CAAK,SAAS,KAAA,EAAM;AACpB,IAAA,IAAI,OAAA,GAAU,CAAA,EAAG,IAAA,CAAK,QAAA,CAAS,WAAA,EAAa,EAAE,MAAA,EAAQ,EAAE,OAAA,EAAQ,EAAG,CAAA;AAAA,EACrE;AAAA;AAAA,EAGA,qBAAA,GAA8B;AAC5B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA,EAGA,wBAAA,GAAiC;AAC/B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,OAAA,EAA+B;AACnC,IAAA,OAAO,IAAA,CAAK,eAAA,IAAmB,IAAA,CAAK,YAAA,CAAa,SAAS,OAAO,CAAA;AAAA,EACnE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,0BAA0B,OAAA,EAA4B;AACpD,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,MAAM,QAAQ,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA,IAAK,OAAA,CAAQ,QAAQ,gBAAgB,CAAA;AACrE,IAAA,IAAI,KAAA,EAAO;AACX,IAAA,IAAA,CAAK,QAAQ,OAAO,CAAA;AAGpB,IAAA,IAAA,CAAK,QAAA,CAAS,OAAO,OAAO,CAAA;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,OAAA,GAAgB;AACd,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,gBAAA,EAAiB;AACtB,IAAA,KAAA,MAAW,OAAA,IAAW,KAAK,cAAA,EAAgB;AACzC,MAAA,IAAI,KAAK,KAAA,CAAM,OAAO,GAAG,IAAA,CAAK,QAAA,CAAS,SAAS,KAAK,CAAA;AAAA,IACvD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,gBAAA,GAAyB;AACvB,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,IAAI,CAAC,IAAA,CAAK,eAAA,IAAmB,OAAO,qBAAqB,WAAA,EAAa;AACtE,IAAA,IAAA,CAAK,SAAA,GAAY,IAAI,gBAAA,CAAiB,CAAC,cAAc,IAAA,CAAK,YAAA,CAAa,SAAS,CAAC,CAAA;AACjF,IAAA,IAAA,CAAK,SAAA,CAAU,QAAQ,IAAA,CAAK,YAAA,EAAc,EAAE,SAAA,EAAW,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,CAAA;AAAA,EAC9E;AAAA,EAEA,cAAA,GAAuB;AACrB,IAAA,IAAA,CAAK,WAAW,UAAA,EAAW;AAC3B,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,WAAW,OAAA,EAA4B;AACrC,IAAA,IAAI,CAAC,KAAK,iBAAA,EAAmB;AAC7B,IAAA,OAAA,CAAQ,gBAAA,CAAiB,YAAA,EAAc,IAAA,CAAK,QAAQ,CAAA;AACpD,IAAA,OAAA,CAAQ,gBAAA,CAAiB,YAAA,EAAc,IAAA,CAAK,QAAQ,CAAA;AACpD,IAAA,OAAA,CAAQ,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,QAAQ,CAAA;AACjD,IAAA,OAAA,CAAQ,gBAAA,CAAiB,UAAA,EAAY,IAAA,CAAK,QAAQ,CAAA;AAAA,EACpD;AAAA,EAEA,aAAa,OAAA,EAA4B;AACvC,IAAA,OAAA,CAAQ,mBAAA,CAAoB,YAAA,EAAc,IAAA,CAAK,QAAQ,CAAA;AACvD,IAAA,OAAA,CAAQ,mBAAA,CAAoB,YAAA,EAAc,IAAA,CAAK,QAAQ,CAAA;AACvD,IAAA,OAAA,CAAQ,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,QAAQ,CAAA;AACpD,IAAA,OAAA,CAAQ,mBAAA,CAAoB,UAAA,EAAY,IAAA,CAAK,QAAQ,CAAA;AAAA,EACvD;AAAA;AAAA,EAGA,QAAQ,KAAA,EAAoB;AAC1B,IAAA,MAAM,MAAA,GAAU,KAAA,CAAM,aAAA,IAAiB,KAAA,CAAM,MAAA;AAC7C,IAAA,MAAM,OAAA,GAAU,MAAA,EAAQ,OAAA,CAAqB,gBAAgB,CAAA;AAC7D,IAAA,IAAI,OAAA,EAAS,IAAA,CAAK,aAAA,CAAc,OAAA,EAAS,MAAM,CAAA;AAAA,EACjD;AAAA;AAAA,EAGA,aAAa,SAAA,EAAmC;AAC9C,IAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AAChC,MAAA,KAAA,MAAW,IAAA,IAAQ,SAAS,UAAA,EAAY;AACtC,QAAA,IAAI,EAAE,gBAAgB,WAAA,CAAA,EAAc;AACpC,QAAA,IAAI,KAAK,OAAA,CAAQ,gBAAgB,GAAG,IAAA,CAAK,QAAA,CAAS,MAAM,KAAK,CAAA;AAC7D,QAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,gBAAA,CAA8B,gBAAgB,CAAA,EAAG;AAC1E,UAAA,IAAA,CAAK,QAAA,CAAS,SAAS,KAAK,CAAA;AAAA,QAC9B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAA,CAAS,SAAsB,MAAA,EAAuB;AACpD,IAAA,IAAI,IAAA,CAAK,OAAO,GAAA,CAAI,OAAO,KAAK,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AAC/D,IAAA,IAAI,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA,EAAG;AAEhC,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,YAAA,CAAa,iBAAiB,CAAA,IAAK,EAAA;AACxD,IAAA,MAAM,SAAA,GAAY,eAAA,CAAgB,GAAA,CAAI,IAAI,CAAA;AAE1C,IAAA,IAAI,CAAC,OAAA,CAAQ,YAAA,CAAa,MAAM,CAAA,EAAG;AACjC,MAAA,OAAA,CAAQ,YAAA,CAAa,MAAA,EAAQ,SAAA,GAAY,OAAA,GAAU,QAAQ,CAAA;AAAA,IAC7D;AACA,IAAA,OAAA,CAAQ,YAAA,CAAa,oBAAoB,SAAS,CAAA;AAClD,IAAA,IAAA,CAAK,MAAA,CAAO,KAAK,OAAO,CAAA;AAExB,IAAA,IAAA,CAAK,WAAW,OAAO,CAAA;AAEvB,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,WAAA,EAAa,IAAA,EAAK,IAAK,EAAA;AAC5C,IAAA,IAAA,CAAK,QAAA,CAAS,MAAA,EAAQ,EAAE,MAAA,EAAQ,OAAA,EAAS,MAAA,EAAQ,EAAE,IAAA,EAAM,OAAA,EAAS,IAAA,EAAK,EAAG,CAAA;AAC1E,IAAA,IAAI,UAAU,IAAA,EAAM;AAClB,MAAA,MAAA,CAAO,aAAA;AAAA,QACL,IAAI,WAAA,CAAY,4BAAA,EAA8B,EAAE,MAAA,EAAQ,EAAE,OAAA,EAAS,IAAA,EAAM,SAAA,EAAU,EAAG;AAAA,OACxF;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,YAAY,OAAO,CAAA;AACxB,IAAA,IAAA,CAAK,WAAA,EAAY;AAAA,EACnB;AAAA;AAAA,EAGA,WAAA,GAAoB;AAClB,IAAA,IAAI,IAAA,CAAK,YAAY,CAAA,EAAG;AACxB,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,MAAA,GAAS,IAAA,CAAK,QAAA,EAAU;AACzC,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA;AAC5B,MAAA,IAAI,CAAC,MAAA,EAAQ;AACb,MAAA,IAAA,CAAK,aAAA,CAAc,QAAQ,OAAO,CAAA;AAAA,IACpC;AAAA,EACF;AAAA,EAEA,WAAA,CAAY,OAAA,EAAsB,QAAA,GAAW,IAAA,CAAK,aAAA,EAAqB;AACrE,IAAA,IAAI,YAAY,CAAA,EAAG;AACnB,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA;AACxC,IAAA,IAAI,UAAU,EAAA,EAAI,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,SAAS,EAAE,CAAA;AAChD,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,MAAM,KAAK,aAAA,CAAc,OAAA,EAAS,SAAS,CAAA,EAAG,QAAQ,CAAA;AAClF,IAAA,IAAA,CAAK,MAAA,CAAO,IAAI,OAAA,EAAS;AAAA,MACvB,EAAA;AAAA,MACA,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,MACpB,SAAA,EAAW,QAAA;AAAA,MACX,MAAA,EAAQ,QAAA,EAAU,MAAA,oBAAU,IAAI,GAAA;AAAI,KACrC,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAA,CAAO,SAAsB,MAAA,EAAiC;AAC5D,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA;AACrC,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,KAAA,CAAM,MAAA,CAAO,IAAI,MAAM,CAAA;AACvB,IAAA,IAAI,KAAA,CAAM,OAAO,CAAA,EAAG;AAEpB,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,KAAA,CAAM,EAAE,CAAA;AAO3B,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,CAAM,aAAa,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA,CAAM,SAAA,CAAU,CAAA;AAC9E,IAAA,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,OAAA,EAAS,EAAE,EAAA,EAAI,CAAA,EAAG,SAAA,EAAW,CAAA,EAAG,SAAA,EAAW,MAAA,EAAQ,KAAA,CAAM,MAAA,EAAQ,CAAA;AAAA,EACnF;AAAA;AAAA,EAGA,OAAA,CAAQ,SAAsB,MAAA,EAAiC;AAC7D,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA;AACrC,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,KAAA,CAAM,MAAA,CAAO,OAAO,MAAM,CAAA;AAE1B,IAAA,IAAI,KAAA,CAAM,MAAA,CAAO,IAAA,GAAO,CAAA,EAAG;AAE3B,IAAA,IAAI,KAAA,CAAM,OAAO,CAAA,EAAG;AACpB,IAAA,IAAA,CAAK,WAAA,CAAY,OAAA,EAAS,KAAA,CAAM,SAAS,CAAA;AAAA,EAC3C;AAAA;AAAA,EAGA,QAAQ,OAAA,EAA4B;AAClC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA;AACrC,IAAA,IAAI,OAAO,EAAA,EAAI,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,MAAM,EAAE,CAAA;AAC1C,IAAA,IAAA,CAAK,MAAA,CAAO,OAAO,OAAO,CAAA;AAC1B,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA;AACzC,IAAA,IAAI,UAAU,EAAA,EAAI,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,OAAO,CAAC,CAAA;AAC7C,IAAA,IAAA,CAAK,aAAa,OAAO,CAAA;AAAA,EAC3B;AAAA;AAAA,EAGA,aAAA,CAAc,SAAsB,MAAA,EAA4C;AAM9E,IAAA,IAAI,CAAC,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA,IAAK,CAAC,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AACjE,IAAA,IAAA,CAAK,QAAQ,OAAO,CAAA;AACpB,IAAA,IAAA,CAAK,QAAA,CAAS,IAAI,OAAO,CAAA;AAEzB,IAAA,OAAA,CAAQ,YAAA,CAAa,oBAAoB,SAAS,CAAA;AAElD,IAAA,MAAM,WAAW,MAAY;AAG3B,MAAA,IAAI,CAAC,IAAA,CAAK,QAAA,CAAS,MAAA,CAAO,OAAO,CAAA,EAAG;AACpC,MAAA,OAAA,CAAQ,MAAA,EAAO;AACf,MAAA,IAAA,CAAK,QAAA,CAAS,WAAW,EAAE,MAAA,EAAQ,EAAE,OAAA,EAAS,OAAA,EAAS,MAAA,EAAO,EAAG,CAAA;AAAA,IACnE,CAAA;AAEA,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,aAAA,CAAc,OAAO,CAAA;AAC7C,IAAA,IAAI,aAAa,CAAA,EAAG;AAClB,MAAA,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,QAAA,EAAU,UAAU,CAAA;AAAA,IACvC,CAAA,MAAO;AACL,MAAA,QAAA,EAAS;AAAA,IACX;AAAA,EACF;AAAA;AAAA,EAGA,cAAc,EAAA,EAAyB;AACrC,IAAA,IAAI,OAAO,MAAA,CAAO,gBAAA,KAAqB,UAAA,EAAY,OAAO,CAAA;AAC1D,IAAA,OAAO,oBAAA,CAAqB,MAAA,CAAO,gBAAA,CAAiB,EAAE,CAAC,CAAA;AAAA,EACzD;AACF","file":"flash_controller.js","sourcesContent":["/**\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 * 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 { SafeTimeout } from \"./safe_timeout\";\n\n/** Computed transition fields needed to determine when an element has settled. */\nexport type TransitionStyle = Pick<\n CSSStyleDeclaration,\n \"transitionProperty\" | \"transitionDuration\" | \"transitionDelay\"\n>;\n\n/** Parsed transition property paired with its effective duration and delay. */\ninterface TransitionTiming {\n property: string;\n totalMs: number;\n}\n\n/** Parses a CSS `<time>` into milliseconds, returning `0` for malformed values. */\nfunction timeMs(value: string): number {\n const trimmed = value.trim();\n const amount = Number.parseFloat(trimmed);\n if (!Number.isFinite(amount)) return 0;\n if (trimmed.endsWith(\"ms\")) return amount;\n if (trimmed.endsWith(\"s\")) return amount * 1000;\n return 0;\n}\n\n/** Splits a computed comma-separated CSS list into normalized, non-empty items. */\nfunction cssList(value: string): string[] {\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\n/**\n * Pairs transition properties, durations, and delays using CSS list repetition.\n *\n * The `transition-property` list determines the number of transitions. Shorter\n * duration/delay lists repeat, while extra values are ignored. A negative delay\n * shortens the remaining wall-clock time, and a non-positive total is settled.\n */\nfunction transitionTimings(style: TransitionStyle): TransitionTiming[] {\n const properties = cssList(style.transitionProperty);\n const durations = cssList(style.transitionDuration).map(timeMs);\n const delays = cssList(style.transitionDelay).map(timeMs);\n const effectiveProperties =\n properties.length > 0\n ? properties\n : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => \"all\");\n const effectiveDurations = durations.length > 0 ? durations : [0];\n const effectiveDelays = delays.length > 0 ? delays : [0];\n\n return effectiveProperties\n .filter((property) => property !== \"none\")\n .map((property, index) => ({\n property,\n totalMs: Math.max(\n 0,\n (effectiveDurations[index % effectiveDurations.length] ?? 0) +\n (effectiveDelays[index % effectiveDelays.length] ?? 0),\n ),\n }));\n}\n\n/** Maximum total over already-parsed timings (0 when the list is empty). */\nfunction maxTotalMs(timings: TransitionTiming[]): number {\n return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);\n}\n\n/** Maximum effective transition duration plus delay, in milliseconds. */\nexport function maxTransitionTotalMs(style: TransitionStyle): number {\n return maxTotalMs(transitionTimings(style));\n}\n\n/** Optional tuning for a single {@link TransitionCompletion.wait} call. */\nexport interface TransitionWaitOptions {\n /**\n * Positive wall-clock override (ms) for the bounded fallback, replacing the\n * auto-computed `max + 50ms` timer verbatim. It also keeps the wait armed for\n * a computed 0ms transition (no synchronous completion): the consumer asserts\n * a transition budget that computed styles cannot see. Non-positive and\n * non-finite (`NaN` / `Infinity`) values are ignored.\n */\n timeoutMs?: number;\n}\n\n/**\n * Owns one cancellable wait for an element's CSS transitions to settle.\n *\n * When available, active CSS transitions provide their expanded property names.\n * Tracking those names lets shorthand declarations settle from their longhand\n * terminal events. A terminal event from an interrupted phase is ignored while a\n * replacement transition for the same property is still active. Because that set\n * is sampled when the wait is armed, a property whose transition only starts a\n * frame later would otherwise be settled over; the still-running set is therefore\n * re-read before completing and adopted when it is not yet empty. Without Web\n * Animations data, explicit transition properties retain the declared-property\n * behavior and the ambiguous `transition-property: all` form waits until the\n * computed maximum wall-clock time. A `max + 50ms` timeout is always armed\n * because browsers emit no terminal event when a property never transitions, an\n * ancestor disappears, or rendering is otherwise interrupted; a consumer with\n * better knowledge can replace that fallback via\n * {@link TransitionWaitOptions.timeoutMs}.\n */\nexport class TransitionCompletion {\n readonly #timers = new SafeTimeout();\n #element: HTMLElement | null = null;\n #complete: (() => void) | null = null;\n #pendingProperties: Set<string> | null = null;\n #deadline = 0;\n\n /**\n * Replaces any prior wait and invokes `complete` synchronously for a 0ms\n * transition (including when `getComputedStyle` is unavailable).\n *\n * With a positive `options.timeoutMs` the synchronous fast-path is skipped and\n * the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).\n */\n wait(element: HTMLElement, complete: () => void, options: TransitionWaitOptions = {}): void {\n this.cancel();\n\n const requested = options.timeoutMs ?? 0;\n const override = Number.isFinite(requested) && requested > 0 ? requested : 0;\n // A non-browser / partial-DOM environment cannot report a computable\n // transition, which is indistinguishable from a 0ms one.\n const timings =\n typeof window.getComputedStyle === \"function\"\n ? transitionTimings(window.getComputedStyle(element))\n : [];\n const maximum = maxTotalMs(timings);\n if (maximum <= 0 && override <= 0) {\n complete();\n return;\n }\n\n this.#element = element;\n this.#complete = complete;\n this.#deadline = Date.now() + maximum;\n const activeProperties = this.#activeTransitionProperties(element);\n this.#pendingProperties =\n activeProperties.size > 0 ? activeProperties : this.#explicitPendingProperties(timings);\n element.addEventListener(\"transitionend\", this.#onTerminal);\n element.addEventListener(\"transitioncancel\", this.#onTerminal);\n this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);\n }\n\n /** Cancels the pending wait without invoking its completion callback. */\n cancel(): void {\n this.#complete = null;\n this.#teardown();\n }\n\n /**\n * Handles terminal events from the observed element only.\n *\n * For explicit property lists, every declared positive-time property must\n * settle. For `all`, no reliable property set exists, so an event can finish\n * only after the computed maximum time; the safety timeout owns the usual path.\n */\n readonly #onTerminal = (event: Event): void => {\n if (event.target !== this.#element) return;\n const transitionEvent = event as TransitionEvent;\n // A pseudo-element's transition is reported with its originating element\n // as `target`; it must not settle the element's own transition.\n if (transitionEvent.pseudoElement) return;\n\n if (this.#pendingProperties) {\n const propertyName = transitionEvent.propertyName;\n const active = this.#activeTransitionProperties(this.#element);\n // An interrupted transition queues its cancel event before the replacement\n // phase starts, but dispatch can happen after the new wait is armed. The\n // replacement is then the active transition for this property, so the stale\n // terminal event must not settle the new phase.\n if (active.has(propertyName)) return;\n if (!this.#pendingProperties.delete(propertyName)) return;\n if (this.#pendingProperties.size > 0) return;\n // The tracked set is an arm-time snapshot, so a transition the phase starts\n // one frame later is absent from it. Re-reading the still-running set before\n // settling adopts those stragglers instead of completing over them; the\n // fallback timer stays armed, so this cannot extend the wait unboundedly.\n if (active.size > 0) {\n this.#pendingProperties = active;\n return;\n }\n this.#finish();\n return;\n }\n\n if (Date.now() >= this.#deadline) this.#finish();\n };\n\n /**\n * Returns active CSS transition properties expanded to the names reported by\n * terminal events. CSS animations and pseudo-element effects are excluded.\n */\n #activeTransitionProperties(element: HTMLElement | null): Set<string> {\n if (!element || typeof element.getAnimations !== \"function\") return new Set();\n\n try {\n const properties = element.getAnimations().flatMap((animation) => {\n if (animation.playState === \"idle\" || animation.playState === \"finished\") return [];\n const effect = animation.effect as KeyframeEffect | null;\n // A pseudo-element effect names the originating element as its target, so\n // `target` alone cannot separate it. Its terminal events are ignored, so\n // tracking its properties would only strand the wait on the fallback.\n if (effect?.pseudoElement) return [];\n const target = effect?.target;\n if (target && target !== element) return [];\n const property = (animation as Partial<CSSTransition>).transitionProperty;\n return typeof property === \"string\" && property.length > 0 ? [property] : [];\n });\n return new Set(properties);\n } catch {\n // Partial DOM implementations can expose getAnimations without supporting\n // it. The declared-property tracker and bounded timer remain safe fallbacks.\n return new Set();\n }\n }\n\n /** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */\n #explicitPendingProperties(timings: TransitionTiming[]): Set<string> | null {\n if (timings.some(({ property }) => property === \"all\")) return null;\n const pending = new Set(\n timings.filter(({ totalMs }) => totalMs > 0).map(({ property }) => property),\n );\n // An empty set is only reachable through a timeout override (a 0ms transition\n // otherwise completes synchronously). With nothing to track, fall back to the\n // deadline rule so any terminal event can settle the wait, exactly like `all`.\n return pending.size > 0 ? pending : null;\n }\n\n /** Completes exactly once, releasing listeners and the fallback before the callback. */\n #finish(): void {\n const complete = this.#complete;\n if (!complete) return;\n this.#complete = null;\n this.#teardown();\n complete();\n }\n\n /** Releases the exact element listeners and timer owned by the current wait. */\n #teardown(): void {\n this.#timers.clearAll();\n this.#element?.removeEventListener(\"transitionend\", this.#onTerminal);\n this.#element?.removeEventListener(\"transitioncancel\", this.#onTerminal);\n this.#element = null;\n this.#pendingProperties = null;\n this.#deadline = 0;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { maxTransitionTotalMs } from \"../utils/transition_completion\";\n\n/** Flash types that map to an assertive `alert` (everything else is a polite `status`). */\nconst ASSERTIVE_TYPES = new Set([\"alert\", \"error\"]);\n\n/** Selector for message targets, used by the MutationObserver to spot dynamic inserts. */\nconst MESSAGE_SELECTOR = '[data-stimeo--flash-target=\"message\"]';\n\n/** Per-message auto-dismiss timer bookkeeping (id 0 means paused). */\ninterface FlashTimer {\n id: number;\n startedAt: number;\n remaining: number;\n /** Active pause reasons; the timer resumes only once every one is released. */\n paused: Set<\"focus\" | \"hover\">;\n}\n\n/**\n * Headless **Rails flash bridge**: turns server-rendered (and Turbo Stream-inserted)\n * `flash` elements into live-region announcements with auto-dismiss and a stacking\n * cap (no dedicated APG pattern; follows the WAI-ARIA status/alert guidance and WCAG\n * 2.2 **4.1.3 Status Messages**). The general-purpose sibling is Toast; this one is\n * specialized to the Rails `flash` convention.\n *\n * Markup contract (identifier: `stimeo--flash`):\n * <div data-controller=\"stimeo--flash\" data-stimeo--flash-target=\"region\">\n * <!-- server-rendered or Turbo Stream-inserted -->\n * <div data-stimeo--flash-target=\"message\" data-flash-type=\"notice\">Saved</div>\n * </div>\n *\n * Each message is mapped by `data-flash-type` to `role=\"status\"` (notice) or\n * `role=\"alert\"` (alert/error), flagged `data-flash-state=\"visible\"`, auto-dismissed\n * after `duration` (paused while hovered *or* focused when `pauseOnHover`, and\n * resumed only once both are released), and capped at `max` simultaneous messages.\n * A close button wired to the `dismiss` action removes one manually.\n *\n * `dismiss` dispatches `{ element, reason }`.\n *\n * `show` dispatches `{ type, message }`.\n * `reconcile` dispatches `{ removed: number }` — the messages the Turbo cache\n * rewind took out, which no `dismiss` will report because nobody dismissed them.\n *\n * @remarks\n * Reading is **delegated to the shared Announcer** — but only for the *initial*,\n * page-loaded messages: an in-place live region present at load is not announced by\n * assistive tech, so the controller bridges those via an `stimeo--announcer:announce`\n * event. Messages inserted *later* (Turbo Stream) are announced by their own freshly\n * inserted `role`, exactly like Toast, so they are not bridged again (no double\n * announcement). Behavior only — no styling; `data-flash-state=\"leaving\"` lets CSS\n * animate removal. Focus is never moved (WCAG 2.2 4.1.3): pausing never removes a\n * message, so the control under the pointer or holding focus stays put. The\n * observation follows a `region` element replaced at runtime; the managed set is the\n * current region's subtree, and a message that leaves it gives up its stacking slot,\n * its pending auto-dismiss, and any removal already scheduled. The observer, timers,\n * and per-message listeners are torn down on `disconnect()` (Turbo navigation\n * included), and the managed flashes leave the page before Turbo caches it so a\n * restored snapshot does not replay a notification the visitor already received.\n */\nexport class FlashController extends Controller<HTMLElement> {\n static override targets = [\"region\", \"message\"];\n static override values = {\n duration: { type: Number, default: 5000 },\n pauseOnHover: { type: Boolean, default: true },\n max: { type: Number, default: 0 },\n };\n static actions = [\"dismiss\"] as const;\n static events = [\"show\", \"dismiss\", \"reconcile\"] as const;\n\n declare readonly regionTarget: HTMLElement;\n declare readonly messageTargets: HTMLElement[];\n declare readonly hasRegionTarget: boolean;\n\n declare durationValue: number;\n declare pauseOnHoverValue: boolean;\n declare maxValue: number;\n\n readonly #timers = new SafeTimeout();\n #observer: MutationObserver | null = null;\n /** Whether the controller is between `connect()` and `disconnect()`. */\n #connected = false;\n /** Auto-dismiss timer state keyed by message element. */\n readonly #state = new Map<HTMLElement, FlashTimer>();\n /** Messages already processed, in insertion order, to enforce `max` and avoid double work. */\n readonly #order: HTMLElement[] = [];\n /**\n * Messages between `leaving` and their removal. {@link FlashController.#beginDismiss}\n * releases the bookkeeping above *before* the transition wait, so for that window the\n * element is in the DOM but in neither collection — without this set a re-scan would\n * read it as a brand-new flash and show it a second time.\n */\n readonly #leaving = new Set<HTMLElement>();\n\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n readonly #onEnter = (event: Event): void =>\n this.#pause(event.currentTarget as HTMLElement, event.type === \"focusin\" ? \"focus\" : \"hover\");\n readonly #onLeave = (event: Event): void =>\n this.#resume(event.currentTarget as HTMLElement, event.type === \"focusout\" ? \"focus\" : \"hover\");\n\n override connect(): void {\n this.#connected = true;\n // Initial, server-rendered flashes: bridge them to the Announcer because an\n // in-place live region present at page load is not announced on its own.\n for (const message of this.messageTargets) {\n if (this.#owns(message)) this.#process(message, true);\n }\n this.#syncObservation();\n this.#beforeCache.activate();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#beforeCache.deactivate();\n this.#stopObserving();\n this.#timers.clearAll();\n for (const message of this.#order) this.#unbindPause(message);\n this.#state.clear();\n this.#order.length = 0;\n // The pending finalizes died with the timers above, so a message still marked\n // `leaving` is free to be shown again by the next connect (a snapshot restore).\n this.#leaving.clear();\n }\n\n /**\n * Takes the managed flashes out of the page just before Turbo freezes it, so a\n * restored snapshot carries no notification the visitor has already received: the\n * fresh `connect()` there reads a leftover flash as a brand-new one and announces it\n * a second time. A message that never auto-dismisses (`duration: 0`) is one of these\n * too — that value governs the timer, not what belongs in a cached page. Removal\n * only: `dismiss` reports a dismissal, and freezing the page is not one.\n */\n #rewindForCache(): void {\n const removed = this.#order.length;\n for (const message of [...this.#order]) {\n message.remove();\n this.#forget(message);\n }\n for (const message of this.#leaving) message.remove();\n this.#leaving.clear();\n if (removed > 0) this.dispatch(\"reconcile\", { detail: { removed } });\n }\n\n /** Follows a `region` element swapped in — or arriving — at runtime (Turbo Stream). */\n regionTargetConnected(): void {\n this.#resync();\n }\n\n /** Releases the observation when the `region` element leaves the target set. */\n regionTargetDisconnected(): void {\n this.#resync();\n }\n\n /**\n * Whether this controller owns `message`. Ownership is the current `region`'s\n * subtree: a message target anywhere else in the controller's scope is the\n * consumer's, and so is one in a region that has gone away. The initial scan, a\n * re-scan after a `region` swap, and a departure from the target set all resolve\n * ownership through this one test; the observation gets it structurally, by watching\n * that subtree and nothing else.\n */\n #owns(message: HTMLElement): boolean {\n return this.hasRegionTarget && this.regionTarget.contains(message);\n }\n\n /**\n * Releases a message that left the target set (a Turbo Stream `remove`, the consumer\n * detaching the node, or a morph that rewrote the target attribute in place): it\n * stops occupying a `max` slot, and both its pending auto-dismiss and an already\n * scheduled removal are cancelled. A move *within* the region keeps all of them —\n * which is why the element must still be a message to be treated as one: ownership\n * alone reads an in-place attribute rewrite as a move, and a node outside the target\n * set belongs to the consumer, so nothing here may dismiss it.\n */\n messageTargetDisconnected(message: HTMLElement): void {\n if (!this.#connected) return;\n const moved = this.#owns(message) && message.matches(MESSAGE_SELECTOR);\n if (moved) return;\n this.#forget(message);\n // Ownership is re-checked at both ends of the leaving transition: dropping the\n // claim here is what the pending finalize reads to leave the node alone.\n this.#leaving.delete(message);\n }\n\n /**\n * Re-points the observation after a `region` swap and picks up the messages the\n * new element brought with it (dynamic inserts, so their own `role` announces\n * them). The `#connected` guard is load-bearing: Stimulus runs target callbacks\n * for the initial markup *before* `connect()` and again during teardown *after*\n * `disconnect()`, and re-observing there would outlive the controller.\n */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncObservation();\n for (const message of this.messageTargets) {\n if (this.#owns(message)) this.#process(message, false);\n }\n }\n\n /**\n * Points the mutation observation at the current `region` target, re-resolved on\n * every sync rather than captured at connect, so an element swapped in at runtime\n * is observed instead of the detached original.\n */\n #syncObservation(): void {\n this.#stopObserving();\n if (!this.hasRegionTarget || typeof MutationObserver === \"undefined\") return;\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(this.regionTarget, { childList: true, subtree: true });\n }\n\n #stopObserving(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /**\n * Pause-on-hover/focus listeners, bound and unbound as a pair so the two sides\n * stay in sync. Binding is gated by `pauseOnHover`; unbinding is unconditional\n * and idempotent (a no-op when nothing was bound), which keeps teardown correct\n * even if `pauseOnHover` were ever toggled during a message's life.\n */\n #bindPause(message: HTMLElement): void {\n if (!this.pauseOnHoverValue) return;\n message.addEventListener(\"mouseenter\", this.#onEnter);\n message.addEventListener(\"mouseleave\", this.#onLeave);\n message.addEventListener(\"focusin\", this.#onEnter);\n message.addEventListener(\"focusout\", this.#onLeave);\n }\n\n #unbindPause(message: HTMLElement): void {\n message.removeEventListener(\"mouseenter\", this.#onEnter);\n message.removeEventListener(\"mouseleave\", this.#onLeave);\n message.removeEventListener(\"focusin\", this.#onEnter);\n message.removeEventListener(\"focusout\", this.#onLeave);\n }\n\n /** Dismisses the flash whose close control fired the event. */\n dismiss(event: Event): void {\n const target = (event.currentTarget || event.target) as HTMLElement | null;\n const message = target?.closest<HTMLElement>(MESSAGE_SELECTOR);\n if (message) this.#beginDismiss(message, \"user\");\n }\n\n /** Processes messages added after connect (Turbo Stream); their own role announces them. */\n #onMutations(mutations: MutationRecord[]): void {\n for (const mutation of mutations) {\n for (const node of mutation.addedNodes) {\n if (!(node instanceof HTMLElement)) continue;\n if (node.matches(MESSAGE_SELECTOR)) this.#process(node, false);\n for (const message of node.querySelectorAll<HTMLElement>(MESSAGE_SELECTOR)) {\n this.#process(message, false);\n }\n }\n }\n }\n\n /**\n * Applies role/state, wires pause listeners, schedules auto-dismiss, and either\n * bridges to the Announcer (`bridge`, for initial flashes) or leaves the message's\n * own role to do the announcing (dynamic inserts). Idempotent per message.\n */\n #process(message: HTMLElement, bridge: boolean): void {\n if (this.#state.has(message) || this.#order.includes(message)) return;\n if (this.#leaving.has(message)) return;\n\n const type = message.getAttribute(\"data-flash-type\") ?? \"\";\n const assertive = ASSERTIVE_TYPES.has(type);\n // Don't clobber an authored role; otherwise map the flash type to a live role.\n if (!message.hasAttribute(\"role\")) {\n message.setAttribute(\"role\", assertive ? \"alert\" : \"status\");\n }\n message.setAttribute(\"data-flash-state\", \"visible\");\n this.#order.push(message);\n\n this.#bindPause(message);\n\n const text = message.textContent?.trim() ?? \"\";\n this.dispatch(\"show\", { target: message, detail: { type, message: text } });\n if (bridge && text) {\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", { detail: { message: text, assertive } }),\n );\n }\n\n this.#startTimer(message);\n this.#enforceMax();\n }\n\n /** Removes the oldest visible flashes once the count exceeds `max` (0 = unlimited). */\n #enforceMax(): void {\n if (this.maxValue <= 0) return;\n while (this.#order.length > this.maxValue) {\n const oldest = this.#order[0];\n if (!oldest) break;\n this.#beginDismiss(oldest, \"limit\");\n }\n }\n\n #startTimer(message: HTMLElement, duration = this.durationValue): void {\n if (duration <= 0) return;\n const existing = this.#state.get(message);\n if (existing?.id) this.#timers.clear(existing.id);\n const id = this.#timers.set(() => this.#beginDismiss(message, \"timeout\"), duration);\n this.#state.set(message, {\n id,\n startedAt: Date.now(),\n remaining: duration,\n paused: existing?.paused ?? new Set(),\n });\n }\n\n /**\n * Pauses a message's auto-dismiss, banking the time left (hover/focus, WCAG 2.2.1).\n * Hover and focus are independent reasons: the remaining time is banked on the\n * first of them, and {@link FlashController.#resume} waits for the last one.\n */\n #pause(message: HTMLElement, reason: \"focus\" | \"hover\"): void {\n const timer = this.#state.get(message);\n if (!timer) return;\n timer.paused.add(reason);\n if (timer.id === 0) return;\n\n this.#timers.clear(timer.id);\n // The bank has a floor of 1ms, so a deadline already spent when the pause arrives\n // (the timer sat queued in a throttled tab or behind a long task) settles right\n // after the last reason is released rather than during the event that paused it.\n // Pausing therefore never removes a message — which is what keeps the pointer's\n // target under the pointer and the focused control focused (WCAG 2.2 4.1.3).\n // Banking zero instead would strand the message: nothing resumes a spent window.\n const remaining = Math.max(1, timer.remaining - (Date.now() - timer.startedAt));\n this.#state.set(message, { id: 0, startedAt: 0, remaining, paused: timer.paused });\n }\n\n /** Resumes a paused message's auto-dismiss with the banked time. */\n #resume(message: HTMLElement, reason: \"focus\" | \"hover\"): void {\n const timer = this.#state.get(message);\n if (!timer) return;\n timer.paused.delete(reason);\n // Still held by the other reason (the pointer left, focus stayed, or vice versa).\n if (timer.paused.size > 0) return;\n // Only resume a genuinely paused timer; a running one keeps its own deadline.\n if (timer.id !== 0) return;\n this.#startTimer(message, timer.remaining);\n }\n\n /** Releases every per-message resource: timer, stacking slot, pause listeners. */\n #forget(message: HTMLElement): void {\n const timer = this.#state.get(message);\n if (timer?.id) this.#timers.clear(timer.id);\n this.#state.delete(message);\n const index = this.#order.indexOf(message);\n if (index !== -1) this.#order.splice(index, 1);\n this.#unbindPause(message);\n }\n\n /** Marks a message leaving, then removes it after its CSS transition and emits dismiss. */\n #beginDismiss(message: HTMLElement, reason: \"timeout\" | \"user\" | \"limit\"): void {\n // One removal, one `dismiss`: a close control fired during the leaving\n // transition, or a repeated request, finds the message already released and\n // must not start a second finalize. Reading the bookkeeping rather than\n // `data-flash-state` also keeps `#enforceMax`'s loop terminating, since the\n // release below is what shrinks `#order`.\n if (!this.#state.has(message) && !this.#order.includes(message)) return;\n this.#forget(message);\n this.#leaving.add(message);\n\n message.setAttribute(\"data-flash-state\", \"leaving\");\n\n const finalize = (): void => {\n // The claim taken above is the license to remove: a message that lost the target\n // attribute during the transition belongs to the consumer by the time this runs.\n if (!this.#leaving.delete(message)) return;\n message.remove();\n this.dispatch(\"dismiss\", { detail: { element: message, reason } });\n };\n\n const transition = this.#transitionMs(message);\n if (transition > 0) {\n this.#timers.set(finalize, transition);\n } else {\n finalize();\n }\n }\n\n /** Maximum transition total (duration + delay) of `el` in ms (0 when none / unsupported). */\n #transitionMs(el: HTMLElement): number {\n if (typeof window.getComputedStyle !== \"function\") return 0;\n return maxTransitionTotalMs(window.getComputedStyle(el));\n }\n}\n"]}
|
|
@@ -19,7 +19,9 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
19
19
|
* net so the state never sticks). `minDuration` keeps the skeleton up long enough to
|
|
20
20
|
* avoid a flicker.
|
|
21
21
|
*
|
|
22
|
-
* `start` and `
|
|
22
|
+
* `start`, `end`, and `reconcile` dispatch `{}`. The last of those reports that
|
|
23
|
+
* the Turbo cache rewind ended a load the frame never finished — `end` would
|
|
24
|
+
* claim the frame arrived.
|
|
23
25
|
*
|
|
24
26
|
* @remarks
|
|
25
27
|
* Behavior only — it ships no skeleton markup or styling (pair with Skeleton/CSS);
|
|
@@ -55,7 +57,7 @@ declare class FrameLoadingController extends Controller<HTMLElement> {
|
|
|
55
57
|
default: boolean;
|
|
56
58
|
};
|
|
57
59
|
};
|
|
58
|
-
static events: readonly ["start", "end"];
|
|
60
|
+
static events: readonly ["start", "end", "reconcile"];
|
|
59
61
|
readonly contentTarget: HTMLElement;
|
|
60
62
|
readonly skeletonTarget: HTMLElement;
|
|
61
63
|
readonly overlayTarget: HTMLElement;
|
|
@@ -213,7 +213,7 @@ var FrameLoadingController = class extends Controller {
|
|
|
213
213
|
minDuration: { type: Number, default: 0 },
|
|
214
214
|
restoreFocus: { type: Boolean, default: true }
|
|
215
215
|
};
|
|
216
|
-
static events = ["start", "end"];
|
|
216
|
+
static events = ["start", "end", "reconcile"];
|
|
217
217
|
#timeouts = new SafeTimeout();
|
|
218
218
|
#floor = new MinDurationFloor(this.#timeouts);
|
|
219
219
|
#gate = new DetachGate();
|
|
@@ -289,6 +289,7 @@ var FrameLoadingController = class extends Controller {
|
|
|
289
289
|
this.#loading = false;
|
|
290
290
|
this.#floor.cancel();
|
|
291
291
|
this.#rewindHooks();
|
|
292
|
+
this.dispatch("reconcile", { detail: {} });
|
|
292
293
|
}
|
|
293
294
|
/**
|
|
294
295
|
* Clears every hook the loading state writes. Shared by the three ways a load can
|