stimeo-ui 0.16.0 → 0.17.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 +31 -0
- package/README.md +8 -1
- package/dist/cable/index.d.ts +79 -23
- package/dist/cable/index.js +363 -436
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +4 -2
- package/dist/controllers/accordion_controller.js +166 -55
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +12 -2
- package/dist/controllers/alert_dialog_controller.js +862 -221
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +35 -5
- package/dist/controllers/announcer_controller.js +135 -198
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.js +0 -10
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +30 -6
- package/dist/controllers/auto_submit_controller.js +271 -127
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +171 -105
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +0 -130
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +18 -0
- package/dist/controllers/bulk_select_controller.js +262 -52
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +24 -5
- package/dist/controllers/calendar_controller.js +175 -209
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +34 -7
- package/dist/controllers/carousel_controller.js +461 -399
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +17 -1
- package/dist/controllers/character_counter_controller.js +246 -156
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +7 -2
- package/dist/controllers/checkbox_controller.js +32 -74
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +16 -5
- package/dist/controllers/clipboard_controller.js +168 -147
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +16 -4
- package/dist/controllers/collapsible_controller.js +301 -120
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +18 -6
- package/dist/controllers/color_picker_controller.js +149 -139
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +23 -5
- package/dist/controllers/combobox_controller.js +360 -133
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +45 -14
- package/dist/controllers/command_palette_controller.js +970 -358
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +15 -2
- package/dist/controllers/conditional_fields_controller.js +166 -124
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +49 -12
- package/dist/controllers/confirm_controller.js +939 -248
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +31 -4
- package/dist/controllers/context_menu_controller.js +256 -128
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +13 -1
- package/dist/controllers/count_up_controller.js +73 -21
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +45 -1
- package/dist/controllers/countdown_controller.js +183 -148
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +21 -2
- package/dist/controllers/currency_input_controller.js +134 -158
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +15 -4
- package/dist/controllers/data_grid_controller.js +114 -186
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +30 -5
- package/dist/controllers/date_range_picker_controller.js +288 -206
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +13 -1
- package/dist/controllers/dialog_controller.js +858 -223
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +24 -7
- package/dist/controllers/direct_upload_controller.js +73 -153
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.js +14 -48
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +19 -0
- package/dist/controllers/dismissible_controller.js +175 -21
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +39 -11
- package/dist/controllers/drawer_controller.js +933 -341
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +31 -1
- package/dist/controllers/dropdown_controller.js +312 -82
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +158 -68
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +67 -53
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +60 -18
- package/dist/controllers/file_dropzone_controller.js +311 -207
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +67 -39
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +31 -8
- package/dist/controllers/flash_controller.js +172 -314
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +12 -3
- package/dist/controllers/focus_controller.js +654 -240
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +147 -132
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +13 -86
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +41 -9
- package/dist/controllers/frame_loading_controller.js +264 -213
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +22 -2
- package/dist/controllers/highlight_controller.js +107 -75
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +56 -3
- package/dist/controllers/hover_card_controller.js +309 -118
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +42 -1
- package/dist/controllers/idle_controller.js +267 -101
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js +71 -72
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +32 -13
- package/dist/controllers/intersection_controller.js +147 -106
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +16 -0
- package/dist/controllers/lazy_frame_controller.js +9 -65
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +34 -2
- package/dist/controllers/listbox_controller.js +306 -204
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +1 -1
- package/dist/controllers/local_time_controller.js +54 -64
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +11 -3
- package/dist/controllers/masonry_controller.js +70 -93
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +32 -7
- package/dist/controllers/menu_controller.js +214 -156
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +4 -4
- package/dist/controllers/menubar_controller.js +170 -329
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +20 -0
- package/dist/controllers/meter_controller.js +151 -67
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +32 -4
- package/dist/controllers/multi_select_controller.js +593 -358
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +8 -1
- package/dist/controllers/navigation_menu_controller.js +124 -232
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +10 -0
- package/dist/controllers/nested_form_controller.js +352 -182
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +23 -0
- package/dist/controllers/network_status_controller.js +242 -54
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +26 -2
- package/dist/controllers/number_input_controller.js +320 -263
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +16 -76
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +20 -4
- package/dist/controllers/otp_controller.js +198 -193
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
- package/dist/controllers/overflow_indicator_controller.js +116 -113
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +48 -11
- package/dist/controllers/overflow_menu_controller.js +343 -283
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +35 -6
- package/dist/controllers/pagination_controller.js +295 -119
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +45 -8
- package/dist/controllers/password_reveal_controller.js +389 -121
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +14 -5
- package/dist/controllers/password_strength_controller.js +99 -153
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +11 -4
- package/dist/controllers/persist_controller.js +237 -103
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +19 -7
- package/dist/controllers/pointer_drag_controller.js +185 -165
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +38 -2
- package/dist/controllers/popover_controller.js +236 -97
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +44 -4
- package/dist/controllers/portal_controller.js +209 -76
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +10 -4
- package/dist/controllers/preview_guard_controller.js +232 -116
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +11 -0
- package/dist/controllers/progress_controller.js +125 -56
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +14 -5
- package/dist/controllers/radio_group_controller.js +117 -137
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +43 -3
- package/dist/controllers/range_slider_controller.js +192 -105
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +20 -4
- package/dist/controllers/rating_controller.js +418 -207
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +2 -0
- package/dist/controllers/read_more_controller.js +134 -63
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +1 -1
- package/dist/controllers/reading_progress_controller.js +147 -135
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +18 -4
- package/dist/controllers/relative_time_controller.js +145 -110
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
- package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- package/dist/controllers/reset_on_restore_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +28 -2
- package/dist/controllers/resizable_controller.js +255 -139
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +39 -84
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -3
- package/dist/controllers/scroll_area_controller.js +428 -225
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +0 -53
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
- package/dist/controllers/scroll_visibility_controller.js +395 -145
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +6 -1
- package/dist/controllers/scrollspy_controller.js +181 -242
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +19 -4
- package/dist/controllers/separator_controller.js +267 -140
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +64 -7
- package/dist/controllers/sidebar_controller.js +1023 -386
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +23 -0
- package/dist/controllers/skeleton_controller.js +248 -97
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +27 -1
- package/dist/controllers/slider_controller.js +159 -79
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
- package/dist/controllers/smart_sticky_header_controller.js +162 -63
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +0 -112
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +39 -4
- package/dist/controllers/spinner_controller.js +277 -169
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +5 -0
- package/dist/controllers/step_indicator_controller.js +99 -46
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +12 -3
- package/dist/controllers/stepper_controller.js +114 -73
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +6 -0
- package/dist/controllers/stick_to_bottom_controller.js +262 -138
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +11 -0
- package/dist/controllers/sticky_observer_controller.js +214 -49
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +23 -8
- package/dist/controllers/submit_once_controller.js +294 -205
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -1
- package/dist/controllers/switch_controller.js +33 -60
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +2 -2
- package/dist/controllers/tabs_controller.js +35 -29
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +12 -0
- package/dist/controllers/tags_input_controller.js +136 -145
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
- package/dist/controllers/textarea_autosize_controller.js +71 -58
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +21 -9
- package/dist/controllers/theme_controller.js +130 -112
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +18 -1
- package/dist/controllers/time_picker_controller.js +146 -80
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +36 -11
- package/dist/controllers/toast_controller.js +186 -297
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +12 -3
- package/dist/controllers/toggle_group_controller.js +95 -136
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +68 -98
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +45 -3
- package/dist/controllers/tooltip_controller.js +158 -124
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +22 -4
- package/dist/controllers/transition_controller.js +123 -119
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +21 -11
- package/dist/controllers/tree_view_controller.js +150 -345
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6370 -9346
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +23 -3
- package/dist/inspector/cli.js +92 -37
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +92 -56
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +8 -8
- package/dist/inspector/manifest.json +787 -100
- package/dist/positioning/index.d.ts +8 -0
- package/dist/positioning/index.js +148 -47
- package/dist/positioning/index.js.map +1 -1
- package/package.json +6 -6
- package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
- package/dist/controllers/reset_before_cache_controller.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/composition_tracker.ts","../../src/controllers/editable_controller.ts"],"names":[],"mappings":"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","file":"editable_controller.js","sourcesContent":["/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Minimal `input` shape needed to tell a composition echo from a fresh edit. */\nexport interface ConfirmedInputSignal {\n readonly target: EventTarget | null;\n readonly inputType?: string;\n}\n\n/**\n * The `inputType` values an engine reports on an `input` that belongs to a\n * composition rather than to an edit the user just made.\n */\nconst COMPOSITION_INPUT_TYPES = new Set([\n \"insertCompositionText\",\n \"insertFromComposition\",\n \"deleteCompositionText\",\n \"deleteByComposition\",\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 * The tracker also owns the other half of that lifecycle: **the `input` some\n * engines send after `compositionend` to echo the text just confirmed.** A\n * consumer that ran `onEnd` and then handled the echo would commit the same\n * confirmation twice, so {@link CompositionTracker.consumesConfirmedInput} tells\n * the echo apart and the consumer returns early on it.\n *\n * **What the echo is not, is the harder half.** Not every engine sends one —\n * Chromium ends a confirmation at `compositionend` with no `input` after it — so\n * a window that waits for the echo and only closes on the next key would swallow\n * the first edit that arrives without one: dictation, autofill, a drop,\n * `insertText`. Two things keep that from happening. The window closes on the\n * next anything — a key, a new composition, any `input`, losing the field — and\n * an `input` that reports an `inputType` outside {@link COMPOSITION_INPUT_TYPES}\n * is an edit, so it is never folded. Only an engine that reports no `inputType`\n * at all is left relying on the window alone.\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 * onInput(event: InputEvent): void {\n * if (this.#composition.consumesConfirmedInput(event)) return;\n * if (this.#composition.isComposing(event)) return;\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 /** The field whose confirming `input` is still owed, while the window is open. */\n #confirmedTarget: EventTarget | null = null;\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 target.addEventListener(\"keydown\", this.#handleKeydown);\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 target.removeEventListener(\"keydown\", this.#handleKeydown);\n this.#activeTargets.delete(target);\n if (this.#confirmedTarget === target) this.#confirmedTarget = null;\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 target.removeEventListener(\"keydown\", this.#handleKeydown);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n this.#confirmedTarget = null;\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 /**\n * Whether `event` is the `input` echoing the composition just confirmed.\n *\n * Asking closes the window either way, so one confirmation is folded at most\n * once and a consumer asks once per `input`.\n */\n consumesConfirmedInput(event: ConfirmedInputSignal): boolean {\n const confirmed = this.#confirmedTarget;\n this.#confirmedTarget = null;\n if (confirmed === null || confirmed !== event.target) return false;\n // An engine that reports no kind at all leaves `inputType` empty, and the\n // window is then the only signal there is.\n const inputType = event.inputType;\n return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);\n }\n\n readonly #handleStart = (event: Event): void => {\n this.#confirmedTarget = null;\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.#confirmedTarget = event.target;\n this.#onEnd?.(event);\n };\n\n /** A key on an observed field opens an edit of its own, so no echo is owed. */\n readonly #handleKeydown = (): void => {\n this.#confirmedTarget = null;\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\n\n/**\n * Headless, accessible inline-editing behavior.\n *\n * Markup contract (identifier: `stimeo--editable`):\n * <div data-controller=\"stimeo--editable\"\n * data-stimeo--editable-submit-on-blur-value=\"true\">\n * <button type=\"button\" aria-label=\"Edit title\"\n * data-stimeo--editable-target=\"display\"\n * data-action=\"click->stimeo--editable#edit\n * keydown->stimeo--editable#onDisplayKeydown\">Current title</button>\n * <input type=\"text\" aria-label=\"Title\" hidden\n * data-stimeo--editable-target=\"input\"\n * data-action=\"keydown->stimeo--editable#onKeydown\" />\n * </div>\n *\n * There is no dedicated APG pattern; this implements a display ⇄ edit toggle with\n * focus management and keyboard commit/cancel, leaning on native form-control\n * labeling (WCAG 1.3.1 / 4.1.2). The look is the consumer's CSS, keyed off\n * `data-mode`.\n *\n * Behavior provided:\n * - Activating the display element (`Enter`/`Space` via the `<button>`, or `F2`)\n * enters edit mode, focuses the input, and selects its text.\n * - `Enter` (single-line) or `Ctrl+Enter` / `Cmd+Enter` (multiline `<textarea>`)\n * saves; `Escape` cancels. Both return focus to the display element. Keys\n * pressed during an IME composition act on the composition, never the edit:\n * a conversion-cancelling `Escape` keeps editing and a conversion-confirming\n * `Enter` never saves (lifecycle-tracked, covering events that omit\n * `isComposing`).\n * - Losing focus while editing saves when `submitOnBlur` is true (the default),\n * honoring wherever focus moved. The departure is watched on the controller\n * element, so it is seen from wherever focus sat — the input, or a Save or\n * Cancel button beside it — and no per-control wiring is required. Focus\n * landing on another element inside this controller has not left the edit, so\n * it commits nothing. When `submitOnBlur` is false, editing is kept.\n * - `save()`, `cancel()`, and `revert()` are actions, so a pointer-only user\n * reaches every path a keyboard user does. `revert()` restores the value from\n * before the last save once, for a consumer whose server rejected it.\n * - Saving dispatches `stimeo--editable:change` with `{ value, previous }` only\n * when the value actually changed; cancelling dispatches\n * `stimeo--editable:cancel` with an empty `{}` detail.\n *\n * The value is whatever the display element declares. A `data-value` attribute\n * holds it when the element's text is a rendering of the value rather than the\n * value itself — a formatted date, a grouped number, an icon beside a label.\n * Without that attribute the trimmed text is the value, and it must then be a\n * string the editing control accepts: `<input type=\"date\">` keeps `2026-08-23`\n * but sanitizes `August 23, 2026` away to an empty string. Saving updates\n * whichever of the two carries the value, so a rendered display keeps its markup\n * and the consumer re-renders it from the `change` event.\n */\nexport class EditableController extends Controller<HTMLElement> {\n static override targets = [\"display\", \"input\"];\n static override values = {\n submitOnBlur: { type: Boolean, default: true },\n };\n static actions = [\"cancel\", \"edit\", \"onDisplayKeydown\", \"onKeydown\", \"revert\", \"save\"] as const;\n static events = [\"cancel\", \"change\"] as const;\n\n declare readonly displayTarget: HTMLElement;\n declare readonly inputTarget: HTMLInputElement | HTMLTextAreaElement;\n declare readonly hasDisplayTarget: boolean;\n declare readonly hasInputTarget: boolean;\n\n declare submitOnBlurValue: boolean;\n\n /** The value captured when edit mode began, used to detect real changes. */\n #previousValue = \"\";\n\n /**\n * The value the last save replaced, or `null` when there is nothing to undo.\n * Cleared on connect: a value from before a page restore is unrecoverable, so\n * `revert()` must not resurrect one.\n */\n #revertValue: string | null = null;\n\n /**\n * Owns IME lifecycle state for the edit surface, so a keydown that belongs to\n * a composition (cancel or confirm) is never treated as an edit command.\n */\n readonly #composition = new CompositionTracker();\n\n /**\n * Watches focus leaving the editor from wherever it currently sits.\n *\n * `focusout` bubbles where `blur` does not, so one listener on the root sees\n * every departure — including one from a Save or Cancel button the consumer\n * placed beside the input. Binding the input alone would make the promise\n * \"saves wherever focus moved\" true only for focus that leaves the input\n * itself, and tabbing straight past an inner button would strand the editor\n * open.\n *\n * @stimeoRuntimeOnly `submitOnBlur` decides whether this one blur commits the edit.\n */\n readonly #onFocusOut = (event: FocusEvent): void => {\n // Entering edit mode hides the display element, and hiding the element that\n // holds focus reports a departure with nowhere to go. That hand-off is the\n // edit starting, not the user leaving it, and the input is focused moments\n // later in the same task. The display is hidden for the whole edit, so it\n // can never be the source of a real departure from one.\n const from = event.target;\n if (this.hasDisplayTarget && from instanceof Node && this.displayTarget.contains(from)) return;\n // Focus reaching another element inside this controller has not left the\n // edit. Without this the commit would beat an inner button's own click —\n // and a Cancel button would save what it promises to discard.\n const next = event.relatedTarget;\n if (next instanceof Node && this.element.contains(next)) return;\n if (this.submitOnBlurValue) this.#commit(false);\n };\n\n /** Establishes the initial display mode (display shown, input hidden). */\n override connect(): void {\n // The tracker is idempotent, so observing here and in the target callback\n // keeps IME tracking correct whichever of the two the framework runs first.\n if (this.hasInputTarget) this.#composition.observe(this.inputTarget);\n this.element.addEventListener(\"focusout\", this.#onFocusOut);\n this.#revertValue = null;\n this.#setMode(\"display\");\n }\n\n /** Releases the composition and focus listeners so nothing outlives the element. */\n override disconnect(): void {\n this.#composition.disconnect();\n this.element.removeEventListener(\"focusout\", this.#onFocusOut);\n }\n\n /** Tracks an input added initially or after connect (e.g. a Turbo swap). */\n inputTargetConnected(input: HTMLElement): void {\n this.#composition.observe(input);\n this.#applyMode();\n }\n\n /** Removes composition listeners when the active input is replaced or removed. */\n inputTargetDisconnected(input: HTMLElement): void {\n this.#composition.unobserve(input);\n }\n\n /** Re-hides or re-shows a display element that arrived after the mode was set. */\n displayTargetConnected(): void {\n this.#applyMode();\n }\n\n /** Enters edit mode: seeds the input from the declared value, focuses, selects. */\n edit(): void {\n if (this.#isEditing || !this.hasInputTarget || !this.hasDisplayTarget) return;\n this.#previousValue = this.#currentValue;\n this.inputTarget.value = this.#previousValue;\n this.#setMode(\"editing\");\n this.inputTarget.focus();\n this.inputTarget.select();\n }\n\n /** Commits the edit and returns focus to the display element. */\n save(): void {\n this.#commit(true);\n }\n\n /** Discards edits, returns to display mode, and dispatches `cancel`. */\n cancel(): void {\n if (!this.#isEditing) return;\n this.#setMode(\"display\");\n if (this.hasDisplayTarget) this.displayTarget.focus();\n this.dispatch(\"cancel\", { detail: {} });\n }\n\n /**\n * Puts back the value the last save replaced — for a consumer whose server\n * rejected it. Silent by design: a `change` here would re-enter the same\n * handler that asked for the undo. One save, one undo.\n */\n revert(): void {\n if (this.#revertValue === null || this.#isEditing) return;\n this.#writeValue(this.#revertValue);\n this.#revertValue = null;\n }\n\n /** Adds `F2` as an editing entry point alongside the button's native activation. */\n onDisplayKeydown(event: KeyboardEvent): void {\n if (event.key === \"F2\") {\n event.preventDefault();\n this.edit();\n }\n }\n\n /** Commits on `Enter` (or `Ctrl+Enter` when multiline) and cancels on `Escape`. */\n onKeydown(event: KeyboardEvent): void {\n // Keys fired during IME composition steer the composition (cancel a\n // conversion, confirm a candidate) and must never cancel or save the edit.\n if (this.#composition.isComposing(event)) return;\n if (event.key === \"Escape\") {\n // Leave a press an inner handler already owned.\n if (event.defaultPrevented) return;\n event.preventDefault();\n this.cancel();\n return;\n }\n if (event.key === \"Enter\") {\n // A descendant widget that already claimed the key (a completion popup\n // confirming its candidate) must not ALSO commit the edit — composition\n // depends on this yield. Kept separate from the Escape guard above, which\n // answers a different question: who owns an Escape press.\n if (event.defaultPrevented) return;\n // A bare Enter inserts a newline in a textarea; only Ctrl+Enter (or Cmd+Enter on\n // macOS, the platform-conventional commit chord) saves there.\n if (this.#isMultiline && !(event.ctrlKey || event.metaKey)) return;\n event.preventDefault();\n this.#commit(true);\n }\n }\n\n /**\n * Returns to display mode, storing the input's value and dispatching `change`\n * when it differs from where editing began.\n *\n * @param restoreFocus - Move focus back to the display element (explicit\n * commit) rather than honoring the user's new focus target (blur).\n */\n #commit(restoreFocus: boolean): void {\n if (!this.#isEditing || !this.hasInputTarget) return;\n // One normalization for all three consumers below, so the stored value, the\n // comparison, and the detail can never disagree about what was saved.\n const value = this.inputTarget.value.trim();\n const previous = this.#previousValue;\n this.#writeValue(value);\n this.#setMode(\"display\");\n if (restoreFocus && this.hasDisplayTarget) this.displayTarget.focus();\n if (value !== previous) {\n this.#revertValue = previous;\n this.dispatch(\"change\", { detail: { value, previous } });\n }\n }\n\n /** Stores `value` wherever the display element declares it. */\n #writeValue(value: string): void {\n if (!this.hasDisplayTarget) return;\n const display = this.displayTarget;\n // Text that renders the value is the consumer's to compose; overwriting it\n // with the raw value would destroy a rendering this controller cannot rebuild.\n if (display.hasAttribute(\"data-value\")) display.dataset.value = value;\n else display.textContent = value;\n }\n\n /** Derives both elements' visibility from the mode currently in the DOM. */\n #applyMode(): void {\n const editing = this.#isEditing;\n if (this.hasDisplayTarget) this.displayTarget.hidden = editing;\n if (this.hasInputTarget) this.inputTarget.hidden = !editing;\n }\n\n /** Records the mode, then brings both elements in line with it. */\n #setMode(mode: \"display\" | \"editing\"): void {\n this.element.dataset.mode = mode;\n this.#applyMode();\n }\n\n /** The value the display element declares, trimmed. */\n get #currentValue(): string {\n const display = this.displayTarget;\n return (display.dataset.value ?? display.textContent ?? \"\").trim();\n }\n\n /** Whether the editing control is a multi-line `<textarea>`. */\n get #isMultiline(): boolean {\n return this.inputTarget.tagName === \"TEXTAREA\";\n }\n\n /** Whether the controller is currently in edit mode. */\n get #isEditing(): boolean {\n return this.element.dataset.mode === \"editing\";\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/shared_registry.ts","../../src/utils/attribute_lease.ts","../../src/utils/composition_tracker.ts","../../src/controllers/editable_controller.ts"],"names":[],"mappings":"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","file":"editable_controller.js","sourcesContent":["/**\n * One registry per page for every copy of the library.\n *\n * Each `stimeo-ui/controllers/*` file, and each opt-in entry, is built on its own and carries\n * its own copy of every utility it uses, so a page that imports controllers one by one, or one\n * of them next to the barrel, evaluates a utility several times. A registry that coordinates\n * instances of different controllers — the stacks of leased values, the loans of a\n * `tabindex`, the Escape layers, the focus traps — must not be split by that: a dialog inside\n * a drawer, or a character counter and a form field on one input, coordinate whichever files\n * they came from. So such a registry lives once on `globalThis`, under a `Symbol.for` key the\n * copies agree on, and the first copy to ask for it creates it.\n *\n * The key names the registry and the version of its shape\n * (`stimeo-ui.<utility>.registry.v<n>`). Copies whose registries have the same shape share\n * one; a release that changes the shape changes the version, so a copy of another release\n * keeps a registry of its own instead of reading entries it does not understand — the two\n * then coordinate within each release only. An entry another copy put in a registry is read\n * through its fields alone, never through `#private` members, which belong to the class of\n * the copy that created it.\n */\n\n/**\n * The registry stored under `key`, created by `create` on first use.\n *\n * @param key - `stimeo-ui.<utility>.registry.v<n>`: the version is that of the shape `create`\n * returns.\n * @param create - Builds the empty registry.\n */\nexport function sharedRegistry<T extends object>(key: string, create: () => T): T {\n const symbol = Symbol.for(key);\n const scope = globalThis as unknown as Record<symbol, T | undefined>;\n const existing = scope[symbol];\n if (existing !== undefined) return existing;\n const registry = create();\n Object.defineProperty(globalThis, symbol, { value: registry });\n return registry;\n}\n","import { sharedRegistry } from \"./shared_registry\";\n\n/** What a lease's record says: the author's value, its last write, the leases beneath. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n readonly written: string | null;\n /** Whether it hands back what the lease right beneath it wrote last. */\n readonly linked: boolean;\n /** The records of the other owners' leases beneath it, nearest first. */\n readonly beneath: readonly string[];\n}\n\n/** One lease's hold on one element's attribute, in the stack every lease of it shares. */\ninterface AttributeHold {\n /** The lease holding it; `null` for a hold read from a copy's record until its lease takes it. */\n lease: object | null;\n readonly record: string;\n written: string | null;\n /** What it hands back with nothing beneath it: the author's value. */\n readonly original: string | null;\n /** Whether it hands back what the hold beneath it wrote last instead. */\n linked: boolean;\n}\n\n/** The holds on each element by attribute, and every lease's record name by attribute. */\ninterface AttributeLeaseRegistry {\n readonly stacks: WeakMap<Element, Map<string, AttributeHold[]>>;\n readonly names: Map<string, Set<string>>;\n}\n\n/** Whether a parsed record entry is an attribute value: a string, or `null` for absence. */\nfunction isAttributeValue(value: unknown): value is string | null {\n return value === null || typeof value === \"string\";\n}\n\n/**\n * Reads the record a lease left on `element`: the author's value, the value the lease wrote\n * last, whether it hands back the value beneath it (`false` before the names when it does not)\n * and the records beneath it. `undefined` when the record is missing or holds anything else.\n */\nfunction readLeaseRecord(element: Element, record: string): AttributeLeaseRecord | undefined {\n try {\n const parsed: unknown = JSON.parse(element.getAttribute(record) ?? \"null\");\n if (!Array.isArray(parsed)) return undefined;\n const [original, written, ...rest] = parsed as unknown[];\n const linked = rest[0] !== false;\n const beneath = linked ? rest : rest.slice(1);\n if (!beneath.every((under): under is string => typeof under === \"string\")) return undefined;\n return isAttributeValue(original) && isAttributeValue(written)\n ? { original, written, linked, beneath }\n : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * The value `element`'s author wrote for `attribute`, as the record a lease of any owner leaves\n * on the element carries it; `undefined` while no lease leaves one, as when nothing has moved\n * the attribute off the author's value. A field a reveal turned from `password` into `text`\n * still reads `\"password\"` here.\n */\nexport function leasedAuthorValue(element: Element, attribute: string): string | null | undefined {\n const suffix = `-${attribute}-lease`;\n for (const name of element.getAttributeNames()) {\n if (!name.startsWith(\"data-\") || !name.endsWith(suffix)) continue;\n const record = readLeaseRecord(element, name);\n if (record) return record.original;\n }\n return undefined;\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 * **Every lease of an attribute shares one stack of holds per element**, whatever its owner\n * and whichever file of the package constructed it (the stacks are one per page): the holds\n * sit in the order their leases first wrote the element, each with the value it wrote last.\n * The element's value is the highest hold's that carries it, and the page's when none does.\n * A write reaches the element unless a hold above the writer carries the element's value: the\n * top hold always shows its writes, a hold beneath it shows nothing while a hold above keeps\n * its value on the element, and once the page has put a value there that no hold carries, the\n * next write of any hold replaces it. A return gives a value back only from the hold the\n * element's value is: the value the hold beneath it wrote last, or the author's once nothing\n * is beneath it; any other return only leaves the stack, and the page's value stays. Holds\n * that wrote the same value are told apart by the stack, never by the value: the higher one\n * keeps it. A first write over a top hold whose value the page has replaced takes the\n * element's value as the author's. Two instances of one controller on one element — an\n * element outside their scope — stack the same way.\n *\n * A **base lease** (`{ base: true }`) holds the element's own state rather than a value over\n * it — a form field's ARIA on its control is one — so its first write goes to the bottom of\n * the stack, beneath the leases of other owners already there: a temporary value another\n * owner leased stays shown and returns to the base lease's latest value.\n *\n * **The stack travels with the element.** While a hold shows a value other than the author's,\n * lies on another hold or is covered by one, the element carries its owner's record\n * `data-<owner>-<attribute>-lease`: JSON of the author's value and the value the hold wrote\n * last (each a string, or `null` for an absent attribute), `false` where the hold hands back\n * the author's value rather than the latest value of the hold beneath it, and the records of\n * every hold beneath it, nearest first. Returning the last lease of an owner removes its\n * record. A copy of the element — a page Turbo restores from its cache is one — therefore\n * carries the stack, and the first lease to touch the copy reads it back under the record\n * names of the leases constructed so far, which include every lease that wrote a record the\n * cache holds, and orders the holds by the records each one lists beneath it. Each lease takes\n * the hold its owner's record describes over in place. Complete records for distinct owners\n * preserve their order across a copy. The records describe holds by owner, not by instance:\n * one owner holding the element more than once leaves only its topmost hold's record, which\n * the first of its leases to write or return on the copy takes. Lower holds of that owner and\n * their positions are absent; mutually listed owners cannot express their full live order.\n * A hold that showed the author's value alone carries no record and is taken by its first\n * write on the copy. Missing or unreadable records also leave their holds unknown, so copying\n * the DOM does not promise identical decisions for every live stack. Without a record it can\n * read, a new stack takes the current value as the author's. A Turbo morph removes records\n * absent from the server HTML; existing holds remain in memory until their leases return.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events,\n * and nothing rewinds a live page before Turbo caches it: `turbo:before-cache` also\n * fires on pages that stay.\n */\nexport class AttributeLease<T extends Element = Element> {\n /** The holds on each element, bottom first, and the record names; one per page. */\n static readonly #registry = sharedRegistry(\n \"stimeo-ui.attribute-lease.registry.v1\",\n (): AttributeLeaseRegistry => ({ stacks: new WeakMap(), names: new Map() }),\n );\n readonly #attribute: string;\n readonly #record: string;\n readonly #base: boolean;\n readonly #held = new Set<T>();\n\n /**\n * @param attribute - The attribute whose temporary values this lease owns.\n * @param owner - Names the record on the element; the controller's identifier.\n * @param options - `base`: written beneath the other owners' leases.\n */\n constructor(attribute: string, owner: string, options: { readonly base?: boolean } = {}) {\n this.#attribute = attribute;\n this.#record = `data-${owner}-${attribute}-lease`;\n this.#base = options.base === true;\n const names = AttributeLease.#registry.names;\n names.set(attribute, (names.get(attribute) ?? new Set<string>()).add(this.#record));\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const stack = this.#stack(element);\n const hold = this.#take(stack) ?? this.#acquire(element, stack);\n this.#held.add(element);\n const shows = !this.#covered(element, stack, hold);\n hold.written = value;\n this.#mark(element, stack);\n if (shows) this.#reflect(element, value);\n }\n\n /**\n * Returns one lease, also on a restored copy, to the value the hold beneath it wrote last or\n * the author's, without overwriting a value a consumer wrote since. Says whether there was a\n * lease to return: a hold this lease has, or the hold a copy's record describes while no live\n * lease of the owner has taken it — a copy of a page an earlier connection wrote carries one.\n */\n return(element: T): boolean {\n const held = this.#held.delete(element);\n const known = AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);\n const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];\n const hold = this.#take(stack);\n if (!hold) return false;\n const carries = element.getAttribute(this.#attribute) === hold.written;\n const owns = carries && !this.#covered(element, stack, hold);\n const index = stack.indexOf(hold);\n stack.splice(index, 1);\n const above = stack[index];\n if (above) above.linked &&= hold.linked;\n this.#mark(element, stack);\n if (!owns) return true;\n const below = hold.linked ? stack[index - 1] : undefined;\n this.#reflect(element, below ? below.written : hold.original);\n return true;\n }\n\n /** Returns every lease it holds, with the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#held)) this.return(element);\n }\n\n /** The element's stack, read from its records the first time. */\n #stack(element: T): AttributeHold[] {\n const stacks = AttributeLease.#registry.stacks;\n const byAttribute = stacks.get(element) ?? new Map<string, AttributeHold[]>();\n stacks.set(element, byAttribute);\n const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);\n byAttribute.set(this.#attribute, stack);\n return stack;\n }\n\n /** The holds a copy's records describe, each above every hold its record lists beneath. */\n #seed(element: T): AttributeHold[] {\n const found: (AttributeLeaseRecord & { readonly record: string })[] = [];\n for (const record of AttributeLease.#registry.names.get(this.#attribute) ?? []) {\n const read = readLeaseRecord(element, record);\n if (read) found.push({ ...read, record });\n }\n const depth = ({ beneath }: (typeof found)[number]) =>\n beneath.filter((name) => found.some((other) => other.record === name)).length;\n return found\n .sort((one, other) => depth(one) - depth(other))\n .map(({ record, written, original, linked }, index) => ({\n lease: null,\n record,\n written,\n original,\n linked: linked && index > 0,\n }));\n }\n\n /** Its hold in `stack`, taking over one read from its record. */\n #take(stack: AttributeHold[]): AttributeHold | undefined {\n const hold =\n stack.find((held) => held.lease === this) ??\n stack.find((held) => held.lease === null && held.record === this.#record);\n if (hold) hold.lease = this;\n return hold;\n }\n\n /** A new hold: on top, over the top hold while the element shows it; a base one at the bottom. */\n #acquire(element: T, stack: AttributeHold[]): AttributeHold {\n const current = element.getAttribute(this.#attribute);\n const top = stack.at(-1);\n const bottom = stack[0];\n const over = top?.written === current ? top : undefined;\n const from = this.#base ? bottom : over;\n const hold: AttributeHold = {\n lease: this,\n record: this.#record,\n written: current,\n original: from ? from.original : current,\n linked: !this.#base && over !== undefined,\n };\n if (this.#base && bottom) {\n bottom.linked = true;\n stack.unshift(hold);\n } else stack.push(hold);\n return hold;\n }\n\n /** Whether a hold above `hold` carries the element's value, which is then that hold's. */\n #covered(element: T, stack: readonly AttributeHold[], hold: AttributeHold): boolean {\n const current = element.getAttribute(this.#attribute);\n return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);\n }\n\n /** Leaves each owner's record of its topmost hold, or none where nothing needs one. */\n #mark(element: T, stack: readonly AttributeHold[]): void {\n const records = new Map<string, string | null>([[this.#record, null]]);\n stack.forEach((hold, index) => {\n const names = stack.slice(0, index).map((under) => under.record);\n const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);\n const marker = hold.linked || beneath.length === 0 ? [] : [false];\n records.set(\n hold.record,\n hold.written === hold.original && beneath.length === 0 && index === stack.length - 1\n ? null\n : JSON.stringify([hold.original, hold.written, ...marker, ...beneath]),\n );\n });\n for (const [name, recorded] of records) {\n if (element.getAttribute(name) === recorded) continue;\n if (recorded === null) element.removeAttribute(name);\n else element.setAttribute(name, recorded);\n }\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","/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Minimal `input` shape needed to tell a composition echo from a fresh edit. */\nexport interface ConfirmedInputSignal {\n readonly target: EventTarget | null;\n readonly inputType?: string;\n}\n\n/**\n * The `inputType` values an engine reports on an `input` that belongs to a\n * composition rather than to an edit the user just made.\n */\nconst COMPOSITION_INPUT_TYPES = new Set([\n \"insertCompositionText\",\n \"insertFromComposition\",\n \"deleteCompositionText\",\n \"deleteByComposition\",\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 * The tracker also owns the other half of that lifecycle: **the `input` some\n * engines send after `compositionend` to echo the text just confirmed.** A\n * consumer that ran `onEnd` and then handled the echo would commit the same\n * confirmation twice, so {@link CompositionTracker.consumesConfirmedInput} tells\n * the echo apart and the consumer returns early on it.\n *\n * **What the echo is not, is the harder half.** Not every engine sends one —\n * Chromium ends a confirmation at `compositionend` with no `input` after it — so\n * a window that waits for the echo and only closes on the next key would swallow\n * the first edit that arrives without one: dictation, autofill, a drop,\n * `insertText`. Two things keep that from happening. The window closes on the\n * next anything — a key, a new composition, any `input`, losing the field — and\n * an `input` that reports an `inputType` outside {@link COMPOSITION_INPUT_TYPES}\n * is an edit, so it is never folded. Only an engine that reports no `inputType`\n * at all is left relying on the window alone.\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 * onInput(event: InputEvent): void {\n * if (this.#composition.consumesConfirmedInput(event)) return;\n * if (this.#composition.isComposing(event)) return;\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 /** The field whose confirming `input` is still owed, while the window is open. */\n #confirmedTarget: EventTarget | null = null;\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 target.addEventListener(\"keydown\", this.#handleKeydown);\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 target.removeEventListener(\"keydown\", this.#handleKeydown);\n this.#activeTargets.delete(target);\n if (this.#confirmedTarget === target) this.#confirmedTarget = null;\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 target.removeEventListener(\"keydown\", this.#handleKeydown);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n this.#confirmedTarget = null;\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 /**\n * Whether `event` is the `input` echoing the composition just confirmed.\n *\n * Asking closes the window either way, so one confirmation is folded at most\n * once and a consumer asks once per `input`.\n */\n consumesConfirmedInput(event: ConfirmedInputSignal): boolean {\n const confirmed = this.#confirmedTarget;\n this.#confirmedTarget = null;\n if (confirmed === null || confirmed !== event.target) return false;\n // An engine that reports no kind at all leaves `inputType` empty, and the\n // window is then the only signal there is.\n const inputType = event.inputType;\n return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);\n }\n\n readonly #handleStart = (event: Event): void => {\n this.#confirmedTarget = null;\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.#confirmedTarget = event.target;\n this.#onEnd?.(event);\n };\n\n /** A key on an observed field opens an edit of its own, so no echo is owed. */\n readonly #handleKeydown = (): void => {\n this.#confirmedTarget = null;\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\n\n/**\n * Headless, accessible inline-editing behavior.\n *\n * Markup contract (identifier: `stimeo--editable`):\n * <div data-controller=\"stimeo--editable\"\n * data-stimeo--editable-submit-on-blur-value=\"true\">\n * <button type=\"button\" aria-label=\"Edit title\"\n * data-stimeo--editable-target=\"display\"\n * data-action=\"click->stimeo--editable#edit\n * keydown->stimeo--editable#onDisplayKeydown\">Current title</button>\n * <input type=\"text\" aria-label=\"Title\" hidden\n * data-stimeo--editable-target=\"input\"\n * data-action=\"keydown->stimeo--editable#onKeydown\" />\n * </div>\n *\n * There is no dedicated APG pattern; this implements a display ⇄ edit toggle with\n * focus management and keyboard commit/cancel, leaning on native form-control\n * labeling (WCAG 1.3.1 / 4.1.2). The look is the consumer's CSS, keyed off\n * `data-mode`.\n *\n * Behavior provided:\n * - Activating the display element (`Enter`/`Space` via the `<button>`, or `F2`)\n * enters edit mode, focuses the input, and selects its text.\n * - `Enter` (single-line) or `Ctrl+Enter` / `Cmd+Enter` (multiline `<textarea>`)\n * saves; `Escape` cancels. Both return focus to the display element. Keys\n * pressed during an IME composition act on the composition, never the edit:\n * a conversion-cancelling `Escape` keeps editing and a conversion-confirming\n * `Enter` never saves (lifecycle-tracked, covering events that omit\n * `isComposing`).\n * - Losing focus while editing saves when `submitOnBlur` is true (the default),\n * honoring wherever focus moved. The departure is watched on the controller\n * element, so it is seen from wherever focus sat — the input, or a Save or\n * Cancel button beside it — and no per-control wiring is required. Focus\n * landing on another element inside this controller has not left the edit, so\n * it commits nothing. When `submitOnBlur` is false, editing is kept.\n * - `save()`, `cancel()`, and `revert()` are actions, so a pointer-only user\n * reaches every path a keyboard user does. `revert()` restores the value from\n * before the last save once, for a consumer whose server rejected it.\n * - Saving dispatches `stimeo--editable:change` with `{ value, previous }` only\n * when the value actually changed; cancelling dispatches\n * `stimeo--editable:cancel` with an empty `{}` detail.\n *\n * The value is whatever the display element declares. A `data-value` attribute\n * holds it when the element's text is a rendering of the value rather than the\n * value itself — a formatted date, a grouped number, an icon beside a label.\n * Without that attribute the trimmed text is the value, and it must then be a\n * string the editing control accepts: `<input type=\"date\">` keeps `2026-08-23`\n * but sanitizes `August 23, 2026` away to an empty string. Saving updates\n * whichever of the two carries the value, so a rendered display keeps its markup\n * and the consumer re-renders it from the `change` event.\n */\nexport class EditableController extends Controller<HTMLElement> {\n static override targets = [\"display\", \"input\"];\n static override values = {\n submitOnBlur: { type: Boolean, default: true },\n };\n static actions = [\"cancel\", \"edit\", \"onDisplayKeydown\", \"onKeydown\", \"revert\", \"save\"] as const;\n static events = [\"cancel\", \"change\"] as const;\n\n declare readonly displayTarget: HTMLElement;\n declare readonly displayTargets: HTMLElement[];\n declare readonly inputTarget: HTMLInputElement | HTMLTextAreaElement;\n declare readonly inputTargets: HTMLElement[];\n declare readonly hasDisplayTarget: boolean;\n declare readonly hasInputTarget: boolean;\n\n declare submitOnBlurValue: boolean;\n\n /** The value captured when edit mode began, used to detect real changes. */\n #previousValue = \"\";\n\n /**\n * The value the last save replaced, or `null` when there is nothing to undo.\n * Cleared on connect: a value from before a page restore is unrecoverable, so\n * `revert()` must not resurrect one.\n */\n #revertValue: string | null = null;\n\n /**\n * Owns IME lifecycle state for the edit surface, so a keydown that belongs to\n * a composition (cancel or confirm) is never treated as an edit command.\n */\n readonly #composition = new CompositionTracker();\n\n /** Owns the `hidden` written on both elements, so one that departs gets its own back. */\n readonly #hidden = new AttributeLease<HTMLElement>(\"hidden\", this.identifier);\n\n /** Whether `connect()` has run and `disconnect()` has not since. */\n #connected = false;\n\n /**\n * Watches focus leaving the editor from wherever it currently sits.\n *\n * `focusout` bubbles where `blur` does not, so one listener on the root sees\n * every departure — including one from a Save or Cancel button the consumer\n * placed beside the input. Binding the input alone would make the promise\n * \"saves wherever focus moved\" true only for focus that leaves the input\n * itself, and tabbing straight past an inner button would strand the editor\n * open.\n *\n * @stimeoRuntimeOnly `submitOnBlur` decides whether this one blur commits the edit.\n */\n readonly #onFocusOut = (event: FocusEvent): void => {\n // Entering edit mode hides the display element, and hiding the element that\n // holds focus reports a departure with nowhere to go. That hand-off is the\n // edit starting, not the user leaving it, and the input is focused moments\n // later in the same task. The display is hidden for the whole edit, so it\n // can never be the source of a real departure from one.\n const from = event.target;\n if (this.hasDisplayTarget && from instanceof Node && this.displayTarget.contains(from)) return;\n // Focus reaching another element inside this controller has not left the\n // edit. Without this the commit would beat an inner button's own click —\n // and a Cancel button would save what it promises to discard.\n const next = event.relatedTarget;\n if (next instanceof Node && this.element.contains(next)) return;\n if (this.submitOnBlurValue) this.#commit(false);\n };\n\n /** Establishes the initial display mode (display shown, input hidden). */\n override connect(): void {\n this.#connected = true;\n // The tracker is idempotent, so observing here and in the target callback\n // keeps IME tracking correct whichever of the two the framework runs first.\n if (this.hasInputTarget) this.#composition.observe(this.inputTarget);\n this.element.addEventListener(\"focusout\", this.#onFocusOut);\n this.#revertValue = null;\n this.#setMode(\"display\");\n }\n\n /** Releases the composition and focus listeners so nothing outlives the element. */\n override disconnect(): void {\n this.#connected = false;\n this.#composition.disconnect();\n this.element.removeEventListener(\"focusout\", this.#onFocusOut);\n }\n\n /** Tracks an input added initially or after connect (e.g. a Turbo swap). */\n inputTargetConnected(input: HTMLElement): void {\n this.#composition.observe(input);\n this.#applyMode();\n }\n\n /**\n * Removes the departing input's composition listeners, gives one that no longer\n * resolves its own `hidden` back, even after `disconnect()`, and while connected\n * re-derives the visibility of the input that stays.\n */\n inputTargetDisconnected(input: HTMLElement): void {\n this.#composition.unobserve(input);\n if (!this.inputTargets.includes(input)) this.#hidden.return(input);\n if (this.#connected) this.#applyMode();\n }\n\n /** Re-hides or re-shows a display element that arrived after the mode was set. */\n displayTargetConnected(): void {\n this.#applyMode();\n }\n\n /**\n * Gives a display element that no longer resolves its own `hidden` back, even after\n * `disconnect()`, and while connected re-hides or re-shows the one that stays.\n */\n displayTargetDisconnected(display: HTMLElement): void {\n if (!this.displayTargets.includes(display)) this.#hidden.return(display);\n if (this.#connected) this.#applyMode();\n }\n\n /** Enters edit mode: seeds the input from the declared value, focuses, selects. */\n edit(): void {\n if (this.#isEditing || !this.hasInputTarget || !this.hasDisplayTarget) return;\n this.#previousValue = this.#currentValue;\n this.inputTarget.value = this.#previousValue;\n this.#setMode(\"editing\");\n this.inputTarget.focus();\n this.inputTarget.select();\n }\n\n /** Commits the edit and returns focus to the display element. */\n save(): void {\n this.#commit(true);\n }\n\n /** Discards edits, returns to display mode, and dispatches `cancel`. */\n cancel(): void {\n if (!this.#isEditing) return;\n this.#setMode(\"display\");\n if (this.hasDisplayTarget) this.displayTarget.focus();\n this.dispatch(\"cancel\", { detail: {} });\n }\n\n /**\n * Puts back the value the last save replaced — for a consumer whose server\n * rejected it. Silent by design: a `change` here would re-enter the same\n * handler that asked for the undo. One save, one undo.\n */\n revert(): void {\n if (this.#revertValue === null || this.#isEditing) return;\n this.#writeValue(this.#revertValue);\n this.#revertValue = null;\n }\n\n /** Adds `F2` as an editing entry point alongside the button's native activation. */\n onDisplayKeydown(event: KeyboardEvent): void {\n if (event.key === \"F2\") {\n event.preventDefault();\n this.edit();\n }\n }\n\n /** Commits on `Enter` (or `Ctrl+Enter` when multiline) and cancels on `Escape`. */\n onKeydown(event: KeyboardEvent): void {\n // Keys fired during IME composition steer the composition (cancel a\n // conversion, confirm a candidate) and must never cancel or save the edit.\n if (this.#composition.isComposing(event)) return;\n if (event.key === \"Escape\") {\n // Leave a press an inner handler already owned.\n if (event.defaultPrevented) return;\n event.preventDefault();\n this.cancel();\n return;\n }\n if (event.key === \"Enter\") {\n // A descendant widget that already claimed the key (a completion popup\n // confirming its candidate) must not ALSO commit the edit — composition\n // depends on this yield. Kept separate from the Escape guard above, which\n // answers a different question: who owns an Escape press.\n if (event.defaultPrevented) return;\n // A bare Enter inserts a newline in a textarea; only Ctrl+Enter (or Cmd+Enter on\n // macOS, the platform-conventional commit chord) saves there.\n if (this.#isMultiline && !(event.ctrlKey || event.metaKey)) return;\n event.preventDefault();\n this.#commit(true);\n }\n }\n\n /**\n * Returns to display mode, storing the input's value and dispatching `change`\n * when it differs from where editing began.\n *\n * @param restoreFocus - Move focus back to the display element (explicit\n * commit) rather than honoring the user's new focus target (blur).\n */\n #commit(restoreFocus: boolean): void {\n if (!this.#isEditing || !this.hasInputTarget) return;\n // One normalization for all three consumers below, so the stored value, the\n // comparison, and the detail can never disagree about what was saved.\n const value = this.inputTarget.value.trim();\n const previous = this.#previousValue;\n this.#writeValue(value);\n this.#setMode(\"display\");\n if (restoreFocus && this.hasDisplayTarget) this.displayTarget.focus();\n if (value !== previous) {\n this.#revertValue = previous;\n this.dispatch(\"change\", { detail: { value, previous } });\n }\n }\n\n /** Stores `value` wherever the display element declares it. */\n #writeValue(value: string): void {\n if (!this.hasDisplayTarget) return;\n const display = this.displayTarget;\n // Text that renders the value is the consumer's to compose; overwriting it\n // with the raw value would destroy a rendering this controller cannot rebuild.\n if (display.hasAttribute(\"data-value\")) display.dataset.value = value;\n else display.textContent = value;\n }\n\n /** Derives both elements' visibility from the mode currently in the DOM. */\n #applyMode(): void {\n const editing = this.#isEditing;\n if (this.hasDisplayTarget) this.#hidden.write(this.displayTarget, editing ? \"\" : null);\n if (this.hasInputTarget) this.#hidden.write(this.inputTarget, editing ? null : \"\");\n }\n\n /** Records the mode, then brings both elements in line with it. */\n #setMode(mode: \"display\" | \"editing\"): void {\n this.element.dataset.mode = mode;\n this.#applyMode();\n }\n\n /** The value the display element declares, trimmed. */\n get #currentValue(): string {\n const display = this.displayTarget;\n return (display.dataset.value ?? display.textContent ?? \"\").trim();\n }\n\n /** Whether the editing control is a multi-line `<textarea>`. */\n get #isMultiline(): boolean {\n return this.inputTarget.tagName === \"TEXTAREA\";\n }\n\n /** Whether the controller is currently in edit mode. */\n get #isEditing(): boolean {\n return this.element.dataset.mode === \"editing\";\n }\n}\n"]}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/empty_state_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/announce.ts
|
|
6
3
|
function announce(message, options = {}) {
|
|
7
4
|
const text = message.trim();
|
|
8
5
|
if (text.length === 0) return;
|
|
@@ -19,7 +16,70 @@ function fillTemplate(template, values) {
|
|
|
19
16
|
});
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
var ListenerSet = class {
|
|
20
|
+
#abort = new AbortController();
|
|
21
|
+
add(target, type, handler, options) {
|
|
22
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
23
|
+
}
|
|
24
|
+
dispose() {
|
|
25
|
+
this.#abort.abort();
|
|
26
|
+
this.#abort = new AbortController();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
var MicrotaskCoalescer = class {
|
|
31
|
+
#run;
|
|
32
|
+
#queued = false;
|
|
33
|
+
#active = false;
|
|
34
|
+
#generation = 0;
|
|
35
|
+
constructor(run) {
|
|
36
|
+
this.#run = run;
|
|
37
|
+
}
|
|
38
|
+
activate() {
|
|
39
|
+
this.#active = true;
|
|
40
|
+
}
|
|
41
|
+
cancel() {
|
|
42
|
+
this.#active = false;
|
|
43
|
+
this.#queued = false;
|
|
44
|
+
this.#generation += 1;
|
|
45
|
+
}
|
|
46
|
+
schedule() {
|
|
47
|
+
if (!this.#active || this.#queued) return;
|
|
48
|
+
this.#queued = true;
|
|
49
|
+
const generation = this.#generation;
|
|
50
|
+
queueMicrotask(() => {
|
|
51
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
52
|
+
this.#queued = false;
|
|
53
|
+
this.#run();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
var MorphRenderWatcher = class {
|
|
59
|
+
#listeners = new ListenerSet();
|
|
60
|
+
#pass;
|
|
61
|
+
#element = null;
|
|
62
|
+
#onMorph = () => this.#pass.schedule();
|
|
63
|
+
constructor(run) {
|
|
64
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
65
|
+
}
|
|
66
|
+
observe(element) {
|
|
67
|
+
if (this.#element === element) return;
|
|
68
|
+
this.disconnect();
|
|
69
|
+
this.#element = element;
|
|
70
|
+
this.#pass.activate();
|
|
71
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
72
|
+
}
|
|
73
|
+
schedule() {
|
|
74
|
+
this.#pass.schedule();
|
|
75
|
+
}
|
|
76
|
+
disconnect() {
|
|
77
|
+
this.#listeners.dispose();
|
|
78
|
+
this.#pass.cancel();
|
|
79
|
+
this.#element = null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
23
83
|
var EmptyStateController = class extends Controller {
|
|
24
84
|
static targets = ["list", "empty"];
|
|
25
85
|
static values = {
|
|
@@ -29,68 +89,40 @@ var EmptyStateController = class extends Controller {
|
|
|
29
89
|
};
|
|
30
90
|
static events = ["change"];
|
|
31
91
|
#observer = null;
|
|
32
|
-
/** Whether the controller is between `connect()` and `disconnect()`. */
|
|
33
92
|
#connected = false;
|
|
34
|
-
/** Last applied empty state; `null` until the first sync so connect emits nothing. */
|
|
35
93
|
#empty = null;
|
|
94
|
+
#morphRender = new MorphRenderWatcher(() => this.#resync());
|
|
36
95
|
connect() {
|
|
96
|
+
this.#morphRender.observe(this.element);
|
|
37
97
|
this.#connected = true;
|
|
38
98
|
this.#syncObservation();
|
|
39
99
|
this.#update();
|
|
40
100
|
}
|
|
41
101
|
disconnect() {
|
|
102
|
+
this.#morphRender.disconnect();
|
|
42
103
|
this.#connected = false;
|
|
43
104
|
this.#stopObserving();
|
|
44
105
|
}
|
|
45
|
-
/** Follows a `list` element swapped in at runtime (Turbo Stream `replace` / morph). */
|
|
46
106
|
listTargetConnected() {
|
|
47
107
|
this.#resync();
|
|
48
108
|
}
|
|
49
|
-
/** Releases the observation when the `list` element leaves the target set. */
|
|
50
109
|
listTargetDisconnected() {
|
|
51
110
|
this.#resync();
|
|
52
111
|
}
|
|
53
|
-
/** Syncs an `empty` element that arrives — or is replaced — at runtime. */
|
|
54
112
|
emptyTargetConnected() {
|
|
55
113
|
this.#resync();
|
|
56
114
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Syncs the `empty` element that remains when one leaves the target set. A
|
|
59
|
-
* single-target getter resolves to the first `empty` element in document order,
|
|
60
|
-
* so a swap that inserts the replacement *before* removing the original (Turbo
|
|
61
|
-
* Stream `after` / `before` / `append` followed by `remove`) leaves the
|
|
62
|
-
* replacement untouched until the original goes — this callback is that moment.
|
|
63
|
-
*/
|
|
64
115
|
emptyTargetDisconnected() {
|
|
65
116
|
this.#resync();
|
|
66
117
|
}
|
|
67
|
-
/** Re-renders when application code (or a Turbo morph) changes `itemSelector` at runtime. */
|
|
68
118
|
itemSelectorValueChanged() {
|
|
69
119
|
this.#resync();
|
|
70
120
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Re-points the observation and re-renders after a target or selector change.
|
|
73
|
-
* The `#connected` guard is load-bearing: Stimulus runs value and target
|
|
74
|
-
* callbacks for the initial markup *before* `connect()` and runs target
|
|
75
|
-
* callbacks during teardown *after* `disconnect()`, and re-observing there
|
|
76
|
-
* would outlive the controller.
|
|
77
|
-
*/
|
|
78
121
|
#resync() {
|
|
79
122
|
if (!this.#connected) return;
|
|
80
123
|
this.#syncObservation();
|
|
81
124
|
this.#update();
|
|
82
125
|
}
|
|
83
|
-
/**
|
|
84
|
-
* Points the mutation observation at the current `list` target — re-resolved on
|
|
85
|
-
* every sync rather than captured at connect, so an element swapped in at
|
|
86
|
-
* runtime is observed instead of the detached original.
|
|
87
|
-
*
|
|
88
|
-
* The observation covers exactly what the count predicate reads. With no
|
|
89
|
-
* `itemSelector` the count is the child element count, which only `childList`
|
|
90
|
-
* can change. With one, the predicate reads the children themselves, so
|
|
91
|
-
* attribute and descendant mutations are watched too — and the controller's own
|
|
92
|
-
* writes are filtered back out, or toggling `hidden` would re-enter the render.
|
|
93
|
-
*/
|
|
94
126
|
#syncObservation() {
|
|
95
127
|
this.#stopObserving();
|
|
96
128
|
if (!this.hasListTarget || typeof MutationObserver === "undefined") return;
|
|
@@ -108,21 +140,11 @@ var EmptyStateController = class extends Controller {
|
|
|
108
140
|
this.#observer?.disconnect();
|
|
109
141
|
this.#observer = null;
|
|
110
142
|
}
|
|
111
|
-
/**
|
|
112
|
-
* Whether a mutation can change the item count. An attribute written on a
|
|
113
|
-
* target this controller owns is its own echo — `hidden` on `list` / `empty`,
|
|
114
|
-
* and the hooks on the controller element when the list *is* that element.
|
|
115
|
-
*/
|
|
116
143
|
#affectsCount(record) {
|
|
117
144
|
if (record.type !== "attributes") return true;
|
|
118
145
|
const own = record.target === this.listTarget || this.hasEmptyTarget && record.target === this.emptyTarget;
|
|
119
146
|
return !own;
|
|
120
147
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Renders the count, then reports a crossed boundary. The announcement copy is
|
|
123
|
-
* read here rather than while rendering: it is the wording of the report, not
|
|
124
|
-
* an input to what is displayed.
|
|
125
|
-
*/
|
|
126
148
|
#update() {
|
|
127
149
|
const count = this.#render();
|
|
128
150
|
if (count === null) return;
|
|
@@ -135,12 +157,6 @@ var EmptyStateController = class extends Controller {
|
|
|
135
157
|
fillTemplate(empty ? this.announceTextValue : this.announceFilledTextValue, { count })
|
|
136
158
|
);
|
|
137
159
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Syncs visibility and the state hooks to the current item count, and returns
|
|
140
|
-
* it. `null` when there is no `list` target to count.
|
|
141
|
-
*
|
|
142
|
-
* @stimeoRenderRoot
|
|
143
|
-
*/
|
|
144
160
|
#render() {
|
|
145
161
|
if (!this.hasListTarget) return null;
|
|
146
162
|
const count = this.#count();
|
|
@@ -155,7 +171,6 @@ var EmptyStateController = class extends Controller {
|
|
|
155
171
|
if (this.hasEmptyTarget) this.emptyTarget.hidden = !empty;
|
|
156
172
|
return count;
|
|
157
173
|
}
|
|
158
|
-
/** Item count: element children matching `itemSelector`, or all element children. */
|
|
159
174
|
#count() {
|
|
160
175
|
const selector = this.itemSelectorValue;
|
|
161
176
|
if (selector.length === 0) return this.listTarget.childElementCount;
|
|
@@ -168,5 +183,4 @@ var EmptyStateController = class extends Controller {
|
|
|
168
183
|
};
|
|
169
184
|
|
|
170
185
|
export { EmptyStateController };
|
|
171
|
-
//# sourceMappingURL=empty_state_controller.js.map
|
|
172
186
|
//# sourceMappingURL=empty_state_controller.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/announce.ts","../../src/controllers/empty_state_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;;;ACDO,IAAM,oBAAA,GAAN,cAAmC,UAAA,CAAwB;AAAA,EAChE,OAAgB,OAAA,GAAU,CAAC,MAAA,EAAQ,OAAO,CAAA;AAAA,EAC1C,OAAgB,MAAA,GAAS;AAAA,IACvB,YAAA,EAAc,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IAC1C,YAAA,EAAc,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IAC1C,kBAAA,EAAoB,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA;AAAG,GAClD;AAAA,EACA,OAAO,MAAA,GAAS,CAAC,QAAQ,CAAA;AAAA,EAWzB,SAAA,GAAqC,IAAA;AAAA;AAAA,EAErC,UAAA,GAAa,KAAA;AAAA;AAAA,EAEb,MAAA,GAAyB,IAAA;AAAA,EAEhB,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,gBAAA,EAAiB;AACtB,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,cAAA,EAAe;AAAA,EACtB;AAAA;AAAA,EAGA,mBAAA,GAA4B;AAC1B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA,EAGA,sBAAA,GAA+B;AAC7B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA,EAGA,oBAAA,GAA6B;AAC3B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,uBAAA,GAAgC;AAC9B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA,EAGA,wBAAA,GAAiC;AAC/B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,OAAA,GAAgB;AACd,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,gBAAA,EAAiB;AACtB,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,gBAAA,GAAyB;AACvB,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,IAAI,CAAC,IAAA,CAAK,aAAA,IAAiB,OAAO,qBAAqB,WAAA,EAAa;AACpE,IAAA,MAAM,YAAA,GAAe,IAAA,CAAK,iBAAA,CAAkB,MAAA,GAAS,CAAA;AACrD,IAAA,IAAA,CAAK,SAAA,GAAY,IAAI,gBAAA,CAAiB,CAAC,OAAA,KAAY;AACjD,MAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,CAAC,MAAA,KAAW,IAAA,CAAK,cAAc,MAAM,CAAC,CAAA,EAAG,IAAA,CAAK,OAAA,EAAQ;AAAA,IACzE,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,IAAA,CAAK,UAAA,EAAY;AAAA,MACtC,SAAA,EAAW,IAAA;AAAA,MACX,OAAA,EAAS,YAAA;AAAA,MACT,UAAA,EAAY;AAAA,KACb,CAAA;AAAA,EACH;AAAA,EAEA,cAAA,GAAuB;AACrB,IAAA,IAAA,CAAK,WAAW,UAAA,EAAW;AAC3B,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,MAAA,EAAiC;AAC7C,IAAA,IAAI,MAAA,CAAO,IAAA,KAAS,YAAA,EAAc,OAAO,IAAA;AACzC,IAAA,MAAM,GAAA,GACJ,OAAO,MAAA,KAAW,IAAA,CAAK,cACtB,IAAA,CAAK,cAAA,IAAkB,MAAA,CAAO,MAAA,KAAW,IAAA,CAAK,WAAA;AACjD,IAAA,OAAO,CAAC,GAAA;AAAA,EACV;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAA,GAAgB;AACd,IAAA,MAAM,KAAA,GAAQ,KAAK,OAAA,EAAQ;AAC3B,IAAA,IAAI,UAAU,IAAA,EAAM;AACpB,IAAA,MAAM,QAAQ,KAAA,KAAU,CAAA;AAGxB,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,KAAW,IAAA,IAAQ,UAAU,IAAA,CAAK,MAAA;AACvD,IAAA,IAAA,CAAK,MAAA,GAAS,KAAA;AACd,IAAA,IAAI,CAAC,OAAA,EAAS;AAEd,IAAA,IAAA,CAAK,QAAA,CAAS,UAAU,EAAE,MAAA,EAAQ,EAAE,KAAA,EAAO,KAAA,IAAS,CAAA;AAGpD,IAAA,QAAA;AAAA,MACE,YAAA,CAAa,QAAQ,IAAA,CAAK,iBAAA,GAAoB,KAAK,uBAAA,EAAyB,EAAE,OAAO;AAAA,KACvF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAA,GAAyB;AACvB,IAAA,IAAI,CAAC,IAAA,CAAK,aAAA,EAAe,OAAO,IAAA;AAChC,IAAA,MAAM,KAAA,GAAQ,KAAK,MAAA,EAAO;AAC1B,IAAA,MAAM,QAAQ,KAAA,KAAU,CAAA;AAExB,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,MAAA,CAAO,KAAK,CAAC,CAAA;AACrD,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,MAAM,CAAA;AAAA,IAChD,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,YAAY,CAAA;AAAA,IAC3C;AACA,IAAA,IAAA,CAAK,WAAW,MAAA,GAAS,KAAA;AACzB,IAAA,IAAI,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,WAAA,CAAY,SAAS,CAAC,KAAA;AACpD,IAAA,OAAO,KAAA;AAAA,EACT;AAAA;AAAA,EAGA,MAAA,GAAiB;AACf,IAAA,MAAM,WAAW,IAAA,CAAK,iBAAA;AACtB,IAAA,IAAI,QAAA,CAAS,MAAA,KAAW,CAAA,EAAG,OAAO,KAAK,UAAA,CAAW,iBAAA;AAClD,IAAA,IAAI;AACF,MAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,UAAA,CAAW,QAAQ,CAAA,CAAE,MAAA,CAAO,CAAC,KAAA,KAAU,KAAA,CAAM,OAAA,CAAQ,QAAQ,CAAC,CAAA,CAAE,MAAA;AAAA,IACzF,CAAA,CAAA,MAAQ;AAEN,MAAA,OAAO,KAAK,UAAA,CAAW,iBAAA;AAAA,IACzB;AAAA,EACF;AACF","file":"empty_state_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","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\n\n/**\n * Headless empty-state behavior: shows an \"empty\" placeholder when a list has no\n * items and hides it once one arrives (and vice-versa), watching the list with a\n * `MutationObserver` (no dedicated APG pattern; follows the WCAG \"status messages\"\n * practice when announcing).\n *\n * Markup contract (identifier: `stimeo--empty-state`):\n * <div data-controller=\"stimeo--empty-state\">\n * <ul data-stimeo--empty-state-target=\"list\"><!-- Turbo Stream rows --></ul>\n * <p data-stimeo--empty-state-target=\"empty\" hidden>No items</p>\n * </div>\n *\n * Counts `list`'s child items (all element children, or those matching\n * `itemSelector`) on connect and on every mutation that can change that count,\n * toggles `hidden` on `list` / `empty` at the 0 ↔ 1+ boundary, and reflects\n * `data-empty` / `data-count` on the controller element. Crossing the boundary\n * dispatches `change` after the display is updated, so a listener reads the\n * state the crossing produced.\n *\n * `change` dispatches `{ count, empty }`.\n *\n * @remarks\n * Behavior only — the placeholder's look/copy is the consumer's. State is derived\n * from the DOM (no module-scope state), so `connect()` re-syncs after a Turbo\n * Stream insertion. Both targets are re-resolved at runtime: a `list` element\n * swapped in re-points the observation, and a swapped-in `empty` element is\n * re-synced — including when the replacement is inserted while the original is\n * still in the document. The `MutationObserver` is severed on `disconnect()`\n * (Turbo navigation included). `announceText` / `announceFilledText` are read at\n * the crossing and handed to the page's announcer.\n *\n * Ownership note: this controller deliberately owns `hidden` on the `list` / `empty`\n * targets as its single source of truth for which one is shown, rather than only\n * emitting `data-empty` and delegating visibility to consumer CSS. The toggle is\n * unconditional (set every sync), so there is nothing to save/restore and no\n * authored `hidden` to preserve — the displayed half is always a pure function of\n * the item count. Consumers wanting CSS-driven visibility should not also set\n * `hidden` on these targets themselves.\n */\nexport class EmptyStateController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"empty\"];\n static override values = {\n itemSelector: { type: String, default: \"\" },\n announceText: { type: String, default: \"\" },\n announceFilledText: { type: String, default: \"\" },\n };\n static events = [\"change\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly emptyTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasEmptyTarget: boolean;\n\n declare itemSelectorValue: string;\n declare announceTextValue: string;\n declare announceFilledTextValue: string;\n\n #observer: MutationObserver | null = null;\n /** Whether the controller is between `connect()` and `disconnect()`. */\n #connected = false;\n /** Last applied empty state; `null` until the first sync so connect emits nothing. */\n #empty: boolean | null = null;\n\n override connect(): void {\n this.#connected = true;\n this.#syncObservation();\n this.#update();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#stopObserving();\n }\n\n /** Follows a `list` element swapped in at runtime (Turbo Stream `replace` / morph). */\n listTargetConnected(): void {\n this.#resync();\n }\n\n /** Releases the observation when the `list` element leaves the target set. */\n listTargetDisconnected(): void {\n this.#resync();\n }\n\n /** Syncs an `empty` element that arrives — or is replaced — at runtime. */\n emptyTargetConnected(): void {\n this.#resync();\n }\n\n /**\n * Syncs the `empty` element that remains when one leaves the target set. A\n * single-target getter resolves to the first `empty` element in document order,\n * so a swap that inserts the replacement *before* removing the original (Turbo\n * Stream `after` / `before` / `append` followed by `remove`) leaves the\n * replacement untouched until the original goes — this callback is that moment.\n */\n emptyTargetDisconnected(): void {\n this.#resync();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `itemSelector` at runtime. */\n itemSelectorValueChanged(): void {\n this.#resync();\n }\n\n /**\n * Re-points the observation and re-renders after a target or selector change.\n * The `#connected` guard is load-bearing: Stimulus runs value and target\n * callbacks for the initial markup *before* `connect()` and runs target\n * callbacks during teardown *after* `disconnect()`, and re-observing there\n * would outlive the controller.\n */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncObservation();\n this.#update();\n }\n\n /**\n * Points the mutation observation at the current `list` target — re-resolved on\n * every sync rather than captured at connect, so an element swapped in at\n * runtime is observed instead of the detached original.\n *\n * The observation covers exactly what the count predicate reads. With no\n * `itemSelector` the count is the child element count, which only `childList`\n * can change. With one, the predicate reads the children themselves, so\n * attribute and descendant mutations are watched too — and the controller's own\n * writes are filtered back out, or toggling `hidden` would re-enter the render.\n */\n #syncObservation(): void {\n this.#stopObserving();\n if (!this.hasListTarget || typeof MutationObserver === \"undefined\") return;\n const watchesItems = this.itemSelectorValue.length > 0;\n this.#observer = new MutationObserver((records) => {\n if (records.some((record) => this.#affectsCount(record))) this.#update();\n });\n this.#observer.observe(this.listTarget, {\n childList: true,\n subtree: watchesItems,\n attributes: watchesItems,\n });\n }\n\n #stopObserving(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /**\n * Whether a mutation can change the item count. An attribute written on a\n * target this controller owns is its own echo — `hidden` on `list` / `empty`,\n * and the hooks on the controller element when the list *is* that element.\n */\n #affectsCount(record: MutationRecord): boolean {\n if (record.type !== \"attributes\") return true;\n const own =\n record.target === this.listTarget ||\n (this.hasEmptyTarget && record.target === this.emptyTarget);\n return !own;\n }\n\n /**\n * Renders the count, then reports a crossed boundary. The announcement copy is\n * read here rather than while rendering: it is the wording of the report, not\n * an input to what is displayed.\n */\n #update(): void {\n const count = this.#render();\n if (count === null) return;\n const empty = count === 0;\n // Report only when the 0 ↔ 1+ boundary is crossed (not on the initial sync,\n // and not for count changes that stay non-empty, e.g. 2 → 3).\n const crossed = this.#empty !== null && empty !== this.#empty;\n this.#empty = empty;\n if (!crossed) return;\n\n this.dispatch(\"change\", { detail: { count, empty } });\n // The crossing is the news, and the page's announcer reads it: a region that\n // only becomes live when the empty state appears is not reliably announced.\n announce(\n fillTemplate(empty ? this.announceTextValue : this.announceFilledTextValue, { count }),\n );\n }\n\n /**\n * Syncs visibility and the state hooks to the current item count, and returns\n * it. `null` when there is no `list` target to count.\n *\n * @stimeoRenderRoot\n */\n #render(): number | null {\n if (!this.hasListTarget) return null;\n const count = this.#count();\n const empty = count === 0;\n\n this.element.setAttribute(\"data-count\", String(count));\n if (empty) {\n this.element.setAttribute(\"data-empty\", \"true\");\n } else {\n this.element.removeAttribute(\"data-empty\");\n }\n this.listTarget.hidden = empty;\n if (this.hasEmptyTarget) this.emptyTarget.hidden = !empty;\n return count;\n }\n\n /** Item count: element children matching `itemSelector`, or all element children. */\n #count(): number {\n const selector = this.itemSelectorValue;\n if (selector.length === 0) return this.listTarget.childElementCount;\n try {\n return Array.from(this.listTarget.children).filter((child) => child.matches(selector)).length;\n } catch {\n // An invalid selector (author typo) must not crash the controller — count all.\n return this.listTarget.childElementCount;\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/listener_set.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/morph_render_watcher.ts","../../src/controllers/empty_state_controller.ts"],"names":[],"mappings":"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","file":"empty_state_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 * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt. {@link\n * ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, which places `dispose()` in its\n * cleanup sequence. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, or one that\n * lives for less than the connection, keeps its own pair of calls.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included, which has to match at release time and no\n * longer has a second place to drift from.\n */\n add(\n target: EventTarget,\n type: string,\n handler: EventListener,\n options?: Omit<AddEventListenerOptions, \"signal\">,\n ): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\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 * @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 { ListenerSet } from \"./listener_set\";\nimport { MicrotaskCoalescer } from \"./microtask_coalescer\";\n\n/**\n * Coalesces retained-element morphs and explicit lifecycle requests into one pass.\n *\n * Observe the controller's element after its initial state is settled. Morphs on\n * that element and bubbling morphs on its descendants share the same microtask\n * as {@link schedule}. The callback repairs derived output; the controller owns\n * any logical transition reporting. {@link disconnect} releases the listener and\n * cancels pending work, including work from an earlier connected lifetime.\n */\nexport class MorphRenderWatcher {\n readonly #listeners = new ListenerSet();\n readonly #pass: MicrotaskCoalescer;\n #element: Element | null = null;\n readonly #onMorph = (): void => this.#pass.schedule();\n\n /** @param run - the single pass that repairs the settled controller's output. */\n constructor(run: () => void) {\n this.#pass = new MicrotaskCoalescer(run);\n }\n\n /** Opens observation; observing the same element again preserves queued work. */\n observe(element: Element): void {\n if (this.#element === element) return;\n this.disconnect();\n this.#element = element;\n this.#pass.activate();\n this.#listeners.add(element, \"turbo:morph-element\", this.#onMorph);\n }\n\n /** Requests one pass; requests outside an observed lifetime are ignored. */\n schedule(): void {\n this.#pass.schedule();\n }\n\n /** Releases observation and pending work synchronously; safe to repeat. */\n disconnect(): void {\n this.#listeners.dispose();\n this.#pass.cancel();\n this.#element = null;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { MorphRenderWatcher } from \"../utils/morph_render_watcher\";\n\n/**\n * Headless empty-state behavior: shows an \"empty\" placeholder when a list has no\n * items and hides it once one arrives (and vice-versa), watching the list with a\n * `MutationObserver` (no dedicated APG pattern; follows the WCAG \"status messages\"\n * practice when announcing).\n *\n * Markup contract (identifier: `stimeo--empty-state`):\n * <div data-controller=\"stimeo--empty-state\">\n * <ul data-stimeo--empty-state-target=\"list\"><!-- Turbo Stream rows --></ul>\n * <p data-stimeo--empty-state-target=\"empty\" hidden>No items</p>\n * </div>\n *\n * Counts `list`'s child items (all element children, or those matching\n * `itemSelector`) on connect and on every mutation that can change that count,\n * toggles `hidden` on `list` / `empty` at the 0 ↔ 1+ boundary, and reflects\n * `data-empty` / `data-count` on the controller element. Crossing the boundary\n * dispatches `change` after the display is updated, so a listener reads the\n * state the crossing produced.\n *\n * `change` dispatches `{ count, empty }`.\n *\n * @remarks\n * Behavior only — the placeholder's look/copy is the consumer's. State is derived\n * from the DOM (no module-scope state), so `connect()` re-syncs after a Turbo\n * Stream insertion. Both targets are re-resolved at runtime: a `list` element\n * swapped in re-points the observation, and a swapped-in `empty` element is\n * re-synced — including when the replacement is inserted while the original is\n * still in the document. The `MutationObserver` is severed on `disconnect()`\n * (Turbo navigation included). `announceText` / `announceFilledText` are read at\n * the crossing and handed to the page's announcer.\n *\n * Ownership note: this controller deliberately owns `hidden` on the `list` / `empty`\n * targets as its single source of truth for which one is shown, rather than only\n * emitting `data-empty` and delegating visibility to consumer CSS. The toggle is\n * unconditional (set every sync), so there is nothing to save/restore and no\n * authored `hidden` to preserve — the displayed half is always a pure function of\n * the item count. Consumers wanting CSS-driven visibility should not also set\n * `hidden` on these targets themselves.\n */\nexport class EmptyStateController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"empty\"];\n static override values = {\n itemSelector: { type: String, default: \"\" },\n announceText: { type: String, default: \"\" },\n announceFilledText: { type: String, default: \"\" },\n };\n static events = [\"change\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly emptyTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasEmptyTarget: boolean;\n\n declare itemSelectorValue: string;\n declare announceTextValue: string;\n declare announceFilledTextValue: string;\n\n #observer: MutationObserver | null = null;\n /** Whether the controller is between `connect()` and `disconnect()`. */\n #connected = false;\n /** Last applied empty state; `null` until the first sync so connect emits nothing. */\n #empty: boolean | null = null;\n\n readonly #morphRender = new MorphRenderWatcher(() => this.#resync());\n\n override connect(): void {\n this.#morphRender.observe(this.element);\n this.#connected = true;\n this.#syncObservation();\n this.#update();\n }\n\n override disconnect(): void {\n this.#morphRender.disconnect();\n this.#connected = false;\n this.#stopObserving();\n }\n\n /** Follows a `list` element swapped in at runtime (Turbo Stream `replace` / morph). */\n listTargetConnected(): void {\n this.#resync();\n }\n\n /** Releases the observation when the `list` element leaves the target set. */\n listTargetDisconnected(): void {\n this.#resync();\n }\n\n /** Syncs an `empty` element that arrives — or is replaced — at runtime. */\n emptyTargetConnected(): void {\n this.#resync();\n }\n\n /**\n * Syncs the `empty` element that remains when one leaves the target set. A\n * single-target getter resolves to the first `empty` element in document order,\n * so a swap that inserts the replacement *before* removing the original (Turbo\n * Stream `after` / `before` / `append` followed by `remove`) leaves the\n * replacement untouched until the original goes — this callback is that moment.\n */\n emptyTargetDisconnected(): void {\n this.#resync();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `itemSelector` at runtime. */\n itemSelectorValueChanged(): void {\n this.#resync();\n }\n\n /**\n * Re-points the observation and re-renders after a target or selector change.\n * The `#connected` guard is load-bearing: Stimulus runs value and target\n * callbacks for the initial markup *before* `connect()` and runs target\n * callbacks during teardown *after* `disconnect()`, and re-observing there\n * would outlive the controller.\n */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncObservation();\n this.#update();\n }\n\n /**\n * Points the mutation observation at the current `list` target — re-resolved on\n * every sync rather than captured at connect, so an element swapped in at\n * runtime is observed instead of the detached original.\n *\n * The observation covers exactly what the count predicate reads. With no\n * `itemSelector` the count is the child element count, which only `childList`\n * can change. With one, the predicate reads the children themselves, so\n * attribute and descendant mutations are watched too — and the controller's own\n * writes are filtered back out, or toggling `hidden` would re-enter the render.\n */\n #syncObservation(): void {\n this.#stopObserving();\n if (!this.hasListTarget || typeof MutationObserver === \"undefined\") return;\n const watchesItems = this.itemSelectorValue.length > 0;\n this.#observer = new MutationObserver((records) => {\n if (records.some((record) => this.#affectsCount(record))) this.#update();\n });\n this.#observer.observe(this.listTarget, {\n childList: true,\n subtree: watchesItems,\n attributes: watchesItems,\n });\n }\n\n #stopObserving(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /**\n * Whether a mutation can change the item count. An attribute written on a\n * target this controller owns is its own echo — `hidden` on `list` / `empty`,\n * and the hooks on the controller element when the list *is* that element.\n */\n #affectsCount(record: MutationRecord): boolean {\n if (record.type !== \"attributes\") return true;\n const own =\n record.target === this.listTarget ||\n (this.hasEmptyTarget && record.target === this.emptyTarget);\n return !own;\n }\n\n /**\n * Renders the count, then reports a crossed boundary. The announcement copy is\n * read here rather than while rendering: it is the wording of the report, not\n * an input to what is displayed.\n */\n #update(): void {\n const count = this.#render();\n if (count === null) return;\n const empty = count === 0;\n // Report only when the 0 ↔ 1+ boundary is crossed (not on the initial sync,\n // and not for count changes that stay non-empty, e.g. 2 → 3).\n const crossed = this.#empty !== null && empty !== this.#empty;\n this.#empty = empty;\n if (!crossed) return;\n\n this.dispatch(\"change\", { detail: { count, empty } });\n // The crossing is the news, and the page's announcer reads it: a region that\n // only becomes live when the empty state appears is not reliably announced.\n announce(\n fillTemplate(empty ? this.announceTextValue : this.announceFilledTextValue, { count }),\n );\n }\n\n /**\n * Syncs visibility and the state hooks to the current item count, and returns\n * it. `null` when there is no `list` target to count.\n *\n * @stimeoRenderRoot\n */\n #render(): number | null {\n if (!this.hasListTarget) return null;\n const count = this.#count();\n const empty = count === 0;\n\n this.element.setAttribute(\"data-count\", String(count));\n if (empty) {\n this.element.setAttribute(\"data-empty\", \"true\");\n } else {\n this.element.removeAttribute(\"data-empty\");\n }\n this.listTarget.hidden = empty;\n if (this.hasEmptyTarget) this.emptyTarget.hidden = !empty;\n return count;\n }\n\n /** Item count: element children matching `itemSelector`, or all element children. */\n #count(): number {\n const selector = this.itemSelectorValue;\n if (selector.length === 0) return this.listTarget.childElementCount;\n try {\n return Array.from(this.listTarget.children).filter((child) => child.matches(selector)).length;\n } catch {\n // An invalid selector (author typo) must not crash the controller — count all.\n return this.listTarget.childElementCount;\n }\n }\n}\n"]}
|
|
@@ -54,7 +54,10 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
54
54
|
* - The accepted set is mirrored onto the native input, so a plain form submit
|
|
55
55
|
* carries the dropped files with no JavaScript from the consumer. Where
|
|
56
56
|
* `DataTransfer` cannot be constructed the previews and events still work and the
|
|
57
|
-
* input keeps whatever the native dialog last put there.
|
|
57
|
+
* input keeps whatever the native dialog last put there. An input that takes
|
|
58
|
+
* over — in one task, or after an earlier one leaves in a later task — gets the
|
|
59
|
+
* accepted set too, silently; the files on one that departs are its own form value
|
|
60
|
+
* and stay.
|
|
58
61
|
* - Removing a file revokes its `objectURL` and moves focus to the next (else
|
|
59
62
|
* previous) remove button, falling back to the trigger.
|
|
60
63
|
* - Additions, rejections, removals, and the drag affordance are handed to the
|
|
@@ -67,18 +70,29 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
67
70
|
* disabled `fieldset` the zone never becomes a drop target, matching what the
|
|
68
71
|
* native click and keyboard paths already do.
|
|
69
72
|
* - Replacing the `list` target rebinds delegated removal and moves the
|
|
70
|
-
* client-only previews into the replacement; a
|
|
71
|
-
*
|
|
73
|
+
* client-only previews into the replacement; a list inserted behind the current
|
|
74
|
+
* one receives them only once the current one leaves. A morph that empties the
|
|
75
|
+
* list in place — a Turbo morph does, as the server never rendered them — moves
|
|
76
|
+
* them back at once, and the drag and rejection hooks the morph took off with them.
|
|
77
|
+
* A preview taken out any other way comes back the same way: the remove button is
|
|
78
|
+
* what drops a file.
|
|
72
79
|
* `reject` dispatches `{ file: File, reason: "type" | "size" | "duplicate" | "count" }`.
|
|
73
80
|
* `change` and `reconcile` dispatch `{ files: File[] }`.
|
|
74
81
|
*
|
|
75
82
|
* @remarks
|
|
76
83
|
* Previews are client-only state that no restored snapshot can revive — the `File`
|
|
77
|
-
* objects and their `blob:` URLs die with the page.
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
84
|
+
* objects and their `blob:` URLs die with the page. Every item this controller renders
|
|
85
|
+
* carries `data-<identifier>-generated`, so a page Turbo restores from its cache, which
|
|
86
|
+
* still shows the items of the page it copied, is recognised when it connects: a
|
|
87
|
+
* connection that holds no selection of its own removes the marked items it finds in
|
|
88
|
+
* its lists, empties the native input, withdraws both state attributes, and reports the
|
|
89
|
+
* empty selection as `reconcile`. Rows that cannot be removed and do not count towards
|
|
90
|
+
* `maxFiles` therefore never stay on a restored page. Nothing is undone on
|
|
91
|
+
* `turbo:before-cache`, which Turbo also dispatches on pages that stay — a promoted
|
|
92
|
+
* frame navigation, a state-less `popstate`, a refresh of a cached URL, a
|
|
93
|
+
* `data-turbo-permanent` element carried to the next page — where the selection is the
|
|
94
|
+
* one the form is about to submit. A `disconnect()` that turns out to be an in-page move
|
|
95
|
+
* keeps the selection intact, and so does a permanent element's reconnection.
|
|
82
96
|
*/
|
|
83
97
|
declare class FileDropzoneController extends Controller<HTMLElement> {
|
|
84
98
|
#private;
|
|
@@ -125,9 +139,20 @@ declare class FileDropzoneController extends Controller<HTMLElement> {
|
|
|
125
139
|
default: string;
|
|
126
140
|
};
|
|
127
141
|
};
|
|
142
|
+
static valueConstraints: {
|
|
143
|
+
maxSize: {
|
|
144
|
+
readonly finite: true;
|
|
145
|
+
readonly min: 0;
|
|
146
|
+
};
|
|
147
|
+
maxFiles: {
|
|
148
|
+
readonly finite: true;
|
|
149
|
+
readonly min: 0;
|
|
150
|
+
};
|
|
151
|
+
};
|
|
128
152
|
static actions: readonly ["onChange", "onDragLeave", "onDragOver", "onDrop", "openDialog"];
|
|
129
153
|
static events: readonly ["change", "reject", "reconcile"];
|
|
130
154
|
readonly inputTarget: HTMLInputElement;
|
|
155
|
+
readonly inputTargets: HTMLInputElement[];
|
|
131
156
|
readonly listTarget: HTMLElement;
|
|
132
157
|
readonly listTargets: HTMLElement[];
|
|
133
158
|
readonly triggerTarget: HTMLElement;
|
|
@@ -138,6 +163,7 @@ declare class FileDropzoneController extends Controller<HTMLElement> {
|
|
|
138
163
|
readonly hasItemTemplateTarget: boolean;
|
|
139
164
|
readonly hasZoneTarget: boolean;
|
|
140
165
|
readonly zoneTarget: HTMLElement;
|
|
166
|
+
readonly zoneTargets: HTMLElement[];
|
|
141
167
|
maxSizeValue: number;
|
|
142
168
|
maxFilesValue: number;
|
|
143
169
|
allowDuplicatesValue: boolean;
|
|
@@ -148,7 +174,7 @@ declare class FileDropzoneController extends Controller<HTMLElement> {
|
|
|
148
174
|
announceRejectedSizeTextValue: string;
|
|
149
175
|
announceRejectedDuplicateTextValue: string;
|
|
150
176
|
announceRejectedCountTextValue: string;
|
|
151
|
-
/**
|
|
177
|
+
/** Re-arms the template diagnostic and discards the items a restored page carries. */
|
|
152
178
|
connect(): void;
|
|
153
179
|
/**
|
|
154
180
|
* Releases the delegated listeners. The selection itself survives an in-page
|
|
@@ -157,22 +183,38 @@ declare class FileDropzoneController extends Controller<HTMLElement> {
|
|
|
157
183
|
*/
|
|
158
184
|
disconnect(): void;
|
|
159
185
|
/**
|
|
160
|
-
* Binds removal
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
186
|
+
* Binds removal on every list this controller renders into — the one present at
|
|
187
|
+
* connect and any Turbo puts in its place — watches it for previews taken out of it,
|
|
188
|
+
* and restores client-only previews into the list that is first, so a list waiting
|
|
189
|
+
* behind the current one stays empty. This is the only place the listener is
|
|
190
|
+
* attached, so the pair with {@link listTargetDisconnected} keeps it from outliving
|
|
191
|
+
* the element it is on; the watch ends with the connection.
|
|
164
192
|
*/
|
|
165
193
|
listTargetConnected(list: HTMLElement): void;
|
|
166
|
-
/** Releases
|
|
194
|
+
/** Releases the list that disconnected and moves the previews into the list that stays. */
|
|
167
195
|
listTargetDisconnected(list: HTMLElement): void;
|
|
196
|
+
/** Mirrors the accepted set onto an input that arrives after connect. */
|
|
197
|
+
inputTargetConnected(): void;
|
|
198
|
+
/** Mirrors the accepted set onto the input left once an earlier one departs. */
|
|
199
|
+
inputTargetDisconnected(): void;
|
|
200
|
+
/** Writes the drag and rejection hooks onto a zone that arrives after connect. */
|
|
201
|
+
zoneTargetConnected(): void;
|
|
202
|
+
/**
|
|
203
|
+
* Gives a zone that no longer resolves as the target its own hooks back — after
|
|
204
|
+
* `disconnect()` too, since dropping the identifier leaves the element on the page —
|
|
205
|
+
* and, while connected, writes them onto the zone left.
|
|
206
|
+
*/
|
|
207
|
+
zoneTargetDisconnected(zone: HTMLElement): void;
|
|
168
208
|
/** Opens the native file dialog. Bound via `data-action` (trigger click). */
|
|
169
209
|
openDialog(): void;
|
|
170
210
|
/**
|
|
171
|
-
* Adds the files chosen through the native dialog
|
|
172
|
-
*
|
|
173
|
-
*
|
|
211
|
+
* Adds the files chosen through the native dialog, read from the input the `change`
|
|
212
|
+
* came from — an input waiting behind the one in use included — or from the input in
|
|
213
|
+
* use for an event no input dispatched and for a call with no event. The dialog
|
|
214
|
+
* leaves only its own answer on that input, so the accepted set is written back over
|
|
215
|
+
* the input in use once the batch is validated.
|
|
174
216
|
*/
|
|
175
|
-
onChange(): void;
|
|
217
|
+
onChange(event?: Event): void;
|
|
176
218
|
/**
|
|
177
219
|
* Marks the zone as a drop target and announces the affordance once per drag.
|
|
178
220
|
* Bound via `data-action` (dragover). Leaving the default alone is what refuses
|