stimeo-ui 0.3.0 → 0.5.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 +124 -0
- package/dist/controllers/alert_dialog_controller.js +32 -5
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +32 -6
- package/dist/controllers/announcer_controller.js +255 -20
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.d.ts +2 -2
- package/dist/controllers/aspect_ratio_controller.js +1 -1
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +5 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.js +5 -1
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js +8 -3
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +4 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +4 -3
- package/dist/controllers/color_picker_controller.js +52 -2
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js +32 -5
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +32 -5
- 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 +2 -2
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +24 -8
- package/dist/controllers/countdown_controller.js +117 -13
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +4 -1
- package/dist/controllers/date_range_picker_controller.js +55 -1
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +32 -5
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +1 -1
- package/dist/controllers/direct_upload_controller.js +3 -3
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js +32 -5
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +36 -11
- package/dist/controllers/empty_state_controller.js +128 -23
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +25 -5
- package/dist/controllers/flash_controller.js +161 -21
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.js +32 -5
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +8 -2
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +31 -3
- package/dist/controllers/frame_loading_controller.js +261 -27
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +8 -4
- package/dist/controllers/highlight_controller.js +38 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +2 -1
- package/dist/controllers/idle_controller.js +13 -2
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +19 -3
- package/dist/controllers/local_time_controller.js +102 -6
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +1 -1
- package/dist/controllers/masonry_controller.js +1 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +22 -2
- package/dist/controllers/meter_controller.js +147 -26
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +25 -11
- package/dist/controllers/network_status_controller.js +29 -11
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +8 -0
- package/dist/controllers/number_input_controller.js +191 -24
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +34 -7
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js +5 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +1 -1
- package/dist/controllers/password_strength_controller.js +1 -1
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +10 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/portal_controller.js +10 -0
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +17 -2
- package/dist/controllers/progress_controller.js +123 -12
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +27 -1
- package/dist/controllers/range_slider_controller.js +449 -93
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +4 -1
- package/dist/controllers/rating_controller.js +55 -0
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +13 -0
- package/dist/controllers/relative_time_controller.js +135 -12
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +1 -1
- package/dist/controllers/scroll_area_controller.js +1 -1
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js +13 -17
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +1 -11
- package/dist/controllers/sidebar_controller.js +37 -8
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +7 -2
- package/dist/controllers/skeleton_controller.js +143 -22
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +17 -1
- package/dist/controllers/slider_controller.js +342 -50
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +50 -12
- package/dist/controllers/spinner_controller.js +244 -28
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +13 -2
- package/dist/controllers/step_indicator_controller.js +85 -6
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.js +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +26 -2
- package/dist/controllers/stick_to_bottom_controller.js +60 -10
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -8
- package/dist/controllers/switch_controller.js +162 -18
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +3 -0
- package/dist/controllers/time_picker_controller.js +6 -3
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +1 -2
- package/dist/controllers/tree_view_controller.js +19 -1
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +2278 -596
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +76 -1
- package/dist/inspector/cli.js +937 -76
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +997 -78
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +20 -20
- package/dist/inspector/manifest.json +428 -33
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/network_status_controller.ts"],"names":[],"mappings":"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","file":"network_status_controller.js","sourcesContent":["/**\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 { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless online/offline banner behavior built on the live-region practice (no\n * dedicated APG pattern).\n *\n * Markup contract (identifier: `stimeo--network-status`):\n * <div data-controller=\"stimeo--network-status\">\n * <div role=\"alert\" hidden data-stimeo--network-status-target=\"offline\">\n * You are offline.\n * </div>\n * <div role=\"status\" hidden data-stimeo--network-status-target=\"online\">\n * Back online.\n * </div>\n * </div>\n *\n * Reads `navigator.onLine` on connect and subscribes to the `window`\n * `online`/`offline` events, toggling the matching banner. The offline banner is\n * `role=\"alert\"` (assertive) because losing connectivity is urgent; the recovery\n * banner is `role=\"status\"` (polite).\n *\n * @remarks\n * Behavior only. `navigator.onLine` is the browser's *guess* — it does not\n * guarantee server reachability, which stays the consumer's job. To make the\n * announcement reliable across assistive tech (merely un-hiding a static banner\n * is flaky), the controller re-writes the banner's text on each transition,\n * guarded so an unchanged state never re-announces. The event listeners and the\n * auto-hide timer are removed/cleared on `disconnect()` (Turbo included).\n */\nexport class NetworkStatusController extends Controller<HTMLElement> {\n static override targets = [\"offline\", \"online\"];\n static override values = {\n onlineAutoHide: { type: Number, default: 0 },\n };\n static events = [\"change\"] as const;\n\n declare readonly offlineTarget: HTMLElement;\n declare readonly onlineTarget: HTMLElement;\n declare readonly hasOfflineTarget: boolean;\n declare readonly hasOnlineTarget: boolean;\n\n declare onlineAutoHideValue: number;\n\n readonly #timers = new SafeTimeout();\n\n /** Last known connectivity; guards against duplicate-state re-announcements. */\n #online = true;\n /** Banner text captured from the markup so transitions can re-write it. */\n #offlineMessage = \"\";\n #onlineMessage = \"\";\n\n readonly #handleOnline = (): void => this.#update(true);\n readonly #handleOffline = (): void => this.#update(false);\n\n override connect(): void {\n this.#offlineMessage = this.hasOfflineTarget\n ? (this.offlineTarget.textContent ?? \"\").trim()\n : \"\";\n this.#onlineMessage = this.hasOnlineTarget ? (this.onlineTarget.textContent ?? \"\").trim() : \"\";\n\n // Normalize initial visibility so a missing `hidden` in the markup cannot\n // strand a stale banner (e.g. an offline notice showing while online).\n if (this.hasOfflineTarget) this.offlineTarget.hidden = true;\n if (this.hasOnlineTarget) this.onlineTarget.hidden = true;\n\n this.#online = navigator.onLine;\n // On connect, surface only the offline state; do not flash a \"back online\"\n // banner just because the page loaded while connected.\n this.element.setAttribute(\"data-state\", this.#online ? \"online\" : \"offline\");\n if (!this.#online) this.#showOffline();\n\n window.addEventListener(\"online\", this.#handleOnline);\n window.addEventListener(\"offline\", this.#handleOffline);\n }\n\n override disconnect(): void {\n window.removeEventListener(\"online\", this.#handleOnline);\n window.removeEventListener(\"offline\", this.#handleOffline);\n this.#timers.clearAll();\n }\n\n /** Applies a connectivity transition, guarded against duplicate states. */\n #update(online: boolean): void {\n if (online === this.#online) return;\n this.#online = online;\n this.element.setAttribute(\"data-state\", online ? \"online\" : \"offline\");\n if (online) {\n this.#showOnline();\n } else {\n this.#showOffline();\n }\n this.dispatch(\"change\", { detail: { online } });\n }\n\n /** Shows the offline banner and hides the recovery banner. */\n #showOffline(): void {\n this.#timers.clearAll();\n if (this.hasOnlineTarget) this.onlineTarget.hidden = true;\n if (this.hasOfflineTarget) {\n this.offlineTarget.textContent = this.#offlineMessage;\n this.offlineTarget.hidden = false;\n }\n }\n\n /** Shows the recovery banner, optionally auto-hiding it after `onlineAutoHide`. */\n #showOnline(): void {\n if (this.hasOfflineTarget) this.offlineTarget.hidden = true;\n if (!this.hasOnlineTarget) return;\n this.onlineTarget.textContent = this.#onlineMessage;\n this.onlineTarget.hidden = false;\n if (this.onlineAutoHideValue > 0) {\n this.#timers.set(() => {\n this.onlineTarget.hidden = true;\n }, this.onlineAutoHideValue);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/safe_timeout.ts","../../src/controllers/network_status_controller.ts"],"names":[],"mappings":";;;;;AAoBO,SAAS,QAAA,CAAS,OAAA,EAAiB,OAAA,GAAmC,EAAC,EAAS;AACrF,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,EAAK;AAC1B,EAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACvB,EAAA,MAAA,CAAO,aAAA;AAAA,IACL,IAAI,YAAY,4BAAA,EAA8B;AAAA,MAC5C,QAAQ,EAAE,OAAA,EAAS,MAAM,SAAA,EAAW,OAAA,CAAQ,cAAc,IAAA;AAAK,KAChE;AAAA,GACH;AACF;AAUO,SAAS,YAAA,CAAa,UAAkB,MAAA,EAAiD;AAC9F,EAAA,OAAO,QAAA,CAAS,OAAA,CAAQ,6BAAA,EAA+B,CAAC,OAAO,IAAA,KAAiB;AAC9E,IAAA,MAAM,WAAA,GAAc,OAAO,IAAI,CAAA;AAC/B,IAAA,OAAO,WAAA,KAAgB,MAAA,GAAY,KAAA,GAAQ,MAAA,CAAO,WAAW,CAAA;AAAA,EAC/D,CAAC,CAAA;AACH;;;ACnBA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AAmBO,IAAM,WAAA,GAAN,cAA0B,aAAA,CAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,MAAM;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAClB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EACxB;AACF,CAAA;;;ACpEO,IAAM,uBAAA,GAAN,cAAsC,UAAA,CAAwB;AAAA,EACnE,OAAgB,OAAA,GAAU,CAAC,SAAA,EAAW,QAAQ,CAAA;AAAA,EAC9C,OAAgB,MAAA,GAAS;AAAA,IACvB,YAAA,EAAc,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IAC1C,kBAAA,EAAoB,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IAChD,cAAA,EAAgB,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA;AAAE,GAC7C;AAAA,EACA,OAAO,MAAA,GAAS,CAAC,QAAQ,CAAA;AAAA,EAWhB,OAAA,GAAU,IAAI,WAAA,EAAY;AAAA;AAAA,EAGnC,OAAA,GAAU,IAAA;AAAA,EAED,aAAA,GAAgB,MAAY,IAAA,CAAK,OAAA,CAAQ,IAAI,CAAA;AAAA,EAC7C,cAAA,GAAiB,MAAY,IAAA,CAAK,OAAA,CAAQ,KAAK,CAAA;AAAA,EAE/C,OAAA,GAAgB;AAGvB,IAAA,IAAI,IAAA,CAAK,gBAAA,EAAkB,IAAA,CAAK,aAAA,CAAc,MAAA,GAAS,IAAA;AACvD,IAAA,IAAI,IAAA,CAAK,eAAA,EAAiB,IAAA,CAAK,YAAA,CAAa,MAAA,GAAS,IAAA;AAErD,IAAA,IAAA,CAAK,UAAU,SAAA,CAAU,MAAA;AAGzB,IAAA,IAAA,CAAK,QAAQ,YAAA,CAAa,YAAA,EAAc,IAAA,CAAK,OAAA,GAAU,WAAW,SAAS,CAAA;AAC3E,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,EAAS,IAAA,CAAK,YAAA,EAAa;AAErC,IAAA,MAAA,CAAO,gBAAA,CAAiB,QAAA,EAAU,IAAA,CAAK,aAAa,CAAA;AACpD,IAAA,MAAA,CAAO,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,cAAc,CAAA;AAAA,EACxD;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,MAAA,CAAO,mBAAA,CAAoB,QAAA,EAAU,IAAA,CAAK,aAAa,CAAA;AACvD,IAAA,MAAA,CAAO,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,cAAc,CAAA;AACzD,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,QAAQ,MAAA,EAAuB;AAC7B,IAAA,IAAI,MAAA,KAAW,KAAK,OAAA,EAAS;AAC7B,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AACf,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,MAAA,GAAS,WAAW,SAAS,CAAA;AACrE,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,IAAA,CAAK,WAAA,EAAY;AAAA,IACnB,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,YAAA,EAAa;AAAA,IACpB;AAGA,IAAA,QAAA,CAAS,YAAA,CAAa,SAAS,IAAA,CAAK,uBAAA,GAA0B,KAAK,iBAAA,EAAmB,EAAE,CAAA,EAAG;AAAA,MACzF,WAAW,CAAC;AAAA,KACb,CAAA;AACD,IAAA,IAAA,CAAK,SAAS,QAAA,EAAU,EAAE,QAAQ,EAAE,MAAA,IAAU,CAAA;AAAA,EAChD;AAAA;AAAA,EAGA,YAAA,GAAqB;AACnB,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AACtB,IAAA,IAAI,IAAA,CAAK,eAAA,EAAiB,IAAA,CAAK,YAAA,CAAa,MAAA,GAAS,IAAA;AACrD,IAAA,IAAI,IAAA,CAAK,gBAAA,EAAkB,IAAA,CAAK,aAAA,CAAc,MAAA,GAAS,KAAA;AAAA,EACzD;AAAA;AAAA,EAGA,WAAA,GAAoB;AAClB,IAAA,IAAI,IAAA,CAAK,gBAAA,EAAkB,IAAA,CAAK,aAAA,CAAc,MAAA,GAAS,IAAA;AACvD,IAAA,IAAI,CAAC,KAAK,eAAA,EAAiB;AAC3B,IAAA,IAAA,CAAK,aAAa,MAAA,GAAS,KAAA;AAC3B,IAAA,IAAI,IAAA,CAAK,sBAAsB,CAAA,EAAG;AAChC,MAAA,IAAA,CAAK,OAAA,CAAQ,IAAI,MAAM;AACrB,QAAA,IAAA,CAAK,aAAa,MAAA,GAAS,IAAA;AAAA,MAC7B,CAAA,EAAG,KAAK,mBAAmB,CAAA;AAAA,IAC7B;AAAA,EACF;AACF","file":"network_status_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless online/offline banner behavior built on the live-region practice (no\n * dedicated APG pattern).\n *\n * Markup contract (identifier: `stimeo--network-status`):\n * <div data-controller=\"stimeo--network-status\"\n * data-stimeo--network-status-announce-text-value=\"You are offline.\"\n * data-stimeo--network-status-announce-online-text-value=\"Back online.\">\n * <div hidden data-stimeo--network-status-target=\"offline\">\n * You are offline.\n * </div>\n * <div hidden data-stimeo--network-status-target=\"online\">\n * Back online.\n * </div>\n * </div>\n *\n * Reads `navigator.onLine` on connect and subscribes to the `window`\n * `online`/`offline` events, toggling the matching banner. The banners are the\n * visual half; assistive tech hears the transition through `announceText`\n * (assertive, because losing connectivity is urgent) and `announceOnlineText`\n * (polite), which go to the page's announcer.\n *\n * @remarks\n * Behavior only. `navigator.onLine` is the browser's *guess* — it does not\n * guarantee server reachability, which stays the consumer's job. A banner that is\n * merely un-hidden at the moment of the change is not reliably read, which is why\n * the wording goes to the page's announcer — a region that stands in the document\n * independently of this component. The transition is guarded so an unchanged\n * state never re-announces. The event listeners and the auto-hide timer are\n * removed/cleared on `disconnect()` (Turbo included).\n */\nexport class NetworkStatusController extends Controller<HTMLElement> {\n static override targets = [\"offline\", \"online\"];\n static override values = {\n announceText: { type: String, default: \"\" },\n announceOnlineText: { type: String, default: \"\" },\n onlineAutoHide: { type: Number, default: 0 },\n };\n static events = [\"change\"] as const;\n\n declare readonly offlineTarget: HTMLElement;\n declare readonly onlineTarget: HTMLElement;\n declare readonly hasOfflineTarget: boolean;\n declare readonly hasOnlineTarget: boolean;\n\n declare onlineAutoHideValue: number;\n declare announceTextValue: string;\n declare announceOnlineTextValue: string;\n\n readonly #timers = new SafeTimeout();\n\n /** Last known connectivity; guards against duplicate-state re-announcements. */\n #online = true;\n\n readonly #handleOnline = (): void => this.#update(true);\n readonly #handleOffline = (): void => this.#update(false);\n\n override connect(): void {\n // Normalize initial visibility so a missing `hidden` in the markup cannot\n // strand a stale banner (e.g. an offline notice showing while online).\n if (this.hasOfflineTarget) this.offlineTarget.hidden = true;\n if (this.hasOnlineTarget) this.onlineTarget.hidden = true;\n\n this.#online = navigator.onLine;\n // On connect, surface only the offline state; do not flash a \"back online\"\n // banner just because the page loaded while connected.\n this.element.setAttribute(\"data-state\", this.#online ? \"online\" : \"offline\");\n if (!this.#online) this.#showOffline();\n\n window.addEventListener(\"online\", this.#handleOnline);\n window.addEventListener(\"offline\", this.#handleOffline);\n }\n\n override disconnect(): void {\n window.removeEventListener(\"online\", this.#handleOnline);\n window.removeEventListener(\"offline\", this.#handleOffline);\n this.#timers.clearAll();\n }\n\n /**\n * Applies a connectivity transition, guarded against duplicate states.\n *\n * The event goes out last, so a listener reading `data-state` or a banner's\n * visibility sees the state the transition landed on rather than the previous one.\n */\n #update(online: boolean): void {\n if (online === this.#online) return;\n this.#online = online;\n this.element.setAttribute(\"data-state\", online ? \"online\" : \"offline\");\n if (online) {\n this.#showOnline();\n } else {\n this.#showOffline();\n }\n // The banner is the visual half; reading it out is the announcer's job, because\n // a region that is only revealed at the moment of the change is not reliably read.\n announce(fillTemplate(online ? this.announceOnlineTextValue : this.announceTextValue, {}), {\n assertive: !online,\n });\n this.dispatch(\"change\", { detail: { online } });\n }\n\n /** Shows the offline banner and hides the recovery banner. */\n #showOffline(): void {\n this.#timers.clearAll();\n if (this.hasOnlineTarget) this.onlineTarget.hidden = true;\n if (this.hasOfflineTarget) this.offlineTarget.hidden = false;\n }\n\n /** Shows the recovery banner, optionally auto-hiding it after `onlineAutoHide`. */\n #showOnline(): void {\n if (this.hasOfflineTarget) this.offlineTarget.hidden = true;\n if (!this.hasOnlineTarget) return;\n this.onlineTarget.hidden = false;\n if (this.onlineAutoHideValue > 0) {\n this.#timers.set(() => {\n this.onlineTarget.hidden = true;\n }, this.onlineAutoHideValue);\n }\n }\n}\n"]}
|
|
@@ -42,6 +42,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
42
42
|
* never double-steps — the trailing click after a hold is swallowed.
|
|
43
43
|
* - Typed input is clamped and snapped to the step grid on `change`;
|
|
44
44
|
* `stimeo--number-input:change` is dispatched on every committed change.
|
|
45
|
+
* - `step` must be finite and positive; invalid runtime input falls back to `1`,
|
|
46
|
+
* while finite range endpoints remain reachable off the grid.
|
|
45
47
|
*/
|
|
46
48
|
declare class NumberInputController extends Controller<HTMLElement> {
|
|
47
49
|
#private;
|
|
@@ -80,6 +82,12 @@ declare class NumberInputController extends Controller<HTMLElement> {
|
|
|
80
82
|
connect(): void;
|
|
81
83
|
/** Releases the pointer guards and tears down every pending timer and hold state. */
|
|
82
84
|
disconnect(): void;
|
|
85
|
+
/** Silently reconciles a minimum changed by application code or a Turbo morph. */
|
|
86
|
+
minValueChanged(): void;
|
|
87
|
+
/** Silently reconciles a maximum changed by application code or a Turbo morph. */
|
|
88
|
+
maxValueChanged(): void;
|
|
89
|
+
/** Silently reconciles a step changed by application code or a Turbo morph. */
|
|
90
|
+
stepValueChanged(): void;
|
|
83
91
|
/** Increases by one step. Bound via `data-action` (click). */
|
|
84
92
|
increment(): void;
|
|
85
93
|
/** Decreases by one step. Bound via `data-action` (click). */
|
|
@@ -8,6 +8,39 @@ 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/microtask_coalescer.ts
|
|
12
|
+
var MicrotaskCoalescer = class {
|
|
13
|
+
#run;
|
|
14
|
+
#queued = false;
|
|
15
|
+
#active = false;
|
|
16
|
+
#generation = 0;
|
|
17
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
18
|
+
constructor(run) {
|
|
19
|
+
this.#run = run;
|
|
20
|
+
}
|
|
21
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
22
|
+
activate() {
|
|
23
|
+
this.#active = true;
|
|
24
|
+
}
|
|
25
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
26
|
+
cancel() {
|
|
27
|
+
this.#active = false;
|
|
28
|
+
this.#queued = false;
|
|
29
|
+
this.#generation += 1;
|
|
30
|
+
}
|
|
31
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
32
|
+
schedule() {
|
|
33
|
+
if (!this.#active || this.#queued) return;
|
|
34
|
+
this.#queued = true;
|
|
35
|
+
const generation = this.#generation;
|
|
36
|
+
queueMicrotask(() => {
|
|
37
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
38
|
+
this.#queued = false;
|
|
39
|
+
this.#run();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
11
44
|
// src/utils/safe_timeout.ts
|
|
12
45
|
var TimerRegistry = class {
|
|
13
46
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -75,6 +108,112 @@ var SafeInterval = class extends TimerRegistry {
|
|
|
75
108
|
}
|
|
76
109
|
};
|
|
77
110
|
|
|
111
|
+
// src/utils/stepped_value.ts
|
|
112
|
+
function effectiveStep(step) {
|
|
113
|
+
return Number.isFinite(step) && step > 0 ? step : 1;
|
|
114
|
+
}
|
|
115
|
+
function snapSteppedValue(raw, range) {
|
|
116
|
+
if (!(range.min <= range.max)) return finiteFallback(range.min, range.max);
|
|
117
|
+
const input = Number.isNaN(raw) ? finiteFallback(range.min, range.max) : raw;
|
|
118
|
+
const clamped = Math.min(range.max, Math.max(range.min, input));
|
|
119
|
+
const step = effectiveStep(range.step);
|
|
120
|
+
const base = stepBase(range);
|
|
121
|
+
const candidates = [];
|
|
122
|
+
if (Number.isFinite(range.min)) candidates.push(range.min);
|
|
123
|
+
if (Number.isFinite(range.max)) candidates.push(range.max);
|
|
124
|
+
const gridPosition = (clamped - base) / step;
|
|
125
|
+
if (Number.isFinite(gridPosition)) {
|
|
126
|
+
addGridCandidate(candidates, Math.floor(gridPosition), range, base, step);
|
|
127
|
+
addGridCandidate(candidates, Math.ceil(gridPosition), range, base, step);
|
|
128
|
+
}
|
|
129
|
+
if (candidates.length === 0) return clamped;
|
|
130
|
+
let nearest = candidates[0];
|
|
131
|
+
let nearestDistance = Math.abs(clamped - nearest);
|
|
132
|
+
for (const candidate of candidates.slice(1)) {
|
|
133
|
+
const distance = Math.abs(clamped - candidate);
|
|
134
|
+
if (distance < nearestDistance || nearlyEqual(distance, nearestDistance) && candidate > nearest) {
|
|
135
|
+
nearest = candidate;
|
|
136
|
+
nearestDistance = distance;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return nearest;
|
|
140
|
+
}
|
|
141
|
+
function stepSteppedValue(current, count, range) {
|
|
142
|
+
const value = snapSteppedValue(current, range);
|
|
143
|
+
const distance = Math.abs(Math.trunc(count));
|
|
144
|
+
if (!Number.isFinite(distance) || distance === 0) return value;
|
|
145
|
+
const direction = Math.sign(count);
|
|
146
|
+
const adjacent = adjacentSteppedValue(value, direction, range);
|
|
147
|
+
const raw = adjacent + direction * (distance - 1) * effectiveStep(range.step);
|
|
148
|
+
return snapSteppedValue(raw, range);
|
|
149
|
+
}
|
|
150
|
+
function adjacentSteppedValue(current, direction, range) {
|
|
151
|
+
const step = effectiveStep(range.step);
|
|
152
|
+
const base = stepBase(range);
|
|
153
|
+
const candidates = [];
|
|
154
|
+
if (direction > 0) {
|
|
155
|
+
const position2 = (current - base) / step;
|
|
156
|
+
if (Number.isFinite(position2)) {
|
|
157
|
+
let gridIndex = Math.floor(position2) + 1;
|
|
158
|
+
let candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
159
|
+
if (candidate < current || nearlyEqual(candidate, current)) {
|
|
160
|
+
gridIndex += 1;
|
|
161
|
+
candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
162
|
+
}
|
|
163
|
+
if (candidate > current && !nearlyEqual(candidate, current) && within(candidate, range.min, range.max)) {
|
|
164
|
+
candidates.push(clamp(candidate, range));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return clamp(Math.min(...candidates), range);
|
|
168
|
+
}
|
|
169
|
+
const position = (current - base) / step;
|
|
170
|
+
if (Number.isFinite(position)) {
|
|
171
|
+
let gridIndex = Math.ceil(position) - 1;
|
|
172
|
+
let candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
173
|
+
if (candidate > current || nearlyEqual(candidate, current)) {
|
|
174
|
+
gridIndex -= 1;
|
|
175
|
+
candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
176
|
+
}
|
|
177
|
+
if (candidate < current && !nearlyEqual(candidate, current) && within(candidate, range.min, range.max)) {
|
|
178
|
+
candidates.push(clamp(candidate, range));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return clamp(Math.max(...candidates), range);
|
|
182
|
+
}
|
|
183
|
+
function addGridCandidate(candidates, index, range, base, step) {
|
|
184
|
+
const candidate = cleanGridValue(base + index * step, base, step);
|
|
185
|
+
if (within(candidate, range.min, range.max)) candidates.push(clamp(candidate, range));
|
|
186
|
+
}
|
|
187
|
+
function stepBase(range) {
|
|
188
|
+
if (range.base !== void 0 && Number.isFinite(range.base)) return range.base;
|
|
189
|
+
return Number.isFinite(range.min) ? range.min : 0;
|
|
190
|
+
}
|
|
191
|
+
function cleanGridValue(value, base, step) {
|
|
192
|
+
const precision = Math.max(decimalPlaces(base), decimalPlaces(step));
|
|
193
|
+
return precision <= 100 ? Number(value.toFixed(precision)) : value;
|
|
194
|
+
}
|
|
195
|
+
function decimalPlaces(value) {
|
|
196
|
+
const [coefficient = "", exponentText] = Math.abs(value).toString().toLowerCase().split("e");
|
|
197
|
+
const fractionLength = coefficient.split(".")[1]?.length ?? 0;
|
|
198
|
+
const exponent = exponentText === void 0 ? 0 : Number(exponentText);
|
|
199
|
+
return Math.max(0, fractionLength - exponent);
|
|
200
|
+
}
|
|
201
|
+
function within(value, min, max) {
|
|
202
|
+
return (value > min || nearlyEqual(value, min)) && (value < max || nearlyEqual(value, max));
|
|
203
|
+
}
|
|
204
|
+
function clamp(value, range) {
|
|
205
|
+
return Math.min(range.max, Math.max(range.min, value));
|
|
206
|
+
}
|
|
207
|
+
function nearlyEqual(left, right) {
|
|
208
|
+
const scale = Math.max(Math.abs(left), Math.abs(right));
|
|
209
|
+
return Number.isFinite(left) && Number.isFinite(right) && Math.abs(left - right) <= Number.EPSILON * scale;
|
|
210
|
+
}
|
|
211
|
+
function finiteFallback(min, max) {
|
|
212
|
+
if (Number.isFinite(min)) return min;
|
|
213
|
+
if (Number.isFinite(max)) return max;
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
78
217
|
// src/controllers/number_input_controller.ts
|
|
79
218
|
var NumberInputController = class _NumberInputController extends Controller {
|
|
80
219
|
static targets = ["input", "increment", "decrement"];
|
|
@@ -109,24 +248,24 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
109
248
|
#repeatedDuringHold = false;
|
|
110
249
|
/** True when the next `click` is the trailing one after a hold and must be ignored. */
|
|
111
250
|
#suppressNextClick = false;
|
|
251
|
+
/** Collapses runtime range/step changes into one silent input reconciliation. */
|
|
252
|
+
#repaint = new MicrotaskCoalescer(() => this.#reconcile());
|
|
112
253
|
/** Normalizes any initial value and wires the focus/hold pointer guards. */
|
|
113
254
|
connect() {
|
|
255
|
+
this.#repaint.activate();
|
|
114
256
|
if (!this.hasInputTarget) return;
|
|
115
|
-
|
|
116
|
-
this.#write(this.#normalize(this.#currentValue()));
|
|
117
|
-
} else {
|
|
118
|
-
this.#updateButtons(this.#currentValue());
|
|
119
|
-
}
|
|
257
|
+
this.#reconcile();
|
|
120
258
|
this.#guards = new AbortController();
|
|
121
259
|
const { signal } = this.#guards;
|
|
122
|
-
if (this.hasIncrementTarget) this.#wireButton(this.incrementTarget,
|
|
123
|
-
if (this.hasDecrementTarget) this.#wireButton(this.decrementTarget, -
|
|
260
|
+
if (this.hasIncrementTarget) this.#wireButton(this.incrementTarget, 1, signal);
|
|
261
|
+
if (this.hasDecrementTarget) this.#wireButton(this.decrementTarget, -1, signal);
|
|
124
262
|
window.addEventListener("pointerup", this.#stopHold, { signal });
|
|
125
263
|
window.addEventListener("pointercancel", this.#stopHold, { signal });
|
|
126
264
|
window.addEventListener("blur", this.#stopHold, { signal });
|
|
127
265
|
}
|
|
128
266
|
/** Releases the pointer guards and tears down every pending timer and hold state. */
|
|
129
267
|
disconnect() {
|
|
268
|
+
this.#repaint.cancel();
|
|
130
269
|
this.#guards?.abort();
|
|
131
270
|
this.#guards = null;
|
|
132
271
|
this.#holdActive = false;
|
|
@@ -135,16 +274,28 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
135
274
|
this.#holdTimeouts.clearAll();
|
|
136
275
|
this.#holdIntervals.clearAll();
|
|
137
276
|
}
|
|
277
|
+
/** Silently reconciles a minimum changed by application code or a Turbo morph. */
|
|
278
|
+
minValueChanged() {
|
|
279
|
+
this.#repaint.schedule();
|
|
280
|
+
}
|
|
281
|
+
/** Silently reconciles a maximum changed by application code or a Turbo morph. */
|
|
282
|
+
maxValueChanged() {
|
|
283
|
+
this.#repaint.schedule();
|
|
284
|
+
}
|
|
285
|
+
/** Silently reconciles a step changed by application code or a Turbo morph. */
|
|
286
|
+
stepValueChanged() {
|
|
287
|
+
this.#repaint.schedule();
|
|
288
|
+
}
|
|
138
289
|
/** Increases by one step. Bound via `data-action` (click). */
|
|
139
290
|
increment() {
|
|
140
291
|
if (this.#consumeSuppressedClick()) return;
|
|
141
|
-
this.#
|
|
292
|
+
this.#commitStep(1);
|
|
142
293
|
this.inputTarget.focus();
|
|
143
294
|
}
|
|
144
295
|
/** Decreases by one step. Bound via `data-action` (click). */
|
|
145
296
|
decrement() {
|
|
146
297
|
if (this.#consumeSuppressedClick()) return;
|
|
147
|
-
this.#
|
|
298
|
+
this.#commitStep(-1);
|
|
148
299
|
this.inputTarget.focus();
|
|
149
300
|
}
|
|
150
301
|
/** Clamps and snaps a typed value. Bound via `data-action` (change). */
|
|
@@ -155,20 +306,19 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
155
306
|
/** Keyboard stepping per the APG spinbutton model. */
|
|
156
307
|
onKeydown(event) {
|
|
157
308
|
if (isReservedArrowChord(event)) return;
|
|
158
|
-
const page = this.pageStepValue > 0 ? this.pageStepValue : this.stepValue * 10;
|
|
159
309
|
let next = null;
|
|
160
310
|
switch (event.key) {
|
|
161
311
|
case "ArrowUp":
|
|
162
|
-
next = this.#currentValue()
|
|
312
|
+
next = stepSteppedValue(this.#currentValue(), 1, this.#steppedRange);
|
|
163
313
|
break;
|
|
164
314
|
case "ArrowDown":
|
|
165
|
-
next = this.#currentValue() - this
|
|
315
|
+
next = stepSteppedValue(this.#currentValue(), -1, this.#steppedRange);
|
|
166
316
|
break;
|
|
167
317
|
case "PageUp":
|
|
168
|
-
next = this.#currentValue() +
|
|
318
|
+
next = this.pageStepValue > 0 ? this.#currentValue() + this.pageStepValue : stepSteppedValue(this.#currentValue(), 10, this.#steppedRange);
|
|
169
319
|
break;
|
|
170
320
|
case "PageDown":
|
|
171
|
-
next = this.#currentValue() -
|
|
321
|
+
next = this.pageStepValue > 0 ? this.#currentValue() - this.pageStepValue : stepSteppedValue(this.#currentValue(), -10, this.#steppedRange);
|
|
172
322
|
break;
|
|
173
323
|
case "Home":
|
|
174
324
|
if (!Number.isFinite(this.minValue)) return;
|
|
@@ -189,14 +339,14 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
189
339
|
* hold; leaving the button while held stops it (the global listeners cover
|
|
190
340
|
* release/cancel/blur).
|
|
191
341
|
*/
|
|
192
|
-
#wireButton(button,
|
|
193
|
-
button.addEventListener("pointerdown", (event) => this.#armHold(event, button,
|
|
342
|
+
#wireButton(button, direction, signal) {
|
|
343
|
+
button.addEventListener("pointerdown", (event) => this.#armHold(event, button, direction), {
|
|
194
344
|
signal
|
|
195
345
|
});
|
|
196
346
|
button.addEventListener("pointerleave", this.#stopHold, { signal });
|
|
197
347
|
}
|
|
198
348
|
/** Starts a hold: focus the input, then schedule the first repeat after a delay. */
|
|
199
|
-
#armHold(event, button,
|
|
349
|
+
#armHold(event, button, direction) {
|
|
200
350
|
const pointerButton = event.button;
|
|
201
351
|
if (typeof pointerButton === "number" && pointerButton !== 0) return;
|
|
202
352
|
if (button.disabled) return;
|
|
@@ -207,13 +357,13 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
207
357
|
this.#repeatedDuringHold = false;
|
|
208
358
|
this.#suppressNextClick = false;
|
|
209
359
|
this.#holdTimeouts.set(() => {
|
|
210
|
-
if (!this.#
|
|
360
|
+
if (!this.#commitStep(direction)) {
|
|
211
361
|
this.#stopHold();
|
|
212
362
|
return;
|
|
213
363
|
}
|
|
214
364
|
this.#repeatedDuringHold = true;
|
|
215
365
|
this.#holdIntervals.set(() => {
|
|
216
|
-
if (!this.#
|
|
366
|
+
if (!this.#commitStep(direction)) this.#stopHold();
|
|
217
367
|
}, _NumberInputController.#HOLD_REPEAT_MS);
|
|
218
368
|
}, _NumberInputController.#HOLD_DELAY_MS);
|
|
219
369
|
}
|
|
@@ -254,6 +404,23 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
254
404
|
if (changed) this.dispatch("change", { detail: { value } });
|
|
255
405
|
return changed;
|
|
256
406
|
}
|
|
407
|
+
/** Commits an adjacent endpoint/grid value and reports whether it moved. */
|
|
408
|
+
#commitStep(count) {
|
|
409
|
+
return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange));
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Silently reflects the current value after a range or step morph.
|
|
413
|
+
*
|
|
414
|
+
* @stimeoRenderRoot
|
|
415
|
+
*/
|
|
416
|
+
#reconcile() {
|
|
417
|
+
if (!this.hasInputTarget) return;
|
|
418
|
+
if (this.inputTarget.value.trim() !== "") {
|
|
419
|
+
this.#write(this.#normalize(this.#currentValue()));
|
|
420
|
+
} else {
|
|
421
|
+
this.#updateButtons(this.#currentValue());
|
|
422
|
+
}
|
|
423
|
+
}
|
|
257
424
|
/** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */
|
|
258
425
|
#write(value) {
|
|
259
426
|
this.inputTarget.value = String(value);
|
|
@@ -300,11 +467,11 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
300
467
|
}
|
|
301
468
|
/** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */
|
|
302
469
|
#normalize(raw) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return
|
|
470
|
+
return snapSteppedValue(raw, this.#steppedRange);
|
|
471
|
+
}
|
|
472
|
+
/** Shared range configuration; finite endpoints remain allowed off the grid. */
|
|
473
|
+
get #steppedRange() {
|
|
474
|
+
return { min: this.minValue, max: this.maxValue, step: this.stepValue };
|
|
308
475
|
}
|
|
309
476
|
};
|
|
310
477
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/safe_timeout.ts","../../src/controllers/number_input_controller.ts"],"names":[],"mappings":"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","file":"number_input_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 * 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 { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { SafeInterval, SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless, accessible number / spin-button behavior.\n *\n * Markup contract (identifier: `stimeo--number-input`):\n * <div data-controller=\"stimeo--number-input\"\n * data-stimeo--number-input-min-value=\"0\"\n * data-stimeo--number-input-max-value=\"100\"\n * data-stimeo--number-input-step-value=\"1\">\n * <button type=\"button\" aria-label=\"Decrease\" tabindex=\"-1\"\n * data-stimeo--number-input-target=\"decrement\"\n * data-action=\"click->stimeo--number-input#decrement\">−</button>\n * <input type=\"number\" min=\"0\" max=\"100\" step=\"1\" value=\"0\"\n * data-stimeo--number-input-target=\"input\"\n * data-action=\"change->stimeo--number-input#onInput\n * keydown->stimeo--number-input#onKeydown\" />\n * <button type=\"button\" aria-label=\"Increase\" tabindex=\"-1\"\n * data-stimeo--number-input-target=\"increment\"\n * data-action=\"click->stimeo--number-input#increment\">+</button>\n * </div>\n *\n * Implements the WAI-ARIA APG **Spinbutton** pattern. The arrow/step logic is\n * fully owned by the controller (not delegated to the browser's native number\n * stepping) so the behavior is identical for a native `<input type=\"number\">` and\n * a custom `role=\"spinbutton\"` host.\n *\n * @remarks\n * Behavior only — the consumer styles the field and buttons. The input is the\n * sole Tab stop; the buttons are `tabindex=\"-1\"` and keep focus on the input\n * (pointerdown is suppressed), so stepping never moves focus away.\n *\n * Behavior provided:\n * - `ArrowUp`/`ArrowDown` step by `step`; `PageUp`/`PageDown` by `pageStep`\n * (default `step × 10`); `Home`/`End` jump to a finite `min`/`max`.\n * - Increment/decrement buttons step too, and are `disabled` at the bounds (focus\n * is returned to the input before a focused button is disabled).\n * - **Press-and-hold auto-repeat**: holding a step button steps once, then after a\n * short delay repeats until release, the bound is reached, or the element\n * disconnects. The `click` binding stays the single-step path (a quick click, a\n * synthesized/programmatic click, or assistive activation), so a normal click\n * never double-steps — the trailing click after a hold is swallowed.\n * - Typed input is clamped and snapped to the step grid on `change`;\n * `stimeo--number-input:change` is dispatched on every committed change.\n */\nexport class NumberInputController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"increment\", \"decrement\"];\n static override values = {\n min: { type: Number, default: Number.NEGATIVE_INFINITY },\n max: { type: Number, default: Number.POSITIVE_INFINITY },\n step: { type: Number, default: 1 },\n pageStep: { type: Number, default: 0 },\n };\n static actions = [\"decrement\", \"increment\", \"onInput\", \"onKeydown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly hasInputTarget: boolean;\n declare readonly incrementTarget: HTMLButtonElement;\n declare readonly hasIncrementTarget: boolean;\n declare readonly decrementTarget: HTMLButtonElement;\n declare readonly hasDecrementTarget: boolean;\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare pageStepValue: number;\n\n /** Delay (ms) a button must be held before auto-repeat starts. */\n static readonly #HOLD_DELAY_MS = 400;\n /** Interval (ms) between auto-repeat steps once a hold has started. */\n static readonly #HOLD_REPEAT_MS = 80;\n /**\n * Window (ms) after a hold ends during which the trailing synthetic `click` is\n * swallowed. A safety net for the (rare) case where that click never arrives\n * (e.g. the pointer was released off the button): the flag self-clears instead\n * of poisoning the next legitimate click.\n */\n static readonly #SUPPRESS_RESET_MS = 250;\n\n /** Aborts the pointer listeners on disconnect so none outlive the element. */\n #guards: AbortController | null = null;\n /** Timers for the hold delay and the suppress-reset safety net. */\n readonly #holdTimeouts = new SafeTimeout();\n /** The running auto-repeat interval (one at a time). */\n readonly #holdIntervals = new SafeInterval();\n /** True while a hold is armed/running, making `#stopHold` idempotent. */\n #holdActive = false;\n /** True once a hold actually produced a repeated step (vs. a quick click). */\n #repeatedDuringHold = false;\n /** True when the next `click` is the trailing one after a hold and must be ignored. */\n #suppressNextClick = false;\n\n /** Normalizes any initial value and wires the focus/hold pointer guards. */\n override connect(): void {\n if (!this.hasInputTarget) return;\n if (this.inputTarget.value.trim() !== \"\") {\n this.#write(this.#normalize(this.#currentValue()));\n } else {\n this.#updateButtons(this.#currentValue());\n }\n this.#guards = new AbortController();\n const { signal } = this.#guards;\n if (this.hasIncrementTarget) this.#wireButton(this.incrementTarget, this.stepValue, signal);\n if (this.hasDecrementTarget) this.#wireButton(this.decrementTarget, -this.stepValue, signal);\n // A pointer released or focus lost anywhere stops a running hold, even when\n // the button stopped receiving its own pointerup (e.g. it became disabled at\n // the bound, or the pointer was released off it).\n window.addEventListener(\"pointerup\", this.#stopHold, { signal });\n window.addEventListener(\"pointercancel\", this.#stopHold, { signal });\n window.addEventListener(\"blur\", this.#stopHold, { signal });\n }\n\n /** Releases the pointer guards and tears down every pending timer and hold state. */\n override disconnect(): void {\n this.#guards?.abort();\n this.#guards = null;\n this.#holdActive = false;\n this.#repeatedDuringHold = false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.clearAll();\n this.#holdIntervals.clearAll();\n }\n\n /** Increases by one step. Bound via `data-action` (click). */\n increment(): void {\n if (this.#consumeSuppressedClick()) return;\n this.#commit(this.#currentValue() + this.stepValue);\n this.inputTarget.focus();\n }\n\n /** Decreases by one step. Bound via `data-action` (click). */\n decrement(): void {\n if (this.#consumeSuppressedClick()) return;\n this.#commit(this.#currentValue() - this.stepValue);\n this.inputTarget.focus();\n }\n\n /** Clamps and snaps a typed value. Bound via `data-action` (change). */\n onInput(): void {\n if (this.inputTarget.value.trim() === \"\") return;\n this.#commit(this.#currentValue());\n }\n\n /** Keyboard stepping per the APG spinbutton model. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n const page = this.pageStepValue > 0 ? this.pageStepValue : this.stepValue * 10;\n let next: number | null = null;\n switch (event.key) {\n case \"ArrowUp\":\n next = this.#currentValue() + this.stepValue;\n break;\n case \"ArrowDown\":\n next = this.#currentValue() - this.stepValue;\n break;\n case \"PageUp\":\n next = this.#currentValue() + page;\n break;\n case \"PageDown\":\n next = this.#currentValue() - page;\n break;\n case \"Home\":\n if (!Number.isFinite(this.minValue)) return;\n next = this.minValue;\n break;\n case \"End\":\n if (!Number.isFinite(this.maxValue)) return;\n next = this.maxValue;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#commit(next);\n }\n\n /**\n * Wires a step button: `pointerdown` keeps focus on the input and arms the\n * hold; leaving the button while held stops it (the global listeners cover\n * release/cancel/blur).\n */\n #wireButton(button: HTMLButtonElement, delta: number, signal: AbortSignal): void {\n button.addEventListener(\"pointerdown\", (event) => this.#armHold(event, button, delta), {\n signal,\n });\n button.addEventListener(\"pointerleave\", this.#stopHold, { signal });\n }\n\n /** Starts a hold: focus the input, then schedule the first repeat after a delay. */\n #armHold(event: Event, button: HTMLButtonElement, delta: number): void {\n // Ignore secondary buttons (right/middle) when the event exposes one.\n const pointerButton = (event as PointerEvent).button;\n if (typeof pointerButton === \"number\" && pointerButton !== 0) return;\n if (button.disabled) return;\n event.preventDefault(); // keep focus on the input rather than the button\n this.inputTarget.focus();\n\n this.#stopHold();\n this.#holdActive = true;\n this.#repeatedDuringHold = false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.set(() => {\n if (!this.#commit(this.#currentValue() + delta)) {\n this.#stopHold();\n return;\n }\n this.#repeatedDuringHold = true;\n this.#holdIntervals.set(() => {\n if (!this.#commit(this.#currentValue() + delta)) this.#stopHold();\n }, NumberInputController.#HOLD_REPEAT_MS);\n }, NumberInputController.#HOLD_DELAY_MS);\n }\n\n /**\n * Stops the current hold (idempotent). When the hold actually repeated, the\n * trailing synthetic `click` must be ignored, so it is suppressed until the\n * click consumes it or a short safety-net timeout clears it.\n */\n readonly #stopHold = (): void => {\n if (!this.#holdActive) return;\n this.#holdActive = false;\n this.#holdTimeouts.clearAll();\n this.#holdIntervals.clearAll();\n if (this.#repeatedDuringHold) {\n this.#suppressNextClick = true;\n this.#holdTimeouts.set(() => {\n this.#suppressNextClick = false;\n }, NumberInputController.#SUPPRESS_RESET_MS);\n }\n };\n\n /** Consumes a pending trailing-click suppression; returns true if the click was swallowed. */\n #consumeSuppressedClick(): boolean {\n if (!this.#suppressNextClick) return false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.clearAll();\n return true;\n }\n\n /**\n * Normalizes `raw`, reflects it, and dispatches `change` only when the\n * displayed value actually changes.\n *\n * @returns Whether the value changed (drives the auto-repeat's bound stop).\n */\n #commit(raw: number): boolean {\n const value = this.#normalize(raw);\n const changed = this.inputTarget.value !== String(value);\n this.#write(value);\n if (changed) this.dispatch(\"change\", { detail: { value } });\n return changed;\n }\n\n /** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */\n #write(value: number): void {\n this.inputTarget.value = String(value);\n if (this.inputTarget.getAttribute(\"role\") === \"spinbutton\") {\n this.inputTarget.setAttribute(\"aria-valuenow\", String(value));\n if (Number.isFinite(this.minValue)) {\n this.inputTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n }\n if (Number.isFinite(this.maxValue)) {\n this.inputTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n }\n }\n this.#updateButtons(value);\n }\n\n /** Disables a step button at its bound, returning focus to the input first. */\n #updateButtons(value: number): void {\n if (this.hasIncrementTarget) this.#toggleButton(this.incrementTarget, value < this.maxValue);\n if (this.hasDecrementTarget) this.#toggleButton(this.decrementTarget, value > this.minValue);\n }\n\n /**\n * Enables or disables a step button at its bound, never disabling one while it\n * holds focus. Owns only the `disabled` it sets itself via a marker\n * (`data-number-input-disabled`, like `conditional-fields`/`submit-once`), so an\n * author-disabled button (e.g. the whole control disabled) is never re-enabled.\n */\n #toggleButton(button: HTMLButtonElement, enabled: boolean): void {\n if (enabled) {\n if (button.hasAttribute(\"data-number-input-disabled\")) {\n button.disabled = false;\n button.removeAttribute(\"data-number-input-disabled\");\n }\n return;\n }\n if (button.disabled) return; // already disabled (possibly by the author) — leave it\n if (document.activeElement === button) this.inputTarget.focus();\n button.disabled = true;\n button.setAttribute(\"data-number-input-disabled\", \"\");\n }\n\n /** The current numeric value, falling back to a finite min (else 0) when blank. */\n #currentValue(): number {\n const parsed = Number(this.inputTarget.value);\n if (Number.isFinite(parsed) && this.inputTarget.value.trim() !== \"\") return parsed;\n return Number.isFinite(this.minValue) ? this.minValue : 0;\n }\n\n /** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */\n #normalize(raw: number): number {\n const clamped = Math.min(this.maxValue, Math.max(this.minValue, raw));\n if (this.stepValue <= 0) return clamped;\n const base = Number.isFinite(this.minValue) ? this.minValue : 0;\n const stepped = Math.round((clamped - base) / this.stepValue) * this.stepValue + base;\n return Math.min(this.maxValue, Math.max(this.minValue, stepped));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/safe_timeout.ts","../../src/utils/stepped_value.ts","../../src/controllers/number_input_controller.ts"],"names":["position"],"mappings":"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","file":"number_input_controller.js","sourcesContent":["import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * 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","/** Configuration for a value constrained to a stepped numeric range. */\nexport interface SteppedRange {\n /** Inclusive lower endpoint. May be infinite for an unbounded control. */\n readonly min: number;\n /** Inclusive upper endpoint. May be infinite for an unbounded control. */\n readonly max: number;\n /** Requested step. Non-finite and non-positive values fall back to `1`. */\n readonly step: number;\n /** Step-grid origin. Defaults to a finite `min`, otherwise `0`. */\n readonly base?: number;\n}\n\n/** The positive finite step used at runtime and by keyboard stepping. */\nexport function effectiveStep(step: number): number {\n return Number.isFinite(step) && step > 0 ? step : 1;\n}\n\n/**\n * Clamps a raw value and selects its nearest allowed value.\n *\n * The allowed set is the step grid plus each finite range endpoint. Keeping the\n * endpoints in that set makes an authored `aria-valuemin`/`aria-valuemax`\n * truthful even when the upper endpoint is not aligned to the grid. A value\n * exactly between two allowed values resolves upward.\n */\nexport function snapSteppedValue(raw: number, range: SteppedRange): number {\n if (!(range.min <= range.max)) return finiteFallback(range.min, range.max);\n\n const input = Number.isNaN(raw) ? finiteFallback(range.min, range.max) : raw;\n const clamped = Math.min(range.max, Math.max(range.min, input));\n const step = effectiveStep(range.step);\n const base = stepBase(range);\n const candidates: number[] = [];\n\n if (Number.isFinite(range.min)) candidates.push(range.min);\n if (Number.isFinite(range.max)) candidates.push(range.max);\n\n const gridPosition = (clamped - base) / step;\n if (Number.isFinite(gridPosition)) {\n addGridCandidate(candidates, Math.floor(gridPosition), range, base, step);\n addGridCandidate(candidates, Math.ceil(gridPosition), range, base, step);\n }\n\n if (candidates.length === 0) return clamped;\n let nearest = candidates[0] as number;\n let nearestDistance = Math.abs(clamped - nearest);\n for (const candidate of candidates.slice(1)) {\n const distance = Math.abs(clamped - candidate);\n if (\n distance < nearestDistance ||\n (nearlyEqual(distance, nearestDistance) && candidate > nearest)\n ) {\n nearest = candidate;\n nearestDistance = distance;\n }\n }\n return nearest;\n}\n\n/**\n * Moves through the ordered allowed-value set by an integer number of steps.\n *\n * This differs from adding `step * count` and snapping afterward at an\n * off-grid endpoint: with `max=94, step=10`, one decrement from `94` must land\n * on `90`, and one increment from `90` must reach the advertised maximum `94`.\n */\nexport function stepSteppedValue(current: number, count: number, range: SteppedRange): number {\n const value = snapSteppedValue(current, range);\n const distance = Math.abs(Math.trunc(count));\n if (!Number.isFinite(distance) || distance === 0) return value;\n const direction = Math.sign(count);\n const adjacent = adjacentSteppedValue(value, direction, range);\n\n // The first move accounts for a possibly off-grid endpoint. From there a\n // direct grid jump is equivalent to repeated adjacency, and avoids work that\n // grows with Page-step magnitudes or programmatic counts.\n const raw = adjacent + direction * (distance - 1) * effectiveStep(range.step);\n return snapSteppedValue(raw, range);\n}\n\n/** Returns the next member of the ordered endpoint-plus-grid set. */\nfunction adjacentSteppedValue(current: number, direction: number, range: SteppedRange): number {\n const step = effectiveStep(range.step);\n const base = stepBase(range);\n const candidates: number[] = [];\n\n if (direction > 0) {\n const position = (current - base) / step;\n if (Number.isFinite(position)) {\n let gridIndex = Math.floor(position) + 1;\n let candidate = cleanGridValue(base + gridIndex * step, base, step);\n if (candidate < current || nearlyEqual(candidate, current)) {\n gridIndex += 1;\n candidate = cleanGridValue(base + gridIndex * step, base, step);\n }\n if (\n candidate > current &&\n !nearlyEqual(candidate, current) &&\n within(candidate, range.min, range.max)\n ) {\n candidates.push(clamp(candidate, range));\n }\n }\n return clamp(Math.min(...candidates), range);\n }\n\n const position = (current - base) / step;\n if (Number.isFinite(position)) {\n let gridIndex = Math.ceil(position) - 1;\n let candidate = cleanGridValue(base + gridIndex * step, base, step);\n if (candidate > current || nearlyEqual(candidate, current)) {\n gridIndex -= 1;\n candidate = cleanGridValue(base + gridIndex * step, base, step);\n }\n if (\n candidate < current &&\n !nearlyEqual(candidate, current) &&\n within(candidate, range.min, range.max)\n ) {\n candidates.push(clamp(candidate, range));\n }\n }\n return clamp(Math.max(...candidates), range);\n}\n\n/** Adds one grid value when it falls inside the inclusive range. */\nfunction addGridCandidate(\n candidates: number[],\n index: number,\n range: SteppedRange,\n base: number,\n step: number,\n): void {\n const candidate = cleanGridValue(base + index * step, base, step);\n if (within(candidate, range.min, range.max)) candidates.push(clamp(candidate, range));\n}\n\n/** A finite authored base wins; otherwise the finite lower bound or zero does. */\nfunction stepBase(range: SteppedRange): number {\n if (range.base !== undefined && Number.isFinite(range.base)) return range.base;\n return Number.isFinite(range.min) ? range.min : 0;\n}\n\n/** Removes binary noise without discarding significant integer-place digits. */\nfunction cleanGridValue(value: number, base: number, step: number): number {\n const precision = Math.max(decimalPlaces(base), decimalPlaces(step));\n // `toFixed` accepts at most 100 fractional digits. Below that limit it rounds\n // according to the authored grid precision rather than an arbitrary total\n // significant-digit count, which keeps unit steps distinct at large bases.\n return precision <= 100 ? Number(value.toFixed(precision)) : value;\n}\n\n/** Number of fractional decimal places represented by a finite JS number. */\nfunction decimalPlaces(value: number): number {\n const [coefficient = \"\", exponentText] = Math.abs(value).toString().toLowerCase().split(\"e\");\n const fractionLength = coefficient.split(\".\")[1]?.length ?? 0;\n const exponent = exponentText === undefined ? 0 : Number(exponentText);\n return Math.max(0, fractionLength - exponent);\n}\n\n/** Inclusive range check tolerant only of arithmetic noise at an endpoint. */\nfunction within(value: number, min: number, max: number): boolean {\n return (value > min || nearlyEqual(value, min)) && (value < max || nearlyEqual(value, max));\n}\n\n/** Clamps a candidate whose only overshoot may be floating-point noise. */\nfunction clamp(value: number, range: SteppedRange): number {\n return Math.min(range.max, Math.max(range.min, value));\n}\n\n/** Relative comparison used only to collapse arithmetic noise, not real steps. */\nfunction nearlyEqual(left: number, right: number): boolean {\n const scale = Math.max(Math.abs(left), Math.abs(right));\n return (\n Number.isFinite(left) &&\n Number.isFinite(right) &&\n Math.abs(left - right) <= Number.EPSILON * scale\n );\n}\n\n/** Stable fallback for malformed ranges or `NaN` input. */\nfunction finiteFallback(min: number, max: number): number {\n if (Number.isFinite(min)) return min;\n if (Number.isFinite(max)) return max;\n return 0;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { SafeInterval, SafeTimeout } from \"../utils/safe_timeout\";\nimport { snapSteppedValue, stepSteppedValue } from \"../utils/stepped_value\";\n\n/**\n * Headless, accessible number / spin-button behavior.\n *\n * Markup contract (identifier: `stimeo--number-input`):\n * <div data-controller=\"stimeo--number-input\"\n * data-stimeo--number-input-min-value=\"0\"\n * data-stimeo--number-input-max-value=\"100\"\n * data-stimeo--number-input-step-value=\"1\">\n * <button type=\"button\" aria-label=\"Decrease\" tabindex=\"-1\"\n * data-stimeo--number-input-target=\"decrement\"\n * data-action=\"click->stimeo--number-input#decrement\">−</button>\n * <input type=\"number\" min=\"0\" max=\"100\" step=\"1\" value=\"0\"\n * data-stimeo--number-input-target=\"input\"\n * data-action=\"change->stimeo--number-input#onInput\n * keydown->stimeo--number-input#onKeydown\" />\n * <button type=\"button\" aria-label=\"Increase\" tabindex=\"-1\"\n * data-stimeo--number-input-target=\"increment\"\n * data-action=\"click->stimeo--number-input#increment\">+</button>\n * </div>\n *\n * Implements the WAI-ARIA APG **Spinbutton** pattern. The arrow/step logic is\n * fully owned by the controller (not delegated to the browser's native number\n * stepping) so the behavior is identical for a native `<input type=\"number\">` and\n * a custom `role=\"spinbutton\"` host.\n *\n * @remarks\n * Behavior only — the consumer styles the field and buttons. The input is the\n * sole Tab stop; the buttons are `tabindex=\"-1\"` and keep focus on the input\n * (pointerdown is suppressed), so stepping never moves focus away.\n *\n * Behavior provided:\n * - `ArrowUp`/`ArrowDown` step by `step`; `PageUp`/`PageDown` by `pageStep`\n * (default `step × 10`); `Home`/`End` jump to a finite `min`/`max`.\n * - Increment/decrement buttons step too, and are `disabled` at the bounds (focus\n * is returned to the input before a focused button is disabled).\n * - **Press-and-hold auto-repeat**: holding a step button steps once, then after a\n * short delay repeats until release, the bound is reached, or the element\n * disconnects. The `click` binding stays the single-step path (a quick click, a\n * synthesized/programmatic click, or assistive activation), so a normal click\n * never double-steps — the trailing click after a hold is swallowed.\n * - Typed input is clamped and snapped to the step grid on `change`;\n * `stimeo--number-input:change` is dispatched on every committed change.\n * - `step` must be finite and positive; invalid runtime input falls back to `1`,\n * while finite range endpoints remain reachable off the grid.\n */\nexport class NumberInputController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"increment\", \"decrement\"];\n static override values = {\n min: { type: Number, default: Number.NEGATIVE_INFINITY },\n max: { type: Number, default: Number.POSITIVE_INFINITY },\n step: { type: Number, default: 1 },\n pageStep: { type: Number, default: 0 },\n };\n static actions = [\"decrement\", \"increment\", \"onInput\", \"onKeydown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly hasInputTarget: boolean;\n declare readonly incrementTarget: HTMLButtonElement;\n declare readonly hasIncrementTarget: boolean;\n declare readonly decrementTarget: HTMLButtonElement;\n declare readonly hasDecrementTarget: boolean;\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare pageStepValue: number;\n\n /** Delay (ms) a button must be held before auto-repeat starts. */\n static readonly #HOLD_DELAY_MS = 400;\n /** Interval (ms) between auto-repeat steps once a hold has started. */\n static readonly #HOLD_REPEAT_MS = 80;\n /**\n * Window (ms) after a hold ends during which the trailing synthetic `click` is\n * swallowed. A safety net for the (rare) case where that click never arrives\n * (e.g. the pointer was released off the button): the flag self-clears instead\n * of poisoning the next legitimate click.\n */\n static readonly #SUPPRESS_RESET_MS = 250;\n\n /** Aborts the pointer listeners on disconnect so none outlive the element. */\n #guards: AbortController | null = null;\n /** Timers for the hold delay and the suppress-reset safety net. */\n readonly #holdTimeouts = new SafeTimeout();\n /** The running auto-repeat interval (one at a time). */\n readonly #holdIntervals = new SafeInterval();\n /** True while a hold is armed/running, making `#stopHold` idempotent. */\n #holdActive = false;\n /** True once a hold actually produced a repeated step (vs. a quick click). */\n #repeatedDuringHold = false;\n /** True when the next `click` is the trailing one after a hold and must be ignored. */\n #suppressNextClick = false;\n /** Collapses runtime range/step changes into one silent input reconciliation. */\n readonly #repaint = new MicrotaskCoalescer(() => this.#reconcile());\n\n /** Normalizes any initial value and wires the focus/hold pointer guards. */\n override connect(): void {\n this.#repaint.activate();\n if (!this.hasInputTarget) return;\n this.#reconcile();\n this.#guards = new AbortController();\n const { signal } = this.#guards;\n if (this.hasIncrementTarget) this.#wireButton(this.incrementTarget, 1, signal);\n if (this.hasDecrementTarget) this.#wireButton(this.decrementTarget, -1, signal);\n // A pointer released or focus lost anywhere stops a running hold, even when\n // the button stopped receiving its own pointerup (e.g. it became disabled at\n // the bound, or the pointer was released off it).\n window.addEventListener(\"pointerup\", this.#stopHold, { signal });\n window.addEventListener(\"pointercancel\", this.#stopHold, { signal });\n window.addEventListener(\"blur\", this.#stopHold, { signal });\n }\n\n /** Releases the pointer guards and tears down every pending timer and hold state. */\n override disconnect(): void {\n this.#repaint.cancel();\n this.#guards?.abort();\n this.#guards = null;\n this.#holdActive = false;\n this.#repeatedDuringHold = false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.clearAll();\n this.#holdIntervals.clearAll();\n }\n\n /** Silently reconciles a minimum changed by application code or a Turbo morph. */\n minValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Silently reconciles a maximum changed by application code or a Turbo morph. */\n maxValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Silently reconciles a step changed by application code or a Turbo morph. */\n stepValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Increases by one step. Bound via `data-action` (click). */\n increment(): void {\n if (this.#consumeSuppressedClick()) return;\n this.#commitStep(1);\n this.inputTarget.focus();\n }\n\n /** Decreases by one step. Bound via `data-action` (click). */\n decrement(): void {\n if (this.#consumeSuppressedClick()) return;\n this.#commitStep(-1);\n this.inputTarget.focus();\n }\n\n /** Clamps and snaps a typed value. Bound via `data-action` (change). */\n onInput(): void {\n if (this.inputTarget.value.trim() === \"\") return;\n this.#commit(this.#currentValue());\n }\n\n /** Keyboard stepping per the APG spinbutton model. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n let next: number | null = null;\n switch (event.key) {\n case \"ArrowUp\":\n next = stepSteppedValue(this.#currentValue(), 1, this.#steppedRange);\n break;\n case \"ArrowDown\":\n next = stepSteppedValue(this.#currentValue(), -1, this.#steppedRange);\n break;\n case \"PageUp\":\n next =\n this.pageStepValue > 0\n ? this.#currentValue() + this.pageStepValue\n : stepSteppedValue(this.#currentValue(), 10, this.#steppedRange);\n break;\n case \"PageDown\":\n next =\n this.pageStepValue > 0\n ? this.#currentValue() - this.pageStepValue\n : stepSteppedValue(this.#currentValue(), -10, this.#steppedRange);\n break;\n case \"Home\":\n if (!Number.isFinite(this.minValue)) return;\n next = this.minValue;\n break;\n case \"End\":\n if (!Number.isFinite(this.maxValue)) return;\n next = this.maxValue;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#commit(next);\n }\n\n /**\n * Wires a step button: `pointerdown` keeps focus on the input and arms the\n * hold; leaving the button while held stops it (the global listeners cover\n * release/cancel/blur).\n */\n #wireButton(button: HTMLButtonElement, direction: number, signal: AbortSignal): void {\n button.addEventListener(\"pointerdown\", (event) => this.#armHold(event, button, direction), {\n signal,\n });\n button.addEventListener(\"pointerleave\", this.#stopHold, { signal });\n }\n\n /** Starts a hold: focus the input, then schedule the first repeat after a delay. */\n #armHold(event: Event, button: HTMLButtonElement, direction: number): void {\n // Ignore secondary buttons (right/middle) when the event exposes one.\n const pointerButton = (event as PointerEvent).button;\n if (typeof pointerButton === \"number\" && pointerButton !== 0) return;\n if (button.disabled) return;\n event.preventDefault(); // keep focus on the input rather than the button\n this.inputTarget.focus();\n\n this.#stopHold();\n this.#holdActive = true;\n this.#repeatedDuringHold = false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.set(() => {\n if (!this.#commitStep(direction)) {\n this.#stopHold();\n return;\n }\n this.#repeatedDuringHold = true;\n this.#holdIntervals.set(() => {\n if (!this.#commitStep(direction)) this.#stopHold();\n }, NumberInputController.#HOLD_REPEAT_MS);\n }, NumberInputController.#HOLD_DELAY_MS);\n }\n\n /**\n * Stops the current hold (idempotent). When the hold actually repeated, the\n * trailing synthetic `click` must be ignored, so it is suppressed until the\n * click consumes it or a short safety-net timeout clears it.\n */\n readonly #stopHold = (): void => {\n if (!this.#holdActive) return;\n this.#holdActive = false;\n this.#holdTimeouts.clearAll();\n this.#holdIntervals.clearAll();\n if (this.#repeatedDuringHold) {\n this.#suppressNextClick = true;\n this.#holdTimeouts.set(() => {\n this.#suppressNextClick = false;\n }, NumberInputController.#SUPPRESS_RESET_MS);\n }\n };\n\n /** Consumes a pending trailing-click suppression; returns true if the click was swallowed. */\n #consumeSuppressedClick(): boolean {\n if (!this.#suppressNextClick) return false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.clearAll();\n return true;\n }\n\n /**\n * Normalizes `raw`, reflects it, and dispatches `change` only when the\n * displayed value actually changes.\n *\n * @returns Whether the value changed (drives the auto-repeat's bound stop).\n */\n #commit(raw: number): boolean {\n const value = this.#normalize(raw);\n const changed = this.inputTarget.value !== String(value);\n this.#write(value);\n if (changed) this.dispatch(\"change\", { detail: { value } });\n return changed;\n }\n\n /** Commits an adjacent endpoint/grid value and reports whether it moved. */\n #commitStep(count: number): boolean {\n return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange));\n }\n\n /**\n * Silently reflects the current value after a range or step morph.\n *\n * @stimeoRenderRoot\n */\n #reconcile(): void {\n if (!this.hasInputTarget) return;\n if (this.inputTarget.value.trim() !== \"\") {\n this.#write(this.#normalize(this.#currentValue()));\n } else {\n this.#updateButtons(this.#currentValue());\n }\n }\n\n /** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */\n #write(value: number): void {\n this.inputTarget.value = String(value);\n if (this.inputTarget.getAttribute(\"role\") === \"spinbutton\") {\n this.inputTarget.setAttribute(\"aria-valuenow\", String(value));\n if (Number.isFinite(this.minValue)) {\n this.inputTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n }\n if (Number.isFinite(this.maxValue)) {\n this.inputTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n }\n }\n this.#updateButtons(value);\n }\n\n /** Disables a step button at its bound, returning focus to the input first. */\n #updateButtons(value: number): void {\n if (this.hasIncrementTarget) this.#toggleButton(this.incrementTarget, value < this.maxValue);\n if (this.hasDecrementTarget) this.#toggleButton(this.decrementTarget, value > this.minValue);\n }\n\n /**\n * Enables or disables a step button at its bound, never disabling one while it\n * holds focus. Owns only the `disabled` it sets itself via a marker\n * (`data-number-input-disabled`, like `conditional-fields`/`submit-once`), so an\n * author-disabled button (e.g. the whole control disabled) is never re-enabled.\n */\n #toggleButton(button: HTMLButtonElement, enabled: boolean): void {\n if (enabled) {\n if (button.hasAttribute(\"data-number-input-disabled\")) {\n button.disabled = false;\n button.removeAttribute(\"data-number-input-disabled\");\n }\n return;\n }\n if (button.disabled) return; // already disabled (possibly by the author) — leave it\n if (document.activeElement === button) this.inputTarget.focus();\n button.disabled = true;\n button.setAttribute(\"data-number-input-disabled\", \"\");\n }\n\n /** The current numeric value, falling back to a finite min (else 0) when blank. */\n #currentValue(): number {\n const parsed = Number(this.inputTarget.value);\n if (Number.isFinite(parsed) && this.inputTarget.value.trim() !== \"\") return parsed;\n return Number.isFinite(this.minValue) ? this.minValue : 0;\n }\n\n /** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */\n #normalize(raw: number): number {\n return snapSteppedValue(raw, this.#steppedRange);\n }\n\n /** Shared range configuration; finite endpoints remain allowed off the grid. */\n get #steppedRange() {\n return { min: this.minValue, max: this.maxValue, step: this.stepValue };\n }\n}\n"]}
|