stimeo-ui 0.12.0 → 0.14.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 +71 -0
- package/dist/cable/index.d.ts +68 -14
- package/dist/cable/index.js +438 -152
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +1 -1
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +1 -1
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +1 -1
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +1 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +4 -3
- package/dist/controllers/calendar_controller.js +34 -2
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.js +9 -7
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +2 -2
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +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/command_palette_controller.d.ts +3 -3
- package/dist/controllers/command_palette_controller.js +2 -2
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js +7 -1
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +2 -2
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +1 -1
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +1 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.js +68 -23
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +1 -1
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +3 -3
- package/dist/controllers/date_range_picker_controller.js +34 -10
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +3 -3
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.js +21 -10
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +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 +2 -2
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +2 -2
- package/dist/controllers/dropdown_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 +2 -2
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +7 -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.d.ts +3 -3
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +1 -1
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +1 -1
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +3 -2
- package/dist/controllers/input_mask_controller.js +33 -12
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +10 -5
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +10 -5
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +4 -3
- package/dist/controllers/local_time_controller.js +34 -7
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +2 -2
- package/dist/controllers/masonry_controller.js +1 -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 +10 -2
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +2 -2
- package/dist/controllers/menubar_controller.js +10 -2
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +1 -1
- package/dist/controllers/navigation_menu_controller.js +8 -2
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.d.ts +23 -10
- package/dist/controllers/optimistic_controller.js +141 -20
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js +23 -10
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +1 -1
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +10 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +1 -1
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +2 -1
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +10 -3
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +1 -1
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +2 -2
- package/dist/controllers/portal_controller.js +21 -10
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +1 -1
- package/dist/controllers/radio_group_controller.js +11 -3
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +1 -1
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +3 -3
- package/dist/controllers/relative_time_controller.js +31 -5
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_before_cache_controller.js +23 -10
- package/dist/controllers/reset_before_cache_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.js +7 -2
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +1 -1
- package/dist/controllers/roving_controller.js +7 -3
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +1 -1
- package/dist/controllers/scroll_visibility_controller.js +22 -13
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +1 -1
- package/dist/controllers/scrollspy_controller.js +11 -6
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +1 -1
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -2
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.js +23 -12
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +3 -3
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.js +10 -5
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +3 -3
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +1 -1
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +1 -1
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +5 -5
- package/dist/controllers/theme_controller.js +23 -13
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/toast_controller.js +1 -1
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js +11 -3
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +1 -1
- package/dist/controllers/toolbar_controller.js +7 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +1 -1
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +1 -1
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +4 -3
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +259 -202
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +39 -2
- package/dist/inspector/cli.js +81 -8
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +104 -8
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +5 -5
- package/dist/inspector/manifest.json +132 -3
- package/package.json +9 -11
|
@@ -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.\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/intl_format.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 * Construction of `Intl` formatters for the controllers that take a locale, and\n * the options that go with it, from a declaration.\n *\n * Every `Intl` constructor resolves locale data on the way in, which makes it\n * too costly to rebuild on a hot path, and rejects a malformed language tag,\n * currency code, time zone or digit count with a `RangeError`. Both concerns are\n * handled here once. A formatter is built at most once per distinct request and\n * shared by every caller that asks for the same one. A `RangeError` is caught\n * and answered the way the caller asked: by rebuilding under a fallback locale,\n * or by returning `null` so the caller can leave its authored text alone. Any\n * other failure is a programming fault and propagates.\n */\n\n/** The constructor shape the `Intl` formatter classes share. */\nexport interface IntlFormatterConstructor<F, O extends object> {\n new (locales?: string | string[], options?: O): F;\n}\n\n/** Built formatters, and refusals, per constructor; see `cacheKey` for the key. */\nconst formatters = new WeakMap<object, Map<string, unknown>>();\n\n/**\n * A formatter for `locale` with `options`.\n *\n * With `fallbackLocale`, a `RangeError` from the constructor is answered by\n * building the same options under the fallback instead, so the result is never\n * `null`; the fallback's own failure propagates. Without it, a `RangeError`\n * yields `null` and the caller decides what to show. Errors of any other kind\n * propagate in both forms.\n *\n * Results are cached per constructor and `(locale, options, fallback)`, `null`\n * included, so asking again on every render costs one map lookup.\n */\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale: string,\n): F;\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n): F | null;\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale?: string,\n): F | null {\n const cache = cacheFor(factory);\n const key = cacheKey(locale, options, fallbackLocale);\n if (cache.has(key)) return cache.get(key) as F | null;\n const formatter = construct(factory, locale, options, fallbackLocale);\n cache.set(key, formatter);\n return formatter;\n}\n\n/** The cache for one constructor, created on first use. */\nfunction cacheFor(factory: object): Map<string, unknown> {\n const existing = formatters.get(factory);\n if (existing) return existing;\n const created = new Map<string, unknown>();\n formatters.set(factory, created);\n return created;\n}\n\n/**\n * One key per distinct request. `JSON.stringify` drops an option left\n * `undefined`, which is how the constructor reads it too. Two spellings of the\n * same options differing only in key order key separately; that costs one extra\n * formatter, never a wrong one, and call sites pass a fixed literal anyway.\n */\nfunction cacheKey(\n locale: string | undefined,\n options: object,\n fallbackLocale: string | undefined,\n): string {\n return JSON.stringify([locale, options, fallbackLocale]);\n}\n\n/** Builds the formatter under the `RangeError` contract described on `intlFormatter`. */\nfunction construct<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale: string | undefined,\n): F | null {\n try {\n return new factory(locale, options);\n } catch (error) {\n if (!(error instanceof RangeError)) throw error;\n if (fallbackLocale === undefined) return null;\n return new factory(fallbackLocale, options);\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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { intlFormatter } from \"../utils/intl_format\";\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/** The relative phrasing every pass asks for (\"yesterday\", not \"1 day ago\"). */\nconst RELATIVE_OPTIONS: Intl.RelativeTimeFormatOptions = { numeric: \"auto\" };\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 `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 * (`MicrotaskCoalescer`) rather than leaving the reading frozen. The authored\n * absolute text goes back on the element via `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 // 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 intlFormatter(Intl.RelativeTimeFormat, this.#locale, RELATIVE_OPTIONS);\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,5 +1,27 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
// src/controllers/reset_before_cache_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/declared_value.ts
|
|
6
|
+
function parseDeclared(raw, parse, fallback) {
|
|
7
|
+
try {
|
|
8
|
+
return parse(raw);
|
|
9
|
+
} catch {
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function validSelector(element, raw, fallback) {
|
|
14
|
+
if (raw.length === 0) return fallback;
|
|
15
|
+
return parseDeclared(
|
|
16
|
+
raw,
|
|
17
|
+
(selector) => {
|
|
18
|
+
element.matches(selector);
|
|
19
|
+
return selector;
|
|
20
|
+
},
|
|
21
|
+
fallback
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
3
25
|
// src/controllers/reset_before_cache_controller.ts
|
|
4
26
|
var STATELESS_INPUT_TYPES = /* @__PURE__ */ new Set(["hidden", "submit", "reset", "button", "image"]);
|
|
5
27
|
function restoreField(element) {
|
|
@@ -43,16 +65,7 @@ var ResetBeforeCacheController = class extends Controller {
|
|
|
43
65
|
* silently taking the sweep down.
|
|
44
66
|
*/
|
|
45
67
|
scopeValueChanged() {
|
|
46
|
-
|
|
47
|
-
if (selector.length > 0) {
|
|
48
|
-
try {
|
|
49
|
-
this.element.matches(selector);
|
|
50
|
-
this.#scopeSelector = selector;
|
|
51
|
-
return;
|
|
52
|
-
} catch {
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
this.#scopeSelector = "";
|
|
68
|
+
this.#scopeSelector = validSelector(this.element, this.scopeValue, "");
|
|
56
69
|
}
|
|
57
70
|
connect() {
|
|
58
71
|
document.addEventListener("turbo:before-cache", this.#onBeforeCache);
|
|
@@ -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** — 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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/declared_value.ts","../../src/controllers/reset_before_cache_controller.ts"],"names":[],"mappings":"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","file":"reset_before_cache_controller.js","sourcesContent":["/**\n * Readers for the String Values whose text can only be checked by handing it to\n * a parser: a CSS selector, a regular-expression source, a JSON object.\n *\n * Stimulus reads each of them as an ordinary string, so the controller connects\n * and the flaw surfaces later, inside the first handler that consumes the value\n * (a `SyntaxError` from `new RegExp`, a `DOMException` from the selector engine),\n * where it takes the whole handler down on every event. Parsing here, once, in\n * the `<name>ValueChanged` callback keeps the failure local to the declaration:\n * an unreadable value falls back to its default, the element stays alive, and\n * the hot path only ever receives a value that parsed.\n *\n * Which default a broken declaration falls back to is the caller's contract, so\n * every reader takes (or returns) the fallback rather than choosing one.\n */\n\n/**\n * The result of `parse(raw)`, or `fallback` when `parse` throws.\n *\n * Any exception counts as \"does not parse\": the parsers these Values feed\n * (`RegExp`, `JSON.parse`, the selector engine) all report a malformed input by\n * throwing, and none of them throws for another reason.\n */\nexport function parseDeclared<T>(raw: string, parse: (raw: string) => T, fallback: T): T {\n try {\n return parse(raw);\n } catch {\n return fallback;\n }\n}\n\n/** What {@link validSelector} needs of the element it probes with. */\nexport interface SelectorProbe {\n matches(selector: string): boolean;\n}\n\n/**\n * `raw` when it is a non-empty selector the DOM accepts, otherwise `fallback`.\n *\n * `element.matches` is the probe: the engine parses the selector and throws for\n * one it cannot read. Whether the selector actually matches `element` plays no\n * part, so a selector aimed at another element still passes.\n *\n * The parameter names the one method the probe uses rather than `Element`, so\n * this module carries no DOM type and the readers below stay importable from\n * the Node-side Inspector, which checks the same declarations statically.\n */\nexport function validSelector(element: SelectorProbe, raw: string, fallback: string): string {\n if (raw.length === 0) return fallback;\n return parseDeclared(\n raw,\n (selector) => {\n element.matches(selector);\n return selector;\n },\n fallback,\n );\n}\n\n/** How `compileRegExp` wraps a source before compiling it. */\nexport type RegExpAnchor = \"none\" | \"exact\";\n\n/**\n * `source` compiled as a `RegExp`, or `null` when it does not compile.\n *\n * `\"exact\"` wraps the source as `^(?:source)$`, so the whole input has to match\n * and an alternation inside the source cannot escape the anchors. The source is\n * compiled on its own first, because an unbalanced source can be made to parse\n * by the wrapper's own parentheses — `0)|(1` becomes `^(?:0)|(1)$` — which would\n * accept a declaration that is not a regular expression and leave half of it\n * outside the anchors. The default that replaces a broken source is the\n * caller's, so `null` is returned rather than a fallback pattern.\n */\nexport function compileRegExp(source: string, anchor: RegExpAnchor = \"none\"): RegExp | null {\n return parseDeclared(\n source,\n (text) => {\n const bare = new RegExp(text);\n return anchor === \"exact\" ? new RegExp(`^(?:${text})$`) : bare;\n },\n null,\n );\n}\n\n/**\n * The JSON object `raw` declares, or `null` when the text does not parse or\n * parses to something other than a plain object (`null`, an array, a scalar).\n * Values are returned as parsed; narrowing them is the caller's contract.\n */\nexport function parseJsonObject(raw: string): Record<string, unknown> | null {\n const parsed = parseDeclared<unknown>(raw, (text) => JSON.parse(text), null);\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n return parsed as Record<string, unknown>;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { validSelector } from \"../utils/declared_value\";\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 this.#scopeSelector = validSelector(this.element, this.scopeValue, \"\");\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"]}
|
|
@@ -8,6 +8,12 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
8
8
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// src/utils/event_owner.ts
|
|
12
|
+
function ownerIndex(candidates, node) {
|
|
13
|
+
if (!(node instanceof Node)) return -1;
|
|
14
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
// src/utils/microtask_coalescer.ts
|
|
12
18
|
var MicrotaskCoalescer = class {
|
|
13
19
|
#run;
|
|
@@ -154,8 +160,7 @@ var ResizableController = class extends Controller {
|
|
|
154
160
|
const panes = [];
|
|
155
161
|
if (this.hasPrimaryTarget) panes.push(this.primaryTarget);
|
|
156
162
|
if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);
|
|
157
|
-
const
|
|
158
|
-
const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));
|
|
163
|
+
const current = ownerIndex(panes, event.target);
|
|
159
164
|
const next = panes[(current + 1) % panes.length];
|
|
160
165
|
if (!next) return;
|
|
161
166
|
event.preventDefault();
|
|
@@ -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/**\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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/event_owner.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 * Resolves which of a controller's elements owns a node.\n *\n * A delegated listener hears events from a whole subtree, so the handler's first\n * job is almost always the same question: which item, handle, or control does\n * this `event.target` belong to? The same question comes up for a\n * `MutationRecord.target`, for `document.activeElement`, and for a\n * `relatedTarget` on the way out of a hover region.\n *\n * The answer is `Node.contains()`, which is **inclusive** — an element contains\n * itself — so testing the candidate for identity as well would be redundant.\n *\n * **The guard is what makes this safe to call with a raw event target.**\n * `event.target` is typed `EventTarget | null`, and `contains()` takes a `Node?`:\n * browsers throw `TypeError` for anything else, and `window` — the everyday\n * `EventTarget` that is not a `Node` — is what an event dispatched at it carries.\n * Narrowing here means a caller never has to cast, and the rule cannot drift\n * between the places that ask the question.\n *\n * Scope stays with the caller. These helpers say *which candidate owns the node*,\n * not *whether the node belongs to this controller at all* — a component with\n * nested instances of itself decides that first (by comparing the closest\n * annotated ancestor) and passes the candidates it owns.\n */\n\n/**\n * Index in `candidates` of the first one that is, or contains, `node`.\n *\n * `-1` when none does, when `node` is absent, and when it is an `EventTarget`\n * that is not a `Node`. Candidates are tested in array order, so a nested pair\n * resolves to whichever the caller listed first.\n */\nexport function ownerIndex<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): number {\n if (!(node instanceof Node)) return -1;\n return candidates.findIndex((candidate) => candidate.contains(node));\n}\n\n/**\n * The first candidate that is, or contains, `node`; `null` when none does.\n *\n * A miss indexes the array at `-1`, which reads as `undefined` and lands on the\n * same `null` the absent cases produce.\n */\nexport function ownerOf<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): T | null {\n return candidates[ownerIndex(candidates, node)] ?? null;\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 { ownerIndex } from \"../utils/event_owner\";\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 current = ownerIndex(panes, event.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"]}
|
|
@@ -4,7 +4,7 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
4
4
|
* Headless **roving tabindex**: makes a set of `item`s a single Tab stop and
|
|
5
5
|
* moves focus between them with the arrow keys — the APG roving-tabindex
|
|
6
6
|
* technique, surfaced as a standalone controller. It is the policy layer over the
|
|
7
|
-
* shared
|
|
7
|
+
* shared `RovingTabindex` util (the same split `stimeo--focus` makes over its trap),
|
|
8
8
|
* giving the orientation / wrap / Home-End the util deliberately leaves out. No
|
|
9
9
|
* dedicated APG pattern; it is the keyboard primitive Toolbar / Menu / Radio Group
|
|
10
10
|
* and friends build on. Core (zero dependencies).
|
|
@@ -16,6 +16,12 @@ function hasModifierChord(event) {
|
|
|
16
16
|
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// src/utils/event_owner.ts
|
|
20
|
+
function ownerIndex(candidates, node) {
|
|
21
|
+
if (!(node instanceof Node)) return -1;
|
|
22
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
// src/utils/focus_candidate.ts
|
|
20
26
|
function inheritsFieldsetDisabled(control) {
|
|
21
27
|
let fieldset = control.closest("fieldset[disabled]");
|
|
@@ -182,9 +188,7 @@ var RovingController = class extends Controller {
|
|
|
182
188
|
};
|
|
183
189
|
/** Resolves the item index owning an event target (the item or a descendant). */
|
|
184
190
|
#indexOf(target) {
|
|
185
|
-
|
|
186
|
-
if (!node) return -1;
|
|
187
|
-
return this.itemTargets.findIndex((item) => item === node || item.contains(node));
|
|
191
|
+
return ownerIndex(this.itemTargets, target);
|
|
188
192
|
}
|
|
189
193
|
/** Makes `index` the tab stop (optionally focusing it), emitting `change` once. */
|
|
190
194
|
#activate(index, focus) {
|