stimeo-ui 0.5.0 → 0.7.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 +178 -0
- package/dist/controllers/alert_dialog_controller.d.ts +2 -0
- package/dist/controllers/alert_dialog_controller.js +75 -4
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.d.ts +1 -3
- package/dist/controllers/aspect_ratio_controller.js +19 -11
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +2 -0
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.d.ts +36 -15
- package/dist/controllers/avatar_controller.js +237 -40
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +2 -0
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +2 -0
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +2 -0
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +12 -3
- package/dist/controllers/carousel_controller.js +85 -9
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +52 -20
- package/dist/controllers/character_counter_controller.js +338 -63
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +34 -5
- package/dist/controllers/checkbox_controller.js +136 -25
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +10 -1
- package/dist/controllers/color_picker_controller.js +35 -9
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +2 -0
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +2 -0
- package/dist/controllers/command_palette_controller.js +75 -4
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +42 -14
- package/dist/controllers/conditional_fields_controller.js +345 -51
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +2 -0
- package/dist/controllers/confirm_controller.js +75 -4
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +2 -0
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +4 -0
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +2 -0
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +3 -0
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +23 -4
- package/dist/controllers/date_range_picker_controller.js +157 -30
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +75 -4
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +56 -24
- package/dist/controllers/direct_upload_controller.js +201 -45
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +14 -6
- package/dist/controllers/dirty_form_controller.js +192 -29
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +2 -0
- package/dist/controllers/dismissible_controller.js +83 -18
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js +75 -4
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +2 -0
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +9 -1
- package/dist/controllers/file_dropzone_controller.js +26 -3
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +2 -0
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +4 -0
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +4 -3
- package/dist/controllers/focus_controller.js +75 -4
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.d.ts +50 -10
- package/dist/controllers/form_field_controller.js +280 -62
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.d.ts +10 -8
- package/dist/controllers/form_validation_controller.js +208 -83
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +2 -0
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +2 -0
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +2 -2
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +5 -3
- package/dist/controllers/idle_controller.js +27 -5
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +2 -0
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +2 -0
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +2 -0
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +2 -0
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +2 -0
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +5 -3
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +2 -0
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +48 -12
- package/dist/controllers/multi_select_controller.js +460 -151
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +2 -0
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +2 -0
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +26 -6
- package/dist/controllers/number_input_controller.js +317 -51
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +3 -0
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +2 -0
- package/dist/controllers/overflow_menu_controller.js +6 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +2 -0
- package/dist/controllers/pagination_controller.js +35 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +2 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +5 -3
- package/dist/controllers/password_strength_controller.js +20 -2
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +33 -19
- package/dist/controllers/persist_controller.js +449 -120
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +3 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +2 -2
- package/dist/controllers/popover_controller.js +77 -4
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +3 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +2 -0
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +3 -0
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +44 -15
- package/dist/controllers/radio_group_controller.js +540 -56
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +38 -31
- package/dist/controllers/rating_controller.js +276 -89
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +2 -0
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +2 -0
- package/dist/controllers/resizable_controller.js +33 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +6 -0
- package/dist/controllers/roving_controller.js +60 -5
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +25 -11
- package/dist/controllers/scroll_area_controller.js +557 -125
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
- package/dist/controllers/scroll_visibility_controller.js +33 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +2 -0
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +54 -11
- package/dist/controllers/separator_controller.js +354 -38
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.js +83 -10
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -0
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +2 -0
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +2 -0
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +2 -0
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +2 -0
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +94 -38
- package/dist/controllers/submit_once_controller.js +399 -121
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +2 -0
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +43 -11
- package/dist/controllers/tags_input_controller.js +356 -120
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +2 -0
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +2 -0
- package/dist/controllers/theme_controller.js +8 -6
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +44 -13
- package/dist/controllers/time_picker_controller.js +296 -107
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +4 -0
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +41 -13
- package/dist/controllers/toggle_group_controller.js +378 -55
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +5 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +2 -2
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +4 -3
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +2 -0
- package/dist/controllers/tree_view_controller.js +7 -4
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +4963 -1581
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +123 -6
- package/dist/inspector/cli.js +212 -18
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +274 -50
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +15 -15
- package/dist/inspector/manifest.json +602 -80
- package/dist/positioning/index.d.ts +4 -2
- package/dist/positioning/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/composition_tracker.ts","../../src/utils/safe_timeout.ts","../../src/controllers/auto_submit_controller.ts"],"names":[],"mappings":"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","file":"auto_submit_controller.js","sourcesContent":["/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\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 { CompositionTracker } from \"../utils/composition_tracker\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless **debounced auto-submit** for forms (no dedicated APG pattern). Submits\n * the form a configurable delay after `input`/`change`, so Rails search/filter\n * forms refresh via Turbo without a submit button.\n *\n * Markup contract (identifier: `stimeo--auto-submit`):\n * <form data-controller=\"stimeo--auto-submit\"\n * data-stimeo--auto-submit-debounce-value=\"300\"\n * data-action=\"input->stimeo--auto-submit#submit\n * change->stimeo--auto-submit#submit\">\n * <input type=\"search\" name=\"q\">\n * </form>\n *\n * <!-- Or with the form as a target nested under the controller element: -->\n * <div data-controller=\"stimeo--auto-submit\">\n * <form data-stimeo--auto-submit-target=\"form\"> … </form>\n * </div>\n *\n * @remarks\n * Behavior only — it owns *triggering* the submit (debounce + `requestSubmit`),\n * never the submit itself (Turbo / native form submission) or validation. It\n * **never moves focus** (WCAG 2.2 3.2.2 / 4.1.3): auto-submitting must not yank the\n * caret out of the field. While a result swap is silent for screen-reader users,\n * setting `announce` bridges the completion to the shared `stimeo--announcer`\n * as a safety net; apps can also listen for `stimeo--auto-submit:done`\n * and announce richer text themselves. `aria-busy` marks the in-flight window and\n * `data-auto-submit-pending` the debounce window, for consumer CSS. During IME\n * composition it holds the submit until `compositionend` (the confirmed\n * conversion) so it does not fire on each intermediate keystroke. The debounce\n * timer and the `turbo:submit-end`/composition listeners are torn down on\n * `disconnect()`.\n */\nexport class AutoSubmitController extends Controller<HTMLElement> {\n static override targets = [\"form\"];\n static override values = {\n debounce: { type: Number, default: 300 },\n on: { type: String, default: \"input change\" },\n announce: { type: Boolean, default: false },\n message: { type: String, default: \"\" },\n };\n static actions = [\"submit\"] as const;\n static events = [\"submit\", \"done\"] as const;\n\n declare readonly formTarget: HTMLFormElement;\n declare readonly hasFormTarget: boolean;\n\n declare debounceValue: number;\n declare onValue: string;\n declare announceValue: boolean;\n declare messageValue: string;\n\n /** Debounce timer registry; one `clearAll()` in disconnect tears it down. */\n readonly #timers = new SafeTimeout();\n /** Id of the pending debounce timer, so a new keystroke can reset it. */\n #pendingId = 0;\n\n /** Clears `aria-busy` and emits completion once Turbo finishes the submit. */\n readonly #onSubmitEnd = (): void => {\n this.#form.removeAttribute(\"aria-busy\");\n const message = this.messageValue;\n this.dispatch(\"done\", { detail: { message: message || undefined } });\n // Bridge the silent result swap to the shared Announcer so SR users hear it.\n if (this.announceValue && message) {\n window.dispatchEvent(new CustomEvent(\"stimeo--announcer:announce\", { detail: { message } }));\n }\n };\n\n /** Owns delegated IME lifecycle state and submits confirmed input text. */\n readonly #composition = new CompositionTracker({\n onEnd: (event) => {\n if (this.#triggers(\"input\")) this.#schedule((event.target as HTMLElement | null) ?? null);\n },\n });\n\n override connect(): void {\n this.#form.addEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n this.#composition.observe(this.#form);\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n this.#pendingId = 0;\n this.#composition.disconnect();\n this.#form.removeAttribute(\"data-auto-submit-pending\");\n this.#form.removeEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n }\n\n /**\n * Schedules a debounced submit. Wired to `input`/`change`; the `on` value is an\n * allowlist so a configured subset (e.g. only `change`) is honored even when both\n * are bound in markup. Coalesces rapid events into a single `requestSubmit`.\n */\n submit(event: Event): void {\n if (!this.#triggers(event.type)) return;\n // Ignore `input` events fired mid-IME-composition (e.g. typing kana before the\n // Japanese conversion is confirmed); the confirmed text submits on\n // `compositionend` and the browser's final post-composition `input`.\n if (event.type === \"input\" && this.#composition.isComposing(event as InputEvent)) return;\n this.#schedule((event.target as HTMLElement | null) ?? null);\n }\n\n /** Schedules (and coalesces) the debounced submit for the given trigger. */\n #schedule(trigger: HTMLElement | null): void {\n this.#form.setAttribute(\"data-auto-submit-pending\", \"true\");\n if (this.#pendingId) this.#timers.clear(this.#pendingId);\n\n this.#pendingId = this.#timers.set(() => {\n this.#pendingId = 0;\n this.#form.removeAttribute(\"data-auto-submit-pending\");\n this.dispatch(\"submit\", { detail: { trigger } });\n // `requestSubmit()` runs native constraint validation. If the form is\n // invalid the actual submit never happens — the browser blocks it (or, when\n // a `stimeo--form-validation` set `novalidate`, that controller cancels the\n // submit) — so no `turbo:submit-end` arrives to clear `aria-busy`. Only mark\n // the form busy when the submit will really proceed; still call\n // `requestSubmit()` either way so the validation surfaces to the user.\n if (this.#form.checkValidity()) {\n this.#form.setAttribute(\"aria-busy\", \"true\");\n }\n this.#form.requestSubmit();\n }, this.debounceValue);\n }\n\n /** Resolves the form element (explicit `form` target, else the controller root). */\n get #form(): HTMLFormElement {\n return this.hasFormTarget ? this.formTarget : (this.element as HTMLFormElement);\n }\n\n /** Whether `type` is one of the whitespace-separated event types in `on`. */\n #triggers(type: string): boolean {\n return this.onValue.split(/\\s+/).filter(Boolean).includes(type);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/composition_tracker.ts","../../src/utils/safe_timeout.ts","../../src/controllers/auto_submit_controller.ts"],"names":[],"mappings":"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","file":"auto_submit_controller.js","sourcesContent":["/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\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 { CompositionTracker } from \"../utils/composition_tracker\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless **debounced auto-submit** for forms (no dedicated APG pattern). Submits\n * the form a configurable delay after `input`/`change`, so Rails search/filter\n * forms refresh via Turbo without a submit button.\n *\n * Markup contract (identifier: `stimeo--auto-submit`):\n * <form data-controller=\"stimeo--auto-submit\"\n * data-stimeo--auto-submit-debounce-value=\"300\"\n * data-action=\"input->stimeo--auto-submit#submit\n * change->stimeo--auto-submit#submit\">\n * <input type=\"search\" name=\"q\">\n * </form>\n *\n * <!-- Or with the form as a target nested under the controller element: -->\n * <div data-controller=\"stimeo--auto-submit\">\n * <form data-stimeo--auto-submit-target=\"form\"> … </form>\n * </div>\n *\n * `submit` dispatches `{ trigger }`; `done` dispatches `{ message? }`.\n *\n * @remarks\n * Behavior only — it owns *triggering* the submit (debounce + `requestSubmit`),\n * never the submit itself (Turbo / native form submission) or validation. It\n * **never moves focus** (WCAG 2.2 3.2.2 / 4.1.3): auto-submitting must not yank the\n * caret out of the field. While a result swap is silent for screen-reader users,\n * setting `announce` bridges the completion to the shared `stimeo--announcer`\n * as a safety net; apps can also listen for `stimeo--auto-submit:done`\n * and announce richer text themselves. `aria-busy` marks the in-flight window and\n * `data-auto-submit-pending` the debounce window, for consumer CSS. During IME\n * composition it holds the submit until `compositionend` (the confirmed\n * conversion) so it does not fire on each intermediate keystroke. The debounce\n * timer and the `turbo:submit-end`/composition listeners are torn down on\n * `disconnect()`.\n */\nexport class AutoSubmitController extends Controller<HTMLElement> {\n static override targets = [\"form\"];\n static override values = {\n debounce: { type: Number, default: 300 },\n on: { type: String, default: \"input change\" },\n announce: { type: Boolean, default: false },\n message: { type: String, default: \"\" },\n };\n static actions = [\"submit\"] as const;\n static events = [\"submit\", \"done\"] as const;\n\n declare readonly formTarget: HTMLFormElement;\n declare readonly hasFormTarget: boolean;\n\n declare debounceValue: number;\n declare onValue: string;\n declare announceValue: boolean;\n declare messageValue: string;\n\n /** Debounce timer registry; one `clearAll()` in disconnect tears it down. */\n readonly #timers = new SafeTimeout();\n /** Id of the pending debounce timer, so a new keystroke can reset it. */\n #pendingId = 0;\n\n /** Clears `aria-busy` and emits completion once Turbo finishes the submit. */\n readonly #onSubmitEnd = (): void => {\n this.#form.removeAttribute(\"aria-busy\");\n const message = this.messageValue;\n this.dispatch(\"done\", { detail: { message: message || undefined } });\n // Bridge the silent result swap to the shared Announcer so SR users hear it.\n if (this.announceValue && message) {\n window.dispatchEvent(new CustomEvent(\"stimeo--announcer:announce\", { detail: { message } }));\n }\n };\n\n /** Owns delegated IME lifecycle state and submits confirmed input text. */\n readonly #composition = new CompositionTracker({\n onEnd: (event) => {\n if (this.#triggers(\"input\")) this.#schedule((event.target as HTMLElement | null) ?? null);\n },\n });\n\n override connect(): void {\n this.#form.addEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n this.#composition.observe(this.#form);\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n this.#pendingId = 0;\n this.#composition.disconnect();\n this.#form.removeAttribute(\"data-auto-submit-pending\");\n this.#form.removeEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n }\n\n /**\n * Schedules a debounced submit. Wired to `input`/`change`; the `on` value is an\n * allowlist so a configured subset (e.g. only `change`) is honored even when both\n * are bound in markup. Coalesces rapid events into a single `requestSubmit`.\n */\n submit(event: Event): void {\n if (!this.#triggers(event.type)) return;\n // Ignore `input` events fired mid-IME-composition (e.g. typing kana before the\n // Japanese conversion is confirmed); the confirmed text submits on\n // `compositionend` and the browser's final post-composition `input`.\n if (event.type === \"input\" && this.#composition.isComposing(event as InputEvent)) return;\n this.#schedule((event.target as HTMLElement | null) ?? null);\n }\n\n /** Schedules (and coalesces) the debounced submit for the given trigger. */\n #schedule(trigger: HTMLElement | null): void {\n this.#form.setAttribute(\"data-auto-submit-pending\", \"true\");\n if (this.#pendingId) this.#timers.clear(this.#pendingId);\n\n this.#pendingId = this.#timers.set(() => {\n this.#pendingId = 0;\n this.#form.removeAttribute(\"data-auto-submit-pending\");\n this.dispatch(\"submit\", { detail: { trigger } });\n // `requestSubmit()` runs native constraint validation. If the form is\n // invalid the actual submit never happens — the browser blocks it (or, when\n // a `stimeo--form-validation` set `novalidate`, that controller cancels the\n // submit) — so no `turbo:submit-end` arrives to clear `aria-busy`. Only mark\n // the form busy when the submit will really proceed; still call\n // `requestSubmit()` either way so the validation surfaces to the user.\n if (this.#form.checkValidity()) {\n this.#form.setAttribute(\"aria-busy\", \"true\");\n }\n this.#form.requestSubmit();\n }, this.debounceValue);\n }\n\n /** Resolves the form element (explicit `form` target, else the controller root). */\n get #form(): HTMLFormElement {\n return this.hasFormTarget ? this.formTarget : (this.element as HTMLFormElement);\n }\n\n /** Whether `type` is one of the whitespace-separated event types in `on`. */\n #triggers(type: string): boolean {\n return this.onValue.split(/\\s+/).filter(Boolean).includes(type);\n }\n}\n"]}
|
|
@@ -13,25 +13,28 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
13
13
|
* <span aria-hidden="true" hidden data-stimeo--avatar-target="fallback">JD</span>
|
|
14
14
|
* </span>
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* The `image` and `fallback` targets are optional. A present `src` Value owns the
|
|
17
|
+
* current image's `src`, including an empty Value that explicitly clears it. When
|
|
18
|
+
* the Value is absent, a directly authored `<img src>` is used instead. Runtime
|
|
19
|
+
* Value changes, direct `src` changes, and target replacements all repaint.
|
|
20
|
+
*
|
|
21
|
+
* The accessible name lives on the container (`role="img"` + `aria-label`); the
|
|
22
|
+
* inner image and fallback are `aria-hidden` so assistive technology reads the
|
|
23
|
+
* name once in every visual state.
|
|
21
24
|
*
|
|
22
25
|
* @remarks
|
|
23
26
|
* Behavior only — shape, size, and colour are the consumer's CSS. Initials or
|
|
24
|
-
* colour generation are out of scope:
|
|
25
|
-
* loading
|
|
27
|
+
* colour generation are out of scope: fallback content comes from markup. The
|
|
28
|
+
* `empty`, `loading`, `loaded`, and `error` phases are exposed on `data-state`.
|
|
29
|
+
* A native error from the current image dispatches `stimeo--avatar:error` with
|
|
30
|
+
* `{ src: string }`; reconnecting or reconciling an already-broken image is
|
|
31
|
+
* silent because no new failure event occurred.
|
|
26
32
|
*/
|
|
27
33
|
declare class AvatarController extends Controller<HTMLElement> {
|
|
28
34
|
#private;
|
|
29
35
|
static targets: string[];
|
|
30
36
|
static values: {
|
|
31
|
-
src:
|
|
32
|
-
type: StringConstructor;
|
|
33
|
-
default: string;
|
|
34
|
-
};
|
|
37
|
+
src: StringConstructor;
|
|
35
38
|
};
|
|
36
39
|
static actions: readonly ["onError", "onLoad"];
|
|
37
40
|
static events: readonly ["error"];
|
|
@@ -40,11 +43,29 @@ declare class AvatarController extends Controller<HTMLElement> {
|
|
|
40
43
|
readonly hasImageTarget: boolean;
|
|
41
44
|
readonly hasFallbackTarget: boolean;
|
|
42
45
|
srcValue: string;
|
|
46
|
+
/** Starts retained-DOM observation and silently derives the current phase. */
|
|
43
47
|
connect(): void;
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
+
/** Cancels asynchronous work while retaining the last materialized visual state. */
|
|
49
|
+
disconnect(): void;
|
|
50
|
+
/** Repaints when application code or a Turbo morph changes the source Value. */
|
|
51
|
+
srcValueChanged(): void;
|
|
52
|
+
/** Applies the current source and phase to an image inserted or replaced at runtime. */
|
|
53
|
+
imageTargetConnected(): void;
|
|
54
|
+
/** Returns attributes borrowed from an image that leaves this live avatar. */
|
|
55
|
+
imageTargetDisconnected(image: HTMLImageElement): void;
|
|
56
|
+
/** Applies the current phase to a fallback inserted or replaced at runtime. */
|
|
57
|
+
fallbackTargetConnected(): void;
|
|
58
|
+
/** Returns authored visibility when a fallback leaves this live avatar. */
|
|
59
|
+
fallbackTargetDisconnected(fallback: HTMLElement): void;
|
|
60
|
+
/** Reveals the current image after its native `load` event. */
|
|
61
|
+
onLoad(event: Event): void;
|
|
62
|
+
/**
|
|
63
|
+
* Reveals the fallback after the current image's native `error` event.
|
|
64
|
+
*
|
|
65
|
+
* Dispatches `stimeo--avatar:error` with `{ src: string }`, where `src` is
|
|
66
|
+
* the attempted attribute value. Empty/stale/detached image events are ignored.
|
|
67
|
+
*/
|
|
68
|
+
onError(event: Event): void;
|
|
48
69
|
}
|
|
49
70
|
|
|
50
71
|
export { AvatarController };
|
|
@@ -1,63 +1,260 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
// src/controllers/avatar_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/attribute_lease.ts
|
|
6
|
+
var AttributeLease = class {
|
|
7
|
+
#attribute;
|
|
8
|
+
#records = /* @__PURE__ */ new Map();
|
|
9
|
+
/** @param attribute - The attribute whose temporary values this lease owns. */
|
|
10
|
+
constructor(attribute) {
|
|
11
|
+
this.#attribute = attribute;
|
|
12
|
+
}
|
|
13
|
+
/** Writes or removes the leased attribute while preserving its authored value. */
|
|
14
|
+
write(element, value) {
|
|
15
|
+
const existing = this.#records.get(element);
|
|
16
|
+
if (existing) {
|
|
17
|
+
existing.written = value;
|
|
18
|
+
} else {
|
|
19
|
+
this.#records.set(element, {
|
|
20
|
+
original: element.getAttribute(this.#attribute),
|
|
21
|
+
written: value
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
this.#reflect(element, value);
|
|
25
|
+
}
|
|
26
|
+
/** Returns one lease without overwriting a value subsequently authored by a consumer. */
|
|
27
|
+
return(element) {
|
|
28
|
+
const record = this.#records.get(element);
|
|
29
|
+
if (!record) return;
|
|
30
|
+
this.#records.delete(element);
|
|
31
|
+
const stillOwned = element.getAttribute(this.#attribute) === record.written;
|
|
32
|
+
if (stillOwned) this.#reflect(element, record.original);
|
|
33
|
+
}
|
|
34
|
+
/** Reflects only a real value transition, avoiding self-triggered mutation work. */
|
|
35
|
+
#reflect(element, value) {
|
|
36
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
37
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
38
|
+
else element.setAttribute(this.#attribute, value);
|
|
39
|
+
}
|
|
40
|
+
/** Returns every outstanding lease using the same ownership check as {@link return}. */
|
|
41
|
+
returnAll() {
|
|
42
|
+
for (const element of Array.from(this.#records.keys())) this.return(element);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/utils/before_cache_reset.ts
|
|
47
|
+
var BeforeCacheReset = class _BeforeCacheReset {
|
|
48
|
+
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
49
|
+
static #subscribers = /* @__PURE__ */ new Set();
|
|
50
|
+
/** The shared listener; installed while at least one instance is subscribed. */
|
|
51
|
+
static #onBeforeCache = () => {
|
|
52
|
+
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
53
|
+
};
|
|
54
|
+
#rewind;
|
|
55
|
+
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
56
|
+
constructor(rewind) {
|
|
57
|
+
this.#rewind = rewind;
|
|
58
|
+
}
|
|
59
|
+
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
60
|
+
activate() {
|
|
61
|
+
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
62
|
+
_BeforeCacheReset.#subscribers.add(this);
|
|
63
|
+
if (first) {
|
|
64
|
+
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
68
|
+
deactivate() {
|
|
69
|
+
_BeforeCacheReset.#subscribers.delete(this);
|
|
70
|
+
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
71
|
+
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/utils/microtask_coalescer.ts
|
|
76
|
+
var MicrotaskCoalescer = class {
|
|
77
|
+
#run;
|
|
78
|
+
#queued = false;
|
|
79
|
+
#active = false;
|
|
80
|
+
#generation = 0;
|
|
81
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
82
|
+
constructor(run) {
|
|
83
|
+
this.#run = run;
|
|
84
|
+
}
|
|
85
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
86
|
+
activate() {
|
|
87
|
+
this.#active = true;
|
|
88
|
+
}
|
|
89
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
90
|
+
cancel() {
|
|
91
|
+
this.#active = false;
|
|
92
|
+
this.#queued = false;
|
|
93
|
+
this.#generation += 1;
|
|
94
|
+
}
|
|
95
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
96
|
+
schedule() {
|
|
97
|
+
if (!this.#active || this.#queued) return;
|
|
98
|
+
this.#queued = true;
|
|
99
|
+
const generation = this.#generation;
|
|
100
|
+
queueMicrotask(() => {
|
|
101
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
102
|
+
this.#queued = false;
|
|
103
|
+
this.#run();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
3
108
|
// src/controllers/avatar_controller.ts
|
|
4
109
|
var AvatarController = class extends Controller {
|
|
5
110
|
static targets = ["image", "fallback"];
|
|
6
|
-
static values = {
|
|
7
|
-
src: { type: String, default: "" }
|
|
8
|
-
};
|
|
111
|
+
static values = { src: String };
|
|
9
112
|
static actions = ["onError", "onLoad"];
|
|
10
113
|
static events = ["error"];
|
|
114
|
+
/** Collapses one morph's Value, target, and direct-attribute signals into one pass. */
|
|
115
|
+
#reconcile = new MicrotaskCoalescer(() => this.#render());
|
|
116
|
+
/** Preserves directly-authored source and visibility while targets are controlled. */
|
|
117
|
+
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
118
|
+
#src = new AttributeLease("src");
|
|
119
|
+
#imageHidden = new AttributeLease("hidden");
|
|
120
|
+
#fallbackHidden = new AttributeLease("hidden");
|
|
121
|
+
/** True only while target departure means ownership was actually relinquished. */
|
|
122
|
+
#connected = false;
|
|
123
|
+
/** Watches the non-Value render input used by the direct `<img src>` form. */
|
|
124
|
+
#srcObserver = new MutationObserver((records) => {
|
|
125
|
+
if (records.some((record) => this.hasImageTarget && record.target === this.imageTarget)) {
|
|
126
|
+
this.#reconcile.schedule();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
/** Starts retained-DOM observation and silently derives the current phase. */
|
|
11
130
|
connect() {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
131
|
+
this.#connected = true;
|
|
132
|
+
this.#reconcile.activate();
|
|
133
|
+
this.#beforeCache.activate();
|
|
134
|
+
this.#srcObserver.observe(this.element, {
|
|
135
|
+
attributes: true,
|
|
136
|
+
attributeFilter: ["src"],
|
|
137
|
+
subtree: true
|
|
138
|
+
});
|
|
139
|
+
this.#render();
|
|
140
|
+
}
|
|
141
|
+
/** Cancels asynchronous work while retaining the last materialized visual state. */
|
|
142
|
+
disconnect() {
|
|
143
|
+
this.#connected = false;
|
|
144
|
+
this.#reconcile.cancel();
|
|
145
|
+
this.#beforeCache.deactivate();
|
|
146
|
+
this.#srcObserver.disconnect();
|
|
147
|
+
}
|
|
148
|
+
/** Repaints when application code or a Turbo morph changes the source Value. */
|
|
149
|
+
srcValueChanged() {
|
|
150
|
+
this.#reconcile.schedule();
|
|
151
|
+
}
|
|
152
|
+
/** Applies the current source and phase to an image inserted or replaced at runtime. */
|
|
153
|
+
imageTargetConnected() {
|
|
154
|
+
this.#reconcile.schedule();
|
|
155
|
+
}
|
|
156
|
+
/** Returns attributes borrowed from an image that leaves this live avatar. */
|
|
157
|
+
imageTargetDisconnected(image) {
|
|
158
|
+
if (this.#connected) {
|
|
159
|
+
this.#src.return(image);
|
|
160
|
+
this.#imageHidden.return(image);
|
|
15
161
|
}
|
|
16
|
-
|
|
17
|
-
|
|
162
|
+
this.#reconcile.schedule();
|
|
163
|
+
}
|
|
164
|
+
/** Applies the current phase to a fallback inserted or replaced at runtime. */
|
|
165
|
+
fallbackTargetConnected() {
|
|
166
|
+
this.#reconcile.schedule();
|
|
167
|
+
}
|
|
168
|
+
/** Returns authored visibility when a fallback leaves this live avatar. */
|
|
169
|
+
fallbackTargetDisconnected(fallback) {
|
|
170
|
+
if (this.#connected) this.#fallbackHidden.return(fallback);
|
|
171
|
+
this.#reconcile.schedule();
|
|
172
|
+
}
|
|
173
|
+
/** Reveals the current image after its native `load` event. */
|
|
174
|
+
onLoad(event) {
|
|
175
|
+
const image = this.#eventImage(event);
|
|
176
|
+
if (!image) return;
|
|
177
|
+
if (!image.getAttribute("src")) {
|
|
178
|
+
this.#render();
|
|
179
|
+
return;
|
|
18
180
|
}
|
|
19
|
-
|
|
181
|
+
this.#reflect("loaded");
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Reveals the fallback after the current image's native `error` event.
|
|
185
|
+
*
|
|
186
|
+
* Dispatches `stimeo--avatar:error` with `{ src: string }`, where `src` is
|
|
187
|
+
* the attempted attribute value. Empty/stale/detached image events are ignored.
|
|
188
|
+
*/
|
|
189
|
+
onError(event) {
|
|
190
|
+
const image = this.#eventImage(event);
|
|
191
|
+
if (!image) return;
|
|
192
|
+
const src = image.getAttribute("src");
|
|
20
193
|
if (!src) {
|
|
21
|
-
this.#
|
|
194
|
+
this.#render();
|
|
22
195
|
return;
|
|
23
196
|
}
|
|
24
|
-
|
|
25
|
-
|
|
197
|
+
this.#reflect("error");
|
|
198
|
+
this.dispatch("error", { detail: { src } });
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Materializes the phase from the final source/target DOM after one mutation batch.
|
|
202
|
+
*
|
|
203
|
+
* Cached success and failure are state reconstruction, not new load events, so
|
|
204
|
+
* this pass never dispatches the public `error` event.
|
|
205
|
+
*
|
|
206
|
+
* @stimeoRenderRoot
|
|
207
|
+
*/
|
|
208
|
+
#render() {
|
|
209
|
+
if (!this.hasImageTarget) {
|
|
210
|
+
this.#reflect("empty");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const image = this.imageTarget;
|
|
214
|
+
const src = this.#applySource(image);
|
|
215
|
+
if (!src) {
|
|
216
|
+
this.#reflect("empty");
|
|
26
217
|
return;
|
|
27
218
|
}
|
|
28
|
-
if (
|
|
29
|
-
this
|
|
219
|
+
if (!image.complete) {
|
|
220
|
+
this.#reflect("loading");
|
|
30
221
|
return;
|
|
31
222
|
}
|
|
32
|
-
this.#
|
|
223
|
+
this.#reflect(image.naturalWidth > 0 ? "loaded" : "error");
|
|
33
224
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
this
|
|
225
|
+
/** Applies Value precedence and returns the effective raw `src` attribute. */
|
|
226
|
+
#applySource(image) {
|
|
227
|
+
if (!this.element.hasAttribute("data-stimeo--avatar-src-value")) {
|
|
228
|
+
this.#src.return(image);
|
|
229
|
+
return image.getAttribute("src");
|
|
230
|
+
}
|
|
231
|
+
const desired = this.srcValue.length > 0 ? this.srcValue : null;
|
|
232
|
+
if (image.getAttribute("src") !== desired) this.#src.write(image, desired);
|
|
233
|
+
return desired;
|
|
37
234
|
}
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
this.
|
|
235
|
+
/** Accepts an action only from the singular image target currently controlled. */
|
|
236
|
+
#eventImage(event) {
|
|
237
|
+
const candidate = event.currentTarget;
|
|
238
|
+
if (!(candidate instanceof HTMLImageElement)) return null;
|
|
239
|
+
if (!this.hasImageTarget || candidate !== this.imageTarget) return null;
|
|
240
|
+
return candidate;
|
|
241
|
+
}
|
|
242
|
+
/** Writes the visibility pair and public root state as one controller-owned output. */
|
|
243
|
+
#reflect(state) {
|
|
244
|
+
const showImage = state === "loading" || state === "loaded";
|
|
245
|
+
if (this.hasImageTarget) {
|
|
246
|
+
this.#imageHidden.write(this.imageTarget, showImage ? null : "");
|
|
247
|
+
}
|
|
248
|
+
if (this.hasFallbackTarget) {
|
|
249
|
+
this.#fallbackHidden.write(this.fallbackTarget, showImage ? "" : null);
|
|
250
|
+
}
|
|
251
|
+
this.element.setAttribute("data-state", state);
|
|
43
252
|
}
|
|
44
|
-
/**
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
}
|
|
50
|
-
/** Loaded phase: image visible, fallback hidden. */
|
|
51
|
-
#showImage() {
|
|
52
|
-
if (this.hasImageTarget) this.imageTarget.hidden = false;
|
|
53
|
-
if (this.hasFallbackTarget) this.fallbackTarget.hidden = true;
|
|
54
|
-
this.element.setAttribute("data-state", "loaded");
|
|
55
|
-
}
|
|
56
|
-
/** Error / no-src phase: fallback visible, image hidden. */
|
|
57
|
-
#showFallback() {
|
|
58
|
-
if (this.hasImageTarget) this.imageTarget.hidden = true;
|
|
59
|
-
if (this.hasFallbackTarget) this.fallbackTarget.hidden = false;
|
|
60
|
-
this.element.setAttribute("data-state", "error");
|
|
253
|
+
/** Returns borrowed image and fallback semantics before Turbo snapshots the page. */
|
|
254
|
+
#rewindForCache() {
|
|
255
|
+
this.#src.returnAll();
|
|
256
|
+
this.#imageHidden.returnAll();
|
|
257
|
+
this.#fallbackHidden.returnAll();
|
|
61
258
|
}
|
|
62
259
|
};
|
|
63
260
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/avatar_controller.ts"],"names":[],"mappings":";;;AA0BO,IAAM,gBAAA,GAAN,cAA+B,UAAA,CAAwB;AAAA,EAC5D,OAAgB,OAAA,GAAU,CAAC,OAAA,EAAS,UAAU,CAAA;AAAA,EAC9C,OAAgB,MAAA,GAAS;AAAA,IACvB,GAAA,EAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA;AAAG,GACnC;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,SAAA,EAAW,QAAQ,CAAA;AAAA,EACrC,OAAO,MAAA,GAAS,CAAC,OAAO,CAAA;AAAA,EASf,OAAA,GAAgB;AACvB,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AAExB,MAAA,IAAA,CAAK,aAAA,EAAc;AACnB,MAAA;AAAA,IACF;AAIA,IAAA,IAAI,KAAK,QAAA,EAAU;AACjB,MAAA,IAAA,CAAK,WAAA,CAAY,MAAM,IAAA,CAAK,QAAA;AAAA,IAC9B;AAEA,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,WAAA,CAAY,YAAA,CAAa,KAAK,CAAA;AAC/C,IAAA,IAAI,CAAC,GAAA,EAAK;AAGR,MAAA,IAAA,CAAK,aAAA,EAAc;AACnB,MAAA;AAAA,IACF;AAIA,IAAA,IAAI,KAAK,WAAA,CAAY,QAAA,IAAY,IAAA,CAAK,WAAA,CAAY,eAAe,CAAA,EAAG;AAClE,MAAA,IAAA,CAAK,UAAA,EAAW;AAChB,MAAA;AAAA,IACF;AACA,IAAA,IAAI,KAAK,WAAA,CAAY,QAAA,IAAY,KAAK,WAAA,CAAY,YAAA,KAAiB,KAAK,GAAA,EAAK;AAE3E,MAAA,IAAA,CAAK,OAAA,EAAQ;AACb,MAAA;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,aAAA,EAAc;AAAA,EACrB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,UAAA,EAAW;AAAA,EAClB;AAAA;AAAA,EAGA,OAAA,GAAgB;AACd,IAAA,MAAM,GAAA,GAAM,KAAK,cAAA,GAAkB,IAAA,CAAK,YAAY,YAAA,CAAa,KAAK,KAAK,EAAA,GAAM,EAAA;AACjF,IAAA,IAAA,CAAK,aAAA,EAAc;AACnB,IAAA,IAAA,CAAK,SAAS,OAAA,EAAS,EAAE,QAAQ,EAAE,GAAA,IAAO,CAAA;AAAA,EAC5C;AAAA;AAAA,EAGA,aAAA,GAAsB;AACpB,IAAA,IAAI,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,WAAA,CAAY,MAAA,GAAS,KAAA;AACnD,IAAA,IAAI,IAAA,CAAK,iBAAA,EAAmB,IAAA,CAAK,cAAA,CAAe,MAAA,GAAS,IAAA;AACzD,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,SAAS,CAAA;AAAA,EACnD;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,IAAI,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,WAAA,CAAY,MAAA,GAAS,KAAA;AACnD,IAAA,IAAI,IAAA,CAAK,iBAAA,EAAmB,IAAA,CAAK,cAAA,CAAe,MAAA,GAAS,IAAA;AACzD,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,QAAQ,CAAA;AAAA,EAClD;AAAA;AAAA,EAGA,aAAA,GAAsB;AACpB,IAAA,IAAI,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,WAAA,CAAY,MAAA,GAAS,IAAA;AACnD,IAAA,IAAI,IAAA,CAAK,iBAAA,EAAmB,IAAA,CAAK,cAAA,CAAe,MAAA,GAAS,KAAA;AACzD,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,OAAO,CAAA;AAAA,EACjD;AACF","file":"avatar_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless **Avatar** behavior with image-load fallback. No dedicated APG\n * pattern; it follows the non-text-content practice (WCAG 1.1.1).\n *\n * Markup contract (identifier: `stimeo--avatar`):\n * <span data-controller=\"stimeo--avatar\" role=\"img\" aria-label=\"Jane Doe\"\n * data-stimeo--avatar-src-value=\"/u/123.jpg\">\n * <img alt=\"\" aria-hidden=\"true\"\n * data-stimeo--avatar-target=\"image\"\n * data-action=\"load->stimeo--avatar#onLoad error->stimeo--avatar#onError\" />\n * <span aria-hidden=\"true\" hidden data-stimeo--avatar-target=\"fallback\">JD</span>\n * </span>\n *\n * Watches the `<img>` `load`/`error` events and swaps to the author-provided\n * fallback when the image fails or no `src` is given. The accessible name lives\n * on the container (`role=\"img\"` + `aria-label`); the inner `<img>` and the\n * fallback are `aria-hidden` so assistive tech reads the name once, regardless of\n * which side is visible.\n *\n * @remarks\n * Behavior only — shape, size, and colour are the consumer's CSS. Initials or\n * colour generation are out of scope: the fallback content comes from markup. The\n * loading/loaded/error phase is exposed on `data-state` so CSS can style each.\n */\nexport class AvatarController extends Controller<HTMLElement> {\n static override targets = [\"image\", \"fallback\"];\n static override values = {\n src: { type: String, default: \"\" },\n };\n static actions = [\"onError\", \"onLoad\"] as const;\n static events = [\"error\"] as const;\n\n declare readonly imageTarget: HTMLImageElement;\n declare readonly fallbackTarget: HTMLElement;\n declare readonly hasImageTarget: boolean;\n declare readonly hasFallbackTarget: boolean;\n\n declare srcValue: string;\n\n override connect(): void {\n if (!this.hasImageTarget) {\n // Nothing to monitor: surface the fallback so the container is not empty.\n this.#showFallback();\n return;\n }\n\n // A `src` Value, when present, is the source of truth and is applied to the\n // image; otherwise the markup's own `src` attribute (if any) is honored.\n if (this.srcValue) {\n this.imageTarget.src = this.srcValue;\n }\n\n const src = this.imageTarget.getAttribute(\"src\");\n if (!src) {\n // No image to load at all — go straight to the fallback without emitting an\n // error event (there was no failed load attempt).\n this.#showFallback();\n return;\n }\n\n // A cached image may already be complete by the time the controller connects,\n // in which case the `load` event has fired and won't fire again.\n if (this.imageTarget.complete && this.imageTarget.naturalWidth > 0) {\n this.#showImage();\n return;\n }\n if (this.imageTarget.complete && this.imageTarget.naturalWidth === 0 && src) {\n // Complete but with no intrinsic size means the load already failed.\n this.onError();\n return;\n }\n\n this.#enterLoading();\n }\n\n /** Reveals the image once it has loaded successfully. */\n onLoad(): void {\n this.#showImage();\n }\n\n /** Swaps to the fallback when the image fails and emits `error`. */\n onError(): void {\n const src = this.hasImageTarget ? (this.imageTarget.getAttribute(\"src\") ?? \"\") : \"\";\n this.#showFallback();\n this.dispatch(\"error\", { detail: { src } });\n }\n\n /** Loading phase: keep the image visible (per markup) while it fetches. */\n #enterLoading(): void {\n if (this.hasImageTarget) this.imageTarget.hidden = false;\n if (this.hasFallbackTarget) this.fallbackTarget.hidden = true;\n this.element.setAttribute(\"data-state\", \"loading\");\n }\n\n /** Loaded phase: image visible, fallback hidden. */\n #showImage(): void {\n if (this.hasImageTarget) this.imageTarget.hidden = false;\n if (this.hasFallbackTarget) this.fallbackTarget.hidden = true;\n this.element.setAttribute(\"data-state\", \"loaded\");\n }\n\n /** Error / no-src phase: fallback visible, image hidden. */\n #showFallback(): void {\n if (this.hasImageTarget) this.imageTarget.hidden = true;\n if (this.hasFallbackTarget) this.fallbackTarget.hidden = false;\n this.element.setAttribute(\"data-state\", \"error\");\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/attribute_lease.ts","../../src/utils/before_cache_reset.ts","../../src/utils/microtask_coalescer.ts","../../src/controllers/avatar_controller.ts"],"names":[],"mappings":"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","file":"avatar_controller.js","sourcesContent":["/** One temporarily controlled attribute value and the authored value it displaced. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n written: string | null;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events.\n * A consumer that must not let a derived value become the authored baseline of a\n * cached page returns its leases from its own `turbo:before-cache` rewind, where the\n * matching `disconnect()` also releases the subscription.\n */\nexport class AttributeLease<T extends Element = Element> {\n readonly #attribute: string;\n readonly #records = new Map<T, AttributeLeaseRecord>();\n\n /** @param attribute - The attribute whose temporary values this lease owns. */\n constructor(attribute: string) {\n this.#attribute = attribute;\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const existing = this.#records.get(element);\n if (existing) {\n existing.written = value;\n } else {\n this.#records.set(element, {\n original: element.getAttribute(this.#attribute),\n written: value,\n });\n }\n\n this.#reflect(element, value);\n }\n\n /** Returns one lease without overwriting a value subsequently authored by a consumer. */\n return(element: T): void {\n const record = this.#records.get(element);\n if (!record) return;\n this.#records.delete(element);\n const stillOwned = element.getAttribute(this.#attribute) === record.written;\n if (stillOwned) this.#reflect(element, record.original);\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n if (element.getAttribute(this.#attribute) === value) return;\n if (value === null) element.removeAttribute(this.#attribute);\n else element.setAttribute(this.#attribute, value);\n }\n\n /** Returns every outstanding lease using the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#records.keys())) this.return(element);\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * 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","import { Controller } from \"@hotwired/stimulus\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/** Public rendering phases exposed through the root's `data-state`. */\ntype AvatarState = \"empty\" | \"loading\" | \"loaded\" | \"error\";\n\n/**\n * Headless **Avatar** behavior with image-load fallback. No dedicated APG\n * pattern; it follows the non-text-content practice (WCAG 1.1.1).\n *\n * Markup contract (identifier: `stimeo--avatar`):\n * <span data-controller=\"stimeo--avatar\" role=\"img\" aria-label=\"Jane Doe\"\n * data-stimeo--avatar-src-value=\"/u/123.jpg\">\n * <img alt=\"\" aria-hidden=\"true\"\n * data-stimeo--avatar-target=\"image\"\n * data-action=\"load->stimeo--avatar#onLoad error->stimeo--avatar#onError\" />\n * <span aria-hidden=\"true\" hidden data-stimeo--avatar-target=\"fallback\">JD</span>\n * </span>\n *\n * The `image` and `fallback` targets are optional. A present `src` Value owns the\n * current image's `src`, including an empty Value that explicitly clears it. When\n * the Value is absent, a directly authored `<img src>` is used instead. Runtime\n * Value changes, direct `src` changes, and target replacements all repaint.\n *\n * The accessible name lives on the container (`role=\"img\"` + `aria-label`); the\n * inner image and fallback are `aria-hidden` so assistive technology reads the\n * name once in every visual state.\n *\n * @remarks\n * Behavior only — shape, size, and colour are the consumer's CSS. Initials or\n * colour generation are out of scope: fallback content comes from markup. The\n * `empty`, `loading`, `loaded`, and `error` phases are exposed on `data-state`.\n * A native error from the current image dispatches `stimeo--avatar:error` with\n * `{ src: string }`; reconnecting or reconciling an already-broken image is\n * silent because no new failure event occurred.\n */\nexport class AvatarController extends Controller<HTMLElement> {\n static override targets = [\"image\", \"fallback\"];\n static override values = { src: String };\n static actions = [\"onError\", \"onLoad\"] as const;\n static events = [\"error\"] as const;\n\n declare readonly imageTarget: HTMLImageElement;\n declare readonly fallbackTarget: HTMLElement;\n declare readonly hasImageTarget: boolean;\n declare readonly hasFallbackTarget: boolean;\n declare srcValue: string;\n\n /** Collapses one morph's Value, target, and direct-attribute signals into one pass. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#render());\n /** Preserves directly-authored source and visibility while targets are controlled. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n readonly #src = new AttributeLease<HTMLImageElement>(\"src\");\n readonly #imageHidden = new AttributeLease<HTMLImageElement>(\"hidden\");\n readonly #fallbackHidden = new AttributeLease<HTMLElement>(\"hidden\");\n /** True only while target departure means ownership was actually relinquished. */\n #connected = false;\n /** Watches the non-Value render input used by the direct `<img src>` form. */\n readonly #srcObserver = new MutationObserver((records) => {\n if (records.some((record) => this.hasImageTarget && record.target === this.imageTarget)) {\n this.#reconcile.schedule();\n }\n });\n\n /** Starts retained-DOM observation and silently derives the current phase. */\n override connect(): void {\n this.#connected = true;\n this.#reconcile.activate();\n this.#beforeCache.activate();\n this.#srcObserver.observe(this.element, {\n attributes: true,\n attributeFilter: [\"src\"],\n subtree: true,\n });\n this.#render();\n }\n\n /** Cancels asynchronous work while retaining the last materialized visual state. */\n override disconnect(): void {\n this.#connected = false;\n this.#reconcile.cancel();\n this.#beforeCache.deactivate();\n this.#srcObserver.disconnect();\n }\n\n /** Repaints when application code or a Turbo morph changes the source Value. */\n srcValueChanged(): void {\n this.#reconcile.schedule();\n }\n\n /** Applies the current source and phase to an image inserted or replaced at runtime. */\n imageTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Returns attributes borrowed from an image that leaves this live avatar. */\n imageTargetDisconnected(image: HTMLImageElement): void {\n if (this.#connected) {\n this.#src.return(image);\n this.#imageHidden.return(image);\n }\n this.#reconcile.schedule();\n }\n\n /** Applies the current phase to a fallback inserted or replaced at runtime. */\n fallbackTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Returns authored visibility when a fallback leaves this live avatar. */\n fallbackTargetDisconnected(fallback: HTMLElement): void {\n if (this.#connected) this.#fallbackHidden.return(fallback);\n this.#reconcile.schedule();\n }\n\n /** Reveals the current image after its native `load` event. */\n onLoad(event: Event): void {\n const image = this.#eventImage(event);\n if (!image) return;\n if (!image.getAttribute(\"src\")) {\n this.#render();\n return;\n }\n this.#reflect(\"loaded\");\n }\n\n /**\n * Reveals the fallback after the current image's native `error` event.\n *\n * Dispatches `stimeo--avatar:error` with `{ src: string }`, where `src` is\n * the attempted attribute value. Empty/stale/detached image events are ignored.\n */\n onError(event: Event): void {\n const image = this.#eventImage(event);\n if (!image) return;\n const src = image.getAttribute(\"src\");\n if (!src) {\n this.#render();\n return;\n }\n this.#reflect(\"error\");\n this.dispatch(\"error\", { detail: { src } });\n }\n\n /**\n * Materializes the phase from the final source/target DOM after one mutation batch.\n *\n * Cached success and failure are state reconstruction, not new load events, so\n * this pass never dispatches the public `error` event.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n if (!this.hasImageTarget) {\n this.#reflect(\"empty\");\n return;\n }\n\n const image = this.imageTarget;\n const src = this.#applySource(image);\n if (!src) {\n this.#reflect(\"empty\");\n return;\n }\n if (!image.complete) {\n this.#reflect(\"loading\");\n return;\n }\n this.#reflect(image.naturalWidth > 0 ? \"loaded\" : \"error\");\n }\n\n /** Applies Value precedence and returns the effective raw `src` attribute. */\n #applySource(image: HTMLImageElement): string | null {\n if (!this.element.hasAttribute(\"data-stimeo--avatar-src-value\")) {\n this.#src.return(image);\n return image.getAttribute(\"src\");\n }\n\n const desired = this.srcValue.length > 0 ? this.srcValue : null;\n if (image.getAttribute(\"src\") !== desired) this.#src.write(image, desired);\n return desired;\n }\n\n /** Accepts an action only from the singular image target currently controlled. */\n #eventImage(event: Event): HTMLImageElement | null {\n const candidate = event.currentTarget;\n if (!(candidate instanceof HTMLImageElement)) return null;\n if (!this.hasImageTarget || candidate !== this.imageTarget) return null;\n return candidate;\n }\n\n /** Writes the visibility pair and public root state as one controller-owned output. */\n #reflect(state: AvatarState): void {\n const showImage = state === \"loading\" || state === \"loaded\";\n if (this.hasImageTarget) {\n this.#imageHidden.write(this.imageTarget, showImage ? null : \"\");\n }\n if (this.hasFallbackTarget) {\n this.#fallbackHidden.write(this.fallbackTarget, showImage ? \"\" : null);\n }\n this.element.setAttribute(\"data-state\", state);\n }\n /** Returns borrowed image and fallback semantics before Turbo snapshots the page. */\n #rewindForCache(): void {\n this.#src.returnAll();\n this.#imageHidden.returnAll();\n this.#fallbackHidden.returnAll();\n }\n}\n"]}
|
|
@@ -25,6 +25,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
25
25
|
* author-marked middle items behind a disclosure (`…`) button, which expands
|
|
26
26
|
* them back on demand.
|
|
27
27
|
*
|
|
28
|
+
* `toggle` dispatches `{ expanded: boolean }`.
|
|
29
|
+
*
|
|
28
30
|
* @remarks
|
|
29
31
|
* Behavior only — the consumer owns separators (CSS) and the look. The items to
|
|
30
32
|
* collapse are the author-marked `collapsible` targets (the source of truth),
|