stimeo-ui 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.js +17 -7
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.js +1 -1
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.js +2 -2
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +1 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +1 -1
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js +15 -5
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +15 -5
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +5 -4
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.js +1 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.js +1 -1
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +15 -5
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +2 -2
- package/dist/controllers/direct_upload_controller.js +1 -5
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +2 -2
- package/dist/controllers/drawer_controller.js +17 -7
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +5 -0
- package/dist/controllers/focus_controller.js +45 -7
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +1 -1
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +4 -3
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +2 -2
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +3 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +1 -1
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +2 -2
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +1 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.js +1 -1
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +2 -2
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +1 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +27 -2
- package/dist/controllers/password_reveal_controller.js +102 -3
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +71 -17
- package/dist/controllers/password_strength_controller.js +287 -42
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +3 -2
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -1
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_before_cache_controller.d.ts +1 -2
- package/dist/controllers/reset_before_cache_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +1 -1
- package/dist/controllers/resizable_controller.js +1 -1
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +2 -1
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +33 -11
- package/dist/controllers/scroll_restore_controller.js +127 -27
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +42 -10
- package/dist/controllers/scroll_visibility_controller.js +167 -14
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.js +15 -5
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.js +0 -5
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +3 -2
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +32 -6
- package/dist/controllers/theme_controller.js +151 -40
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +2 -2
- package/dist/controllers/toast_controller.js +2 -2
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +11 -7
- package/dist/controllers/transition_controller.js +79 -15
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +740 -178
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +3 -4
- package/dist/inspector/cli.js +1 -0
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +6 -5
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +3 -3
- package/dist/inspector/manifest.json +7 -4
- package/dist/positioning/index.d.ts +18 -4
- package/dist/positioning/index.js +84 -25
- package/dist/positioning/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/blur_deferral.ts","../../src/utils/layout_observer.ts","../../src/controllers/read_more_controller.ts"],"names":[],"mappings":"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","file":"read_more_controller.js","sourcesContent":["/**\n * Shared bookkeeping for updates that must wait until an element loses focus.\n *\n * A control that becomes redundant while the user is standing on it cannot be\n * removed from the page immediately: hiding a focused element (or turning it\n * into a native `disabled` one) drops focus to `<body>`, stranding a keyboard\n * user mid-widget. The fix every affected controller reached for is the same —\n * keep the element reachable, hold the destructive update, listen for `blur`,\n * and apply it then.\n *\n * {@link BlurDeferral} owns *only* the registry part of that: which elements are\n * currently holding an update back, attaching and detaching the `blur` listener,\n * and guaranteeing teardown. It is deliberately **policy-free** — what the\n * interim state looks like (`aria-disabled`, an ownership marker, nothing at\n * all), and what the deferred update actually is, stay in the controller, which\n * receives the element back through its release callback.\n *\n * Three contracts are worth stating up front, because each one is a hazard the\n * consumers would otherwise hit:\n *\n * - **The release callback fires only on a real `blur`.** `release()` /\n * `releaseAll()` *cancel* a deferral; they do not complete it. Conflating the\n * two makes `overflow-indicator` disable a button that just left the boundary.\n * - **`elements` is a snapshot.** Iterating the live key set while releasing\n * inside the loop is the failure this registry exists to prevent.\n * - **The entry is detached before the callback runs**, so a controller that\n * decides the update is still unsafe can simply defer again.\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 contracts belong on the members.\n *\n * @example\n * ```ts\n * readonly #deferred = new BlurDeferral<HTMLElement>((trigger) => {\n * if (this.#connected) this.#evaluate();\n * });\n *\n * #hide(trigger: HTMLElement) {\n * if (document.activeElement === trigger) {\n * this.#deferred.deferOnly(trigger); // stays visible until blur\n * return;\n * }\n * this.#deferred.releaseAll();\n * trigger.hidden = true;\n * }\n *\n * disconnect() {\n * this.#deferred.releaseAll();\n * }\n * ```\n */\nexport class BlurDeferral<T extends HTMLElement = HTMLElement> {\n /** Elements currently holding an update back, mapped to their `blur` listener. */\n readonly #pending = new Map<T, () => void>();\n /** Called after a pending element blurs and has been detached. */\n readonly #onRelease: (element: T) => void;\n\n /** @param onRelease - Invoked once `element` actually blurs; never on `release`. */\n constructor(onRelease: (element: T) => void) {\n this.#onRelease = onRelease;\n }\n\n /** Number of elements currently holding an update back. */\n get size(): number {\n return this.#pending.size;\n }\n\n /** Snapshot of the pending elements, safe to iterate while releasing them. */\n get elements(): T[] {\n return [...this.#pending.keys()];\n }\n\n /** Whether `element` is currently holding an update back. */\n has(element: T): boolean {\n return this.#pending.has(element);\n }\n\n /** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */\n defer(element: T): void {\n if (this.#pending.has(element)) return;\n const onBlur = (): void => {\n this.#detach(element);\n this.#onRelease(element);\n };\n this.#pending.set(element, onBlur);\n element.addEventListener(\"blur\", onBlur);\n }\n\n /** Defers `element` as the only pending entry, cancelling any others. */\n deferOnly(element: T): void {\n for (const pending of this.elements) {\n if (pending !== element) this.#detach(pending);\n }\n this.defer(element);\n }\n\n /** Cancels `element`'s deferral without completing it; no-ops when not pending. */\n release(element: T): void {\n this.#detach(element);\n }\n\n /** Cancels every deferral without completing any of them. */\n releaseAll(): void {\n for (const element of this.elements) this.#detach(element);\n }\n\n /** Removes the `blur` listener for `element` and forgets it. */\n #detach(element: T): void {\n const onBlur = this.#pending.get(element);\n if (onBlur) element.removeEventListener(\"blur\", onBlur);\n this.#pending.delete(element);\n }\n}\n","/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Layout-sensitive widgets (sliders, resizable panes, scroll spies, popovers)\n * need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BlurDeferral } from \"../utils/blur_deferral\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/**\n * Headless \"read more / truncate\" behavior for visually clamped text.\n *\n * Markup contract (identifier: `stimeo--read-more`):\n * <div data-controller=\"stimeo--read-more\">\n * <p id=\"bio\" data-stimeo--read-more-target=\"content\" data-state=\"collapsed\">…</p>\n * <button data-stimeo--read-more-target=\"trigger\"\n * data-action=\"stimeo--read-more#toggle\"\n * aria-expanded=\"false\" aria-controls=\"bio\" hidden>Read more</button>\n * </div>\n *\n * There is no dedicated APG widget; the toggle borrows the **Disclosure**\n * convention (`aria-expanded`). The visual clamp itself (`-webkit-line-clamp`\n * etc.) is the consumer's CSS, keyed off `data-state`.\n *\n * @remarks\n * Behavior only. The full text always stays in the DOM — the clamp is purely\n * visual, so assistive technology reads everything regardless of state; here\n * `aria-expanded` therefore signals the *visual* expansion, not content hidden\n * from AT. The controller's extra job is **overflow detection**: when the\n * content is not actually clamped (it fits), the toggle is `hidden` so no\n * pointless \"read more\" is offered. Resizes, content changes, and target\n * replacement re-evaluate it; hiding a focused toggle waits until blur.\n */\nexport class ReadMoreController extends Controller<HTMLElement> {\n static override targets = [\"content\", \"trigger\"];\n static override values = {\n collapsed: { type: Boolean, default: true },\n };\n static actions = [\"toggle\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly triggerTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n declare collapsedValue: boolean;\n\n #connected = false;\n #collapsed = true;\n #observedContent: HTMLElement | null = null;\n #contentMutationObserver: MutationObserver | null = null;\n\n readonly #update = (): void => {\n if (this.#connected) this.#evaluateOverflow();\n };\n readonly #layout = new LayoutObserver(this.#update);\n\n /** Holds the trigger's hide back while it has focus; re-evaluates on blur. */\n readonly #deferredHide = new BlurDeferral(() => {\n this.#update();\n });\n\n override connect(): void {\n this.#connected = true;\n // The DOM is the source of truth on reconnect (Turbo cache restore / morph): an\n // explicit `data-state=\"expanded\"`/`\"collapsed\"` is honored verbatim so a block\n // the user expanded *or* collapsed survives a back-navigation, even when the\n // declarative `collapsed` Value disagrees. The Value seeds only a genuinely fresh\n // render (no `data-state` yet). Mirrors `sidebar`'s `#restoreCollapsed`.\n this.#collapsed = this.#initialCollapsed();\n this.#syncTargets();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#stopObservingContent();\n this.#layout.disconnect();\n }\n\n contentTargetConnected(): void {\n this.#syncTargets();\n }\n\n contentTargetDisconnected(): void {\n this.#syncTargets();\n }\n\n triggerTargetConnected(): void {\n this.#syncTargets();\n }\n\n triggerTargetDisconnected(trigger: HTMLElement): void {\n this.#deferredHide.release(trigger);\n this.#syncTargets();\n }\n\n /** Toggles between the collapsed (clamped) and expanded states. */\n toggle(): void {\n if (!this.#connected) return;\n this.#collapsed = !this.#collapsed;\n this.#reflect();\n this.#evaluateOverflow();\n }\n\n #initialCollapsed(): boolean {\n if (this.hasContentTarget) {\n const state = this.contentTarget.getAttribute(\"data-state\");\n if (state === \"expanded\") return false;\n if (state === \"collapsed\") return true;\n }\n return this.collapsedValue;\n }\n\n #reflect(): void {\n if (this.hasContentTarget) {\n this.contentTarget.setAttribute(\"data-state\", this.#collapsed ? \"collapsed\" : \"expanded\");\n }\n if (this.hasTriggerTarget) {\n this.triggerTarget.setAttribute(\"aria-expanded\", this.#collapsed ? \"false\" : \"true\");\n }\n }\n\n #syncTargets(): void {\n if (!this.#connected) return;\n this.#syncContentObservation();\n this.#reflect();\n this.#evaluateOverflow();\n }\n\n #syncContentObservation(): void {\n const next = this.hasContentTarget ? this.contentTarget : null;\n if (next === this.#observedContent) return;\n\n this.#stopObservingContent();\n if (!next) return;\n\n this.#observedContent = next;\n this.#layout.observe(next);\n this.#layout.observeViewport();\n next.addEventListener(\"load\", this.#update, true);\n\n if (typeof MutationObserver !== \"undefined\") {\n this.#contentMutationObserver = new MutationObserver(this.#update);\n this.#contentMutationObserver.observe(next, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n }\n\n #stopObservingContent(): void {\n this.#deferredHide.releaseAll();\n if (this.#observedContent) {\n this.#layout.unobserve(this.#observedContent);\n this.#observedContent.removeEventListener(\"load\", this.#update, true);\n }\n this.#observedContent = null;\n this.#contentMutationObserver?.disconnect();\n this.#contentMutationObserver = null;\n this.#layout.unobserveViewport();\n }\n\n #evaluateOverflow(): void {\n if (!this.hasTriggerTarget || !this.hasContentTarget) return;\n\n const trigger = this.triggerTarget;\n const content = this.contentTarget;\n const useful = !this.#collapsed || content.scrollHeight > content.clientHeight;\n if (useful) {\n this.#deferredHide.releaseAll();\n trigger.hidden = false;\n return;\n }\n\n if (document.activeElement === trigger) {\n trigger.hidden = false;\n this.#deferredHide.deferOnly(trigger);\n return;\n }\n\n this.#deferredHide.releaseAll();\n trigger.hidden = true;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/blur_deferral.ts","../../src/utils/layout_observer.ts","../../src/controllers/read_more_controller.ts"],"names":[],"mappings":";;;;;AAoDO,IAAM,eAAN,MAAwD;AAAA;AAAA,EAEpD,QAAA,uBAAe,GAAA,EAAmB;AAAA;AAAA,EAElC,UAAA;AAAA;AAAA,EAGT,YAAY,SAAA,EAAiC;AAC3C,IAAA,IAAA,CAAK,UAAA,GAAa,SAAA;AAAA,EACpB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,QAAA,CAAS,IAAA;AAAA,EACvB;AAAA;AAAA,EAGA,IAAI,QAAA,GAAgB;AAClB,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,QAAA,CAAS,MAAM,CAAA;AAAA,EACjC;AAAA;AAAA,EAGA,IAAI,OAAA,EAAqB;AACvB,IAAA,OAAO,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA;AAAA,EAClC;AAAA;AAAA,EAGA,MAAM,OAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA,EAAG;AAChC,IAAA,MAAM,SAAS,MAAY;AACzB,MAAA,IAAA,CAAK,QAAQ,OAAO,CAAA;AACpB,MAAA,IAAA,CAAK,WAAW,OAAO,CAAA;AAAA,IACzB,CAAA;AACA,IAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAA,EAAS,MAAM,CAAA;AACjC,IAAA,OAAA,CAAQ,gBAAA,CAAiB,QAAQ,MAAM,CAAA;AAAA,EACzC;AAAA;AAAA,EAGA,UAAU,OAAA,EAAkB;AAC1B,IAAA,KAAA,MAAW,OAAA,IAAW,KAAK,QAAA,EAAU;AACnC,MAAA,IAAI,OAAA,KAAY,OAAA,EAAS,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAAA,IAC/C;AACA,IAAA,IAAA,CAAK,MAAM,OAAO,CAAA;AAAA,EACpB;AAAA;AAAA,EAGA,QAAQ,OAAA,EAAkB;AACxB,IAAA,IAAA,CAAK,QAAQ,OAAO,CAAA;AAAA,EACtB;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,QAAQ,OAAO,CAAA;AAAA,EAC3D;AAAA;AAAA,EAGA,QAAQ,OAAA,EAAkB;AACxB,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA;AACxC,IAAA,IAAI,MAAA,EAAQ,OAAA,CAAQ,mBAAA,CAAoB,MAAA,EAAQ,MAAM,CAAA;AACtD,IAAA,IAAA,CAAK,QAAA,CAAS,OAAO,OAAO,CAAA;AAAA,EAC9B;AACF,CAAA;;;ACjEO,IAAM,iBAAN,MAAqB;AAAA,EACjB,SAAA;AAAA,EACA,sBAAA;AAAA,EACT,eAAA,GAAyC,IAAA;AAAA,EACzC,kBAAA,GAAqB,KAAA;AAAA;AAAA,EAGZ,wBAAwB,MAAY;AAC3C,IAAA,IAAA,CAAK,SAAA,EAAU;AAAA,EACjB,CAAA;AAAA,EAEA,WAAA,CAAY,QAAA,EAA0B,OAAA,GAAiC,EAAC,EAAG;AACzE,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AACjB,IAAA,IAAA,CAAK,sBAAA,GACH,OAAA,CAAQ,qBAAA,KACP,OAAO,cAAA,KAAmB,WAAA,GAAc,IAAA,GAAO,CAAC,EAAA,KAAO,IAAI,cAAA,CAAe,EAAE,CAAA,CAAA;AAAA,EACjF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAQ,OAAA,EAAwB;AAC9B,IAAA,IAAI,CAAC,KAAK,sBAAA,EAAwB;AAClC,IAAA,IAAI,CAAC,KAAK,eAAA,EAAiB;AACzB,MAAA,IAAA,CAAK,eAAA,GAAkB,IAAA,CAAK,sBAAA,CAAuB,MAAM;AACvD,QAAA,IAAA,CAAK,SAAA,EAAU;AAAA,MACjB,CAAC,CAAA;AAAA,IACH;AACA,IAAA,IAAA,CAAK,eAAA,CAAgB,QAAQ,OAAO,CAAA;AAAA,EACtC;AAAA;AAAA,EAGA,UAAU,OAAA,EAAwB;AAChC,IAAA,IAAA,CAAK,eAAA,EAAiB,UAAU,OAAO,CAAA;AAAA,EACzC;AAAA;AAAA,EAGA,eAAA,GAAwB;AACtB,IAAA,IAAI,KAAK,kBAAA,EAAoB;AAC7B,IAAA,IAAA,CAAK,kBAAA,GAAqB,IAAA;AAC1B,IAAA,MAAA,CAAO,gBAAA,CAAiB,QAAA,EAAU,IAAA,CAAK,qBAAqB,CAAA;AAAA,EAC9D;AAAA;AAAA,EAGA,iBAAA,GAA0B;AACxB,IAAA,IAAI,CAAC,KAAK,kBAAA,EAAoB;AAC9B,IAAA,IAAA,CAAK,kBAAA,GAAqB,KAAA;AAC1B,IAAA,MAAA,CAAO,mBAAA,CAAoB,QAAA,EAAU,IAAA,CAAK,qBAAqB,CAAA;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAA,GAAmB;AACjB,IAAA,IAAA,CAAK,iBAAiB,UAAA,EAAW;AACjC,IAAA,IAAA,CAAK,eAAA,GAAkB,IAAA;AACvB,IAAA,IAAA,CAAK,iBAAA,EAAkB;AAAA,EACzB;AACF,CAAA;;;AClFO,IAAM,kBAAA,GAAN,cAAiC,UAAA,CAAwB;AAAA,EAC9D,OAAgB,OAAA,GAAU,CAAC,SAAA,EAAW,SAAS,CAAA;AAAA,EAC/C,OAAgB,MAAA,GAAS;AAAA,IACvB,SAAA,EAAW,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA;AAAK,GAC5C;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,QAAQ,CAAA;AAAA,EAS1B,UAAA,GAAa,KAAA;AAAA,EACb,UAAA,GAAa,IAAA;AAAA,EACb,gBAAA,GAAuC,IAAA;AAAA,EACvC,wBAAA,GAAoD,IAAA;AAAA,EAE3C,UAAU,MAAY;AAC7B,IAAA,IAAI,IAAA,CAAK,UAAA,EAAY,IAAA,CAAK,iBAAA,EAAkB;AAAA,EAC9C,CAAA;AAAA,EACS,OAAA,GAAU,IAAI,cAAA,CAAe,IAAA,CAAK,OAAO,CAAA;AAAA;AAAA,EAGzC,aAAA,GAAgB,IAAI,YAAA,CAAa,MAAM;AAC9C,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf,CAAC,CAAA;AAAA,EAEQ,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAMlB,IAAA,IAAA,CAAK,UAAA,GAAa,KAAK,iBAAA,EAAkB;AACzC,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,qBAAA,EAAsB;AAC3B,IAAA,IAAA,CAAK,QAAQ,UAAA,EAAW;AAAA,EAC1B;AAAA,EAEA,sBAAA,GAA+B;AAC7B,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA,EAEA,yBAAA,GAAkC;AAChC,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA,EAEA,sBAAA,GAA+B;AAC7B,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA,EAEA,0BAA0B,OAAA,EAA4B;AACpD,IAAA,IAAA,CAAK,aAAA,CAAc,QAAQ,OAAO,CAAA;AAClC,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,UAAA,GAAa,CAAC,IAAA,CAAK,UAAA;AACxB,IAAA,IAAA,CAAK,QAAA,EAAS;AACd,IAAA,IAAA,CAAK,iBAAA,EAAkB;AAAA,EACzB;AAAA,EAEA,iBAAA,GAA6B;AAC3B,IAAA,IAAI,KAAK,gBAAA,EAAkB;AACzB,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,aAAA,CAAc,YAAA,CAAa,YAAY,CAAA;AAC1D,MAAA,IAAI,KAAA,KAAU,YAAY,OAAO,KAAA;AACjC,MAAA,IAAI,KAAA,KAAU,aAAa,OAAO,IAAA;AAAA,IACpC;AACA,IAAA,OAAO,IAAA,CAAK,cAAA;AAAA,EACd;AAAA,EAEA,QAAA,GAAiB;AACf,IAAA,IAAI,KAAK,gBAAA,EAAkB;AACzB,MAAA,IAAA,CAAK,cAAc,YAAA,CAAa,YAAA,EAAc,IAAA,CAAK,UAAA,GAAa,cAAc,UAAU,CAAA;AAAA,IAC1F;AACA,IAAA,IAAI,KAAK,gBAAA,EAAkB;AACzB,MAAA,IAAA,CAAK,cAAc,YAAA,CAAa,eAAA,EAAiB,IAAA,CAAK,UAAA,GAAa,UAAU,MAAM,CAAA;AAAA,IACrF;AAAA,EACF;AAAA,EAEA,YAAA,GAAqB;AACnB,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,uBAAA,EAAwB;AAC7B,IAAA,IAAA,CAAK,QAAA,EAAS;AACd,IAAA,IAAA,CAAK,iBAAA,EAAkB;AAAA,EACzB;AAAA,EAEA,uBAAA,GAAgC;AAC9B,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,gBAAA,GAAmB,IAAA,CAAK,aAAA,GAAgB,IAAA;AAC1D,IAAA,IAAI,IAAA,KAAS,KAAK,gBAAA,EAAkB;AAEpC,IAAA,IAAA,CAAK,qBAAA,EAAsB;AAC3B,IAAA,IAAI,CAAC,IAAA,EAAM;AAEX,IAAA,IAAA,CAAK,gBAAA,GAAmB,IAAA;AACxB,IAAA,IAAA,CAAK,OAAA,CAAQ,QAAQ,IAAI,CAAA;AACzB,IAAA,IAAA,CAAK,QAAQ,eAAA,EAAgB;AAC7B,IAAA,IAAA,CAAK,gBAAA,CAAiB,MAAA,EAAQ,IAAA,CAAK,OAAA,EAAS,IAAI,CAAA;AAEhD,IAAA,IAAI,OAAO,qBAAqB,WAAA,EAAa;AAC3C,MAAA,IAAA,CAAK,wBAAA,GAA2B,IAAI,gBAAA,CAAiB,IAAA,CAAK,OAAO,CAAA;AACjE,MAAA,IAAA,CAAK,wBAAA,CAAyB,QAAQ,IAAA,EAAM;AAAA,QAC1C,SAAA,EAAW,IAAA;AAAA,QACX,OAAA,EAAS,IAAA;AAAA,QACT,aAAA,EAAe;AAAA,OAChB,CAAA;AAAA,IACH;AAAA,EACF;AAAA,EAEA,qBAAA,GAA8B;AAC5B,IAAA,IAAA,CAAK,cAAc,UAAA,EAAW;AAC9B,IAAA,IAAI,KAAK,gBAAA,EAAkB;AACzB,MAAA,IAAA,CAAK,OAAA,CAAQ,SAAA,CAAU,IAAA,CAAK,gBAAgB,CAAA;AAC5C,MAAA,IAAA,CAAK,gBAAA,CAAiB,mBAAA,CAAoB,MAAA,EAAQ,IAAA,CAAK,SAAS,IAAI,CAAA;AAAA,IACtE;AACA,IAAA,IAAA,CAAK,gBAAA,GAAmB,IAAA;AACxB,IAAA,IAAA,CAAK,0BAA0B,UAAA,EAAW;AAC1C,IAAA,IAAA,CAAK,wBAAA,GAA2B,IAAA;AAChC,IAAA,IAAA,CAAK,QAAQ,iBAAA,EAAkB;AAAA,EACjC;AAAA,EAEA,iBAAA,GAA0B;AACxB,IAAA,IAAI,CAAC,IAAA,CAAK,gBAAA,IAAoB,CAAC,KAAK,gBAAA,EAAkB;AAEtD,IAAA,MAAM,UAAU,IAAA,CAAK,aAAA;AACrB,IAAA,MAAM,UAAU,IAAA,CAAK,aAAA;AACrB,IAAA,MAAM,SAAS,CAAC,IAAA,CAAK,UAAA,IAAc,OAAA,CAAQ,eAAe,OAAA,CAAQ,YAAA;AAClE,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,IAAA,CAAK,cAAc,UAAA,EAAW;AAC9B,MAAA,OAAA,CAAQ,MAAA,GAAS,KAAA;AACjB,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,QAAA,CAAS,kBAAkB,OAAA,EAAS;AACtC,MAAA,OAAA,CAAQ,MAAA,GAAS,KAAA;AACjB,MAAA,IAAA,CAAK,aAAA,CAAc,UAAU,OAAO,CAAA;AACpC,MAAA;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,cAAc,UAAA,EAAW;AAC9B,IAAA,OAAA,CAAQ,MAAA,GAAS,IAAA;AAAA,EACnB;AACF","file":"read_more_controller.js","sourcesContent":["/**\n * Shared bookkeeping for updates that must wait until an element loses focus.\n *\n * A control that becomes redundant while the user is standing on it cannot be\n * removed from the page immediately: hiding a focused element (or turning it\n * into a native `disabled` one) drops focus to `<body>`, stranding a keyboard\n * user mid-widget. The fix every affected controller reached for is the same —\n * keep the element reachable, hold the destructive update, listen for `blur`,\n * and apply it then.\n *\n * {@link BlurDeferral} owns *only* the registry part of that: which elements are\n * currently holding an update back, attaching and detaching the `blur` listener,\n * and guaranteeing teardown. It is deliberately **policy-free** — what the\n * interim state looks like (`aria-disabled`, an ownership marker, nothing at\n * all), and what the deferred update actually is, stay in the controller, which\n * receives the element back through its release callback.\n *\n * Three contracts are worth stating up front, because each one is a hazard the\n * consumers would otherwise hit:\n *\n * - **The release callback fires only on a real `blur`.** `release()` /\n * `releaseAll()` *cancel* a deferral; they do not complete it. Conflating the\n * two makes `overflow-indicator` disable a button that just left the boundary.\n * - **`elements` is a snapshot.** Iterating the live key set while releasing\n * inside the loop is the failure this registry exists to prevent.\n * - **The entry is detached before the callback runs**, so a controller that\n * decides the update is still unsafe can simply defer again.\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 contracts belong on the members.\n *\n * @example\n * ```ts\n * readonly #deferred = new BlurDeferral<HTMLElement>((trigger) => {\n * if (this.#connected) this.#evaluate();\n * });\n *\n * #hide(trigger: HTMLElement) {\n * if (document.activeElement === trigger) {\n * this.#deferred.deferOnly(trigger); // stays visible until blur\n * return;\n * }\n * this.#deferred.releaseAll();\n * trigger.hidden = true;\n * }\n *\n * disconnect() {\n * this.#deferred.releaseAll();\n * }\n * ```\n */\nexport class BlurDeferral<T extends HTMLElement = HTMLElement> {\n /** Elements currently holding an update back, mapped to their `blur` listener. */\n readonly #pending = new Map<T, () => void>();\n /** Called after a pending element blurs and has been detached. */\n readonly #onRelease: (element: T) => void;\n\n /** @param onRelease - Invoked once `element` actually blurs; never on `release`. */\n constructor(onRelease: (element: T) => void) {\n this.#onRelease = onRelease;\n }\n\n /** Number of elements currently holding an update back. */\n get size(): number {\n return this.#pending.size;\n }\n\n /** Snapshot of the pending elements, safe to iterate while releasing them. */\n get elements(): T[] {\n return [...this.#pending.keys()];\n }\n\n /** Whether `element` is currently holding an update back. */\n has(element: T): boolean {\n return this.#pending.has(element);\n }\n\n /** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */\n defer(element: T): void {\n if (this.#pending.has(element)) return;\n const onBlur = (): void => {\n this.#detach(element);\n this.#onRelease(element);\n };\n this.#pending.set(element, onBlur);\n element.addEventListener(\"blur\", onBlur);\n }\n\n /** Defers `element` as the only pending entry, cancelling any others. */\n deferOnly(element: T): void {\n for (const pending of this.elements) {\n if (pending !== element) this.#detach(pending);\n }\n this.defer(element);\n }\n\n /** Cancels `element`'s deferral without completing it; no-ops when not pending. */\n release(element: T): void {\n this.#detach(element);\n }\n\n /** Cancels every deferral without completing any of them. */\n releaseAll(): void {\n for (const element of this.elements) this.#detach(element);\n }\n\n /** Removes the `blur` listener for `element` and forgets it. */\n #detach(element: T): void {\n const onBlur = this.#pending.get(element);\n if (onBlur) element.removeEventListener(\"blur\", onBlur);\n this.#pending.delete(element);\n }\n}\n","/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BlurDeferral } from \"../utils/blur_deferral\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/**\n * Headless \"read more / truncate\" behavior for visually clamped text.\n *\n * Markup contract (identifier: `stimeo--read-more`):\n * <div data-controller=\"stimeo--read-more\">\n * <p id=\"bio\" data-stimeo--read-more-target=\"content\" data-state=\"collapsed\">…</p>\n * <button data-stimeo--read-more-target=\"trigger\"\n * data-action=\"stimeo--read-more#toggle\"\n * aria-expanded=\"false\" aria-controls=\"bio\" hidden>Read more</button>\n * </div>\n *\n * There is no dedicated APG widget; the toggle borrows the **Disclosure**\n * convention (`aria-expanded`). The visual clamp itself (`-webkit-line-clamp`\n * etc.) is the consumer's CSS, keyed off `data-state`.\n *\n * @remarks\n * Behavior only. The full text always stays in the DOM — the clamp is purely\n * visual, so assistive technology reads everything regardless of state; here\n * `aria-expanded` therefore signals the *visual* expansion, not content hidden\n * from AT. The controller's extra job is **overflow detection**: when the\n * content is not actually clamped (it fits), the toggle is `hidden` so no\n * pointless \"read more\" is offered. Resizes, content changes, and target\n * replacement re-evaluate it; hiding a focused toggle waits until blur.\n */\nexport class ReadMoreController extends Controller<HTMLElement> {\n static override targets = [\"content\", \"trigger\"];\n static override values = {\n collapsed: { type: Boolean, default: true },\n };\n static actions = [\"toggle\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly triggerTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n declare collapsedValue: boolean;\n\n #connected = false;\n #collapsed = true;\n #observedContent: HTMLElement | null = null;\n #contentMutationObserver: MutationObserver | null = null;\n\n readonly #update = (): void => {\n if (this.#connected) this.#evaluateOverflow();\n };\n readonly #layout = new LayoutObserver(this.#update);\n\n /** Holds the trigger's hide back while it has focus; re-evaluates on blur. */\n readonly #deferredHide = new BlurDeferral(() => {\n this.#update();\n });\n\n override connect(): void {\n this.#connected = true;\n // The DOM is the source of truth on reconnect (Turbo cache restore / morph): an\n // explicit `data-state=\"expanded\"`/`\"collapsed\"` is honored verbatim so a block\n // the user expanded *or* collapsed survives a back-navigation, even when the\n // declarative `collapsed` Value disagrees. The Value seeds only a genuinely fresh\n // render (no `data-state` yet).\n this.#collapsed = this.#initialCollapsed();\n this.#syncTargets();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#stopObservingContent();\n this.#layout.disconnect();\n }\n\n contentTargetConnected(): void {\n this.#syncTargets();\n }\n\n contentTargetDisconnected(): void {\n this.#syncTargets();\n }\n\n triggerTargetConnected(): void {\n this.#syncTargets();\n }\n\n triggerTargetDisconnected(trigger: HTMLElement): void {\n this.#deferredHide.release(trigger);\n this.#syncTargets();\n }\n\n /** Toggles between the collapsed (clamped) and expanded states. */\n toggle(): void {\n if (!this.#connected) return;\n this.#collapsed = !this.#collapsed;\n this.#reflect();\n this.#evaluateOverflow();\n }\n\n #initialCollapsed(): boolean {\n if (this.hasContentTarget) {\n const state = this.contentTarget.getAttribute(\"data-state\");\n if (state === \"expanded\") return false;\n if (state === \"collapsed\") return true;\n }\n return this.collapsedValue;\n }\n\n #reflect(): void {\n if (this.hasContentTarget) {\n this.contentTarget.setAttribute(\"data-state\", this.#collapsed ? \"collapsed\" : \"expanded\");\n }\n if (this.hasTriggerTarget) {\n this.triggerTarget.setAttribute(\"aria-expanded\", this.#collapsed ? \"false\" : \"true\");\n }\n }\n\n #syncTargets(): void {\n if (!this.#connected) return;\n this.#syncContentObservation();\n this.#reflect();\n this.#evaluateOverflow();\n }\n\n #syncContentObservation(): void {\n const next = this.hasContentTarget ? this.contentTarget : null;\n if (next === this.#observedContent) return;\n\n this.#stopObservingContent();\n if (!next) return;\n\n this.#observedContent = next;\n this.#layout.observe(next);\n this.#layout.observeViewport();\n next.addEventListener(\"load\", this.#update, true);\n\n if (typeof MutationObserver !== \"undefined\") {\n this.#contentMutationObserver = new MutationObserver(this.#update);\n this.#contentMutationObserver.observe(next, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n }\n\n #stopObservingContent(): void {\n this.#deferredHide.releaseAll();\n if (this.#observedContent) {\n this.#layout.unobserve(this.#observedContent);\n this.#observedContent.removeEventListener(\"load\", this.#update, true);\n }\n this.#observedContent = null;\n this.#contentMutationObserver?.disconnect();\n this.#contentMutationObserver = null;\n this.#layout.unobserveViewport();\n }\n\n #evaluateOverflow(): void {\n if (!this.hasTriggerTarget || !this.hasContentTarget) return;\n\n const trigger = this.triggerTarget;\n const content = this.contentTarget;\n const useful = !this.#collapsed || content.scrollHeight > content.clientHeight;\n if (useful) {\n this.#deferredHide.releaseAll();\n trigger.hidden = false;\n return;\n }\n\n if (document.activeElement === trigger) {\n trigger.hidden = false;\n this.#deferredHide.deferOnly(trigger);\n return;\n }\n\n this.#deferredHide.releaseAll();\n trigger.hidden = true;\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/relative_time_controller.ts"],"names":[],"mappings":"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","file":"relative_time_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * 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","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** A time scale: the upper bound (seconds) it covers and its `Intl` unit/divisor. */\ninterface TimeScale {\n limit: number;\n unit: Intl.RelativeTimeFormatUnit;\n ms: number;\n}\n\n/** Coarsest scale, used as the fallback for anything a year or older. */\nconst YEAR_SCALE: TimeScale = { limit: Number.POSITIVE_INFINITY, unit: \"year\", ms: 31_557_600_000 };\n\n/** Boundaries (in seconds) and the `Intl` unit/divisor used at each scale. */\nconst UNITS: readonly TimeScale[] = [\n { limit: 60, unit: \"second\", ms: 1000 },\n { limit: 3600, unit: \"minute\", ms: 60_000 },\n { limit: 86_400, unit: \"hour\", ms: 3_600_000 },\n { limit: 604_800, unit: \"day\", ms: 86_400_000 },\n { limit: 2_629_800, unit: \"week\", ms: 604_800_000 },\n { limit: 31_557_600, unit: \"month\", ms: 2_629_800_000 },\n YEAR_SCALE,\n];\n\n/**\n * Headless relative-time behavior: renders an absolute timestamp as \"3 minutes\n * ago\" / \"in 2 days\" and keeps it fresh. No dedicated APG pattern; it follows\n * the HTML `<time>` semantics.\n *\n * Markup contract (identifier: `stimeo--relative-time`):\n * <time data-controller=\"stimeo--relative-time\"\n * datetime=\"2026-05-30T12:00:00+09:00\" title=\"2026-05-30 12:00\"\n * data-stimeo--relative-time-locale-value=\"ja\">2026-05-30 12:00</time>\n *\n * Computes the difference from `datetime` to now and formats it with\n * `Intl.RelativeTimeFormat` (a browser standard — no added dependency). The\n * polling interval widens as the timestamp ages (seconds → minutes → hours →\n * days). Past a `threshold`, it falls back to the authored absolute text.\n *\n * @remarks\n * Behavior only. The machine-readable `datetime` attribute is left untouched\n * while only the visible text updates, and the element is intentionally **not**\n * a live region (silent updates, no announcement interruptions). The polling\n * timer is owned by {@link SafeTimeout} and torn down on `disconnect()` (Turbo\n * navigation included).\n *\n * Render inputs are followed at runtime: a morph that swaps `locale`, `threshold`,\n * or `tickInterval` on the live element repaints through one coalesced pass\n * ({@link MicrotaskCoalescer}) rather than leaving the reading frozen. The authored\n * absolute text goes back on the element via {@link BeforeCacheReset} before Turbo\n * caches the page — a snapshot taken mid-render would carry the relative form\n * alone, and that is a form `connect()` must not adopt as the fallback.\n */\nexport class RelativeTimeController extends Controller<HTMLElement> {\n static override values = {\n locale: { type: String, default: \"\" },\n threshold: { type: Number, default: 0 },\n tickInterval: { type: Number, default: 60_000 },\n };\n\n declare localeValue: string;\n declare thresholdValue: number;\n declare tickIntervalValue: number;\n\n readonly #timers = new SafeTimeout();\n /** Collapses a morph that swaps several render inputs at once into one repaint. */\n readonly #resync = new MicrotaskCoalescer(() => this.#resyncToValues());\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /** Epoch ms parsed from `datetime`; `NaN` when absent or invalid. */\n #targetMs = Number.NaN;\n /** The authored absolute text, restored when the threshold fallback kicks in. */\n #absoluteText = \"\";\n\n override connect(): void {\n this.#resync.activate();\n this.#beforeCache.activate();\n // Don't adopt already-rendered relative text as the absolute fallback: on a\n // re-connect against a tree that still holds the live \"3 minutes ago\" text, that\n // relative string would otherwise become `#absoluteText`. Only read the authored\n // textContent before the element has been rendered to a relative form.\n if (this.element.getAttribute(\"data-state\") !== \"relative\") {\n this.#absoluteText = (this.element.textContent ?? \"\").trim();\n }\n this.#targetMs = Date.parse(this.element.getAttribute(\"datetime\") ?? \"\");\n if (Number.isNaN(this.#targetMs)) return;\n this.#schedule();\n }\n\n override disconnect(): void {\n this.#resync.cancel();\n this.#beforeCache.deactivate();\n this.#timers.clearAll();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `locale` at runtime. */\n localeValueChanged(): void {\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `threshold` at runtime. */\n thresholdValueChanged(): void {\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `tickInterval` at runtime. */\n tickIntervalValueChanged(): void {\n this.#resync.schedule();\n }\n\n /**\n * Renders against the current Values and re-arms the poll from now.\n *\n * Render only: it emits no event, and clearing first keeps the single self-arming\n * timer single — scheduling on top of a pending one would double the poll rate for\n * the rest of the session. A stamp whose `datetime` never parsed has nothing to\n * render, and one that already reached its terminal fallback simply renders it\n * again and stops.\n */\n #resyncToValues(): void {\n if (Number.isNaN(this.#targetMs)) return;\n this.#timers.clearAll();\n this.#schedule();\n }\n\n /**\n * Restores the authored absolute text and the pre-render state for the snapshot\n * Turbo is about to take, leaving the live page's poll timer alone.\n *\n * With no authored text held there is nothing to restore, and `data-state` has to\n * stay as it is: that marker is what tells the next `connect()` the visible text is\n * a rendered relative form rather than an absolute fallback to hold on to.\n */\n #rewindForCache(): void {\n if (!this.#absoluteText) return;\n this.element.textContent = this.#absoluteText;\n this.element.removeAttribute(\"data-state\");\n }\n\n /** Renders the current representation and reschedules unless polling can stop. */\n #schedule(): void {\n const nextDelay = this.#applyAndComputeDelay();\n if (nextDelay !== null) {\n this.#timers.set(() => this.#schedule(), nextDelay);\n }\n }\n\n /**\n * Updates the visible text and returns the next poll delay (ms), or `null` when\n * polling can stop: a *past* timestamp that fell back to the absolute text can\n * never leave it again, and a locale the runtime rejects has nothing to render\n * until that value is corrected.\n *\n * @stimeoRenderRoot\n */\n #applyAndComputeDelay(): number | null {\n const deltaMs = this.#targetMs - Date.now();\n const absSeconds = Math.abs(deltaMs) / 1000;\n\n const scale = UNITS.find((u) => absSeconds < u.limit) ?? YEAR_SCALE;\n // Poll no finer than the configured minimum; widen for coarser units so an\n // hours-old stamp is not re-rendered every minute.\n const unitFloor = scale.unit === \"second\" || scale.unit === \"minute\" ? 60_000 : scale.ms;\n const nextDelay = Math.max(this.tickIntervalValue, Math.min(unitFloor, 86_400_000));\n\n // Only switch to the absolute fallback when we actually hold authored absolute\n // text; otherwise (e.g. it could not be recovered after a morph) keep rendering\n // the relative form rather than blanking the element.\n if (this.thresholdValue > 0 && absSeconds >= this.thresholdValue && this.#absoluteText) {\n this.element.textContent = this.#absoluteText;\n this.element.setAttribute(\"data-state\", \"absolute\");\n // A past stamp only ages further, so its fallback is final and polling stops. A\n // future one moves back under the threshold, so keep polling and land 1ms past\n // the crossing: the poll cadence is far coarser than the instant the fallback\n // stops being the correct representation, and the floor does not apply to a hop\n // that exists to leave the fallback rather than to refresh a reading.\n if (deltaMs <= 0) return null;\n return Math.min(nextDelay, deltaMs - this.thresholdValue * 1000 + 1);\n }\n\n const formatter = this.#formatter;\n // Nothing to render in a locale the runtime rejects: leaving the element as\n // authored beats blanking it or guessing at another language.\n if (formatter === null) return null;\n const value = Math.round(deltaMs / scale.ms);\n this.element.textContent = formatter.format(value, scale.unit);\n this.element.setAttribute(\"data-state\", \"relative\");\n return nextDelay;\n }\n\n /**\n * A `RelativeTimeFormat` for the resolved locale (`numeric: \"auto\"`), or `null`\n * when the runtime rejects that locale.\n */\n get #formatter(): Intl.RelativeTimeFormat | null {\n try {\n return new Intl.RelativeTimeFormat(this.#locale, { numeric: \"auto\" });\n } catch {\n // A malformed locale must not break the page: the authored absolute text stays\n // as the graceful fallback, and a corrected value renders on the next pass.\n return null;\n }\n }\n\n /** Locale precedence: the value, then the nearest `lang` up the ancestor chain. */\n get #locale(): string | undefined {\n return this.localeValue || this.element.closest(\"[lang]\")?.getAttribute(\"lang\") || undefined;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/relative_time_controller.ts"],"names":[],"mappings":"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","file":"relative_time_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * 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","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** A time scale: the upper bound (seconds) it covers and its `Intl` unit/divisor. */\ninterface TimeScale {\n limit: number;\n unit: Intl.RelativeTimeFormatUnit;\n ms: number;\n}\n\n/** Coarsest scale, used as the fallback for anything a year or older. */\nconst YEAR_SCALE: TimeScale = { limit: Number.POSITIVE_INFINITY, unit: \"year\", ms: 31_557_600_000 };\n\n/** Boundaries (in seconds) and the `Intl` unit/divisor used at each scale. */\nconst UNITS: readonly TimeScale[] = [\n { limit: 60, unit: \"second\", ms: 1000 },\n { limit: 3600, unit: \"minute\", ms: 60_000 },\n { limit: 86_400, unit: \"hour\", ms: 3_600_000 },\n { limit: 604_800, unit: \"day\", ms: 86_400_000 },\n { limit: 2_629_800, unit: \"week\", ms: 604_800_000 },\n { limit: 31_557_600, unit: \"month\", ms: 2_629_800_000 },\n YEAR_SCALE,\n];\n\n/**\n * Headless relative-time behavior: renders an absolute timestamp as \"3 minutes\n * ago\" / \"in 2 days\" and keeps it fresh. No dedicated APG pattern; it follows\n * the HTML `<time>` semantics.\n *\n * Markup contract (identifier: `stimeo--relative-time`):\n * <time data-controller=\"stimeo--relative-time\"\n * datetime=\"2026-05-30T12:00:00+09:00\" title=\"2026-05-30 12:00\"\n * data-stimeo--relative-time-locale-value=\"ja\">2026-05-30 12:00</time>\n *\n * Computes the difference from `datetime` to now and formats it with\n * `Intl.RelativeTimeFormat` (a browser standard — no added dependency). The\n * polling interval widens as the timestamp ages (seconds → minutes → hours →\n * days). Past a `threshold`, it falls back to the authored absolute text.\n *\n * @remarks\n * Behavior only. The machine-readable `datetime` attribute is left untouched\n * while only the visible text updates, and the element is intentionally **not**\n * a live region (silent updates, no announcement interruptions). The polling\n * timer is owned by {@link SafeTimeout} and torn down on `disconnect()` (Turbo\n * navigation included).\n *\n * Render inputs are followed at runtime: a morph that swaps `locale`, `threshold`,\n * or `tickInterval` on the live element repaints through one coalesced pass\n * ({@link MicrotaskCoalescer}) rather than leaving the reading frozen. The authored\n * absolute text goes back on the element via {@link BeforeCacheReset} before Turbo\n * caches the page — a snapshot taken mid-render would carry the relative form\n * alone, and that is a form `connect()` must not adopt as the fallback.\n */\nexport class RelativeTimeController extends Controller<HTMLElement> {\n static override values = {\n locale: { type: String, default: \"\" },\n threshold: { type: Number, default: 0 },\n tickInterval: { type: Number, default: 60_000 },\n };\n\n declare localeValue: string;\n declare thresholdValue: number;\n declare tickIntervalValue: number;\n\n readonly #timers = new SafeTimeout();\n /** Collapses a morph that swaps several render inputs at once into one repaint. */\n readonly #resync = new MicrotaskCoalescer(() => this.#resyncToValues());\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /** Epoch ms parsed from `datetime`; `NaN` when absent or invalid. */\n #targetMs = Number.NaN;\n /** The authored absolute text, restored when the threshold fallback kicks in. */\n #absoluteText = \"\";\n\n override connect(): void {\n this.#resync.activate();\n this.#beforeCache.activate();\n // Don't adopt already-rendered relative text as the absolute fallback: on a\n // re-connect against a tree that still holds the live \"3 minutes ago\" text, that\n // relative string would otherwise become `#absoluteText`. Only read the authored\n // textContent before the element has been rendered to a relative form.\n if (this.element.getAttribute(\"data-state\") !== \"relative\") {\n this.#absoluteText = (this.element.textContent ?? \"\").trim();\n }\n this.#targetMs = Date.parse(this.element.getAttribute(\"datetime\") ?? \"\");\n if (Number.isNaN(this.#targetMs)) return;\n this.#schedule();\n }\n\n override disconnect(): void {\n this.#resync.cancel();\n this.#beforeCache.deactivate();\n this.#timers.clearAll();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `locale` at runtime. */\n localeValueChanged(): void {\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `threshold` at runtime. */\n thresholdValueChanged(): void {\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `tickInterval` at runtime. */\n tickIntervalValueChanged(): void {\n this.#resync.schedule();\n }\n\n /**\n * Renders against the current Values and re-arms the poll from now.\n *\n * Render only: it emits no event, and clearing first keeps the single self-arming\n * timer single — scheduling on top of a pending one would double the poll rate for\n * the rest of the session. A stamp whose `datetime` never parsed has nothing to\n * render, and one that already reached its terminal fallback simply renders it\n * again and stops.\n */\n #resyncToValues(): void {\n if (Number.isNaN(this.#targetMs)) return;\n this.#timers.clearAll();\n this.#schedule();\n }\n\n /**\n * Restores the authored absolute text and the pre-render state for the snapshot\n * Turbo is about to take, leaving the live page's poll timer alone.\n *\n * With no authored text held there is nothing to restore, and `data-state` has to\n * stay as it is: that marker is what tells the next `connect()` the visible text is\n * a rendered relative form rather than an absolute fallback to hold on to.\n */\n #rewindForCache(): void {\n if (!this.#absoluteText) return;\n this.element.textContent = this.#absoluteText;\n this.element.removeAttribute(\"data-state\");\n }\n\n /** Renders the current representation and reschedules unless polling can stop. */\n #schedule(): void {\n const nextDelay = this.#applyAndComputeDelay();\n if (nextDelay !== null) {\n this.#timers.set(() => this.#schedule(), nextDelay);\n }\n }\n\n /**\n * Updates the visible text and returns the next poll delay (ms), or `null` when\n * polling can stop: a *past* timestamp that fell back to the absolute text can\n * never leave it again, and a locale the runtime rejects has nothing to render\n * until that value is corrected.\n *\n * @stimeoRenderRoot\n */\n #applyAndComputeDelay(): number | null {\n const deltaMs = this.#targetMs - Date.now();\n const absSeconds = Math.abs(deltaMs) / 1000;\n\n const scale = UNITS.find((u) => absSeconds < u.limit) ?? YEAR_SCALE;\n // Poll no finer than the configured minimum; widen for coarser units so an\n // hours-old stamp is not re-rendered every minute.\n const unitFloor = scale.unit === \"second\" || scale.unit === \"minute\" ? 60_000 : scale.ms;\n const nextDelay = Math.max(this.tickIntervalValue, Math.min(unitFloor, 86_400_000));\n\n // Only switch to the absolute fallback when we actually hold authored absolute\n // text; otherwise (e.g. it could not be recovered after a morph) keep rendering\n // the relative form rather than blanking the element.\n if (this.thresholdValue > 0 && absSeconds >= this.thresholdValue && this.#absoluteText) {\n this.element.textContent = this.#absoluteText;\n this.element.setAttribute(\"data-state\", \"absolute\");\n // A past stamp only ages further, so its fallback is final and polling stops. A\n // future one moves back under the threshold, so keep polling and land 1ms past\n // the crossing: the poll cadence is far coarser than the instant the fallback\n // stops being the correct representation, and the floor does not apply to a hop\n // that exists to leave the fallback rather than to refresh a reading.\n if (deltaMs <= 0) return null;\n return Math.min(nextDelay, deltaMs - this.thresholdValue * 1000 + 1);\n }\n\n const formatter = this.#formatter;\n // Nothing to render in a locale the runtime rejects: leaving the element as\n // authored beats blanking it or guessing at another language.\n if (formatter === null) return null;\n const value = Math.round(deltaMs / scale.ms);\n this.element.textContent = formatter.format(value, scale.unit);\n this.element.setAttribute(\"data-state\", \"relative\");\n return nextDelay;\n }\n\n /**\n * A `RelativeTimeFormat` for the resolved locale (`numeric: \"auto\"`), or `null`\n * when the runtime rejects that locale.\n */\n get #formatter(): Intl.RelativeTimeFormat | null {\n try {\n return new Intl.RelativeTimeFormat(this.#locale, { numeric: \"auto\" });\n } catch {\n // A malformed locale must not break the page: the authored absolute text stays\n // as the graceful fallback, and a corrected value renders on the next pass.\n return null;\n }\n }\n\n /** Locale precedence: the value, then the nearest `lang` up the ancestor chain. */\n get #locale(): string | undefined {\n return this.localeValue || this.element.closest(\"[lang]\")?.getAttribute(\"lang\") || undefined;\n }\n}\n"]}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Headless **before-cache reset** —
|
|
5
|
-
* pattern). On `turbo:before-cache` it returns transient UI (open menus/modals,
|
|
4
|
+
* Headless **before-cache reset** — Hotwire-specific, with no APG pattern. On `turbo:before-cache` it returns transient UI (open menus/modals,
|
|
6
5
|
* typed-in values, spinning indicators) to its initial state, so a page restored by
|
|
7
6
|
* the Back button is not frozen mid-interaction. Place one on `<body>`.
|
|
8
7
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/reset_before_cache_controller.ts"],"names":[],"mappings":"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","file":"reset_before_cache_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** Field types whose state is authored markup rather than something a user changed. */\nconst STATELESS_INPUT_TYPES = new Set([\"hidden\", \"submit\", \"reset\", \"button\", \"image\"]);\n\n/**\n * Returns one field to the state its markup declared, discarding what the user\n * changed — the single-field form of a form reset.\n *\n * Assigning `\"\"` would not do this. For a text field it throws away the authored\n * value; for a checkbox, a radio, or a hidden field the `value` IDL attribute\n * writes straight through to the content attribute, so the markup itself is\n * rewritten while the checkedness (the part the user actually moved) is left\n * alone; and for a select it can leave nothing selected at all — a state the page\n * never had.\n */\nfunction restoreField(element: Element): void {\n if (element instanceof HTMLTextAreaElement) {\n element.value = element.defaultValue;\n return;\n }\n if (element instanceof HTMLSelectElement) {\n // An option's declared selectedness is its `selected` content attribute.\n for (const option of element.options) option.selected = option.hasAttribute(\"selected\");\n return;\n }\n if (element instanceof HTMLInputElement) {\n if (element.type === \"checkbox\" || element.type === \"radio\") {\n element.checked = element.defaultChecked;\n } else if (element.type === \"file\") {\n // A file field takes no value but the empty one, so assigning its default\n // back would throw on markup that declared one.\n element.value = \"\";\n } else if (!STATELESS_INPUT_TYPES.has(element.type)) {\n element.value = element.defaultValue;\n }\n }\n}\n\n/**\n * Headless **before-cache reset** — the most Hotwire-specific gap part (no APG\n * pattern). On `turbo:before-cache` it returns transient UI (open menus/modals,\n * typed-in values, spinning indicators) to its initial state, so a page restored by\n * the Back button is not frozen mid-interaction. Place one on `<body>`.\n *\n * Markup contract (identifier: `stimeo--reset-before-cache`):\n * <body data-controller=\"stimeo--reset-before-cache\">\n * <details data-reset-attr=\"open\">…</details> <!-- remove these attributes -->\n * <div data-reset-class=\"is-open is-loading\">…</div> <!-- remove these classes -->\n * <form data-reset-form>…</form> <!-- form.reset() -->\n * <input data-reset-value> <!-- back to its authored state -->\n * <div data-reset-hidden>transient overlay</div> <!-- re-hide -->\n * <div data-reset-remove>flash toast</div> <!-- drop from the DOM -->\n * </body>\n *\n * `scope` narrows the sweep to the first descendant matching that CSS selector;\n * left empty (the default) it covers the whole element. A selector the engine\n * cannot read falls back to that default rather than taking the sweep down with\n * it, and so does one that matches nothing.\n *\n * `reset` dispatches `{}`; `request` dispatches `{}`.\n *\n * @remarks\n * Behavior only and **idempotent** — every run converges on the same initial state,\n * holding no module-scope state. It does the cross-cutting DOM cleanup directly\n * (attribute removal, class removal, restoring a field to its authored state,\n * re-hiding, node removal) and, when\n * `dispatchReset` is on, fires `stimeo--reset-before-cache:request` so individual\n * Stimeo controllers can run their own close logic. The `turbo:before-cache`\n * listener is paired to `connect()` / `disconnect()` so it never double-registers or\n * leaks. {@link reset} is also a public action for manual triggering.\n */\nexport class ResetBeforeCacheController extends Controller<HTMLElement> {\n static override values = {\n scope: { type: String, default: \"\" },\n dispatchReset: { type: Boolean, default: true },\n };\n static actions = [\"reset\"] as const;\n static events = [\"reset\", \"request\"] as const;\n\n declare scopeValue: string;\n declare dispatchResetValue: boolean;\n\n /** The `scope` declaration after validation; empty when it cannot be parsed. */\n #scopeSelector = \"\";\n\n /** Runs the reset just before Turbo caches the snapshot. */\n readonly #onBeforeCache = (): void => this.reset();\n\n /**\n * Validates the scope 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\n * until it is handed to the DOM — and this part runs from a single listener\n * whose whole job is to keep a cached page from freezing. Falling back to the\n * default keeps that job running with a visible, findable result instead of\n * silently taking the sweep down.\n */\n scopeValueChanged(): void {\n const selector = this.scopeValue;\n if (selector.length > 0) {\n try {\n this.element.matches(selector);\n this.#scopeSelector = selector;\n return;\n } catch {\n // Unparsable selector: fall through to the default below.\n }\n }\n this.#scopeSelector = \"\";\n }\n\n override connect(): void {\n document.addEventListener(\"turbo:before-cache\", this.#onBeforeCache);\n }\n\n override disconnect(): void {\n document.removeEventListener(\"turbo:before-cache\", this.#onBeforeCache);\n }\n\n /**\n * Resets transient UI within scope to its initial state. Asks controllers to\n * close (via `request`) first, then applies the declarative `data-reset-*` cleanup,\n * and finally emits `reset`. Safe to call any number of times (idempotent).\n *\n * `dispatchReset` decides only whether the `request` ask goes out; the cleanup\n * and the closing `reset` run either way. Both the listener and this action\n * reach the same sweep, so `reset` is emitted for a manual call too.\n */\n reset(): void {\n const root = this.#scopeRoot();\n\n if (this.dispatchResetValue) this.dispatch(\"request\");\n\n for (const element of root.querySelectorAll(\"[data-reset-attr]\")) {\n for (const name of (element.getAttribute(\"data-reset-attr\") ?? \"\").split(/\\s+/)) {\n if (name) element.removeAttribute(name);\n }\n }\n for (const element of root.querySelectorAll(\"[data-reset-class]\")) {\n for (const token of (element.getAttribute(\"data-reset-class\") ?? \"\").split(/\\s+/)) {\n if (token) element.classList.remove(token);\n }\n }\n for (const element of root.querySelectorAll(\"[data-reset-form]\")) {\n if (element instanceof HTMLFormElement) element.reset();\n }\n for (const element of root.querySelectorAll(\"[data-reset-value]\")) {\n restoreField(element);\n }\n for (const element of root.querySelectorAll<HTMLElement>(\"[data-reset-hidden]\")) {\n element.hidden = true;\n }\n // Removal runs last so it cannot drop a node another rule still needed to visit.\n for (const element of root.querySelectorAll(\"[data-reset-remove]\")) {\n element.remove();\n }\n\n this.dispatch(\"reset\");\n }\n\n /** The scan root: a `scope` descendant when set, else the controller element. */\n #scopeRoot(): Element {\n if (!this.#scopeSelector) return this.element;\n return this.element.querySelector(this.#scopeSelector) ?? this.element;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/controllers/reset_before_cache_controller.ts"],"names":[],"mappings":"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","file":"reset_before_cache_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** Field types whose state is authored markup rather than something a user changed. */\nconst STATELESS_INPUT_TYPES = new Set([\"hidden\", \"submit\", \"reset\", \"button\", \"image\"]);\n\n/**\n * Returns one field to the state its markup declared, discarding what the user\n * changed — the single-field form of a form reset.\n *\n * Assigning `\"\"` would not do this. For a text field it throws away the authored\n * value; for a checkbox, a radio, or a hidden field the `value` IDL attribute\n * writes straight through to the content attribute, so the markup itself is\n * rewritten while the checkedness (the part the user actually moved) is left\n * alone; and for a select it can leave nothing selected at all — a state the page\n * never had.\n */\nfunction restoreField(element: Element): void {\n if (element instanceof HTMLTextAreaElement) {\n element.value = element.defaultValue;\n return;\n }\n if (element instanceof HTMLSelectElement) {\n // An option's declared selectedness is its `selected` content attribute.\n for (const option of element.options) option.selected = option.hasAttribute(\"selected\");\n return;\n }\n if (element instanceof HTMLInputElement) {\n if (element.type === \"checkbox\" || element.type === \"radio\") {\n element.checked = element.defaultChecked;\n } else if (element.type === \"file\") {\n // A file field takes no value but the empty one, so assigning its default\n // back would throw on markup that declared one.\n element.value = \"\";\n } else if (!STATELESS_INPUT_TYPES.has(element.type)) {\n element.value = element.defaultValue;\n }\n }\n}\n\n/**\n * Headless **before-cache reset** — Hotwire-specific, with no APG pattern. On `turbo:before-cache` it returns transient UI (open menus/modals,\n * typed-in values, spinning indicators) to its initial state, so a page restored by\n * the Back button is not frozen mid-interaction. Place one on `<body>`.\n *\n * Markup contract (identifier: `stimeo--reset-before-cache`):\n * <body data-controller=\"stimeo--reset-before-cache\">\n * <details data-reset-attr=\"open\">…</details> <!-- remove these attributes -->\n * <div data-reset-class=\"is-open is-loading\">…</div> <!-- remove these classes -->\n * <form data-reset-form>…</form> <!-- form.reset() -->\n * <input data-reset-value> <!-- back to its authored state -->\n * <div data-reset-hidden>transient overlay</div> <!-- re-hide -->\n * <div data-reset-remove>flash toast</div> <!-- drop from the DOM -->\n * </body>\n *\n * `scope` narrows the sweep to the first descendant matching that CSS selector;\n * left empty (the default) it covers the whole element. A selector the engine\n * cannot read falls back to that default rather than taking the sweep down with\n * it, and so does one that matches nothing.\n *\n * `reset` dispatches `{}`; `request` dispatches `{}`.\n *\n * @remarks\n * Behavior only and **idempotent** — every run converges on the same initial state,\n * holding no module-scope state. It does the cross-cutting DOM cleanup directly\n * (attribute removal, class removal, restoring a field to its authored state,\n * re-hiding, node removal) and, when\n * `dispatchReset` is on, fires `stimeo--reset-before-cache:request` so individual\n * Stimeo controllers can run their own close logic. The `turbo:before-cache`\n * listener is paired to `connect()` / `disconnect()` so it never double-registers or\n * leaks. {@link reset} is also a public action for manual triggering.\n */\nexport class ResetBeforeCacheController extends Controller<HTMLElement> {\n static override values = {\n scope: { type: String, default: \"\" },\n dispatchReset: { type: Boolean, default: true },\n };\n static actions = [\"reset\"] as const;\n static events = [\"reset\", \"request\"] as const;\n\n declare scopeValue: string;\n declare dispatchResetValue: boolean;\n\n /** The `scope` declaration after validation; empty when it cannot be parsed. */\n #scopeSelector = \"\";\n\n /** Runs the reset just before Turbo caches the snapshot. */\n readonly #onBeforeCache = (): void => this.reset();\n\n /**\n * Validates the scope 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\n * until it is handed to the DOM — and this part runs from a single listener\n * whose whole job is to keep a cached page from freezing. Falling back to the\n * default keeps that job running with a visible, findable result instead of\n * silently taking the sweep down.\n */\n scopeValueChanged(): void {\n const selector = this.scopeValue;\n if (selector.length > 0) {\n try {\n this.element.matches(selector);\n this.#scopeSelector = selector;\n return;\n } catch {\n // Unparsable selector: fall through to the default below.\n }\n }\n this.#scopeSelector = \"\";\n }\n\n override connect(): void {\n document.addEventListener(\"turbo:before-cache\", this.#onBeforeCache);\n }\n\n override disconnect(): void {\n document.removeEventListener(\"turbo:before-cache\", this.#onBeforeCache);\n }\n\n /**\n * Resets transient UI within scope to its initial state. Asks controllers to\n * close (via `request`) first, then applies the declarative `data-reset-*` cleanup,\n * and finally emits `reset`. Safe to call any number of times (idempotent).\n *\n * `dispatchReset` decides only whether the `request` ask goes out; the cleanup\n * and the closing `reset` run either way. Both the listener and this action\n * reach the same sweep, so `reset` is emitted for a manual call too.\n */\n reset(): void {\n const root = this.#scopeRoot();\n\n if (this.dispatchResetValue) this.dispatch(\"request\");\n\n for (const element of root.querySelectorAll(\"[data-reset-attr]\")) {\n for (const name of (element.getAttribute(\"data-reset-attr\") ?? \"\").split(/\\s+/)) {\n if (name) element.removeAttribute(name);\n }\n }\n for (const element of root.querySelectorAll(\"[data-reset-class]\")) {\n for (const token of (element.getAttribute(\"data-reset-class\") ?? \"\").split(/\\s+/)) {\n if (token) element.classList.remove(token);\n }\n }\n for (const element of root.querySelectorAll(\"[data-reset-form]\")) {\n if (element instanceof HTMLFormElement) element.reset();\n }\n for (const element of root.querySelectorAll(\"[data-reset-value]\")) {\n restoreField(element);\n }\n for (const element of root.querySelectorAll<HTMLElement>(\"[data-reset-hidden]\")) {\n element.hidden = true;\n }\n // Removal runs last so it cannot drop a node another rule still needed to visit.\n for (const element of root.querySelectorAll(\"[data-reset-remove]\")) {\n element.remove();\n }\n\n this.dispatch(\"reset\");\n }\n\n /** The scan root: a `scope` descendant when set, else the controller element. */\n #scopeRoot(): Element {\n if (!this.#scopeSelector) return this.element;\n return this.element.querySelector(this.#scopeSelector) ?? this.element;\n }\n}\n"]}
|
|
@@ -94,7 +94,7 @@ declare class ResizableController extends Controller<HTMLElement> {
|
|
|
94
94
|
onPointerDown(event: PointerEvent): void;
|
|
95
95
|
/** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */
|
|
96
96
|
onKeydown(event: KeyboardEvent): void;
|
|
97
|
-
/**
|
|
97
|
+
/** Collapses the primary pane to its minimum, or returns it to the last position. */
|
|
98
98
|
toggle(): void;
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -251,7 +251,7 @@ var ResizableController = class extends Controller {
|
|
|
251
251
|
this.#dispatchChange();
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
|
-
/**
|
|
254
|
+
/** Collapses the primary pane to its minimum, or returns it to the last position. */
|
|
255
255
|
toggle() {
|
|
256
256
|
const { min, max } = this.#range;
|
|
257
257
|
if (this.#position > min) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/before_cache_reset.ts","../../src/utils/tabindex_loan.ts","../../src/controllers/resizable_controller.ts"],"names":[],"mappings":"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","file":"resizable_controller.js","sourcesContent":["import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\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","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\n\n/**\n * Shared bookkeeping for a `tabindex` a controller lends an element temporarily.\n *\n * A controller that must move focus somewhere the author never made focusable\n * (a landmark root, a scroll destination) reaches for the same trick: add a\n * `tabindex` just-in-time and hand it back once it is no longer needed. The\n * borrow is the easy half; the return is what the two conditions below are for.\n *\n * **Returning needs two conditions, not one.** Owning the borrow is not enough:\n * the attribute must also still hold the value this instance wrote. A consumer\n * that changed it afterwards — `tabindex=\"0\"` to make the root its own Tab stop\n * — owns it now, and removing it there silently discards authored markup. The\n * bookkeeping is dropped either way, since the loan is over regardless of who\n * ends up owning the value.\n *\n * **Never borrow over an existing value.** An element that already carries a\n * `tabindex` is the author's to control, so there is nothing to lend and nothing\n * to return.\n *\n * Every live loan also owns a shared `turbo:before-cache` subscription. The loan\n * is returned before Turbo can copy it into a snapshot, so consumers get cache\n * safety without duplicating a lifecycle hook; `returnAll()` removes the\n * subscription again as soon as no loan remains.\n *\n * The registry is keyed by element, so a controller borrowing on a single\n * element (`this.element`) and one borrowing across a changing set of targets\n * use the same API — the single-element case is a set of one. It holds no\n * opinion about *when* to borrow or where focus goes next; that stays in the\n * controller.\n *\n * **The API is deliberately two methods.** This file's own doc block is dropped\n * from `dist`, but every member comment is inlined into **each** consumer entry\n * (`tsup` builds with `splitting: false`), so rationale belongs here, only the\n * contract belongs on the members, and every method no consumer calls is still\n * paid for once per consumer entry.\n *\n * @example\n * ```ts\n * readonly #tabindex = new TabindexLoan();\n *\n * #rescueFocus() {\n * this.#tabindex.lend(this.element);\n * this.element.focus();\n * }\n *\n * disconnect() {\n * this.#tabindex.returnAll();\n * }\n * ```\n */\nexport class TabindexLoan<T extends HTMLElement = HTMLElement> {\n readonly #value: string;\n readonly #lent = new Set<T>();\n /** Returns live loans before Turbo can copy them into its page snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.returnAll());\n\n /**\n * @param value - the `tabindex` to lend. `\"-1\"` (the default) is\n * programmatically focusable but not a Tab stop; `\"0\"` is a real Tab stop,\n * which a scroll region with no focusable content of its own needs.\n */\n constructor(value: string = \"-1\") {\n this.#value = value;\n }\n\n /** Lends `element` the value; no-ops when it already carries a `tabindex`. */\n lend(element: T): void {\n if (element.hasAttribute(\"tabindex\")) return;\n element.setAttribute(\"tabindex\", this.#value);\n this.#lent.add(element);\n // Subscribe only while a real loan exists. Keeping this guarantee here means\n // every consumer is Turbo-safe without another lifecycle hook to remember.\n this.#beforeCache.activate();\n }\n\n /** Takes back every loan whose value is still the one that was lent. */\n returnAll(): void {\n for (const element of this.#lent) {\n if (element.getAttribute(\"tabindex\") === this.#value) element.removeAttribute(\"tabindex\");\n }\n this.#lent.clear();\n this.#beforeCache.deactivate();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\n\n/** Value defaults, reused when a declaration cannot be read as a percentage. */\nconst DEFAULT_MIN = 0;\nconst DEFAULT_MAX = 100;\nconst DEFAULT_VALUE = 50;\nconst DEFAULT_STEP = 1;\n\n/**\n * Headless, highly accessible Window Splitter / Resizable panes logic.\n *\n * Markup contract (identifier: `stimeo--resizable`):\n * <div data-controller=\"stimeo--resizable\"\n * data-stimeo--resizable-min-value=\"20\"\n * data-stimeo--resizable-max-value=\"80\"\n * data-stimeo--resizable-step-value=\"1\"\n * data-stimeo--resizable-value-value=\"50\">\n * <div data-stimeo--resizable-target=\"primary\" id=\"pane-1\">Pane A</div>\n * <div role=\"separator\" tabindex=\"0\" aria-orientation=\"vertical\"\n * aria-controls=\"pane-1\" aria-valuemin=\"20\" aria-valuemax=\"80\"\n * aria-valuenow=\"50\" aria-label=\"Resize splitter\"\n * data-stimeo--resizable-target=\"separator\"\n * data-action=\"pointerdown->stimeo--resizable#onPointerDown\n * keydown->stimeo--resizable#onKeydown\"></div>\n * <div data-stimeo--resizable-target=\"secondary\">Pane B</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Window Splitter** pattern:\n * - Robust dragging track using W3C `setPointerCapture` to ensure uninterrupted dragging\n * even when pointers quickly stray outside the separator area.\n * - Arrow keys move the divider by `step` percent, along the axis\n * `aria-orientation` names; `Home` / `End` jump to the ends of the range.\n * - `Enter` collapses the primary pane to the minimum and puts it back where it\n * was, which is what the pattern means by restoring a previous position.\n * - Root-level CSS custom property `--stimeo--resizable-fraction` (0..1) driving\n * presentation styles.\n * - `F6` cycles focus through the panes, which the pattern lists as optional.\n *\n * `change` dispatches `{ value: number, fraction: number }`.\n *\n * @remarks\n * Behavior only. The controller adjusts the CSS custom property on the root element,\n * updates ARIA values on the separator, and emits `stimeo--resizable:change` events.\n *\n * Both the range and the position are percentages. A bound that is not a finite\n * number falls back to its Value default, a maximum below the minimum collapses\n * onto that minimum, and a step that is not a positive finite number falls back\n * to `1` — so an unreadable declaration narrows what the widget can do without\n * ever publishing `NaN` to CSS or to assistive tech.\n *\n * `aria-orientation` is the author's, and its absence means `horizontal` — the\n * value ARIA gives a separator that does not say. Reading any other default here\n * would make the announced axis and the answering arrow keys disagree.\n */\nexport class ResizableController extends Controller<HTMLElement> {\n static override targets = [\"primary\", \"secondary\", \"separator\"];\n static override values = {\n min: { type: Number, default: DEFAULT_MIN },\n max: { type: Number, default: DEFAULT_MAX },\n step: { type: Number, default: DEFAULT_STEP },\n value: { type: Number, default: DEFAULT_VALUE },\n };\n static actions = [\"onKeydown\", \"onPointerDown\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly primaryTarget: HTMLElement;\n declare readonly secondaryTarget: HTMLElement;\n declare readonly separatorTarget: HTMLElement;\n declare readonly hasPrimaryTarget: boolean;\n declare readonly hasSecondaryTarget: boolean;\n declare readonly hasSeparatorTarget: boolean;\n\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare valueValue: number;\n\n /** Where the divider sat before the current collapse; `null` when unknown. */\n #valueBeforeCollapse: number | null = null;\n\n /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */\n #dragAbort: AbortController | null = null;\n\n /** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */\n readonly #paneTabindex = new TabindexLoan();\n\n /** Releases the pane cycle listener bound in {@link connect}. */\n #cycleAbort: AbortController | null = null;\n\n /** Folds the range and position inputs of one morph batch into a single paint. */\n readonly #repaint = new MicrotaskCoalescer(() => this.#render());\n\n override connect(): void {\n // A drag cannot outlive a navigation, so a hook captured mid-drag is stale on\n // arrival. Clearing is unconditional here because disconnect() aborts the\n // drag, leaving no session that an in-page move could carry over.\n this.element.removeAttribute(\"data-dragging\");\n this.#repaint.activate();\n // Stimulus fires the value callbacks before connect, where the coalescer\n // ignores them, so the first paint has to be asked for directly.\n this.#render();\n // Bound on the root rather than the panes so the cycle continues once focus\n // has left the separator — the panes hold consumer markup and the next F6\n // arrives from inside one of them.\n this.#cycleAbort = new AbortController();\n this.element.addEventListener(\"keydown\", this.#onCycleKeydown, {\n signal: this.#cycleAbort.signal,\n });\n }\n\n /** Cancels any active pointer drag so listeners never leak past disconnect. */\n override disconnect(): void {\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n this.#cycleAbort?.abort();\n this.#cycleAbort = null;\n this.#repaint.cancel();\n this.#paneTabindex.returnAll();\n this.element.removeAttribute(\"data-dragging\");\n }\n\n /** Moves focus to the next pane on `F6`, wrapping; entering at the first one. */\n readonly #onCycleKeydown = (event: KeyboardEvent): void => {\n if (event.key !== \"F6\") return;\n // Japanese input methods bind F6 to a conversion, so a press that belongs to\n // a composition steers that conversion and must not move focus away from it.\n if (event.isComposing) return;\n // A descendant widget that already claimed the key must not ALSO cycle the\n // panes; this listener sees consumer markup, so composition depends on it.\n if (event.defaultPrevented) return;\n // The pattern binds a bare F6 only. A chorded one is the browser's or the\n // OS's (Ctrl+F6 cycles frames in several browsers).\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n\n const panes: HTMLElement[] = [];\n if (this.hasPrimaryTarget) panes.push(this.primaryTarget);\n if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);\n\n const target = event.target as Node | null;\n const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));\n // With no panes the modulo is NaN and the read is undefined, which is the\n // same \"nowhere to go\" this guard answers for a cycle that has ends.\n const next = panes[(current + 1) % panes.length];\n if (!next) return;\n\n event.preventDefault();\n this.#paneTabindex.lend(next);\n next.focus();\n };\n\n /** Re-renders when application code (or a Turbo morph) changes `value` at runtime. */\n valueValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `min` at runtime. */\n minValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `max` at runtime. */\n maxValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-publishes range and position onto a separator swapped in after connect. */\n separatorTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Starts active pointer drag tracking and locks capture. */\n onPointerDown(event: PointerEvent): void {\n if (!this.hasSeparatorTarget || event.button !== 0) return;\n\n event.preventDefault();\n\n const separator = this.separatorTarget;\n separator.setPointerCapture(event.pointerId);\n // preventDefault() above suppresses the implicit focus, so move focus\n // explicitly — otherwise keyboard (arrow) adjustments never reach the\n // separator after a pointer interaction (WCAG 2.1.1).\n separator.focus();\n\n this.element.setAttribute(\"data-dragging\", \"true\");\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n separator.addEventListener(\"pointermove\", this.#onPointerMove, { signal: abort.signal });\n separator.addEventListener(\"pointerup\", this.#onPointerUp, { signal: abort.signal });\n separator.addEventListener(\"pointercancel\", this.#onPointerUp, { signal: abort.signal });\n }\n\n /** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n if (!this.hasSeparatorTarget) return;\n\n const { min, max } = this.#range;\n const isVertical = this.#isVertical;\n\n let handled = true;\n let nextValue = this.#position;\n\n switch (event.key) {\n case \"ArrowLeft\":\n if (isVertical) {\n nextValue -= this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowRight\":\n if (isVertical) {\n nextValue += this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowUp\":\n if (!isVertical) {\n nextValue -= this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowDown\":\n if (!isVertical) {\n nextValue += this.#step;\n } else {\n handled = false;\n }\n break;\n case \"Home\":\n nextValue = min;\n break;\n case \"End\":\n nextValue = max;\n break;\n case \"Enter\":\n event.preventDefault();\n this.toggle();\n return;\n default:\n handled = false;\n break;\n }\n\n if (handled) {\n event.preventDefault();\n this.valueValue = Math.max(min, Math.min(nextValue, max));\n this.#render();\n this.#dispatchChange();\n }\n }\n\n /** Double-click or Enter to collapse the primary pane, or put it back. */\n toggle(): void {\n const { min, max } = this.#range;\n if (this.#position > min) {\n this.#valueBeforeCollapse = this.#position;\n this.valueValue = min;\n } else {\n // With nothing remembered — a controller that connected to an already\n // collapsed pane — there is no previous position, so open the pane fully.\n this.valueValue = this.#valueBeforeCollapse ?? max;\n this.#valueBeforeCollapse = null;\n }\n this.#render();\n this.#dispatchChange();\n }\n\n readonly #onPointerMove = (event: PointerEvent): void => {\n if (!this.hasSeparatorTarget) return;\n\n const rect = this.element.getBoundingClientRect();\n const raw = this.#isVertical\n ? (event.clientX - rect.left) / rect.width\n : (event.clientY - rect.top) / rect.height;\n\n // A container with no extent divides by zero; treat that as the near end\n // rather than letting it reach the published fraction.\n const fraction = Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0;\n const { min, max } = this.#range;\n\n this.valueValue = Math.max(min, Math.min(Math.round(fraction * 100), max));\n this.#render();\n };\n\n readonly #onPointerUp = (event: PointerEvent): void => {\n // Ending the drag must not depend on the separator still being there: a\n // target removed mid-drag would otherwise strand the hook and the listeners.\n this.element.removeAttribute(\"data-dragging\");\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.releasePointerCapture(event.pointerId);\n }\n\n this.#dispatchChange();\n };\n\n /**\n * Publishes the position: the fraction consumer CSS multiplies a pane by, and\n * the range assistive tech reads off the separator. The clamped position is\n * written back to the Value so it, ARIA, CSS, and dispatched events agree.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const { min, max } = this.#range;\n const position = this.#position;\n\n if (this.valueValue !== position) {\n this.valueValue = position;\n }\n\n this.element.style.setProperty(\"--stimeo--resizable-fraction\", String(position / 100));\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.setAttribute(\"aria-valuenow\", String(position));\n this.separatorTarget.setAttribute(\"aria-valuemin\", String(min));\n this.separatorTarget.setAttribute(\"aria-valuemax\", String(max));\n }\n }\n\n #dispatchChange(): void {\n const fraction = this.valueValue / 100;\n this.dispatch(\"change\", { detail: { value: this.valueValue, fraction } });\n }\n\n /** The declared range after validation; an unreadable bound uses its default. */\n get #range(): { readonly min: number; readonly max: number } {\n const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;\n const max = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;\n // A maximum below the minimum describes no range at all; collapsing it onto\n // the minimum keeps the position inside something that can be announced.\n return { min, max: Math.max(min, max) };\n }\n\n /** The declared position, clamped into the validated range. */\n get #position(): number {\n const { min, max } = this.#range;\n const value = Number.isFinite(this.valueValue) ? this.valueValue : DEFAULT_VALUE;\n return Math.max(min, Math.min(value, max));\n }\n\n /** The keyboard increment; a non-positive or unreadable one uses the default. */\n get #step(): number {\n return Number.isFinite(this.stepValue) && this.stepValue > 0 ? this.stepValue : DEFAULT_STEP;\n }\n\n /** Whether the divider runs vertically; a separator that does not say is horizontal. */\n get #isVertical(): boolean {\n return (\n this.hasSeparatorTarget &&\n this.separatorTarget.getAttribute(\"aria-orientation\") === \"vertical\"\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/before_cache_reset.ts","../../src/utils/tabindex_loan.ts","../../src/controllers/resizable_controller.ts"],"names":[],"mappings":"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","file":"resizable_controller.js","sourcesContent":["import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\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","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\n\n/**\n * Shared bookkeeping for a `tabindex` a controller lends an element temporarily.\n *\n * A controller that must move focus somewhere the author never made focusable\n * (a landmark root, a scroll destination) reaches for the same trick: add a\n * `tabindex` just-in-time and hand it back once it is no longer needed. The\n * borrow is the easy half; the return is what the two conditions below are for.\n *\n * **Returning needs two conditions, not one.** Owning the borrow is not enough:\n * the attribute must also still hold the value this instance wrote. A consumer\n * that changed it afterwards — `tabindex=\"0\"` to make the root its own Tab stop\n * — owns it now, and removing it there silently discards authored markup. The\n * bookkeeping is dropped either way, since the loan is over regardless of who\n * ends up owning the value.\n *\n * **Never borrow over an existing value.** An element that already carries a\n * `tabindex` is the author's to control, so there is nothing to lend and nothing\n * to return.\n *\n * Every live loan also owns a shared `turbo:before-cache` subscription. The loan\n * is returned before Turbo can copy it into a snapshot, so consumers get cache\n * safety without duplicating a lifecycle hook; `returnAll()` removes the\n * subscription again as soon as no loan remains.\n *\n * The registry is keyed by element, so a controller borrowing on a single\n * element (`this.element`) and one borrowing across a changing set of targets\n * use the same API — the single-element case is a set of one. It holds no\n * opinion about *when* to borrow or where focus goes next; that stays in the\n * controller.\n *\n * **The API is deliberately two methods.** This file's own doc block is dropped\n * from `dist`, but every member comment is inlined into **each** consumer entry\n * (`tsup` builds with `splitting: false`), so rationale belongs here, only the\n * contract belongs on the members, and every method no consumer calls is still\n * paid for once per consumer entry.\n *\n * @example\n * ```ts\n * readonly #tabindex = new TabindexLoan();\n *\n * #rescueFocus() {\n * this.#tabindex.lend(this.element);\n * this.element.focus();\n * }\n *\n * disconnect() {\n * this.#tabindex.returnAll();\n * }\n * ```\n */\nexport class TabindexLoan<T extends HTMLElement = HTMLElement> {\n readonly #value: string;\n readonly #lent = new Set<T>();\n /** Returns live loans before Turbo can copy them into its page snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.returnAll());\n\n /**\n * @param value - the `tabindex` to lend. `\"-1\"` (the default) is\n * programmatically focusable but not a Tab stop; `\"0\"` is a real Tab stop,\n * which a scroll region with no focusable content of its own needs.\n */\n constructor(value: string = \"-1\") {\n this.#value = value;\n }\n\n /** Lends `element` the value; no-ops when it already carries a `tabindex`. */\n lend(element: T): void {\n if (element.hasAttribute(\"tabindex\")) return;\n element.setAttribute(\"tabindex\", this.#value);\n this.#lent.add(element);\n // Subscribe only while a real loan exists. Keeping this guarantee here means\n // every consumer is Turbo-safe without another lifecycle hook to remember.\n this.#beforeCache.activate();\n }\n\n /** Takes back every loan whose value is still the one that was lent. */\n returnAll(): void {\n for (const element of this.#lent) {\n if (element.getAttribute(\"tabindex\") === this.#value) element.removeAttribute(\"tabindex\");\n }\n this.#lent.clear();\n this.#beforeCache.deactivate();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\n\n/** Value defaults, reused when a declaration cannot be read as a percentage. */\nconst DEFAULT_MIN = 0;\nconst DEFAULT_MAX = 100;\nconst DEFAULT_VALUE = 50;\nconst DEFAULT_STEP = 1;\n\n/**\n * Headless, highly accessible Window Splitter / Resizable panes logic.\n *\n * Markup contract (identifier: `stimeo--resizable`):\n * <div data-controller=\"stimeo--resizable\"\n * data-stimeo--resizable-min-value=\"20\"\n * data-stimeo--resizable-max-value=\"80\"\n * data-stimeo--resizable-step-value=\"1\"\n * data-stimeo--resizable-value-value=\"50\">\n * <div data-stimeo--resizable-target=\"primary\" id=\"pane-1\">Pane A</div>\n * <div role=\"separator\" tabindex=\"0\" aria-orientation=\"vertical\"\n * aria-controls=\"pane-1\" aria-valuemin=\"20\" aria-valuemax=\"80\"\n * aria-valuenow=\"50\" aria-label=\"Resize splitter\"\n * data-stimeo--resizable-target=\"separator\"\n * data-action=\"pointerdown->stimeo--resizable#onPointerDown\n * keydown->stimeo--resizable#onKeydown\"></div>\n * <div data-stimeo--resizable-target=\"secondary\">Pane B</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Window Splitter** pattern:\n * - Robust dragging track using W3C `setPointerCapture` to ensure uninterrupted dragging\n * even when pointers quickly stray outside the separator area.\n * - Arrow keys move the divider by `step` percent, along the axis\n * `aria-orientation` names; `Home` / `End` jump to the ends of the range.\n * - `Enter` collapses the primary pane to the minimum and puts it back where it\n * was, which is what the pattern means by restoring a previous position.\n * - Root-level CSS custom property `--stimeo--resizable-fraction` (0..1) driving\n * presentation styles.\n * - `F6` cycles focus through the panes, which the pattern lists as optional.\n *\n * `change` dispatches `{ value: number, fraction: number }`.\n *\n * @remarks\n * Behavior only. The controller adjusts the CSS custom property on the root element,\n * updates ARIA values on the separator, and emits `stimeo--resizable:change` events.\n *\n * Both the range and the position are percentages. A bound that is not a finite\n * number falls back to its Value default, a maximum below the minimum collapses\n * onto that minimum, and a step that is not a positive finite number falls back\n * to `1` — so an unreadable declaration narrows what the widget can do without\n * ever publishing `NaN` to CSS or to assistive tech.\n *\n * `aria-orientation` is the author's, and its absence means `horizontal` — the\n * value ARIA gives a separator that does not say. Reading any other default here\n * would make the announced axis and the answering arrow keys disagree.\n */\nexport class ResizableController extends Controller<HTMLElement> {\n static override targets = [\"primary\", \"secondary\", \"separator\"];\n static override values = {\n min: { type: Number, default: DEFAULT_MIN },\n max: { type: Number, default: DEFAULT_MAX },\n step: { type: Number, default: DEFAULT_STEP },\n value: { type: Number, default: DEFAULT_VALUE },\n };\n static actions = [\"onKeydown\", \"onPointerDown\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly primaryTarget: HTMLElement;\n declare readonly secondaryTarget: HTMLElement;\n declare readonly separatorTarget: HTMLElement;\n declare readonly hasPrimaryTarget: boolean;\n declare readonly hasSecondaryTarget: boolean;\n declare readonly hasSeparatorTarget: boolean;\n\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare valueValue: number;\n\n /** Where the divider sat before the current collapse; `null` when unknown. */\n #valueBeforeCollapse: number | null = null;\n\n /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */\n #dragAbort: AbortController | null = null;\n\n /** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */\n readonly #paneTabindex = new TabindexLoan();\n\n /** Releases the pane cycle listener bound in {@link connect}. */\n #cycleAbort: AbortController | null = null;\n\n /** Folds the range and position inputs of one morph batch into a single paint. */\n readonly #repaint = new MicrotaskCoalescer(() => this.#render());\n\n override connect(): void {\n // A drag cannot outlive a navigation, so a hook captured mid-drag is stale on\n // arrival. Clearing is unconditional here because disconnect() aborts the\n // drag, leaving no session that an in-page move could carry over.\n this.element.removeAttribute(\"data-dragging\");\n this.#repaint.activate();\n // Stimulus fires the value callbacks before connect, where the coalescer\n // ignores them, so the first paint has to be asked for directly.\n this.#render();\n // Bound on the root rather than the panes so the cycle continues once focus\n // has left the separator — the panes hold consumer markup and the next F6\n // arrives from inside one of them.\n this.#cycleAbort = new AbortController();\n this.element.addEventListener(\"keydown\", this.#onCycleKeydown, {\n signal: this.#cycleAbort.signal,\n });\n }\n\n /** Cancels any active pointer drag so listeners never leak past disconnect. */\n override disconnect(): void {\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n this.#cycleAbort?.abort();\n this.#cycleAbort = null;\n this.#repaint.cancel();\n this.#paneTabindex.returnAll();\n this.element.removeAttribute(\"data-dragging\");\n }\n\n /** Moves focus to the next pane on `F6`, wrapping; entering at the first one. */\n readonly #onCycleKeydown = (event: KeyboardEvent): void => {\n if (event.key !== \"F6\") return;\n // Japanese input methods bind F6 to a conversion, so a press that belongs to\n // a composition steers that conversion and must not move focus away from it.\n if (event.isComposing) return;\n // A descendant widget that already claimed the key must not ALSO cycle the\n // panes; this listener sees consumer markup, so composition depends on it.\n if (event.defaultPrevented) return;\n // The pattern binds a bare F6 only. A chorded one is the browser's or the\n // OS's (Ctrl+F6 cycles frames in several browsers).\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n\n const panes: HTMLElement[] = [];\n if (this.hasPrimaryTarget) panes.push(this.primaryTarget);\n if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);\n\n const target = event.target as Node | null;\n const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));\n // With no panes the modulo is NaN and the read is undefined, which is the\n // same \"nowhere to go\" this guard answers for a cycle that has ends.\n const next = panes[(current + 1) % panes.length];\n if (!next) return;\n\n event.preventDefault();\n this.#paneTabindex.lend(next);\n next.focus();\n };\n\n /** Re-renders when application code (or a Turbo morph) changes `value` at runtime. */\n valueValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `min` at runtime. */\n minValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `max` at runtime. */\n maxValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-publishes range and position onto a separator swapped in after connect. */\n separatorTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Starts active pointer drag tracking and locks capture. */\n onPointerDown(event: PointerEvent): void {\n if (!this.hasSeparatorTarget || event.button !== 0) return;\n\n event.preventDefault();\n\n const separator = this.separatorTarget;\n separator.setPointerCapture(event.pointerId);\n // preventDefault() above suppresses the implicit focus, so move focus\n // explicitly — otherwise keyboard (arrow) adjustments never reach the\n // separator after a pointer interaction (WCAG 2.1.1).\n separator.focus();\n\n this.element.setAttribute(\"data-dragging\", \"true\");\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n separator.addEventListener(\"pointermove\", this.#onPointerMove, { signal: abort.signal });\n separator.addEventListener(\"pointerup\", this.#onPointerUp, { signal: abort.signal });\n separator.addEventListener(\"pointercancel\", this.#onPointerUp, { signal: abort.signal });\n }\n\n /** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n if (!this.hasSeparatorTarget) return;\n\n const { min, max } = this.#range;\n const isVertical = this.#isVertical;\n\n let handled = true;\n let nextValue = this.#position;\n\n switch (event.key) {\n case \"ArrowLeft\":\n if (isVertical) {\n nextValue -= this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowRight\":\n if (isVertical) {\n nextValue += this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowUp\":\n if (!isVertical) {\n nextValue -= this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowDown\":\n if (!isVertical) {\n nextValue += this.#step;\n } else {\n handled = false;\n }\n break;\n case \"Home\":\n nextValue = min;\n break;\n case \"End\":\n nextValue = max;\n break;\n case \"Enter\":\n event.preventDefault();\n this.toggle();\n return;\n default:\n handled = false;\n break;\n }\n\n if (handled) {\n event.preventDefault();\n this.valueValue = Math.max(min, Math.min(nextValue, max));\n this.#render();\n this.#dispatchChange();\n }\n }\n\n /** Collapses the primary pane to its minimum, or returns it to the last position. */\n toggle(): void {\n const { min, max } = this.#range;\n if (this.#position > min) {\n this.#valueBeforeCollapse = this.#position;\n this.valueValue = min;\n } else {\n // With nothing remembered — a controller that connected to an already\n // collapsed pane — there is no previous position, so open the pane fully.\n this.valueValue = this.#valueBeforeCollapse ?? max;\n this.#valueBeforeCollapse = null;\n }\n this.#render();\n this.#dispatchChange();\n }\n\n readonly #onPointerMove = (event: PointerEvent): void => {\n if (!this.hasSeparatorTarget) return;\n\n const rect = this.element.getBoundingClientRect();\n const raw = this.#isVertical\n ? (event.clientX - rect.left) / rect.width\n : (event.clientY - rect.top) / rect.height;\n\n // A container with no extent divides by zero; treat that as the near end\n // rather than letting it reach the published fraction.\n const fraction = Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0;\n const { min, max } = this.#range;\n\n this.valueValue = Math.max(min, Math.min(Math.round(fraction * 100), max));\n this.#render();\n };\n\n readonly #onPointerUp = (event: PointerEvent): void => {\n // Ending the drag must not depend on the separator still being there: a\n // target removed mid-drag would otherwise strand the hook and the listeners.\n this.element.removeAttribute(\"data-dragging\");\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.releasePointerCapture(event.pointerId);\n }\n\n this.#dispatchChange();\n };\n\n /**\n * Publishes the position: the fraction consumer CSS multiplies a pane by, and\n * the range assistive tech reads off the separator. The clamped position is\n * written back to the Value so it, ARIA, CSS, and dispatched events agree.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const { min, max } = this.#range;\n const position = this.#position;\n\n if (this.valueValue !== position) {\n this.valueValue = position;\n }\n\n this.element.style.setProperty(\"--stimeo--resizable-fraction\", String(position / 100));\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.setAttribute(\"aria-valuenow\", String(position));\n this.separatorTarget.setAttribute(\"aria-valuemin\", String(min));\n this.separatorTarget.setAttribute(\"aria-valuemax\", String(max));\n }\n }\n\n #dispatchChange(): void {\n const fraction = this.valueValue / 100;\n this.dispatch(\"change\", { detail: { value: this.valueValue, fraction } });\n }\n\n /** The declared range after validation; an unreadable bound uses its default. */\n get #range(): { readonly min: number; readonly max: number } {\n const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;\n const max = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;\n // A maximum below the minimum describes no range at all; collapsing it onto\n // the minimum keeps the position inside something that can be announced.\n return { min, max: Math.max(min, max) };\n }\n\n /** The declared position, clamped into the validated range. */\n get #position(): number {\n const { min, max } = this.#range;\n const value = Number.isFinite(this.valueValue) ? this.valueValue : DEFAULT_VALUE;\n return Math.max(min, Math.min(value, max));\n }\n\n /** The keyboard increment; a non-positive or unreadable one uses the default. */\n get #step(): number {\n return Number.isFinite(this.stepValue) && this.stepValue > 0 ? this.stepValue : DEFAULT_STEP;\n }\n\n /** Whether the divider runs vertically; a separator that does not say is horizontal. */\n get #isVertical(): boolean {\n return (\n this.hasSeparatorTarget &&\n this.separatorTarget.getAttribute(\"aria-orientation\") === \"vertical\"\n );\n }\n}\n"]}
|
|
@@ -23,7 +23,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
23
23
|
* are **delegated on the container** — `keydown` for movement and `focusin` to
|
|
24
24
|
* sync the tab stop when focus arrives by click or programmatically — so
|
|
25
25
|
* dynamically added/removed items need no per-item `data-action`. It emits
|
|
26
|
-
* `change`
|
|
26
|
+
* `change` when a key press or an incoming focus moves the tabbable item; connect
|
|
27
|
+
* and target reconciliation re-establish the tab stop silently.
|
|
27
28
|
*
|
|
28
29
|
* `change` dispatches `{ index, item }`.
|
|
29
30
|
*
|