stimeo-ui 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cable/index.d.ts +67 -13
  3. package/dist/cable/index.js +438 -152
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/alert_dialog_controller.d.ts +1 -1
  6. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  7. package/dist/controllers/announcer_controller.d.ts +1 -1
  8. package/dist/controllers/announcer_controller.js.map +1 -1
  9. package/dist/controllers/auto_submit_controller.js.map +1 -1
  10. package/dist/controllers/breadcrumb_controller.d.ts +1 -1
  11. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  12. package/dist/controllers/calendar_controller.d.ts +2 -1
  13. package/dist/controllers/calendar_controller.js +10 -2
  14. package/dist/controllers/calendar_controller.js.map +1 -1
  15. package/dist/controllers/carousel_controller.js +1 -1
  16. package/dist/controllers/carousel_controller.js.map +1 -1
  17. package/dist/controllers/character_counter_controller.js.map +1 -1
  18. package/dist/controllers/clipboard_controller.d.ts +1 -1
  19. package/dist/controllers/clipboard_controller.js.map +1 -1
  20. package/dist/controllers/collapsible_controller.js.map +1 -1
  21. package/dist/controllers/color_picker_controller.d.ts +1 -1
  22. package/dist/controllers/color_picker_controller.js.map +1 -1
  23. package/dist/controllers/command_palette_controller.d.ts +3 -3
  24. package/dist/controllers/command_palette_controller.js +2 -2
  25. package/dist/controllers/command_palette_controller.js.map +1 -1
  26. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  27. package/dist/controllers/confirm_controller.d.ts +2 -2
  28. package/dist/controllers/confirm_controller.js.map +1 -1
  29. package/dist/controllers/context_menu_controller.d.ts +1 -1
  30. package/dist/controllers/context_menu_controller.js.map +1 -1
  31. package/dist/controllers/countdown_controller.d.ts +1 -1
  32. package/dist/controllers/countdown_controller.js.map +1 -1
  33. package/dist/controllers/data_grid_controller.d.ts +1 -1
  34. package/dist/controllers/data_grid_controller.js.map +1 -1
  35. package/dist/controllers/date_range_picker_controller.js +10 -10
  36. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  37. package/dist/controllers/dialog_controller.d.ts +1 -1
  38. package/dist/controllers/dialog_controller.js.map +1 -1
  39. package/dist/controllers/direct_upload_controller.js.map +1 -1
  40. package/dist/controllers/dirty_form_controller.js.map +1 -1
  41. package/dist/controllers/dismissible_controller.js.map +1 -1
  42. package/dist/controllers/drawer_controller.d.ts +2 -2
  43. package/dist/controllers/drawer_controller.js +2 -2
  44. package/dist/controllers/drawer_controller.js.map +1 -1
  45. package/dist/controllers/dropdown_controller.d.ts +1 -1
  46. package/dist/controllers/dropdown_controller.js.map +1 -1
  47. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  48. package/dist/controllers/flash_controller.js.map +1 -1
  49. package/dist/controllers/focus_controller.d.ts +2 -2
  50. package/dist/controllers/focus_controller.js.map +1 -1
  51. package/dist/controllers/form_validation_controller.js.map +1 -1
  52. package/dist/controllers/frame_loading_controller.d.ts +3 -3
  53. package/dist/controllers/frame_loading_controller.js.map +1 -1
  54. package/dist/controllers/highlight_controller.js.map +1 -1
  55. package/dist/controllers/hover_card_controller.d.ts +1 -1
  56. package/dist/controllers/hover_card_controller.js.map +1 -1
  57. package/dist/controllers/idle_controller.d.ts +1 -1
  58. package/dist/controllers/idle_controller.js.map +1 -1
  59. package/dist/controllers/input_mask_controller.js.map +1 -1
  60. package/dist/controllers/intersection_controller.js.map +1 -1
  61. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  62. package/dist/controllers/listbox_controller.js.map +1 -1
  63. package/dist/controllers/local_time_controller.d.ts +1 -1
  64. package/dist/controllers/local_time_controller.js.map +1 -1
  65. package/dist/controllers/masonry_controller.d.ts +1 -1
  66. package/dist/controllers/masonry_controller.js.map +1 -1
  67. package/dist/controllers/menu_controller.d.ts +1 -1
  68. package/dist/controllers/menu_controller.js.map +1 -1
  69. package/dist/controllers/menubar_controller.d.ts +2 -2
  70. package/dist/controllers/menubar_controller.js.map +1 -1
  71. package/dist/controllers/navigation_menu_controller.d.ts +1 -1
  72. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  73. package/dist/controllers/nested_form_controller.js.map +1 -1
  74. package/dist/controllers/network_status_controller.js.map +1 -1
  75. package/dist/controllers/number_input_controller.js.map +1 -1
  76. package/dist/controllers/optimistic_controller.d.ts +23 -10
  77. package/dist/controllers/optimistic_controller.js +141 -20
  78. package/dist/controllers/optimistic_controller.js.map +1 -1
  79. package/dist/controllers/overflow_indicator_controller.d.ts +1 -1
  80. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  81. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  82. package/dist/controllers/pagination_controller.js.map +1 -1
  83. package/dist/controllers/password_reveal_controller.js.map +1 -1
  84. package/dist/controllers/password_strength_controller.d.ts +2 -1
  85. package/dist/controllers/password_strength_controller.js.map +1 -1
  86. package/dist/controllers/persist_controller.js.map +1 -1
  87. package/dist/controllers/popover_controller.d.ts +1 -1
  88. package/dist/controllers/popover_controller.js.map +1 -1
  89. package/dist/controllers/portal_controller.d.ts +2 -2
  90. package/dist/controllers/portal_controller.js.map +1 -1
  91. package/dist/controllers/radio_group_controller.d.ts +1 -1
  92. package/dist/controllers/radio_group_controller.js.map +1 -1
  93. package/dist/controllers/relative_time_controller.d.ts +3 -3
  94. package/dist/controllers/relative_time_controller.js.map +1 -1
  95. package/dist/controllers/roving_controller.d.ts +1 -1
  96. package/dist/controllers/roving_controller.js.map +1 -1
  97. package/dist/controllers/scroll_area_controller.js.map +1 -1
  98. package/dist/controllers/scrollspy_controller.js.map +1 -1
  99. package/dist/controllers/sidebar_controller.d.ts +1 -1
  100. package/dist/controllers/sidebar_controller.js.map +1 -1
  101. package/dist/controllers/skeleton_controller.d.ts +2 -2
  102. package/dist/controllers/skeleton_controller.js.map +1 -1
  103. package/dist/controllers/spinner_controller.d.ts +3 -3
  104. package/dist/controllers/spinner_controller.js.map +1 -1
  105. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  106. package/dist/controllers/submit_once_controller.d.ts +3 -3
  107. package/dist/controllers/submit_once_controller.js.map +1 -1
  108. package/dist/controllers/switch_controller.js.map +1 -1
  109. package/dist/controllers/textarea_autosize_controller.d.ts +1 -1
  110. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  111. package/dist/controllers/theme_controller.d.ts +3 -2
  112. package/dist/controllers/theme_controller.js.map +1 -1
  113. package/dist/controllers/toast_controller.js +1 -1
  114. package/dist/controllers/toast_controller.js.map +1 -1
  115. package/dist/controllers/toggle_group_controller.js.map +1 -1
  116. package/dist/controllers/toolbar_controller.d.ts +1 -1
  117. package/dist/controllers/toolbar_controller.js.map +1 -1
  118. package/dist/controllers/tooltip_controller.d.ts +1 -1
  119. package/dist/controllers/tooltip_controller.js.map +1 -1
  120. package/dist/controllers/transition_controller.d.ts +1 -1
  121. package/dist/controllers/transition_controller.js.map +1 -1
  122. package/dist/controllers/tree_view_controller.d.ts +4 -3
  123. package/dist/controllers/tree_view_controller.js.map +1 -1
  124. package/dist/index.js +101 -38
  125. package/dist/index.js.map +1 -1
  126. package/dist/inspector/cli.d.ts +1 -1
  127. package/dist/inspector/cli.js +1 -1
  128. package/dist/inspector/cli.js.map +1 -1
  129. package/dist/inspector/cli_bin.js +1 -1
  130. package/dist/inspector/cli_bin.js.map +1 -1
  131. package/dist/inspector/examples.json +1 -1
  132. package/dist/inspector/manifest.json +6 -3
  133. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/microtask_coalescer.ts","../../src/utils/safe_storage.ts","../../src/utils/safe_timeout.ts","../../src/utils/string_list.ts","../../src/controllers/persist_controller.ts"],"names":[],"mappings":"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","file":"persist_controller.js","sourcesContent":["/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\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 #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/** A successful guarded storage operation. */\nexport interface StorageSuccess<T> {\n readonly ok: true;\n readonly value: T;\n}\n\n/** A guarded storage operation rejected by the browser. */\nexport interface StorageFailure {\n readonly ok: false;\n readonly error: unknown;\n}\n\n/** Result of accessing browser storage without allowing an exception to escape. */\nexport type StorageResult<T> = StorageSuccess<T> | StorageFailure;\n\n/**\n * Guarded `localStorage` access shared by controllers that persist client state.\n *\n * `window.localStorage` can throw while resolving the storage object or while\n * reading, writing, or removing a key. The result keeps an unavailable store\n * distinct from an available store whose key is unset, so callers can expose an\n * accurate success or failure state without duplicating exception guards.\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns A successful result containing the stored string or `null`, or a\n * failure result when storage is unavailable.\n */\nexport function readLocalStorage(key: string): StorageResult<string | null> {\n try {\n return { ok: true, value: window.localStorage.getItem(key) };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n * @returns Success only after `setItem` completes; otherwise the browser error.\n */\nexport function writeLocalStorage(key: string, value: string): StorageResult<void> {\n try {\n window.localStorage.setItem(key, value);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Removes `key` from `localStorage`.\n *\n * @param key - The storage key to remove.\n * @returns Success only after `removeItem` completes; otherwise the browser error.\n */\nexport function removeLocalStorage(key: string): StorageResult<void> {\n try {\n window.localStorage.removeItem(key);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\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","/**\n * JSON string-list parsing for the controllers that take a list as a Value.\n *\n * Stimulus offers an `Array` Value type, but its reader runs `JSON.parse` inside\n * the value observer — **before** the `<name>ValueChanged` callback — and\n * rethrows on malformed text. The throw propagates out of the observer that runs\n * during connection, so a single unparseable attribute stops the controller from\n * connecting at all: no lifecycle callback runs and the element is left inert\n * rather than degraded. A consumer cannot guard against it either, because the\n * callback that would hold the `try` never runs.\n *\n * Declaring the attribute as a `String` Value and parsing it here keeps a\n * malformed declaration local to the value it declares. The attribute text is\n * identical either way (`'[\"a\", \"b\"]'`), so markup does not change with the\n * declaration.\n */\n\n/**\n * Parses `raw` as a JSON array of strings.\n *\n * Returns `fallback` when the text is absent, unparseable, or not an array, so a\n * malformed declaration behaves like an omitted one. A parsed array keeps only\n * its string entries: every caller indexes or compares strings, and a stray\n * number would otherwise sit in the list without ever matching.\n *\n * An explicit `\"[]\"` is honoured as an empty list rather than falling back — it\n * is the only way to declare \"none\" against a non-empty default.\n *\n * @param raw - the attribute text, e.g. `'[\"password\"]'`.\n * @param fallback - the list to use when `raw` declares nothing usable.\n */\nexport function parseStringList(raw: string, fallback: readonly string[] = []): string[] {\n // The common case is an undeclared attribute, and `JSON.parse(\"\")` answers it\n // by throwing. Reaching the same fallback without building an exception keeps\n // the callers that ask per element off that path.\n const text = raw.trim();\n if (text.length === 0) return [...fallback];\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return [...fallback];\n }\n\n if (!Array.isArray(parsed)) return [...fallback];\n return parsed.filter((entry): entry is string => typeof entry === \"string\");\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { readLocalStorage, removeLocalStorage, writeLocalStorage } from \"../utils/safe_storage\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { parseStringList } from \"../utils/string_list\";\n\n/** Field controls this controller can persist. */\ntype PersistField = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;\n\n/** Values that can be restored without coercing an untrusted storage payload. */\ntype PersistValue = string | boolean | string[];\n\n/** One stable field entry in the versioned storage payload. */\ninterface PersistPayloadField {\n readonly key: string;\n readonly value: PersistValue;\n}\n\n/** Versioned draft payload stored under one controller namespace. */\ninterface PersistPayload {\n readonly version: 1;\n readonly fields: PersistPayloadField[];\n}\n\ntype PersistOperation = \"read\" | \"write\" | \"remove\";\ntype PersistErrorReason = \"unavailable\" | \"invalid-payload\";\n\n/** One scheduled write, including the namespace it was created for. */\ninterface PendingSave {\n readonly id: number;\n readonly logicalKey: string;\n}\n\n/** Input types that carry no meaningful, restorable value. */\nconst NON_VALUE_TYPES = new Set([\"file\", \"submit\", \"reset\", \"button\", \"image\"]);\n/** Passwords are never persisted, even when a custom exclusion list is supplied. */\nconst SENSITIVE_TYPES = new Set([\"password\"]);\n/** Field types and names skipped by the default configuration. */\nconst DEFAULT_EXCLUDE = [\"authenticity_token\", \"_method\", \"utf8\"];\n/** localStorage key prefix so drafts never clobber unrelated app storage. */\nconst STORAGE_PREFIX = \"stimeo--persist:\";\n/** Separator for disambiguating repeated same-name fields. */\nconst OCCURRENCE_SEP = \"\\u0000\";\n/** The one observed attribute that can move fields between nested Persist hosts. */\nconst OWNERSHIP_ATTRIBUTE = \"data-controller\";\nconst DEFAULT_DEBOUNCE = 400;\n\n/** Whether a parsed JSON value is a plain object-like record. */\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** Whether a parsed JSON value belongs to the persisted field-value union. */\nconst isPersistValue = (value: unknown): value is PersistValue =>\n typeof value === \"string\" ||\n typeof value === \"boolean\" ||\n (Array.isArray(value) && value.every((item) => typeof item === \"string\"));\n\n/** Parses and validates the complete versioned payload without type assertions. */\nconst parsePayload = (raw: string): PersistPayload | null => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.fields)) return null;\n\n const fields: PersistPayloadField[] = [];\n const keys = new Set<string>();\n for (const candidate of parsed.fields) {\n if (!isRecord(candidate)) return null;\n const { key, value } = candidate;\n if (typeof key !== \"string\" || key.length === 0 || keys.has(key)) return null;\n if (!isPersistValue(value)) return null;\n keys.add(key);\n fields.push({ key, value });\n }\n return { version: 1, fields };\n};\n\n/** Whether a node is one of the native controls supported by Persist. */\nconst isPersistField = (node: unknown): node is PersistField =>\n node instanceof HTMLInputElement ||\n node instanceof HTMLTextAreaElement ||\n node instanceof HTMLSelectElement;\n\n/**\n * Headless draft-autosave behavior: persists form controls to `localStorage`\n * and restores them across Turbo navigation and reloads.\n *\n * Markup contract (identifier: `stimeo--persist`):\n * <form data-controller=\"stimeo--persist\"\n * data-stimeo--persist-key-value=\"contact-draft\">\n * <input name=\"name\">\n * <textarea name=\"message\"></textarea>\n * </form>\n *\n * The optional `field` targets restrict persistence to explicitly targeted\n * controls. The public `clear` action removes the active draft. `key`,\n * `debounce`, `exclude`, and `clearOn` Values can change while connected;\n * `clearOn` is an event name whose occurrence invokes `clear`.\n *\n * `restore`, `save`, and `clear` dispatch `{ key: string }`. `error` dispatches\n * `{ key: string, operation: \"read\" | \"write\" | \"remove\", reason:\n * \"unavailable\" | \"invalid-payload\" }`. Restore writes are intentionally\n * silent native-control mutations; consumers synchronize from the bubbling\n * `restore` event without treating a restored draft as user input.\n *\n * @remarks\n * Behavior only — restoration never moves focus. Passwords are never stored;\n * the default exclusion also omits Rails request metadata while preserving\n * application-owned hidden fields. Distinct logical keys isolate instances;\n * instances using the same key intentionally share one last-writer-wins draft.\n * Every listener, timer, observer, and queued reconciliation is released on\n * `disconnect()`, after any pending edit is flushed through the normal save\n * path. A reconnect removes stale restored state before reading storage.\n */\nexport class PersistController extends Controller<HTMLElement> {\n static override targets = [\"field\"];\n static override values = {\n key: { type: String, default: \"\" },\n debounce: { type: Number, default: DEFAULT_DEBOUNCE },\n // A malformed JSON list must fall back without aborting Stimulus connection.\n exclude: { type: String, default: \"\" },\n clearOn: { type: String, default: \"\" },\n };\n static actions = [\"clear\"] as const;\n static events = [\"restore\", \"save\", \"clear\", \"error\"] as const;\n\n declare readonly fieldTargets: Element[];\n declare readonly hasFieldTarget: boolean;\n\n declare keyValue: string;\n declare debounceValue: number;\n declare excludeValue: string;\n declare clearOnValue: string;\n\n readonly #timeouts = new SafeTimeout();\n readonly #restoreDynamic = new MicrotaskCoalescer(() => this.#restoreDynamicFields());\n readonly #observer = new MutationObserver((records) => this.#onMutations(records));\n #pendingSave: PendingSave | null = null;\n #connected = false;\n #logicalKey: string | null = null;\n #payload: PersistPayload | null = null;\n #registeredClearOn: string | null = null;\n #excluded: readonly string[] = DEFAULT_EXCLUDE;\n #knownFields = new WeakSet<PersistField>();\n #forcedRestore = new Set<PersistField>();\n\n readonly #onInput = (event: Event): void => {\n const field = event.target;\n if (!isPersistField(field) || !this.#ownsField(field)) return;\n if (this.hasFieldTarget && !this.fieldTargets.includes(field)) return;\n if (this.#keyOf(field) === null || !this.#persistable(field, this.#excluded)) return;\n this.#scheduleSave();\n };\n\n readonly #onClearEvent = (): void => {\n this.clear();\n };\n\n override connect(): void {\n if (this.#connected) return;\n this.#connected = true;\n this.#logicalKey = this.#resolveLogicalKey();\n this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);\n this.#knownFields = new WeakSet<PersistField>();\n this.#forcedRestore.clear();\n this.element.removeAttribute(\"data-persist-restored\");\n\n this.element.addEventListener(\"input\", this.#onInput);\n this.element.addEventListener(\"change\", this.#onInput);\n this.#syncClearOn();\n this.#restoreDynamic.activate();\n this.#observer.observe(this.element, {\n attributes: true,\n attributeFilter: [\n \"checked\",\n \"data-controller\",\n \"data-stimeo--persist-target\",\n \"id\",\n \"multiple\",\n \"name\",\n \"selected\",\n \"type\",\n \"value\",\n ],\n childList: true,\n subtree: true,\n });\n this.#loadActiveDraft();\n }\n\n override disconnect(): void {\n this.#flushPendingSave();\n this.#connected = false;\n this.element.removeEventListener(\"input\", this.#onInput);\n this.element.removeEventListener(\"change\", this.#onInput);\n this.#unbindClearOn();\n this.#observer.disconnect();\n this.#restoreDynamic.cancel();\n this.#forcedRestore.clear();\n this.#timeouts.clearAll();\n this.element.removeAttribute(\"data-persist-restored\");\n this.#payload = null;\n }\n\n /** Drops the active draft after storage confirms the removal. */\n clear(): void {\n const logicalKey = this.#logicalKey ?? this.#resolveLogicalKey();\n if (logicalKey === null) return;\n this.#cancelPendingSave();\n const result = removeLocalStorage(this.#storageKey(logicalKey));\n if (!result.ok) {\n this.#dispatchError(logicalKey, \"remove\", \"unavailable\");\n return;\n }\n this.#payload = null;\n this.element.removeAttribute(\"data-persist-restored\");\n this.dispatch(\"clear\", { detail: { key: logicalKey } });\n }\n\n /** Rebinds the external clear event when its Value changes. */\n clearOnValueChanged(): void {\n if (this.#connected) this.#syncClearOn();\n }\n\n /** Switches storage namespaces without writing old edits under the new key. */\n keyValueChanged(): void {\n if (this.#connected) this.#switchLogicalKey();\n }\n\n /** Reschedules a pending write against the current debounce delay. */\n debounceValueChanged(): void {\n if (this.#connected && this.#pendingSave !== null) this.#scheduleSave();\n }\n\n /** Restores every field the new exclusion list makes eligible. */\n excludeValueChanged(): void {\n const previous = this.#excluded;\n this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);\n if (!this.#connected) return;\n // A field restored once stays in #knownFields, so releasing it from the exclusion\n // list has to name it explicitly to reach the next batch. Fields the list never\n // covered keep their live value, which is what the next batch already guarantees.\n for (const field of this.#candidateFields()) {\n if (this.#persistable(field, this.#excluded) && !this.#persistable(field, previous)) {\n this.#forcedRestore.add(field);\n }\n }\n this.#restoreDynamic.schedule();\n }\n\n /** Schedules a debounced save. */\n #scheduleSave(): void {\n const logicalKey = this.#logicalKey;\n if (logicalKey === null) return;\n this.#cancelPendingSave();\n const id = this.#timeouts.set(() => {\n this.#pendingSave = null;\n this.#save(logicalKey);\n }, this.#debounceDelay);\n this.#pendingSave = { id, logicalKey };\n }\n\n /** Cancels the currently pending save, if any. */\n #cancelPendingSave(): void {\n const pending = this.#pendingSave;\n if (pending === null) return;\n this.#timeouts.clear(pending.id);\n this.#pendingSave = null;\n }\n\n /** Flushes a pending edit through the same success/error event path as a timer. */\n #flushPendingSave(): void {\n const pending = this.#pendingSave;\n if (pending === null) return;\n this.#cancelPendingSave();\n this.#save(pending.logicalKey);\n }\n\n /** Writes the active payload and emits `save` only after storage succeeds. */\n #save(logicalKey: string): boolean {\n const payload = this.#serialize();\n const result = writeLocalStorage(this.#storageKey(logicalKey), JSON.stringify(payload));\n if (!result.ok) {\n this.#dispatchError(logicalKey, \"write\", \"unavailable\");\n return false;\n }\n this.#payload = payload;\n this.dispatch(\"save\", { detail: { key: logicalKey } });\n return true;\n }\n\n /** Serializes every persistable field into a versioned, typed payload. */\n #serialize(): PersistPayload {\n const fields: PersistPayloadField[] = [];\n for (const { field, key } of this.#fieldEntries()) {\n if (field instanceof HTMLInputElement && field.type === \"checkbox\") {\n fields.push({ key, value: field.checked });\n } else if (field instanceof HTMLInputElement && field.type === \"radio\") {\n if (field.checked) fields.push({ key, value: field.value });\n } else if (field instanceof HTMLSelectElement && field.multiple) {\n fields.push({\n key,\n value: Array.from(field.selectedOptions).map((option) => option.value),\n });\n } else {\n fields.push({ key, value: field.value });\n }\n }\n return { version: 1, fields };\n }\n\n /** Reads, validates, and restores the active namespace. */\n #loadActiveDraft(): void {\n this.element.removeAttribute(\"data-persist-restored\");\n this.#payload = null;\n this.#knownFields = new WeakSet<PersistField>();\n this.#forcedRestore.clear();\n const logicalKey = this.#logicalKey;\n if (logicalKey === null) {\n this.#markCurrentFieldsKnown();\n return;\n }\n\n const result = readLocalStorage(this.#storageKey(logicalKey));\n if (!result.ok) {\n this.#dispatchError(logicalKey, \"read\", \"unavailable\");\n this.#markCurrentFieldsKnown();\n return;\n }\n if (result.value === null) {\n this.#markCurrentFieldsKnown();\n return;\n }\n\n const payload = parsePayload(result.value);\n if (payload === null) {\n const removal = removeLocalStorage(this.#storageKey(logicalKey));\n // Cleanup precedes notification so an error consumer can safely replace\n // the invalid value without that repair being deleted on callback return.\n this.#dispatchError(logicalKey, \"read\", \"invalid-payload\");\n if (!removal.ok) this.#dispatchError(logicalKey, \"remove\", \"unavailable\");\n this.#markCurrentFieldsKnown();\n return;\n }\n this.#payload = payload;\n this.#restoreEntries(null);\n }\n\n /** Restores fields inserted or made eligible after the initial connection. */\n #restoreDynamicFields(): void {\n const selected = new Set<PersistField>(this.#forcedRestore);\n this.#forcedRestore.clear();\n for (const { field } of this.#fieldEntries()) {\n if (!this.#knownFields.has(field)) selected.add(field);\n }\n this.#restoreEntries(selected);\n }\n\n /** Applies the active payload to all fields, or only the supplied candidates. */\n #restoreEntries(selected: ReadonlySet<PersistField> | null): void {\n const entries = this.#fieldEntries();\n for (const { field } of entries) this.#knownFields.add(field);\n const payload = this.#payload;\n if (payload === null) return;\n const savedFields = new Map(payload.fields.map((field) => [field.key, field]));\n const restoredKeys = new Set<string>();\n const processedRadios = new Set<string>();\n\n for (const entry of entries) {\n if (selected !== null && !selected.has(entry.field)) continue;\n const saved = savedFields.get(entry.key);\n if (saved === undefined) continue;\n const { value } = saved;\n\n if (entry.field instanceof HTMLInputElement && entry.field.type === \"radio\") {\n if (processedRadios.has(entry.key)) continue;\n processedRadios.add(entry.key);\n const radios = entries\n .filter(\n ({ key, field }) =>\n key === entry.key && field instanceof HTMLInputElement && field.type === \"radio\",\n )\n .map(({ field }) => field as HTMLInputElement);\n if (typeof value !== \"string\" || !radios.some((radio) => radio.value === value)) continue;\n for (const radio of radios) radio.checked = radio.value === value;\n restoredKeys.add(entry.key);\n continue;\n }\n\n if (this.#applyValue(entry.field, value)) restoredKeys.add(entry.key);\n }\n\n if (restoredKeys.size > 0) {\n // Property writes can reflect to observed attributes in DOM implementations.\n // Drop only records produced by this restore before notifying consumers, so\n // authored mutations made from a restore listener remain observable.\n this.#observer.takeRecords();\n this.element.setAttribute(\"data-persist-restored\", \"true\");\n this.dispatch(\"restore\", { detail: { key: this.#logicalKey } });\n }\n }\n\n /** Applies a type-compatible stored value without coercion. */\n #applyValue(field: PersistField, value: PersistValue): boolean {\n if (field instanceof HTMLInputElement && field.type === \"checkbox\") {\n if (typeof value !== \"boolean\") return false;\n field.checked = value;\n return true;\n }\n if (field instanceof HTMLSelectElement && field.multiple) {\n if (!Array.isArray(value)) return false;\n const available = new Set(Array.from(field.options).map((option) => option.value));\n const selected = value.filter((candidate) => available.has(candidate));\n if (value.length > 0 && selected.length === 0) return false;\n const selectedValues = new Set(selected);\n for (const option of Array.from(field.options)) {\n option.selected = selectedValues.has(option.value);\n }\n return true;\n }\n if (typeof value !== \"string\") return false;\n if (field instanceof HTMLSelectElement) {\n const exists = Array.from(field.options).some((option) => option.value === value);\n if (!exists) return false;\n }\n field.value = value;\n return true;\n }\n\n /** Every control this instance owns, before the exclusion list narrows them. */\n #candidateFields(): PersistField[] {\n return this.hasFieldTarget\n ? this.fieldTargets.filter(isPersistField)\n : Array.from(this.element.querySelectorAll<PersistField>(\"input, textarea, select\")).filter(\n (field) => this.#ownsField(field),\n );\n }\n\n /**\n * Pairs persistable fields with DOM-order occurrence keys. Every HTML radio\n * group (same name and form owner) occupies one shared occurrence slot.\n */\n #fieldEntries(): Array<{ field: PersistField; key: string }> {\n const entries: Array<{ field: PersistField; key: string }> = [];\n const eligible: Array<{ field: PersistField; name: string }> = [];\n for (const field of this.#candidateFields()) {\n const name = this.#keyOf(field);\n if (name === null || !this.#persistable(field, this.#excluded)) continue;\n eligible.push({ field, name });\n }\n\n const nextOccurrence = new Map<string, number>();\n const radioSlots = new Map<string, Map<HTMLFormElement | null, string>>();\n const allocate = (name: string): string => {\n const occurrence = nextOccurrence.get(name) ?? 0;\n nextOccurrence.set(name, occurrence + 1);\n return occurrence === 0 ? name : `${name}${OCCURRENCE_SEP}${occurrence}`;\n };\n for (const { field, name } of eligible) {\n if (field instanceof HTMLInputElement && field.type === \"radio\") {\n const groups = radioSlots.get(name) ?? new Map<HTMLFormElement | null, string>();\n radioSlots.set(name, groups);\n let key = groups.get(field.form);\n if (key === undefined) {\n key = allocate(name);\n groups.set(field.form, key);\n }\n entries.push({ field, key });\n continue;\n }\n entries.push({ field, key: allocate(name) });\n }\n return entries;\n }\n\n /** Whether a field carries a restorable, non-excluded value. */\n #persistable(field: PersistField, excluded: readonly string[]): boolean {\n if (NON_VALUE_TYPES.has(field.type) || SENSITIVE_TYPES.has(field.type)) return false;\n if (excluded.includes(field.type)) return false;\n if (field.name.length > 0 && excluded.includes(field.name)) return false;\n return true;\n }\n\n /** A stable storage sub-key for a field (its name, else id). */\n #keyOf(field: PersistField): string | null {\n return field.name || field.id || null;\n }\n\n /** Whether this instance, rather than a nested Persist host, owns a field. */\n #ownsField(field: PersistField): boolean {\n return field.closest('[data-controller~=\"stimeo--persist\"]') === this.element;\n }\n\n /** Marks all currently eligible fields without applying a payload. */\n #markCurrentFieldsKnown(): void {\n for (const { field } of this.#fieldEntries()) this.#knownFields.add(field);\n }\n\n /** Switches namespace after flushing the old namespace's pending edit. */\n #switchLogicalKey(): void {\n const next = this.#resolveLogicalKey();\n if (next === this.#logicalKey) return;\n this.#flushPendingSave();\n this.#logicalKey = next;\n this.#loadActiveDraft();\n }\n\n /** Synchronizes the exact event name owned by the `clearOn` Value. */\n #syncClearOn(): void {\n const next = this.#validEventName(this.clearOnValue);\n if (next === this.#registeredClearOn) return;\n this.#unbindClearOn();\n if (next === null) return;\n this.element.addEventListener(next, this.#onClearEvent);\n this.#registeredClearOn = next;\n }\n\n /** Removes the event listener using the name that was actually registered. */\n #unbindClearOn(): void {\n if (this.#registeredClearOn === null) return;\n this.element.removeEventListener(this.#registeredClearOn, this.#onClearEvent);\n this.#registeredClearOn = null;\n }\n\n /** Returns a non-whitespace event type, or null when the hook is disabled. */\n #validEventName(value: string): string | null {\n const name = value.trim();\n return name.length > 0 && !/\\s/.test(name) ? name : null;\n }\n\n /** Collects dynamic controls and select-option changes into one restore pass. */\n #onMutations(records: MutationRecord[]): void {\n let rootIdChanged = false;\n for (const record of records) {\n if (record.type === \"attributes\") {\n if (record.target === this.element && record.attributeName === \"id\") {\n rootIdChanged = true;\n }\n this.#collectAttributeCandidate(record.target, record.attributeName);\n continue;\n }\n for (const node of record.addedNodes) this.#collectRestoreCandidate(node);\n }\n if (rootIdChanged && this.keyValue.length === 0) this.#switchLogicalKey();\n this.#restoreDynamic.schedule();\n }\n\n /**\n * Adds the controls one mutated attribute can affect. Only an ownership change on a\n * descendant moves fields between Persist hosts, so that is the single case worth a\n * subtree sweep; every other observed attribute describes one control, and sweeping\n * from its container would re-apply the stored draft over edits still being debounced.\n */\n #collectAttributeCandidate(target: Node, attributeName: string | null): void {\n if (attributeName === OWNERSHIP_ATTRIBUTE && target !== this.element) {\n this.#collectRestoreCandidate(target);\n return;\n }\n this.#collectControlCandidate(target);\n }\n\n /** Adds a native control, or the select that owns a mutated option. */\n #collectControlCandidate(node: Node): void {\n if (isPersistField(node)) {\n this.#forcedRestore.add(node);\n return;\n }\n if (node instanceof HTMLOptionElement) {\n const select = node.closest(\"select\");\n if (select) this.#forcedRestore.add(select);\n }\n }\n\n /** Adds every control inside an inserted or newly owned subtree. */\n #collectRestoreCandidate(node: Node): void {\n this.#collectControlCandidate(node);\n if (!(node instanceof Element)) return;\n for (const field of node.querySelectorAll<PersistField>(\"input, textarea, select\")) {\n this.#forcedRestore.add(field);\n }\n }\n\n /** Resolves the logical key from the Value, falling back to the host id. */\n #resolveLogicalKey(): string | null {\n const key = this.keyValue || this.element.id;\n return key.length > 0 ? key : null;\n }\n\n /** Namespaces one logical key within localStorage. */\n #storageKey(logicalKey: string): string {\n return `${STORAGE_PREFIX}${logicalKey}`;\n }\n\n /** Normalizes invalid debounce Values to the documented default. */\n get #debounceDelay(): number {\n const value = this.debounceValue;\n return Number.isFinite(value) && value >= 0 ? value : DEFAULT_DEBOUNCE;\n }\n\n /** Dispatches one observable storage failure without exposing browser errors. */\n #dispatchError(key: string, operation: PersistOperation, reason: PersistErrorReason): void {\n this.dispatch(\"error\", { detail: { key, operation, reason } });\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/microtask_coalescer.ts","../../src/utils/safe_storage.ts","../../src/utils/safe_timeout.ts","../../src/utils/string_list.ts","../../src/controllers/persist_controller.ts"],"names":[],"mappings":"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","file":"persist_controller.js","sourcesContent":["/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\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 #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/** A successful guarded storage operation. */\nexport interface StorageSuccess<T> {\n readonly ok: true;\n readonly value: T;\n}\n\n/** A guarded storage operation rejected by the browser. */\nexport interface StorageFailure {\n readonly ok: false;\n readonly error: unknown;\n}\n\n/** Result of accessing browser storage without allowing an exception to escape. */\nexport type StorageResult<T> = StorageSuccess<T> | StorageFailure;\n\n/**\n * Guarded `localStorage` access shared by controllers that persist client state.\n *\n * `window.localStorage` can throw while resolving the storage object or while\n * reading, writing, or removing a key. The result keeps an unavailable store\n * distinct from an available store whose key is unset, so callers can expose an\n * accurate success or failure state without duplicating exception guards.\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns A successful result containing the stored string or `null`, or a\n * failure result when storage is unavailable.\n */\nexport function readLocalStorage(key: string): StorageResult<string | null> {\n try {\n return { ok: true, value: window.localStorage.getItem(key) };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n * @returns Success only after `setItem` completes; otherwise the browser error.\n */\nexport function writeLocalStorage(key: string, value: string): StorageResult<void> {\n try {\n window.localStorage.setItem(key, value);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Removes `key` from `localStorage`.\n *\n * @param key - The storage key to remove.\n * @returns Success only after `removeItem` completes; otherwise the browser error.\n */\nexport function removeLocalStorage(key: string): StorageResult<void> {\n try {\n window.localStorage.removeItem(key);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","/**\n * JSON string-list parsing for the controllers that take a list as a Value.\n *\n * Stimulus offers an `Array` Value type, but its reader runs `JSON.parse` inside\n * the value observer — **before** the `<name>ValueChanged` callback — and\n * rethrows on malformed text. The throw propagates out of the observer that runs\n * during connection, so a single unparseable attribute stops the controller from\n * connecting at all: no lifecycle callback runs and the element is left inert\n * rather than degraded. A consumer cannot guard against it either, because the\n * callback that would hold the `try` never runs.\n *\n * Declaring the attribute as a `String` Value and parsing it here keeps a\n * malformed declaration local to the value it declares. The attribute text is\n * identical either way (`'[\"a\", \"b\"]'`), so markup does not change with the\n * declaration.\n */\n\n/**\n * Parses `raw` as a JSON array of strings.\n *\n * Returns `fallback` when the text is absent, unparseable, or not an array, so a\n * malformed declaration behaves like an omitted one. A parsed array keeps only\n * its string entries: every caller indexes or compares strings, and a stray\n * number would otherwise sit in the list without ever matching.\n *\n * An explicit `\"[]\"` is honoured as an empty list rather than falling back — it\n * is the only way to declare \"none\" against a non-empty default.\n *\n * @param raw - the attribute text, e.g. `'[\"password\"]'`.\n * @param fallback - the list to use when `raw` declares nothing usable.\n */\nexport function parseStringList(raw: string, fallback: readonly string[] = []): string[] {\n // The common case is an undeclared attribute, and `JSON.parse(\"\")` answers it\n // by throwing. Reaching the same fallback without building an exception keeps\n // the callers that ask per element off that path.\n const text = raw.trim();\n if (text.length === 0) return [...fallback];\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return [...fallback];\n }\n\n if (!Array.isArray(parsed)) return [...fallback];\n return parsed.filter((entry): entry is string => typeof entry === \"string\");\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { readLocalStorage, removeLocalStorage, writeLocalStorage } from \"../utils/safe_storage\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { parseStringList } from \"../utils/string_list\";\n\n/** Field controls this controller can persist. */\ntype PersistField = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;\n\n/** Values that can be restored without coercing an untrusted storage payload. */\ntype PersistValue = string | boolean | string[];\n\n/** One stable field entry in the versioned storage payload. */\ninterface PersistPayloadField {\n readonly key: string;\n readonly value: PersistValue;\n}\n\n/** Versioned draft payload stored under one controller namespace. */\ninterface PersistPayload {\n readonly version: 1;\n readonly fields: PersistPayloadField[];\n}\n\ntype PersistOperation = \"read\" | \"write\" | \"remove\";\ntype PersistErrorReason = \"unavailable\" | \"invalid-payload\";\n\n/** One scheduled write, including the namespace it was created for. */\ninterface PendingSave {\n readonly id: number;\n readonly logicalKey: string;\n}\n\n/** Input types that carry no meaningful, restorable value. */\nconst NON_VALUE_TYPES = new Set([\"file\", \"submit\", \"reset\", \"button\", \"image\"]);\n/** Passwords are never persisted, even when a custom exclusion list is supplied. */\nconst SENSITIVE_TYPES = new Set([\"password\"]);\n/** Field types and names skipped by the default configuration. */\nconst DEFAULT_EXCLUDE = [\"authenticity_token\", \"_method\", \"utf8\"];\n/** localStorage key prefix so drafts never clobber unrelated app storage. */\nconst STORAGE_PREFIX = \"stimeo--persist:\";\n/** Separator for disambiguating repeated same-name fields. */\nconst OCCURRENCE_SEP = \"\\u0000\";\n/** The one observed attribute that can move fields between nested Persist hosts. */\nconst OWNERSHIP_ATTRIBUTE = \"data-controller\";\nconst DEFAULT_DEBOUNCE = 400;\n\n/** Whether a parsed JSON value is a plain object-like record. */\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** Whether a parsed JSON value belongs to the persisted field-value union. */\nconst isPersistValue = (value: unknown): value is PersistValue =>\n typeof value === \"string\" ||\n typeof value === \"boolean\" ||\n (Array.isArray(value) && value.every((item) => typeof item === \"string\"));\n\n/** Parses and validates the complete versioned payload without type assertions. */\nconst parsePayload = (raw: string): PersistPayload | null => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.fields)) return null;\n\n const fields: PersistPayloadField[] = [];\n const keys = new Set<string>();\n for (const candidate of parsed.fields) {\n if (!isRecord(candidate)) return null;\n const { key, value } = candidate;\n if (typeof key !== \"string\" || key.length === 0 || keys.has(key)) return null;\n if (!isPersistValue(value)) return null;\n keys.add(key);\n fields.push({ key, value });\n }\n return { version: 1, fields };\n};\n\n/** Whether a node is one of the native controls supported by Persist. */\nconst isPersistField = (node: unknown): node is PersistField =>\n node instanceof HTMLInputElement ||\n node instanceof HTMLTextAreaElement ||\n node instanceof HTMLSelectElement;\n\n/**\n * Headless draft-autosave behavior: persists form controls to `localStorage`\n * and restores them across Turbo navigation and reloads.\n *\n * Markup contract (identifier: `stimeo--persist`):\n * <form data-controller=\"stimeo--persist\"\n * data-stimeo--persist-key-value=\"contact-draft\">\n * <input name=\"name\">\n * <textarea name=\"message\"></textarea>\n * </form>\n *\n * The optional `field` targets restrict persistence to explicitly targeted\n * controls. The public `clear` action removes the active draft. `key`,\n * `debounce`, `exclude`, and `clearOn` Values can change while connected;\n * `clearOn` is an event name whose occurrence invokes `clear`.\n *\n * `restore`, `save`, and `clear` dispatch `{ key: string }`. `error` dispatches\n * `{ key: string, operation: \"read\" | \"write\" | \"remove\", reason:\n * \"unavailable\" | \"invalid-payload\" }`. Restore writes are intentionally\n * silent native-control mutations; consumers synchronize from the bubbling\n * `restore` event without treating a restored draft as user input.\n *\n * @remarks\n * Behavior only — restoration never moves focus. Passwords are never stored;\n * the default exclusion also omits Rails request metadata while preserving\n * application-owned hidden fields. Distinct logical keys isolate instances;\n * instances using the same key intentionally share one last-writer-wins draft.\n * Every listener, timer, observer, and queued reconciliation is released on\n * `disconnect()`, after any pending edit is flushed through the normal save\n * path. A reconnect removes stale restored state before reading storage.\n */\nexport class PersistController extends Controller<HTMLElement> {\n static override targets = [\"field\"];\n static override values = {\n key: { type: String, default: \"\" },\n debounce: { type: Number, default: DEFAULT_DEBOUNCE },\n // A malformed JSON list must fall back without aborting Stimulus connection.\n exclude: { type: String, default: \"\" },\n clearOn: { type: String, default: \"\" },\n };\n static actions = [\"clear\"] as const;\n static events = [\"restore\", \"save\", \"clear\", \"error\"] as const;\n\n declare readonly fieldTargets: Element[];\n declare readonly hasFieldTarget: boolean;\n\n declare keyValue: string;\n declare debounceValue: number;\n declare excludeValue: string;\n declare clearOnValue: string;\n\n readonly #timeouts = new SafeTimeout();\n readonly #restoreDynamic = new MicrotaskCoalescer(() => this.#restoreDynamicFields());\n readonly #observer = new MutationObserver((records) => this.#onMutations(records));\n #pendingSave: PendingSave | null = null;\n #connected = false;\n #logicalKey: string | null = null;\n #payload: PersistPayload | null = null;\n #registeredClearOn: string | null = null;\n #excluded: readonly string[] = DEFAULT_EXCLUDE;\n #knownFields = new WeakSet<PersistField>();\n #forcedRestore = new Set<PersistField>();\n\n readonly #onInput = (event: Event): void => {\n const field = event.target;\n if (!isPersistField(field) || !this.#ownsField(field)) return;\n if (this.hasFieldTarget && !this.fieldTargets.includes(field)) return;\n if (this.#keyOf(field) === null || !this.#persistable(field, this.#excluded)) return;\n this.#scheduleSave();\n };\n\n readonly #onClearEvent = (): void => {\n this.clear();\n };\n\n override connect(): void {\n if (this.#connected) return;\n this.#connected = true;\n this.#logicalKey = this.#resolveLogicalKey();\n this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);\n this.#knownFields = new WeakSet<PersistField>();\n this.#forcedRestore.clear();\n this.element.removeAttribute(\"data-persist-restored\");\n\n this.element.addEventListener(\"input\", this.#onInput);\n this.element.addEventListener(\"change\", this.#onInput);\n this.#syncClearOn();\n this.#restoreDynamic.activate();\n this.#observer.observe(this.element, {\n attributes: true,\n attributeFilter: [\n \"checked\",\n \"data-controller\",\n \"data-stimeo--persist-target\",\n \"id\",\n \"multiple\",\n \"name\",\n \"selected\",\n \"type\",\n \"value\",\n ],\n childList: true,\n subtree: true,\n });\n this.#loadActiveDraft();\n }\n\n override disconnect(): void {\n this.#flushPendingSave();\n this.#connected = false;\n this.element.removeEventListener(\"input\", this.#onInput);\n this.element.removeEventListener(\"change\", this.#onInput);\n this.#unbindClearOn();\n this.#observer.disconnect();\n this.#restoreDynamic.cancel();\n this.#forcedRestore.clear();\n this.#timeouts.clearAll();\n this.element.removeAttribute(\"data-persist-restored\");\n this.#payload = null;\n }\n\n /** Drops the active draft after storage confirms the removal. */\n clear(): void {\n const logicalKey = this.#logicalKey ?? this.#resolveLogicalKey();\n if (logicalKey === null) return;\n this.#cancelPendingSave();\n const result = removeLocalStorage(this.#storageKey(logicalKey));\n if (!result.ok) {\n this.#dispatchError(logicalKey, \"remove\", \"unavailable\");\n return;\n }\n this.#payload = null;\n this.element.removeAttribute(\"data-persist-restored\");\n this.dispatch(\"clear\", { detail: { key: logicalKey } });\n }\n\n /** Rebinds the external clear event when its Value changes. */\n clearOnValueChanged(): void {\n if (this.#connected) this.#syncClearOn();\n }\n\n /** Switches storage namespaces without writing old edits under the new key. */\n keyValueChanged(): void {\n if (this.#connected) this.#switchLogicalKey();\n }\n\n /** Reschedules a pending write against the current debounce delay. */\n debounceValueChanged(): void {\n if (this.#connected && this.#pendingSave !== null) this.#scheduleSave();\n }\n\n /** Restores every field the new exclusion list makes eligible. */\n excludeValueChanged(): void {\n const previous = this.#excluded;\n this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);\n if (!this.#connected) return;\n // A field restored once stays in #knownFields, so releasing it from the exclusion\n // list has to name it explicitly to reach the next batch. Fields the list never\n // covered keep their live value, which is what the next batch already guarantees.\n for (const field of this.#candidateFields()) {\n if (this.#persistable(field, this.#excluded) && !this.#persistable(field, previous)) {\n this.#forcedRestore.add(field);\n }\n }\n this.#restoreDynamic.schedule();\n }\n\n /** Schedules a debounced save. */\n #scheduleSave(): void {\n const logicalKey = this.#logicalKey;\n if (logicalKey === null) return;\n this.#cancelPendingSave();\n const id = this.#timeouts.set(() => {\n this.#pendingSave = null;\n this.#save(logicalKey);\n }, this.#debounceDelay);\n this.#pendingSave = { id, logicalKey };\n }\n\n /** Cancels the currently pending save, if any. */\n #cancelPendingSave(): void {\n const pending = this.#pendingSave;\n if (pending === null) return;\n this.#timeouts.clear(pending.id);\n this.#pendingSave = null;\n }\n\n /** Flushes a pending edit through the same success/error event path as a timer. */\n #flushPendingSave(): void {\n const pending = this.#pendingSave;\n if (pending === null) return;\n this.#cancelPendingSave();\n this.#save(pending.logicalKey);\n }\n\n /** Writes the active payload and emits `save` only after storage succeeds. */\n #save(logicalKey: string): boolean {\n const payload = this.#serialize();\n const result = writeLocalStorage(this.#storageKey(logicalKey), JSON.stringify(payload));\n if (!result.ok) {\n this.#dispatchError(logicalKey, \"write\", \"unavailable\");\n return false;\n }\n this.#payload = payload;\n this.dispatch(\"save\", { detail: { key: logicalKey } });\n return true;\n }\n\n /** Serializes every persistable field into a versioned, typed payload. */\n #serialize(): PersistPayload {\n const fields: PersistPayloadField[] = [];\n for (const { field, key } of this.#fieldEntries()) {\n if (field instanceof HTMLInputElement && field.type === \"checkbox\") {\n fields.push({ key, value: field.checked });\n } else if (field instanceof HTMLInputElement && field.type === \"radio\") {\n if (field.checked) fields.push({ key, value: field.value });\n } else if (field instanceof HTMLSelectElement && field.multiple) {\n fields.push({\n key,\n value: Array.from(field.selectedOptions).map((option) => option.value),\n });\n } else {\n fields.push({ key, value: field.value });\n }\n }\n return { version: 1, fields };\n }\n\n /** Reads, validates, and restores the active namespace. */\n #loadActiveDraft(): void {\n this.element.removeAttribute(\"data-persist-restored\");\n this.#payload = null;\n this.#knownFields = new WeakSet<PersistField>();\n this.#forcedRestore.clear();\n const logicalKey = this.#logicalKey;\n if (logicalKey === null) {\n this.#markCurrentFieldsKnown();\n return;\n }\n\n const result = readLocalStorage(this.#storageKey(logicalKey));\n if (!result.ok) {\n this.#dispatchError(logicalKey, \"read\", \"unavailable\");\n this.#markCurrentFieldsKnown();\n return;\n }\n if (result.value === null) {\n this.#markCurrentFieldsKnown();\n return;\n }\n\n const payload = parsePayload(result.value);\n if (payload === null) {\n const removal = removeLocalStorage(this.#storageKey(logicalKey));\n // Cleanup precedes notification so an error consumer can safely replace\n // the invalid value without that repair being deleted on callback return.\n this.#dispatchError(logicalKey, \"read\", \"invalid-payload\");\n if (!removal.ok) this.#dispatchError(logicalKey, \"remove\", \"unavailable\");\n this.#markCurrentFieldsKnown();\n return;\n }\n this.#payload = payload;\n this.#restoreEntries(null);\n }\n\n /** Restores fields inserted or made eligible after the initial connection. */\n #restoreDynamicFields(): void {\n const selected = new Set<PersistField>(this.#forcedRestore);\n this.#forcedRestore.clear();\n for (const { field } of this.#fieldEntries()) {\n if (!this.#knownFields.has(field)) selected.add(field);\n }\n this.#restoreEntries(selected);\n }\n\n /** Applies the active payload to all fields, or only the supplied candidates. */\n #restoreEntries(selected: ReadonlySet<PersistField> | null): void {\n const entries = this.#fieldEntries();\n for (const { field } of entries) this.#knownFields.add(field);\n const payload = this.#payload;\n if (payload === null) return;\n const savedFields = new Map(payload.fields.map((field) => [field.key, field]));\n const restoredKeys = new Set<string>();\n const processedRadios = new Set<string>();\n\n for (const entry of entries) {\n if (selected !== null && !selected.has(entry.field)) continue;\n const saved = savedFields.get(entry.key);\n if (saved === undefined) continue;\n const { value } = saved;\n\n if (entry.field instanceof HTMLInputElement && entry.field.type === \"radio\") {\n if (processedRadios.has(entry.key)) continue;\n processedRadios.add(entry.key);\n const radios = entries\n .filter(\n ({ key, field }) =>\n key === entry.key && field instanceof HTMLInputElement && field.type === \"radio\",\n )\n .map(({ field }) => field as HTMLInputElement);\n if (typeof value !== \"string\" || !radios.some((radio) => radio.value === value)) continue;\n for (const radio of radios) radio.checked = radio.value === value;\n restoredKeys.add(entry.key);\n continue;\n }\n\n if (this.#applyValue(entry.field, value)) restoredKeys.add(entry.key);\n }\n\n if (restoredKeys.size > 0) {\n // Property writes can reflect to observed attributes in DOM implementations.\n // Drop only records produced by this restore before notifying consumers, so\n // authored mutations made from a restore listener remain observable.\n this.#observer.takeRecords();\n this.element.setAttribute(\"data-persist-restored\", \"true\");\n this.dispatch(\"restore\", { detail: { key: this.#logicalKey } });\n }\n }\n\n /** Applies a type-compatible stored value without coercion. */\n #applyValue(field: PersistField, value: PersistValue): boolean {\n if (field instanceof HTMLInputElement && field.type === \"checkbox\") {\n if (typeof value !== \"boolean\") return false;\n field.checked = value;\n return true;\n }\n if (field instanceof HTMLSelectElement && field.multiple) {\n if (!Array.isArray(value)) return false;\n const available = new Set(Array.from(field.options).map((option) => option.value));\n const selected = value.filter((candidate) => available.has(candidate));\n if (value.length > 0 && selected.length === 0) return false;\n const selectedValues = new Set(selected);\n for (const option of Array.from(field.options)) {\n option.selected = selectedValues.has(option.value);\n }\n return true;\n }\n if (typeof value !== \"string\") return false;\n if (field instanceof HTMLSelectElement) {\n const exists = Array.from(field.options).some((option) => option.value === value);\n if (!exists) return false;\n }\n field.value = value;\n return true;\n }\n\n /** Every control this instance owns, before the exclusion list narrows them. */\n #candidateFields(): PersistField[] {\n return this.hasFieldTarget\n ? this.fieldTargets.filter(isPersistField)\n : Array.from(this.element.querySelectorAll<PersistField>(\"input, textarea, select\")).filter(\n (field) => this.#ownsField(field),\n );\n }\n\n /**\n * Pairs persistable fields with DOM-order occurrence keys. Every HTML radio\n * group (same name and form owner) occupies one shared occurrence slot.\n */\n #fieldEntries(): Array<{ field: PersistField; key: string }> {\n const entries: Array<{ field: PersistField; key: string }> = [];\n const eligible: Array<{ field: PersistField; name: string }> = [];\n for (const field of this.#candidateFields()) {\n const name = this.#keyOf(field);\n if (name === null || !this.#persistable(field, this.#excluded)) continue;\n eligible.push({ field, name });\n }\n\n const nextOccurrence = new Map<string, number>();\n const radioSlots = new Map<string, Map<HTMLFormElement | null, string>>();\n const allocate = (name: string): string => {\n const occurrence = nextOccurrence.get(name) ?? 0;\n nextOccurrence.set(name, occurrence + 1);\n return occurrence === 0 ? name : `${name}${OCCURRENCE_SEP}${occurrence}`;\n };\n for (const { field, name } of eligible) {\n if (field instanceof HTMLInputElement && field.type === \"radio\") {\n const groups = radioSlots.get(name) ?? new Map<HTMLFormElement | null, string>();\n radioSlots.set(name, groups);\n let key = groups.get(field.form);\n if (key === undefined) {\n key = allocate(name);\n groups.set(field.form, key);\n }\n entries.push({ field, key });\n continue;\n }\n entries.push({ field, key: allocate(name) });\n }\n return entries;\n }\n\n /** Whether a field carries a restorable, non-excluded value. */\n #persistable(field: PersistField, excluded: readonly string[]): boolean {\n if (NON_VALUE_TYPES.has(field.type) || SENSITIVE_TYPES.has(field.type)) return false;\n if (excluded.includes(field.type)) return false;\n if (field.name.length > 0 && excluded.includes(field.name)) return false;\n return true;\n }\n\n /** A stable storage sub-key for a field (its name, else id). */\n #keyOf(field: PersistField): string | null {\n return field.name || field.id || null;\n }\n\n /** Whether this instance, rather than a nested Persist host, owns a field. */\n #ownsField(field: PersistField): boolean {\n return field.closest('[data-controller~=\"stimeo--persist\"]') === this.element;\n }\n\n /** Marks all currently eligible fields without applying a payload. */\n #markCurrentFieldsKnown(): void {\n for (const { field } of this.#fieldEntries()) this.#knownFields.add(field);\n }\n\n /** Switches namespace after flushing the old namespace's pending edit. */\n #switchLogicalKey(): void {\n const next = this.#resolveLogicalKey();\n if (next === this.#logicalKey) return;\n this.#flushPendingSave();\n this.#logicalKey = next;\n this.#loadActiveDraft();\n }\n\n /** Synchronizes the exact event name owned by the `clearOn` Value. */\n #syncClearOn(): void {\n const next = this.#validEventName(this.clearOnValue);\n if (next === this.#registeredClearOn) return;\n this.#unbindClearOn();\n if (next === null) return;\n this.element.addEventListener(next, this.#onClearEvent);\n this.#registeredClearOn = next;\n }\n\n /** Removes the event listener using the name that was actually registered. */\n #unbindClearOn(): void {\n if (this.#registeredClearOn === null) return;\n this.element.removeEventListener(this.#registeredClearOn, this.#onClearEvent);\n this.#registeredClearOn = null;\n }\n\n /** Returns a non-whitespace event type, or null when the hook is disabled. */\n #validEventName(value: string): string | null {\n const name = value.trim();\n return name.length > 0 && !/\\s/.test(name) ? name : null;\n }\n\n /** Collects dynamic controls and select-option changes into one restore pass. */\n #onMutations(records: MutationRecord[]): void {\n let rootIdChanged = false;\n for (const record of records) {\n if (record.type === \"attributes\") {\n if (record.target === this.element && record.attributeName === \"id\") {\n rootIdChanged = true;\n }\n this.#collectAttributeCandidate(record.target, record.attributeName);\n continue;\n }\n for (const node of record.addedNodes) this.#collectRestoreCandidate(node);\n }\n if (rootIdChanged && this.keyValue.length === 0) this.#switchLogicalKey();\n this.#restoreDynamic.schedule();\n }\n\n /**\n * Adds the controls one mutated attribute can affect. Only an ownership change on a\n * descendant moves fields between Persist hosts, so that is the single case worth a\n * subtree sweep; every other observed attribute describes one control, and sweeping\n * from its container would re-apply the stored draft over edits still being debounced.\n */\n #collectAttributeCandidate(target: Node, attributeName: string | null): void {\n if (attributeName === OWNERSHIP_ATTRIBUTE && target !== this.element) {\n this.#collectRestoreCandidate(target);\n return;\n }\n this.#collectControlCandidate(target);\n }\n\n /** Adds a native control, or the select that owns a mutated option. */\n #collectControlCandidate(node: Node): void {\n if (isPersistField(node)) {\n this.#forcedRestore.add(node);\n return;\n }\n if (node instanceof HTMLOptionElement) {\n const select = node.closest(\"select\");\n if (select) this.#forcedRestore.add(select);\n }\n }\n\n /** Adds every control inside an inserted or newly owned subtree. */\n #collectRestoreCandidate(node: Node): void {\n this.#collectControlCandidate(node);\n if (!(node instanceof Element)) return;\n for (const field of node.querySelectorAll<PersistField>(\"input, textarea, select\")) {\n this.#forcedRestore.add(field);\n }\n }\n\n /** Resolves the logical key from the Value, falling back to the host id. */\n #resolveLogicalKey(): string | null {\n const key = this.keyValue || this.element.id;\n return key.length > 0 ? key : null;\n }\n\n /** Namespaces one logical key within localStorage. */\n #storageKey(logicalKey: string): string {\n return `${STORAGE_PREFIX}${logicalKey}`;\n }\n\n /** Normalizes invalid debounce Values to the documented default. */\n get #debounceDelay(): number {\n const value = this.debounceValue;\n return Number.isFinite(value) && value >= 0 ? value : DEFAULT_DEBOUNCE;\n }\n\n /** Dispatches one observable storage failure without exposing browser errors. */\n #dispatchError(key: string, operation: PersistOperation, reason: PersistErrorReason): void {\n this.dispatch(\"error\", { detail: { key, operation, reason } });\n }\n}\n"]}
@@ -29,7 +29,7 @@ import { Controller } from '@hotwired/stimulus';
29
29
  * - On open, focus moves to the first focusable element inside the panel (or the
30
30
  * panel itself if it has none).
31
31
  * - `Escape` closes and restores focus to the trigger. While open the panel is a
32
- * layer on the shared {@link EscapeLayer} stack; it claims a press only while
32
+ * layer on the shared `EscapeLayer` stack; it claims a press only while
33
33
  * focus is inside the controller or fell to the body (a click on non-focusable
34
34
  * panel content), so a press aimed at another layer is never consumed here,
35
35
  * and one keypress closes exactly one layer.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/escape_layer.ts","../../src/utils/focus_candidate.ts","../../src/utils/scroll_dismiss.ts","../../src/controllers/popover_controller.ts"],"names":[],"mappings":"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","file":"popover_controller.js","sourcesContent":["/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\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 on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the three conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","/**\n * Invokes `onScroll` when the page — or any scrollable ancestor of `element` —\n * scrolls, returning a cleanup that detaches every listener.\n *\n * `scroll` events do not bubble, and a single capture-phase listener on\n * `window`/`document` is unreliable for descendant scroll containers — so this\n * attaches a `scroll` listener directly to each scroll-parent ancestor (walking\n * `parentElement` and checking `overflow`) plus the window. That mirrors the\n * *intent* of `@floating-ui/dom`'s `autoUpdate` scroll tracking; it is a\n * deliberately simple resolver and does not cross shadow roots. This is precisely\n * the case a consumer cannot cover with `data-action` alone, which is why the\n * surface controllers own it.\n *\n * The listeners are `passive` (the handler never calls `preventDefault`), so they\n * never delay scrolling. Pass the surface's anchor/root so the relevant scroll\n * parents are found even when the floating element is portaled elsewhere.\n */\nexport function observeScrollDismiss(element: Element, onScroll: () => void): () => void {\n const targets: Array<Element | Window> = [...scrollParents(element), window];\n const handler = (): void => onScroll();\n for (const target of targets) {\n target.addEventListener(\"scroll\", handler, { passive: true });\n }\n return () => {\n for (const target of targets) target.removeEventListener(\"scroll\", handler);\n };\n}\n\n/** The scrollable ancestors of `element`, nearest first (window excluded). */\nfunction scrollParents(element: Element): Element[] {\n const parents: Element[] = [];\n let node = element.parentElement;\n while (node) {\n if (isScrollable(node)) parents.push(node);\n node = node.parentElement;\n }\n return parents;\n}\n\n/** Whether `element` is a scroll container (any axis `auto`/`scroll`/`overlay`). */\nfunction isScrollable(element: Element): boolean {\n const style = getComputedStyle(element);\n return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { firstTabStop } from \"../utils/focus_candidate\";\nimport { observeScrollDismiss } from \"../utils/scroll_dismiss\";\n\n/**\n * Headless, accessible **non-modal popover** behavior.\n *\n * Markup contract (identifier: `stimeo--popover`):\n * <div data-controller=\"stimeo--popover\">\n * <button data-stimeo--popover-target=\"trigger\"\n * aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"pop\"\n * data-action=\"click->stimeo--popover#toggle\">Edit profile</button>\n * <div id=\"pop\" data-stimeo--popover-target=\"panel\"\n * role=\"dialog\" aria-label=\"Edit profile\" hidden>…</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Dialog** pattern run *non-modally* (no\n * `aria-modal`, no focus trap, no `inert`/scroll lock). The background stays\n * fully interactive; this is the modeless counterpart to `stimeo--dialog`. For\n * a roving `role=\"menu\"` of commands use `stimeo--menu`; for decorative-only\n * text use `stimeo--tooltip`.\n *\n * @remarks\n * Behavior only — static placement is the consumer's CSS, and dynamic\n * edge-collision avoidance is delegated to the opt-in `stimeo-ui/positioning`\n * module (this controller never imports it, preserving the zero-dep core). State\n * is exposed via the trigger's `aria-expanded` and the panel's `hidden`.\n *\n * Behavior provided:\n * - Click the trigger to toggle (`aria-expanded` + `hidden` reflect state).\n * - On open, focus moves to the first focusable element inside the panel (or the\n * panel itself if it has none).\n * - `Escape` closes and restores focus to the trigger. While open the panel is a\n * layer on the shared {@link EscapeLayer} stack; it claims a press only while\n * focus is inside the controller or fell to the body (a click on non-focusable\n * panel content), so a press aimed at another layer is never consumed here,\n * and one keypress closes exactly one layer.\n * - An outside click (anywhere off the controller element) closes without moving\n * focus. Focus stays at the clicked element, or falls back to the document body\n * for a non-focusable destination.\n * - Because it is modeless, focus is *not* trapped: when `Tab` moves focus out of\n * the controller it closes (detected via bubbling `focusout`) without yanking\n * focus back, so forward and reverse traversal preserve their natural destination.\n * A `focusout` with no destination is ignored because it also occurs for clicks\n * on non-focusable content and when the browser window loses focus.\n * - Opt-in **dismiss on scroll** (`closeOnScroll`): while open, scrolling a tracked\n * scroll-parent ancestor (or the window) closes the panel, the usual convention for\n * anchored popups. Closes without restoring focus (like the modeless `focusout` path)\n * so the close never fights the user's scroll. Off by default.\n */\nexport class PopoverController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"panel\"];\n static override values = {\n closeOnScroll: { type: Boolean, default: false },\n };\n static actions = [\"close\", \"open\", \"toggle\"] as const;\n\n declare readonly triggerTarget: HTMLButtonElement;\n declare readonly panelTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasPanelTarget: boolean;\n declare readonly closeOnScrollValue: boolean;\n\n /** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */\n #stopScrollDismiss: (() => void) | null = null;\n /** Escape-stack membership while open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Starts closed and registers the standing dismissal listeners. */\n override connect(): void {\n this.close();\n document.addEventListener(\"click\", this.#onOutsideClick, true);\n this.element.addEventListener(\"focusout\", this.#onFocusOut);\n }\n\n /**\n * Removes every standing listener registered in {@link connect} plus any active\n * dismiss-on-scroll observers. `removeEventListener` is a no-op when it was\n * never added, so this is safe in the closed state too — no listener outlives\n * the element after a Turbo navigation.\n */\n override disconnect(): void {\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"click\", this.#onOutsideClick, true);\n this.element.removeEventListener(\"focusout\", this.#onFocusOut);\n this.#stopScrollDismiss?.();\n this.#stopScrollDismiss = null;\n }\n\n /** Toggles the popover. Bound via `data-action` (click on the trigger). */\n toggle(): void {\n if (this.#isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /** Opens the panel, reflects state, and moves focus inside it. */\n open(): void {\n if (!this.hasPanelTarget || this.#isOpen) return;\n this.panelTarget.hidden = false;\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"true\");\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(),\n claims: claimsWhileFocusWithin(this.element),\n });\n if (this.closeOnScrollValue && !this.#stopScrollDismiss) {\n // Close (no focus restore) so dismissing never fights the user's scroll.\n this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.close());\n }\n this.#focusFirst();\n }\n\n /** Closes the panel and reflects the collapsed state. Bound via `data-action`. */\n close(): void {\n // Release listeners first, unconditionally: a consumer may remove the panel\n // target while it is open, and an early return would leak scroll observers.\n this.#escapeLayer.deactivate();\n this.#stopScrollDismiss?.();\n this.#stopScrollDismiss = null;\n if (this.hasPanelTarget) this.panelTarget.hidden = true;\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"false\");\n }\n\n /** Moves focus to the first focusable element in the panel, or the panel itself. */\n #focusFirst(): void {\n const first = firstTabStop(this.panelTarget);\n if (first) {\n first.focus();\n return;\n }\n if (!this.panelTarget.hasAttribute(\"tabindex\")) this.panelTarget.tabIndex = -1;\n this.panelTarget.focus();\n }\n\n /** Closes and restores focus to the trigger for explicit keyboard dismissal. */\n #closeAndRestore(): void {\n this.close();\n if (this.hasTriggerTarget) this.triggerTarget.focus();\n }\n\n /** Closes without moving focus when a click lands outside the controller element. */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n const target = event.target;\n if (this.#isOpen && target instanceof Node && !this.element.contains(target)) this.close();\n };\n\n /**\n * Closes when focus leaves the controller for a known external destination\n * (e.g. forward Tab past the panel or reverse Tab past the trigger). Focus is\n * not restored — the natural destination is kept, which is the modeless\n * contract. A null/non-Node destination is indeterminate: browsers use it for\n * clicks on non-focusable content and window deactivation, so the later outside\n * click handler decides pointer dismissal instead.\n */\n readonly #onFocusOut = (event: FocusEvent): void => {\n if (!this.#isOpen) return;\n const next = event.relatedTarget;\n if (!(next instanceof Node) || this.element.contains(next)) return;\n this.close();\n };\n\n /** Whether the panel is currently visible. */\n get #isOpen(): boolean {\n return this.hasPanelTarget && !this.panelTarget.hidden;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/escape_layer.ts","../../src/utils/focus_candidate.ts","../../src/utils/scroll_dismiss.ts","../../src/controllers/popover_controller.ts"],"names":[],"mappings":"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","file":"popover_controller.js","sourcesContent":["/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\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 on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the four 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","/**\n * Invokes `onScroll` when the page — or any scrollable ancestor of `element` —\n * scrolls, returning a cleanup that detaches every listener.\n *\n * `scroll` events do not bubble, and a single capture-phase listener on\n * `window`/`document` is unreliable for descendant scroll containers — so this\n * attaches a `scroll` listener directly to each scroll-parent ancestor (walking\n * `parentElement` and checking `overflow`) plus the window. That mirrors the\n * *intent* of `@floating-ui/dom`'s `autoUpdate` scroll tracking; it is a\n * deliberately simple resolver and does not cross shadow roots. This is precisely\n * the case a consumer cannot cover with `data-action` alone, which is why the\n * surface controllers own it.\n *\n * The listeners are `passive` (the handler never calls `preventDefault`), so they\n * never delay scrolling. Pass the surface's anchor/root so the relevant scroll\n * parents are found even when the floating element is portaled elsewhere.\n */\nexport function observeScrollDismiss(element: Element, onScroll: () => void): () => void {\n const targets: Array<Element | Window> = [...scrollParents(element), window];\n const handler = (): void => onScroll();\n for (const target of targets) {\n target.addEventListener(\"scroll\", handler, { passive: true });\n }\n return () => {\n for (const target of targets) target.removeEventListener(\"scroll\", handler);\n };\n}\n\n/** The scrollable ancestors of `element`, nearest first (window excluded). */\nfunction scrollParents(element: Element): Element[] {\n const parents: Element[] = [];\n let node = element.parentElement;\n while (node) {\n if (isScrollable(node)) parents.push(node);\n node = node.parentElement;\n }\n return parents;\n}\n\n/** Whether `element` is a scroll container (any axis `auto`/`scroll`/`overlay`). */\nfunction isScrollable(element: Element): boolean {\n const style = getComputedStyle(element);\n return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { firstTabStop } from \"../utils/focus_candidate\";\nimport { observeScrollDismiss } from \"../utils/scroll_dismiss\";\n\n/**\n * Headless, accessible **non-modal popover** behavior.\n *\n * Markup contract (identifier: `stimeo--popover`):\n * <div data-controller=\"stimeo--popover\">\n * <button data-stimeo--popover-target=\"trigger\"\n * aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"pop\"\n * data-action=\"click->stimeo--popover#toggle\">Edit profile</button>\n * <div id=\"pop\" data-stimeo--popover-target=\"panel\"\n * role=\"dialog\" aria-label=\"Edit profile\" hidden>…</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Dialog** pattern run *non-modally* (no\n * `aria-modal`, no focus trap, no `inert`/scroll lock). The background stays\n * fully interactive; this is the modeless counterpart to `stimeo--dialog`. For\n * a roving `role=\"menu\"` of commands use `stimeo--menu`; for decorative-only\n * text use `stimeo--tooltip`.\n *\n * @remarks\n * Behavior only — static placement is the consumer's CSS, and dynamic\n * edge-collision avoidance is delegated to the opt-in `stimeo-ui/positioning`\n * module (this controller never imports it, preserving the zero-dep core). State\n * is exposed via the trigger's `aria-expanded` and the panel's `hidden`.\n *\n * Behavior provided:\n * - Click the trigger to toggle (`aria-expanded` + `hidden` reflect state).\n * - On open, focus moves to the first focusable element inside the panel (or the\n * panel itself if it has none).\n * - `Escape` closes and restores focus to the trigger. While open the panel is a\n * layer on the shared `EscapeLayer` stack; it claims a press only while\n * focus is inside the controller or fell to the body (a click on non-focusable\n * panel content), so a press aimed at another layer is never consumed here,\n * and one keypress closes exactly one layer.\n * - An outside click (anywhere off the controller element) closes without moving\n * focus. Focus stays at the clicked element, or falls back to the document body\n * for a non-focusable destination.\n * - Because it is modeless, focus is *not* trapped: when `Tab` moves focus out of\n * the controller it closes (detected via bubbling `focusout`) without yanking\n * focus back, so forward and reverse traversal preserve their natural destination.\n * A `focusout` with no destination is ignored because it also occurs for clicks\n * on non-focusable content and when the browser window loses focus.\n * - Opt-in **dismiss on scroll** (`closeOnScroll`): while open, scrolling a tracked\n * scroll-parent ancestor (or the window) closes the panel, the usual convention for\n * anchored popups. Closes without restoring focus (like the modeless `focusout` path)\n * so the close never fights the user's scroll. Off by default.\n */\nexport class PopoverController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"panel\"];\n static override values = {\n closeOnScroll: { type: Boolean, default: false },\n };\n static actions = [\"close\", \"open\", \"toggle\"] as const;\n\n declare readonly triggerTarget: HTMLButtonElement;\n declare readonly panelTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasPanelTarget: boolean;\n declare readonly closeOnScrollValue: boolean;\n\n /** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */\n #stopScrollDismiss: (() => void) | null = null;\n /** Escape-stack membership while open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Starts closed and registers the standing dismissal listeners. */\n override connect(): void {\n this.close();\n document.addEventListener(\"click\", this.#onOutsideClick, true);\n this.element.addEventListener(\"focusout\", this.#onFocusOut);\n }\n\n /**\n * Removes every standing listener registered in {@link connect} plus any active\n * dismiss-on-scroll observers. `removeEventListener` is a no-op when it was\n * never added, so this is safe in the closed state too — no listener outlives\n * the element after a Turbo navigation.\n */\n override disconnect(): void {\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"click\", this.#onOutsideClick, true);\n this.element.removeEventListener(\"focusout\", this.#onFocusOut);\n this.#stopScrollDismiss?.();\n this.#stopScrollDismiss = null;\n }\n\n /** Toggles the popover. Bound via `data-action` (click on the trigger). */\n toggle(): void {\n if (this.#isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /** Opens the panel, reflects state, and moves focus inside it. */\n open(): void {\n if (!this.hasPanelTarget || this.#isOpen) return;\n this.panelTarget.hidden = false;\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"true\");\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(),\n claims: claimsWhileFocusWithin(this.element),\n });\n if (this.closeOnScrollValue && !this.#stopScrollDismiss) {\n // Close (no focus restore) so dismissing never fights the user's scroll.\n this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.close());\n }\n this.#focusFirst();\n }\n\n /** Closes the panel and reflects the collapsed state. Bound via `data-action`. */\n close(): void {\n // Release listeners first, unconditionally: a consumer may remove the panel\n // target while it is open, and an early return would leak scroll observers.\n this.#escapeLayer.deactivate();\n this.#stopScrollDismiss?.();\n this.#stopScrollDismiss = null;\n if (this.hasPanelTarget) this.panelTarget.hidden = true;\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"false\");\n }\n\n /** Moves focus to the first focusable element in the panel, or the panel itself. */\n #focusFirst(): void {\n const first = firstTabStop(this.panelTarget);\n if (first) {\n first.focus();\n return;\n }\n if (!this.panelTarget.hasAttribute(\"tabindex\")) this.panelTarget.tabIndex = -1;\n this.panelTarget.focus();\n }\n\n /** Closes and restores focus to the trigger for explicit keyboard dismissal. */\n #closeAndRestore(): void {\n this.close();\n if (this.hasTriggerTarget) this.triggerTarget.focus();\n }\n\n /** Closes without moving focus when a click lands outside the controller element. */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n const target = event.target;\n if (this.#isOpen && target instanceof Node && !this.element.contains(target)) this.close();\n };\n\n /**\n * Closes when focus leaves the controller for a known external destination\n * (e.g. forward Tab past the panel or reverse Tab past the trigger). Focus is\n * not restored — the natural destination is kept, which is the modeless\n * contract. A null/non-Node destination is indeterminate: browsers use it for\n * clicks on non-focusable content and window deactivation, so the later outside\n * click handler decides pointer dismissal instead.\n */\n readonly #onFocusOut = (event: FocusEvent): void => {\n if (!this.#isOpen) return;\n const next = event.relatedTarget;\n if (!(next instanceof Node) || this.element.contains(next)) return;\n this.close();\n };\n\n /** Whether the panel is currently visible. */\n get #isOpen(): boolean {\n return this.hasPanelTarget && !this.panelTarget.hidden;\n }\n}\n"]}
@@ -23,7 +23,7 @@ import { Controller } from '@hotwired/stimulus';
23
23
  * @remarks
24
24
  * Behavior only — no positioning (pair with `stimeo-ui/positioning`) and no focus
25
25
  * trapping (pair with `stimeo--focus` or the overlay). Moving a Stimulus element within
26
- * the same document re-fires connect/disconnect; {@link DetachGate} reads that pair as an
26
+ * the same document re-fires connect/disconnect; `DetachGate` reads that pair as an
27
27
  * in-page move rather than a detach, which is what keeps the move safe. For Turbo
28
28
  * compatibility prefer the `content`-target form: the controller then stays on the
29
29
  * in-place source, so its `disconnect()` fires when the original container is replaced
@@ -31,7 +31,7 @@ import { Controller } from '@hotwired/stimulus';
31
31
  * move is idempotent (guarded by the element-keyed bookkeeping) and reversed on
32
32
  * `disconnect()` (Turbo
33
33
  * navigation included). The "in-page move vs real detach" split on `disconnect()` is
34
- * {@link DetachGate}; in the `content` form the source element may even leave a
34
+ * `DetachGate`; in the `content` form the source element may even leave a
35
35
  * scoped application's observed root and the content is still restored, while the
36
36
  * no-`content` form teleporting itself out of the observed root is fire-and-forget
37
37
  * by design (see `disconnect()`).
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/detach_gate.ts","../../src/controllers/portal_controller.ts"],"names":[],"mappings":"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","file":"portal_controller.js","sourcesContent":["/**\n * Shared \"in-page move vs real detach\" discriminator for `disconnect()`.\n *\n * A mid-session `disconnect()` whose element is still in the document is\n * ambiguous. It can be:\n *\n * - an **in-page move** — a consumer (sortable, a teleport) re-inserted the\n * element; Stimulus reconnects the SAME instance in the same mutation batch\n * and the controller's runtime state must survive;\n * - a **detach that keeps the element** — `data-controller` no longer lists the\n * identifier (a Turbo 8 morph), or the element moved outside the observed\n * root (a scoped `Application.start(root)`, a shadow root); no reconnect will\n * ever come and the controller must tear down or it leaks its sessions,\n * document listeners, and moved nodes.\n *\n * Neither available signal decides this alone. A synchronous `data-controller`\n * token check catches the morph case but misses an observed-root exit (the\n * token stays, the reconnect never comes). Deferring teardown one microtask and\n * letting `connect()` cancel it catches the root exit, but leans on Stimulus\n * reconnecting moved elements within the same mutation batch — internal\n * behavior, not a public contract. {@link DetachGate} composes both: the token\n * check is the synchronous fast path (no probe, no reliance on batching), the\n * microtask probe is the fallback for the genuinely ambiguous remainder.\n *\n * Policy stays with the consumer; this util owns the discrimination only:\n *\n * - **Probe** ({@link disconnected}) when the teardown must eventually happen —\n * live drag sessions, document listeners, teleported *content*. Call\n * {@link cancel} from `connect()` (a reconnect means in-page move) and from\n * the head of any direct teardown path (see below).\n * - **Keep** (bare {@link DetachGate.isDetached}) when undoing the ambiguous\n * case would fight the controller's own effect: a controller that moves its\n * OWN element (portal's no-`content` form) exits a scoped observed root as\n * its normal job — a probe-driven teardown would restore the element into\n * the root, reconnect, re-teleport, and disconnect again, forever. Such\n * controllers keep their state on an ambiguous disconnect and tear down only\n * on {@link DetachGate.isDetached}.\n *\n * Event convention for probe consumers: teardown into a dead (detached) tree is\n * silent — consumers restore from their own `connect()`; a detach that keeps the\n * tree alive must end an in-flight session with a `cancel` event, or composing\n * consumers strand their session bookkeeping.\n */\n\n/** The slice of a Stimulus controller the gate inspects. */\nexport interface DetachGateHost {\n readonly element: Element;\n readonly identifier: string;\n}\n\n/**\n * Per-controller-instance gate deciding whether a `disconnect()` is a real\n * detach. Hold one per controller (`#gate = new DetachGate()`) — Stimulus\n * reuses the instance across reconnects, so the pending probe state carries\n * over exactly as the protocol needs.\n *\n * @example\n * ```ts\n * #gate = new DetachGate();\n *\n * connect(): void {\n * this.#gate.cancel(); // a reconnect: the element moved in-page\n * // …\n * }\n *\n * disconnect(): void {\n * this.#gate.disconnected(this, () => this.#teardown());\n * }\n *\n * #teardown(): void {\n * this.#gate.cancel(); // disarm a still-queued probe (double-run guard)\n * // …\n * }\n * ```\n */\nexport class DetachGate {\n /** Set while a probe is queued, waiting for a reconnect to cancel it. */\n #pending = false;\n\n /**\n * True while a probe is queued — the last disconnect was ambiguous and no\n * reconnect has cancelled it yet. Read it from `connect()` to tell the\n * reconnect half of an in-page move from a first connect: a controller whose\n * initialisation restarts a measurement (a min-duration floor, an elapsed\n * counter) must skip it for the move, where nothing actually restarted.\n */\n get pending(): boolean {\n return this.#pending;\n }\n\n /**\n * True when the disconnect is definitely a real detach — the element left\n * the document, or `data-controller` no longer lists the identifier. False\n * means ambiguous (in-page move or observed-root exit), NOT \"alive\".\n */\n static isDetached(host: DetachGateHost): boolean {\n if (!host.element.isConnected) return true;\n const tokens = (host.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return !tokens.includes(host.identifier);\n }\n\n /**\n * Call from `disconnect()`: runs `teardown` synchronously on a definite\n * detach (fast path), otherwise defers it one microtask — a reconnect\n * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.\n * One microtask is the whole probe window: Stimulus reconnects a moved\n * element within the same mutation batch, before the checkpoint drains.\n */\n disconnected(host: DetachGateHost, teardown: () => void): void {\n if (DetachGate.isDetached(host)) {\n // Also disarms a probe a previous ambiguous disconnect left queued\n // (defer, element removed, disconnect again) — exactly one teardown.\n this.#pending = false;\n teardown();\n return;\n }\n this.#pending = true;\n queueMicrotask(() => {\n if (!this.#pending) return;\n this.#pending = false;\n teardown();\n });\n }\n\n /**\n * Disarms a pending probe. Call from `connect()` (the reconnect that proves\n * an in-page move) and from the head of any teardown path not routed through\n * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can\n * never run the teardown a second time.\n */\n cancel(): void {\n this.#pending = false;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { DetachGate } from \"../utils/detach_gate\";\n\n/**\n * Teleport bookkeeping keyed by the controller element (stable across the\n * connect/disconnect churn some DOM runtimes emit when an observed element is moved).\n * Holds the moved node, its placeholder, and the owner — the instance that connected\n * last. Only the owner finishes a probe-driven teardown, so an instance the element has\n * already replaced cannot rewind a teleport the live one holds. The no-`content` form\n * needs no owner check: it restores only on a definite detach, where no successor comes.\n */\nconst portalState = new WeakMap<\n Element,\n { node: HTMLElement; placeholder: Comment; owner: PortalController }\n>();\n\n/**\n * Headless **portal / teleport**: moves an element to another place in the DOM (e.g.\n * directly under `body`) on connect and cleans up on disconnect — the shared substrate\n * for overlays that must escape an ancestor's `overflow: hidden`, `transform`, or\n * stacking context (no APG pattern; a DOM utility).\n *\n * Markup contract (identifier: `stimeo--portal`):\n * <div data-controller=\"stimeo--portal\" data-stimeo--portal-to-value=\"body\">\n * <div data-stimeo--portal-target=\"content\">Teleported content</div>\n * </div>\n *\n * Moves `content` (or `this.element` when no `content` target) into the first element\n * matching `to` (default `body`), `append`ed or `prepend`ed per `position`. A comment\n * placeholder records the original spot so `disconnect()` can return the node there\n * (when `restore`) — or remove it — leaving no orphan behind. The moved node carries\n * `data-portaled`. A `to` the engine cannot read — malformed or empty — falls back to\n * the default; a well-formed selector matching nothing moves nothing.\n *\n * `mount` dispatches `{ target }`; `unmount` dispatches `{}`.\n *\n * @remarks\n * Behavior only — no positioning (pair with `stimeo-ui/positioning`) and no focus\n * trapping (pair with `stimeo--focus` or the overlay). Moving a Stimulus element within\n * the same document re-fires connect/disconnect; {@link DetachGate} reads that pair as an\n * in-page move rather than a detach, which is what keeps the move safe. For Turbo\n * compatibility prefer the `content`-target form: the controller then stays on the\n * in-place source, so its `disconnect()` fires when the original container is replaced\n * and the teleported node is restored/removed rather than orphaned under `body`. The\n * move is idempotent (guarded by the element-keyed bookkeeping) and reversed on\n * `disconnect()` (Turbo\n * navigation included). The \"in-page move vs real detach\" split on `disconnect()` is\n * {@link DetachGate}; in the `content` form the source element may even leave a\n * scoped application's observed root and the content is still restored, while the\n * no-`content` form teleporting itself out of the observed root is fire-and-forget\n * by design (see `disconnect()`).\n */\nexport class PortalController extends Controller<HTMLElement> {\n static override targets = [\"content\"];\n static override values = {\n to: { type: String, default: \"body\" },\n position: { type: String, default: \"append\" },\n restore: { type: Boolean, default: true },\n };\n static events = [\"mount\", \"unmount\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n\n declare toValue: string;\n declare positionValue: string;\n declare restoreValue: boolean;\n\n /** Decides whether a `disconnect()` is an in-page move or a real detach. */\n readonly #gate = new DetachGate();\n\n /** The `to` declaration after validation; the default when it cannot be parsed. */\n #toSelector = \"body\";\n\n /**\n * Validates the destination declaration once, keeping only a selector the engine can\n * read.\n *\n * A selector reads back as an ordinary string, so a malformed one survives until it is\n * handed to the DOM — where it would take the whole teleport down and leave the part\n * silently doing nothing. Falling back to the default puts the node somewhere visible\n * instead, which reads as a mistake and can be traced back to the declaration.\n */\n toValueChanged(): void {\n const selector = this.toValue;\n if (selector.length > 0) {\n try {\n this.element.matches(selector);\n this.#toSelector = selector;\n return;\n } catch {\n // Unparsable selector: fall through to the default below.\n }\n }\n this.#toSelector = \"body\";\n }\n\n override connect(): void {\n // A reconnect proves an in-page move of the source element: disarm the\n // probe the mid-move disconnect() deferred (see disconnect).\n this.#gate.cancel();\n // Already portaled here (idempotent). A later instance for the same element takes\n // ownership, so the teardown belongs to whichever one is still connected.\n const existing = portalState.get(this.element);\n if (existing) {\n existing.owner = this;\n return;\n }\n const node = this.hasContentTarget ? this.contentTarget : this.element;\n const destination = this.#destination();\n if (!destination || destination === node || node.contains(destination)) return;\n\n const placeholder = document.createComment(\"stimeo--portal\");\n node.parentNode?.insertBefore(placeholder, node);\n portalState.set(this.element, { node, placeholder, owner: this });\n\n if (this.positionValue === \"prepend\") {\n destination.prepend(node);\n } else {\n destination.appendChild(node);\n }\n node.setAttribute(\"data-portaled\", \"true\");\n this.dispatch(\"mount\", { detail: { target: destination } });\n }\n\n override disconnect(): void {\n const state = portalState.get(this.element);\n if (!state) return;\n if (state.node === this.element) {\n // No-`content` form: the teleported node IS the controller element, so the\n // teleport itself may exit a scoped application's observed root — the controller\n // doing its job, not a detach. A probe-driven restore would re-enter the root,\n // reconnect, re-teleport and disconnect again, forever. So an ambiguous\n // disconnect (in the document, identifier still listed — also the churn a\n // self-move emits in some runtimes) KEEPS the teleport, and only a definite\n // detach — the element left the DOM, or `data-controller` no longer lists us\n // (a Turbo 8 morph) — restores. The cost, by design: a teleport that left the\n // observed root is fire-and-forget (Stimulus never fires for it again).\n if (!DetachGate.isDetached(this)) return;\n this.#restore(state);\n return;\n }\n // `content` form: the controller element stays put, so its own teleport emits no\n // churn — an ambiguous disconnect means the SOURCE element moved. In-page, the\n // same-batch reconnect cancels the probe and the teleport survives; out of the\n // observed root, no reconnect comes and the probe restores, so the content is\n // never stranded at the destination with a dead owner.\n this.#gate.disconnected(this, () => {\n const current = portalState.get(this.element);\n if (current?.owner === this) this.#restore(current);\n });\n }\n\n /** Returns the node to its placeholder (or removes it) and clears the bookkeeping. */\n #restore(state: { node: HTMLElement; placeholder: Comment }): void {\n portalState.delete(this.element);\n const { node, placeholder } = state;\n\n node.removeAttribute(\"data-portaled\");\n if (this.restoreValue && placeholder.parentNode) {\n placeholder.parentNode.insertBefore(node, placeholder);\n } else {\n node.remove();\n }\n placeholder.remove();\n this.dispatch(\"unmount\", { detail: {} });\n }\n\n /** Resolves the destination from the validated `to` selector. */\n #destination(): Element | null {\n return document.querySelector(this.#toSelector);\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/detach_gate.ts","../../src/controllers/portal_controller.ts"],"names":[],"mappings":"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","file":"portal_controller.js","sourcesContent":["/**\n * Shared \"in-page move vs real detach\" discriminator for `disconnect()`.\n *\n * A mid-session `disconnect()` whose element is still in the document is\n * ambiguous. It can be:\n *\n * - an **in-page move** — a consumer (sortable, a teleport) re-inserted the\n * element; Stimulus reconnects the SAME instance in the same mutation batch\n * and the controller's runtime state must survive;\n * - a **detach that keeps the element** — `data-controller` no longer lists the\n * identifier (a Turbo 8 morph), or the element moved outside the observed\n * root (a scoped `Application.start(root)`, a shadow root); no reconnect will\n * ever come and the controller must tear down or it leaks its sessions,\n * document listeners, and moved nodes.\n *\n * Neither available signal decides this alone. A synchronous `data-controller`\n * token check catches the morph case but misses an observed-root exit (the\n * token stays, the reconnect never comes). Deferring teardown one microtask and\n * letting `connect()` cancel it catches the root exit, but leans on Stimulus\n * reconnecting moved elements within the same mutation batch — internal\n * behavior, not a public contract. {@link DetachGate} composes both: the token\n * check is the synchronous fast path (no probe, no reliance on batching), the\n * microtask probe is the fallback for the genuinely ambiguous remainder.\n *\n * Policy stays with the consumer; this util owns the discrimination only:\n *\n * - **Probe** ({@link disconnected}) when the teardown must eventually happen —\n * live drag sessions, document listeners, teleported *content*. Call\n * {@link cancel} from `connect()` (a reconnect means in-page move) and from\n * the head of any direct teardown path (see below).\n * - **Keep** (bare {@link DetachGate.isDetached}) when undoing the ambiguous\n * case would fight the controller's own effect: a controller that moves its\n * OWN element (portal's no-`content` form) exits a scoped observed root as\n * its normal job — a probe-driven teardown would restore the element into\n * the root, reconnect, re-teleport, and disconnect again, forever. Such\n * controllers keep their state on an ambiguous disconnect and tear down only\n * on {@link DetachGate.isDetached}.\n *\n * Event convention for probe consumers: teardown into a dead (detached) tree is\n * silent — consumers restore from their own `connect()`; a detach that keeps the\n * tree alive must end an in-flight session with a `cancel` event, or composing\n * consumers strand their session bookkeeping.\n */\n\n/** The slice of a Stimulus controller the gate inspects. */\nexport interface DetachGateHost {\n readonly element: Element;\n readonly identifier: string;\n}\n\n/**\n * Per-controller-instance gate deciding whether a `disconnect()` is a real\n * detach. Hold one per controller (`#gate = new DetachGate()`) — Stimulus\n * reuses the instance across reconnects, so the pending probe state carries\n * over exactly as the protocol needs.\n *\n * @example\n * ```ts\n * #gate = new DetachGate();\n *\n * connect(): void {\n * this.#gate.cancel(); // a reconnect: the element moved in-page\n * // …\n * }\n *\n * disconnect(): void {\n * this.#gate.disconnected(this, () => this.#teardown());\n * }\n *\n * #teardown(): void {\n * this.#gate.cancel(); // disarm a still-queued probe (double-run guard)\n * // …\n * }\n * ```\n */\nexport class DetachGate {\n /** Set while a probe is queued, waiting for a reconnect to cancel it. */\n #pending = false;\n\n /**\n * True while a probe is queued — the last disconnect was ambiguous and no\n * reconnect has cancelled it yet. Read it from `connect()` to tell the\n * reconnect half of an in-page move from a first connect: a controller whose\n * initialisation restarts a measurement (a min-duration floor, an elapsed\n * counter) must skip it for the move, where nothing actually restarted.\n */\n get pending(): boolean {\n return this.#pending;\n }\n\n /**\n * True when the disconnect is definitely a real detach — the element left\n * the document, or `data-controller` no longer lists the identifier. False\n * means ambiguous (in-page move or observed-root exit), NOT \"alive\".\n */\n static isDetached(host: DetachGateHost): boolean {\n if (!host.element.isConnected) return true;\n const tokens = (host.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return !tokens.includes(host.identifier);\n }\n\n /**\n * Call from `disconnect()`: runs `teardown` synchronously on a definite\n * detach (fast path), otherwise defers it one microtask — a reconnect\n * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.\n * One microtask is the whole probe window: Stimulus reconnects a moved\n * element within the same mutation batch, before the checkpoint drains.\n */\n disconnected(host: DetachGateHost, teardown: () => void): void {\n if (DetachGate.isDetached(host)) {\n // Also disarms a probe a previous ambiguous disconnect left queued\n // (defer, element removed, disconnect again) — exactly one teardown.\n this.#pending = false;\n teardown();\n return;\n }\n this.#pending = true;\n queueMicrotask(() => {\n if (!this.#pending) return;\n this.#pending = false;\n teardown();\n });\n }\n\n /**\n * Disarms a pending probe. Call from `connect()` (the reconnect that proves\n * an in-page move) and from the head of any teardown path not routed through\n * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can\n * never run the teardown a second time.\n */\n cancel(): void {\n this.#pending = false;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { DetachGate } from \"../utils/detach_gate\";\n\n/**\n * Teleport bookkeeping keyed by the controller element (stable across the\n * connect/disconnect churn some DOM runtimes emit when an observed element is moved).\n * Holds the moved node, its placeholder, and the owner — the instance that connected\n * last. Only the owner finishes a probe-driven teardown, so an instance the element has\n * already replaced cannot rewind a teleport the live one holds. The no-`content` form\n * needs no owner check: it restores only on a definite detach, where no successor comes.\n */\nconst portalState = new WeakMap<\n Element,\n { node: HTMLElement; placeholder: Comment; owner: PortalController }\n>();\n\n/**\n * Headless **portal / teleport**: moves an element to another place in the DOM (e.g.\n * directly under `body`) on connect and cleans up on disconnect — the shared substrate\n * for overlays that must escape an ancestor's `overflow: hidden`, `transform`, or\n * stacking context (no APG pattern; a DOM utility).\n *\n * Markup contract (identifier: `stimeo--portal`):\n * <div data-controller=\"stimeo--portal\" data-stimeo--portal-to-value=\"body\">\n * <div data-stimeo--portal-target=\"content\">Teleported content</div>\n * </div>\n *\n * Moves `content` (or `this.element` when no `content` target) into the first element\n * matching `to` (default `body`), `append`ed or `prepend`ed per `position`. A comment\n * placeholder records the original spot so `disconnect()` can return the node there\n * (when `restore`) — or remove it — leaving no orphan behind. The moved node carries\n * `data-portaled`. A `to` the engine cannot read — malformed or empty — falls back to\n * the default; a well-formed selector matching nothing moves nothing.\n *\n * `mount` dispatches `{ target }`; `unmount` dispatches `{}`.\n *\n * @remarks\n * Behavior only — no positioning (pair with `stimeo-ui/positioning`) and no focus\n * trapping (pair with `stimeo--focus` or the overlay). Moving a Stimulus element within\n * the same document re-fires connect/disconnect; `DetachGate` reads that pair as an\n * in-page move rather than a detach, which is what keeps the move safe. For Turbo\n * compatibility prefer the `content`-target form: the controller then stays on the\n * in-place source, so its `disconnect()` fires when the original container is replaced\n * and the teleported node is restored/removed rather than orphaned under `body`. The\n * move is idempotent (guarded by the element-keyed bookkeeping) and reversed on\n * `disconnect()` (Turbo\n * navigation included). The \"in-page move vs real detach\" split on `disconnect()` is\n * `DetachGate`; in the `content` form the source element may even leave a\n * scoped application's observed root and the content is still restored, while the\n * no-`content` form teleporting itself out of the observed root is fire-and-forget\n * by design (see `disconnect()`).\n */\nexport class PortalController extends Controller<HTMLElement> {\n static override targets = [\"content\"];\n static override values = {\n to: { type: String, default: \"body\" },\n position: { type: String, default: \"append\" },\n restore: { type: Boolean, default: true },\n };\n static events = [\"mount\", \"unmount\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n\n declare toValue: string;\n declare positionValue: string;\n declare restoreValue: boolean;\n\n /** Decides whether a `disconnect()` is an in-page move or a real detach. */\n readonly #gate = new DetachGate();\n\n /** The `to` declaration after validation; the default when it cannot be parsed. */\n #toSelector = \"body\";\n\n /**\n * Validates the destination declaration once, keeping only a selector the engine can\n * read.\n *\n * A selector reads back as an ordinary string, so a malformed one survives until it is\n * handed to the DOM — where it would take the whole teleport down and leave the part\n * silently doing nothing. Falling back to the default puts the node somewhere visible\n * instead, which reads as a mistake and can be traced back to the declaration.\n */\n toValueChanged(): void {\n const selector = this.toValue;\n if (selector.length > 0) {\n try {\n this.element.matches(selector);\n this.#toSelector = selector;\n return;\n } catch {\n // Unparsable selector: fall through to the default below.\n }\n }\n this.#toSelector = \"body\";\n }\n\n override connect(): void {\n // A reconnect proves an in-page move of the source element: disarm the\n // probe the mid-move disconnect() deferred (see disconnect).\n this.#gate.cancel();\n // Already portaled here (idempotent). A later instance for the same element takes\n // ownership, so the teardown belongs to whichever one is still connected.\n const existing = portalState.get(this.element);\n if (existing) {\n existing.owner = this;\n return;\n }\n const node = this.hasContentTarget ? this.contentTarget : this.element;\n const destination = this.#destination();\n if (!destination || destination === node || node.contains(destination)) return;\n\n const placeholder = document.createComment(\"stimeo--portal\");\n node.parentNode?.insertBefore(placeholder, node);\n portalState.set(this.element, { node, placeholder, owner: this });\n\n if (this.positionValue === \"prepend\") {\n destination.prepend(node);\n } else {\n destination.appendChild(node);\n }\n node.setAttribute(\"data-portaled\", \"true\");\n this.dispatch(\"mount\", { detail: { target: destination } });\n }\n\n override disconnect(): void {\n const state = portalState.get(this.element);\n if (!state) return;\n if (state.node === this.element) {\n // No-`content` form: the teleported node IS the controller element, so the\n // teleport itself may exit a scoped application's observed root — the controller\n // doing its job, not a detach. A probe-driven restore would re-enter the root,\n // reconnect, re-teleport and disconnect again, forever. So an ambiguous\n // disconnect (in the document, identifier still listed — also the churn a\n // self-move emits in some runtimes) KEEPS the teleport, and only a definite\n // detach — the element left the DOM, or `data-controller` no longer lists us\n // (a Turbo 8 morph) — restores. The cost, by design: a teleport that left the\n // observed root is fire-and-forget (Stimulus never fires for it again).\n if (!DetachGate.isDetached(this)) return;\n this.#restore(state);\n return;\n }\n // `content` form: the controller element stays put, so its own teleport emits no\n // churn — an ambiguous disconnect means the SOURCE element moved. In-page, the\n // same-batch reconnect cancels the probe and the teleport survives; out of the\n // observed root, no reconnect comes and the probe restores, so the content is\n // never stranded at the destination with a dead owner.\n this.#gate.disconnected(this, () => {\n const current = portalState.get(this.element);\n if (current?.owner === this) this.#restore(current);\n });\n }\n\n /** Returns the node to its placeholder (or removes it) and clears the bookkeeping. */\n #restore(state: { node: HTMLElement; placeholder: Comment }): void {\n portalState.delete(this.element);\n const { node, placeholder } = state;\n\n node.removeAttribute(\"data-portaled\");\n if (this.restoreValue && placeholder.parentNode) {\n placeholder.parentNode.insertBefore(node, placeholder);\n } else {\n node.remove();\n }\n placeholder.remove();\n this.dispatch(\"unmount\", { detail: {} });\n }\n\n /** Resolves the destination from the validated `to` selector. */\n #destination(): Element | null {\n return document.querySelector(this.#toSelector);\n }\n}\n"]}
@@ -20,7 +20,7 @@ import { Controller } from '@hotwired/stimulus';
20
20
  *
21
21
  * @remarks
22
22
  * Behavior only — selection is exposed through `aria-checked`, the single Tab
23
- * stop through roving `tabindex` ({@link RovingTabindex}); the consumer styles
23
+ * stop through roving `tabindex` (`RovingTabindex`); the consumer styles
24
24
  * those attributes. Selection follows focus for enabled radios.
25
25
  *
26
26
  * A supported radio is either `<button type="button">` or a non-interactive