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,30 +1,20 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/breadcrumb_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/blur_deferral.ts
|
|
6
3
|
var BlurDeferral = class {
|
|
7
|
-
/** Elements currently holding an update back, mapped to their `blur` listener. */
|
|
8
4
|
#pending = /* @__PURE__ */ new Map();
|
|
9
|
-
/** Called after a pending element blurs and has been detached. */
|
|
10
5
|
#onRelease;
|
|
11
|
-
/** @param onRelease - Invoked once `element` actually blurs; never on `release`. */
|
|
12
6
|
constructor(onRelease) {
|
|
13
7
|
this.#onRelease = onRelease;
|
|
14
8
|
}
|
|
15
|
-
/** Number of elements currently holding an update back. */
|
|
16
9
|
get size() {
|
|
17
10
|
return this.#pending.size;
|
|
18
11
|
}
|
|
19
|
-
/** Snapshot of the pending elements, safe to iterate while releasing them. */
|
|
20
12
|
get elements() {
|
|
21
13
|
return [...this.#pending.keys()];
|
|
22
14
|
}
|
|
23
|
-
/** Whether `element` is currently holding an update back. */
|
|
24
15
|
has(element) {
|
|
25
16
|
return this.#pending.has(element);
|
|
26
17
|
}
|
|
27
|
-
/** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */
|
|
28
18
|
defer(element) {
|
|
29
19
|
if (this.#pending.has(element)) return;
|
|
30
20
|
const onBlur = () => {
|
|
@@ -34,22 +24,18 @@ var BlurDeferral = class {
|
|
|
34
24
|
this.#pending.set(element, onBlur);
|
|
35
25
|
element.addEventListener("blur", onBlur);
|
|
36
26
|
}
|
|
37
|
-
/** Defers `element` as the only pending entry, cancelling any others. */
|
|
38
27
|
deferOnly(element) {
|
|
39
28
|
for (const pending of this.elements) {
|
|
40
29
|
if (pending !== element) this.#detach(pending);
|
|
41
30
|
}
|
|
42
31
|
this.defer(element);
|
|
43
32
|
}
|
|
44
|
-
/** Cancels `element`'s deferral without completing it; no-ops when not pending. */
|
|
45
33
|
release(element) {
|
|
46
34
|
this.#detach(element);
|
|
47
35
|
}
|
|
48
|
-
/** Cancels every deferral without completing any of them. */
|
|
49
36
|
releaseAll() {
|
|
50
37
|
for (const element of this.elements) this.#detach(element);
|
|
51
38
|
}
|
|
52
|
-
/** Removes the `blur` listener for `element` and forgets it. */
|
|
53
39
|
#detach(element) {
|
|
54
40
|
const onBlur = this.#pending.get(element);
|
|
55
41
|
if (onBlur) element.removeEventListener("blur", onBlur);
|
|
@@ -57,18 +43,15 @@ var BlurDeferral = class {
|
|
|
57
43
|
}
|
|
58
44
|
};
|
|
59
45
|
|
|
60
|
-
// src/utils/layout_observer.ts
|
|
61
46
|
var LayoutObserver = class {
|
|
62
47
|
#callback;
|
|
63
48
|
#resizeObserverFactory;
|
|
64
49
|
#resizeObserver = null;
|
|
65
50
|
#observingViewport = false;
|
|
66
51
|
#loadContainer = null;
|
|
67
|
-
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
68
52
|
#handleViewportResize = () => {
|
|
69
53
|
this.#callback();
|
|
70
54
|
};
|
|
71
|
-
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
72
55
|
#handleDescendantLoad = () => {
|
|
73
56
|
this.#callback();
|
|
74
57
|
};
|
|
@@ -76,11 +59,6 @@ var LayoutObserver = class {
|
|
|
76
59
|
this.#callback = callback;
|
|
77
60
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
78
61
|
}
|
|
79
|
-
/**
|
|
80
|
-
* Starts observing an element's size. Repeated calls observe additional
|
|
81
|
-
* elements through the same shared observer. No-ops when no
|
|
82
|
-
* `ResizeObserver` implementation is available.
|
|
83
|
-
*/
|
|
84
62
|
observe(element) {
|
|
85
63
|
if (!this.#resizeObserverFactory) return;
|
|
86
64
|
if (!this.#resizeObserver) {
|
|
@@ -90,47 +68,28 @@ var LayoutObserver = class {
|
|
|
90
68
|
}
|
|
91
69
|
this.#resizeObserver.observe(element);
|
|
92
70
|
}
|
|
93
|
-
/** Stops observing a single element while leaving any others in place. */
|
|
94
71
|
unobserve(element) {
|
|
95
72
|
this.#resizeObserver?.unobserve(element);
|
|
96
73
|
}
|
|
97
|
-
/** Starts observing viewport resizes. Idempotent: the listener is added once. */
|
|
98
74
|
observeViewport() {
|
|
99
75
|
if (this.#observingViewport) return;
|
|
100
76
|
this.#observingViewport = true;
|
|
101
77
|
window.addEventListener("resize", this.#handleViewportResize);
|
|
102
78
|
}
|
|
103
|
-
/** Stops observing viewport resizes without affecting element observation. */
|
|
104
79
|
unobserveViewport() {
|
|
105
80
|
if (!this.#observingViewport) return;
|
|
106
81
|
this.#observingViewport = false;
|
|
107
82
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
108
83
|
}
|
|
109
|
-
/**
|
|
110
|
-
* Starts reporting a `load` from anywhere inside `container` — an image or a
|
|
111
|
-
* frame settling changes the box it sits in, and it measures as zero high until
|
|
112
|
-
* then. `load` does not bubble, so the subscription is a capture-phase listener
|
|
113
|
-
* on the container itself and nothing the caller spells.
|
|
114
|
-
*
|
|
115
|
-
* **One container at a time.** A further call moves the observation, so a widget
|
|
116
|
-
* whose content element is swapped at runtime releases the element it let go by
|
|
117
|
-
* naming the new one — there is no second place for the release to drift from.
|
|
118
|
-
*/
|
|
119
84
|
observeDescendantLoads(container) {
|
|
120
85
|
this.unobserveDescendantLoads();
|
|
121
86
|
this.#loadContainer = container;
|
|
122
87
|
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
123
88
|
}
|
|
124
|
-
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
125
89
|
unobserveDescendantLoads() {
|
|
126
90
|
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
127
91
|
this.#loadContainer = null;
|
|
128
92
|
}
|
|
129
|
-
/**
|
|
130
|
-
* Releases every observation: disconnects the {@link ResizeObserver} and removes
|
|
131
|
-
* the viewport and descendant-load listeners. Safe to call multiple times. Call
|
|
132
|
-
* this from a controller's `disconnect()`.
|
|
133
|
-
*/
|
|
134
93
|
disconnect() {
|
|
135
94
|
this.#resizeObserver?.disconnect();
|
|
136
95
|
this.#resizeObserver = null;
|
|
@@ -139,30 +98,23 @@ var LayoutObserver = class {
|
|
|
139
98
|
}
|
|
140
99
|
};
|
|
141
100
|
|
|
142
|
-
// src/controllers/breadcrumb_controller.ts
|
|
143
101
|
var OVERFLOW_EPSILON = 1;
|
|
144
102
|
var BreadcrumbController = class extends Controller {
|
|
145
103
|
static targets = ["list", "collapsible", "ellipsis", "trigger"];
|
|
146
104
|
static actions = ["toggle", "update"];
|
|
147
105
|
static events = ["toggle"];
|
|
148
|
-
/** Guards target callbacks that can fire outside the connected window. */
|
|
149
106
|
#connected = false;
|
|
150
|
-
/** Whether the trail currently overflows its container. */
|
|
151
107
|
#overflowing = false;
|
|
152
|
-
/** Whether the user has expanded the collapsed items via the disclosure. */
|
|
153
108
|
#expanded = false;
|
|
154
|
-
/** The list element currently observed (resize + mutations), if any. */
|
|
155
109
|
#observedList = null;
|
|
156
110
|
#listMutations = null;
|
|
157
111
|
#layout = new LayoutObserver(() => this.update());
|
|
158
112
|
#onListMutation = () => {
|
|
159
113
|
this.update();
|
|
160
114
|
};
|
|
161
|
-
/** Holds the ellipsis hide back while it contains focus; re-renders on blur. */
|
|
162
115
|
#deferredHide = new BlurDeferral(() => {
|
|
163
116
|
this.#render();
|
|
164
117
|
});
|
|
165
|
-
/** Starts observing for overflow and renders the initial state. */
|
|
166
118
|
connect() {
|
|
167
119
|
this.#connected = true;
|
|
168
120
|
this.#expanded = this.#initialExpanded();
|
|
@@ -170,7 +122,6 @@ var BreadcrumbController = class extends Controller {
|
|
|
170
122
|
this.#syncListObservation();
|
|
171
123
|
this.update();
|
|
172
124
|
}
|
|
173
|
-
/** Releases the resize/mutation observation (Turbo navigation included). */
|
|
174
125
|
disconnect() {
|
|
175
126
|
this.#connected = false;
|
|
176
127
|
this.#deferredHide.releaseAll();
|
|
@@ -186,91 +137,42 @@ var BreadcrumbController = class extends Controller {
|
|
|
186
137
|
collapsibleTargetConnected() {
|
|
187
138
|
this.#resync();
|
|
188
139
|
}
|
|
189
|
-
/**
|
|
190
|
-
* Re-measures when an `ellipsis` target arrives. The `ellipsis` and `trigger`
|
|
191
|
-
* targets form the disclosure set, a precondition for collapsing at all, so they
|
|
192
|
-
* need the same watching the list and items get: without their callbacks a swap
|
|
193
|
-
* that breaks or completes the set would only take effect at the next resize or
|
|
194
|
-
* list mutation, which may never come.
|
|
195
|
-
*/
|
|
196
140
|
ellipsisTargetConnected() {
|
|
197
141
|
this.#resync();
|
|
198
142
|
}
|
|
199
|
-
/** Re-measures when an `ellipsis` target leaves the disclosure set. */
|
|
200
143
|
ellipsisTargetDisconnected() {
|
|
201
144
|
this.#resync();
|
|
202
145
|
}
|
|
203
|
-
/** Re-measures when a `trigger` target joins the disclosure set. */
|
|
204
146
|
triggerTargetConnected() {
|
|
205
147
|
this.#resync();
|
|
206
148
|
}
|
|
207
|
-
/** Re-measures when a `trigger` target leaves the disclosure set. */
|
|
208
149
|
triggerTargetDisconnected() {
|
|
209
150
|
this.#resync();
|
|
210
151
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Hands `hidden` back when an element stops being a `collapsible` target.
|
|
213
|
-
*
|
|
214
|
-
* The marker is the source of truth for what may be collapsed, so an element
|
|
215
|
-
* that loses it while the controller is live is an always-visible item again —
|
|
216
|
-
* and rendering only walks the *current* targets, so nothing else would ever
|
|
217
|
-
* clear the `hidden` this controller put there.
|
|
218
|
-
*
|
|
219
|
-
* The `#connected` guard is load-bearing, not defensive. Stimulus fires this
|
|
220
|
-
* callback for **every** target during teardown as well (`Context.disconnect()`
|
|
221
|
-
* stops the target observer after `disconnect()` has run), and there the
|
|
222
|
-
* collapsed DOM must survive untouched so Turbo's cache restores the trail in
|
|
223
|
-
* the state the user left it (see the ownership note in {@link BreadcrumbController}).
|
|
224
|
-
*
|
|
225
|
-
* @param element - the element that just left the `collapsible` target set
|
|
226
|
-
*/
|
|
227
152
|
collapsibleTargetDisconnected(element) {
|
|
228
153
|
if (this.#connected) element.hidden = false;
|
|
229
154
|
this.#resync();
|
|
230
155
|
}
|
|
231
|
-
/**
|
|
232
|
-
* Expands or re-collapses the trail and dispatches `toggle`.
|
|
233
|
-
*
|
|
234
|
-
* No-op while the trail fits: there is nothing collapsed to reveal, and the
|
|
235
|
-
* (hidden) trigger would otherwise keep a stale `aria-expanded="true"` that
|
|
236
|
-
* surfaces pre-expanded the next time the trail overflows. No event is
|
|
237
|
-
* dispatched in that case.
|
|
238
|
-
*/
|
|
239
156
|
toggle() {
|
|
240
157
|
if (!this.#connected || !this.#overflowing) return;
|
|
241
158
|
this.#expanded = !this.#expanded;
|
|
242
159
|
this.#render();
|
|
243
160
|
this.dispatch("toggle", { detail: { expanded: this.#expanded } });
|
|
244
161
|
}
|
|
245
|
-
/**
|
|
246
|
-
* Re-measures overflow and re-renders. Wired to resizes and list mutations;
|
|
247
|
-
* exposed as an action so consumers can re-measure after a change the
|
|
248
|
-
* observers cannot see (e.g. a web font swap that only alters text width).
|
|
249
|
-
*/
|
|
250
162
|
update() {
|
|
251
163
|
if (!this.#connected) return;
|
|
252
164
|
this.#overflowing = this.#measureOverflow();
|
|
253
165
|
if (!this.#overflowing) this.#expanded = false;
|
|
254
166
|
this.#render();
|
|
255
167
|
}
|
|
256
|
-
/** Re-attaches the list observation (targets may have been swapped) and re-measures. */
|
|
257
168
|
#resync() {
|
|
258
169
|
if (!this.#connected) return;
|
|
259
170
|
this.#syncListObservation();
|
|
260
171
|
this.update();
|
|
261
172
|
}
|
|
262
|
-
/** Reads the expanded state back from its DOM home (the trigger's `aria-expanded`). */
|
|
263
173
|
#initialExpanded() {
|
|
264
174
|
return this.hasTriggerTarget && this.triggerTarget.getAttribute("aria-expanded") === "true";
|
|
265
175
|
}
|
|
266
|
-
/**
|
|
267
|
-
* Points the resize + mutation observation at the current `list` target.
|
|
268
|
-
*
|
|
269
|
-
* The list is re-resolved rather than captured at connect so a target swapped
|
|
270
|
-
* at runtime (Turbo Stream replacement of the `<ol>`) is observed too. Only
|
|
271
|
-
* `childList` / `subtree` / `characterData` are watched — deliberately **not**
|
|
272
|
-
* `attributes`, which the controller's own `hidden` writes would re-trigger.
|
|
273
|
-
*/
|
|
274
176
|
#syncListObservation() {
|
|
275
177
|
const next = this.hasListTarget ? this.listTarget : null;
|
|
276
178
|
if (next === this.#observedList) return;
|
|
@@ -289,26 +191,6 @@ var BreadcrumbController = class extends Controller {
|
|
|
289
191
|
this.#listMutations?.disconnect();
|
|
290
192
|
this.#listMutations = null;
|
|
291
193
|
}
|
|
292
|
-
/**
|
|
293
|
-
* Measures overflow against the **fully expanded** layout so hiding items does
|
|
294
|
-
* not make the condition oscillate. Reveals every item, then compares the list's
|
|
295
|
-
* own scroll width to its own client width; `#render` immediately re-applies
|
|
296
|
-
* the correct hidden state afterward.
|
|
297
|
-
*
|
|
298
|
-
* Both widths are read from the list element itself (not the host `nav`) so the
|
|
299
|
-
* check is independent of any padding/border on the host — comparing against the
|
|
300
|
-
* host's `clientWidth` would over-report the available space by its padding and
|
|
301
|
-
* miss real overflow in a padded container.
|
|
302
|
-
*
|
|
303
|
-
* With no `collapsible` items there is nothing to collapse, so the trail is
|
|
304
|
-
* never reported as overflowing (which also keeps the disclosure out of the
|
|
305
|
-
* tab order — a button controlling nothing).
|
|
306
|
-
*
|
|
307
|
-
* The full disclosure set is a precondition: collapsing without an `ellipsis`
|
|
308
|
-
* *and* a `trigger` would hide items behind a control that does not exist, so
|
|
309
|
-
* an incomplete set reports "not overflowing" and `#render` degrades the trail
|
|
310
|
-
* to a plain breadcrumb. Losing layout is recoverable; losing the path is not.
|
|
311
|
-
*/
|
|
312
194
|
#measureOverflow() {
|
|
313
195
|
if (!this.hasListTarget || this.collapsibleTargets.length === 0) return false;
|
|
314
196
|
if (!this.hasEllipsisTarget || !this.hasTriggerTarget) return false;
|
|
@@ -316,9 +198,6 @@ var BreadcrumbController = class extends Controller {
|
|
|
316
198
|
this.ellipsisTarget.hidden = true;
|
|
317
199
|
return this.listTarget.scrollWidth > this.listTarget.clientWidth + OVERFLOW_EPSILON;
|
|
318
200
|
}
|
|
319
|
-
/**
|
|
320
|
-
* Applies the collapsed/expanded state to the items, ellipsis, and trigger.
|
|
321
|
-
*/
|
|
322
201
|
#render() {
|
|
323
202
|
const collapsed = this.#overflowing && !this.#expanded;
|
|
324
203
|
const showEllipsis = this.#overflowing && this.collapsibleTargets.length > 0;
|
|
@@ -331,13 +210,6 @@ var BreadcrumbController = class extends Controller {
|
|
|
331
210
|
}
|
|
332
211
|
if (rescueFocus) this.triggerTarget.focus();
|
|
333
212
|
}
|
|
334
|
-
/**
|
|
335
|
-
* Shows or hides the ellipsis item, deferring a hide that would blur the user.
|
|
336
|
-
*
|
|
337
|
-
* Hiding the element that currently holds focus drops focus to `<body>`, so the
|
|
338
|
-
* hide waits for that element's `blur` and is re-applied then (the shared
|
|
339
|
-
* `BlurDeferral` registry).
|
|
340
|
-
*/
|
|
341
213
|
#applyEllipsisVisibility(show) {
|
|
342
214
|
const ellipsis = this.ellipsisTarget;
|
|
343
215
|
if (show) {
|
|
@@ -354,7 +226,6 @@ var BreadcrumbController = class extends Controller {
|
|
|
354
226
|
this.#deferredHide.releaseAll();
|
|
355
227
|
ellipsis.hidden = true;
|
|
356
228
|
}
|
|
357
|
-
/** Whether `element` contains (or is) the currently focused element. */
|
|
358
229
|
#holdsFocus(element) {
|
|
359
230
|
const active = document.activeElement;
|
|
360
231
|
return active instanceof HTMLElement && element.contains(active);
|
|
@@ -362,5 +233,4 @@ var BreadcrumbController = class extends Controller {
|
|
|
362
233
|
};
|
|
363
234
|
|
|
364
235
|
export { BreadcrumbController };
|
|
365
|
-
//# sourceMappingURL=breadcrumb_controller.js.map
|
|
366
236
|
//# sourceMappingURL=breadcrumb_controller.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/blur_deferral.ts","../../src/utils/layout_observer.ts","../../src/controllers/breadcrumb_controller.ts"],"names":[],"mappings":"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","file":"breadcrumb_controller.js","sourcesContent":["/**\n * Shared bookkeeping for updates that must wait until an element loses focus.\n *\n * A control that becomes redundant while the user is standing on it cannot be\n * removed from the page immediately: hiding a focused element (or turning it\n * into a native `disabled` one) drops focus to `<body>`, stranding a keyboard\n * user mid-widget. The fix every affected controller reached for is the same —\n * keep the element reachable, hold the destructive update, listen for `blur`,\n * and apply it then.\n *\n * {@link BlurDeferral} owns *only* the registry part of that: which elements are\n * currently holding an update back, attaching and detaching the `blur` listener,\n * and guaranteeing teardown. It is deliberately **policy-free** — what the\n * interim state looks like (`aria-disabled`, an ownership marker, nothing at\n * all), and what the deferred update actually is, stay in the controller, which\n * receives the element back through its release callback.\n *\n * Three contracts are worth stating up front, because each one is a hazard the\n * consumers would otherwise hit:\n *\n * - **The release callback fires only on a real `blur`.** `release()` /\n * `releaseAll()` *cancel* a deferral; they do not complete it. Conflating the\n * two makes `overflow-indicator` disable a button that just left the boundary.\n * - **`elements` is a snapshot.** Iterating the live key set while releasing\n * inside the loop is the failure this registry exists to prevent.\n * - **The entry is detached before the callback runs**, so a controller that\n * decides the update is still unsafe can simply defer again.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and contracts belong on the members.\n *\n * @example\n * ```ts\n * readonly #deferred = new BlurDeferral<HTMLElement>((trigger) => {\n * if (this.#connected) this.#evaluate();\n * });\n *\n * #hide(trigger: HTMLElement) {\n * if (document.activeElement === trigger) {\n * this.#deferred.deferOnly(trigger); // stays visible until blur\n * return;\n * }\n * this.#deferred.releaseAll();\n * trigger.hidden = true;\n * }\n *\n * disconnect() {\n * this.#deferred.releaseAll();\n * }\n * ```\n */\nexport class BlurDeferral<T extends HTMLElement = HTMLElement> {\n /** Elements currently holding an update back, mapped to their `blur` listener. */\n readonly #pending = new Map<T, () => void>();\n /** Called after a pending element blurs and has been detached. */\n readonly #onRelease: (element: T) => void;\n\n /** @param onRelease - Invoked once `element` actually blurs; never on `release`. */\n constructor(onRelease: (element: T) => void) {\n this.#onRelease = onRelease;\n }\n\n /** Number of elements currently holding an update back. */\n get size(): number {\n return this.#pending.size;\n }\n\n /** Snapshot of the pending elements, safe to iterate while releasing them. */\n get elements(): T[] {\n return [...this.#pending.keys()];\n }\n\n /** Whether `element` is currently holding an update back. */\n has(element: T): boolean {\n return this.#pending.has(element);\n }\n\n /** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */\n defer(element: T): void {\n if (this.#pending.has(element)) return;\n const onBlur = (): void => {\n this.#detach(element);\n this.#onRelease(element);\n };\n this.#pending.set(element, onBlur);\n element.addEventListener(\"blur\", onBlur);\n }\n\n /** Defers `element` as the only pending entry, cancelling any others. */\n deferOnly(element: T): void {\n for (const pending of this.elements) {\n if (pending !== element) this.#detach(pending);\n }\n this.defer(element);\n }\n\n /** Cancels `element`'s deferral without completing it; no-ops when not pending. */\n release(element: T): void {\n this.#detach(element);\n }\n\n /** Cancels every deferral without completing any of them. */\n releaseAll(): void {\n for (const element of this.elements) this.#detach(element);\n }\n\n /** Removes the `blur` listener for `element` and forgets it. */\n #detach(element: T): void {\n const onBlur = this.#pending.get(element);\n if (onBlur) element.removeEventListener(\"blur\", onBlur);\n this.#pending.delete(element);\n }\n}\n","/**\n * Unified layout observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — have three sources that move the layout under them: their\n * *own* box changing — via {@link ResizeObserver} — the *viewport* changing — via the\n * `window` `resize` event — and a descendant *resource settling*, because an image or\n * a frame reports a height of zero until it has loaded. Wiring those three by hand in\n * every controller risks leaked listeners on `disconnect()`. {@link LayoutObserver}\n * owns all three behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * this.#layout.observeDescendantLoads(this.panelTarget);\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n #loadContainer: Element | null = null;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */\n readonly #handleDescendantLoad = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Starts reporting a `load` from anywhere inside `container` — an image or a\n * frame settling changes the box it sits in, and it measures as zero high until\n * then. `load` does not bubble, so the subscription is a capture-phase listener\n * on the container itself and nothing the caller spells.\n *\n * **One container at a time.** A further call moves the observation, so a widget\n * whose content element is swapped at runtime releases the element it let go by\n * naming the new one — there is no second place for the release to drift from.\n */\n observeDescendantLoads(container: Element): void {\n this.unobserveDescendantLoads();\n this.#loadContainer = container;\n container.addEventListener(\"load\", this.#handleDescendantLoad, true);\n }\n\n /** Stops reporting descendant loads without affecting element or viewport observation. */\n unobserveDescendantLoads(): void {\n this.#loadContainer?.removeEventListener(\"load\", this.#handleDescendantLoad, true);\n this.#loadContainer = null;\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and removes\n * the viewport and descendant-load listeners. Safe to call multiple times. Call\n * this from a controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n this.unobserveDescendantLoads();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BlurDeferral } from \"../utils/blur_deferral\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/**\n * Sub-pixel tolerance (px) applied to the overflow comparison.\n *\n * Fractional layout metrics (zoom, `border-box` rounding, fractional font\n * metrics) routinely make `scrollWidth` exceed `clientWidth` by a fraction of a\n * pixel on a trail that visually fits, which would collapse it for no reason.\n */\nconst OVERFLOW_EPSILON = 1;\n\n/**\n * Headless, accessible responsive breadcrumb behavior.\n *\n * Markup contract (identifier: `stimeo--breadcrumb`):\n * <nav data-controller=\"stimeo--breadcrumb\" aria-label=\"Breadcrumb\">\n * <ol data-stimeo--breadcrumb-target=\"list\">\n * <li><a href=\"/\">Home</a></li>\n * <li data-stimeo--breadcrumb-target=\"ellipsis\" hidden>\n * <button type=\"button\" aria-expanded=\"false\" aria-controls=\"bc-a bc-b\"\n * aria-label=\"Show full path\"\n * data-stimeo--breadcrumb-target=\"trigger\"\n * data-action=\"click->stimeo--breadcrumb#toggle\">…</button>\n * </li>\n * <li id=\"bc-a\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a\">Section A</a></li>\n * <li id=\"bc-b\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a/b\">Sub B</a></li>\n * <li><a href=\"/a/b/c\" aria-current=\"page\">Item C</a></li>\n * </ol>\n * </nav>\n *\n * Implements the WAI-ARIA APG **Breadcrumb** pattern. The base structure (`nav`\n * + `ol` + `aria-current=\"page\"`) lives in the markup; this controller adds the\n * responsive behavior: when the trail overflows its container it collapses the\n * author-marked middle items behind a disclosure (`…`) button, which expands\n * them back on demand.\n *\n * `toggle` dispatches `{ expanded: boolean }`.\n *\n * @remarks\n * Behavior only — the consumer owns separators (CSS) and the look. The items to\n * collapse are the author-marked `collapsible` targets (the source of truth),\n * placed between the leading and trailing items that must always stay visible.\n *\n * Behavior provided:\n * - Detects overflow via a shared layout observer (element + viewport resize) and a\n * `MutationObserver` on the list (content edits, Turbo Stream / morph item\n * swaps); the public `update` action re-measures on demand.\n * - While overflowing and not expanded, hides the `collapsible` items and shows\n * the `ellipsis` item; when it fits, shows everything, hides the ellipsis and\n * resets the expanded state (so a later collapse starts collapsed).\n * - The disclosure `trigger` toggles `aria-expanded` and the collapsed items,\n * dispatching `stimeo--breadcrumb:toggle`. `toggle` is a no-op (no state\n * change, no event) while the trail fits, so a hidden trigger can never be\n * left reporting `aria-expanded=\"true\"`.\n *\n * Layout requirements the behavior depends on:\n * - **The list must not wrap** (`white-space: nowrap` / `flex-wrap: nowrap`). A\n * wrapping list grows in height instead of scroll width, so\n * `scrollWidth === clientWidth` always holds and overflow is never detected.\n * - **The list width must follow its container** (no shrink-to-fit / intrinsic\n * width). Overflow is re-measured synchronously inside the resize\n * notification, so a list that widens to its content would never report\n * overflow. There is deliberately no debounce: the pass is one write → one\n * forced read → one write on a single element.\n * - Consumers must not neutralize `[hidden]` (e.g. `display: inline-flex` on the\n * items wins over the UA's `[hidden] { display: none }`); collapsing is\n * expressed exclusively through the `hidden` attribute.\n *\n * Ownership of `hidden`: this controller owns the attribute on an element **for as\n * long as that element is a `collapsible` target and the controller is connected**.\n * Outside that window it does not write it:\n * - lose the marker while connected → the element becomes an always-visible item\n * and the controller clears the `hidden` it owned;\n * - controller `disconnect()` → the collapsed state is left in the DOM on purpose,\n * because that DOM is what Turbo caches and restores.\n *\n * A consumer that also wants to own `hidden` on the same element (for a reason of\n * its own) is not supported: the overflow measurement clears it on every pass to\n * read the expanded width.\n *\n * Focus safety: hiding the element the user is standing on would drop focus to\n * `<body>`. When a collapse would hide the focused item, focus moves to the\n * disclosure `trigger` first; when the ellipsis would be hidden while it holds\n * focus, the hide waits for `blur`.\n *\n * **Fail-safe when the disclosure is incomplete.** Collapsing needs all of\n * `list`, one `collapsible`, `ellipsis`, and `trigger`; miss any and the trail\n * degrades to a plain APG breadcrumb (every item visible, ellipsis hidden,\n * `aria-expanded=\"false\"`, `toggle` a silent no-op). Hiding the middle items\n * with no control that can reveal them puts the path out of reach for good — a\n * too-wide trail is the cheaper failure. Watched at runtime, so a swap that\n * breaks the set releases the items and one that completes it starts collapsing.\n */\nexport class BreadcrumbController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"collapsible\", \"ellipsis\", \"trigger\"];\n static actions = [\"toggle\", \"update\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly collapsibleTargets: HTMLElement[];\n declare readonly ellipsisTarget: HTMLElement;\n declare readonly triggerTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasEllipsisTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n /** Guards target callbacks that can fire outside the connected window. */\n #connected = false;\n /** Whether the trail currently overflows its container. */\n #overflowing = false;\n /** Whether the user has expanded the collapsed items via the disclosure. */\n #expanded = false;\n /** The list element currently observed (resize + mutations), if any. */\n #observedList: HTMLElement | null = null;\n #listMutations: MutationObserver | null = null;\n readonly #layout = new LayoutObserver(() => this.update());\n\n readonly #onListMutation = (): void => {\n this.update();\n };\n\n /** Holds the ellipsis hide back while it contains focus; re-renders on blur. */\n readonly #deferredHide = new BlurDeferral(() => {\n this.#render();\n });\n\n /** Starts observing for overflow and renders the initial state. */\n override connect(): void {\n this.#connected = true;\n // The DOM is the source of truth on reconnect (Turbo cache restore / morph):\n // the trigger's `aria-expanded` is where the expanded state lives, so a trail\n // the user opened survives a back-navigation instead of silently re-collapsing.\n this.#expanded = this.#initialExpanded();\n this.#layout.observeViewport();\n this.#syncListObservation();\n this.update();\n }\n\n /** Releases the resize/mutation observation (Turbo navigation included). */\n override disconnect(): void {\n this.#connected = false;\n this.#deferredHide.releaseAll();\n this.#stopObservingList();\n this.#layout.disconnect();\n }\n\n listTargetConnected(): void {\n this.#resync();\n }\n\n listTargetDisconnected(): void {\n this.#resync();\n }\n\n collapsibleTargetConnected(): void {\n this.#resync();\n }\n\n /**\n * Re-measures when an `ellipsis` target arrives. The `ellipsis` and `trigger`\n * targets form the disclosure set, a precondition for collapsing at all, so they\n * need the same watching the list and items get: without their callbacks a swap\n * that breaks or completes the set would only take effect at the next resize or\n * list mutation, which may never come.\n */\n ellipsisTargetConnected(): void {\n this.#resync();\n }\n\n /** Re-measures when an `ellipsis` target leaves the disclosure set. */\n ellipsisTargetDisconnected(): void {\n this.#resync();\n }\n\n /** Re-measures when a `trigger` target joins the disclosure set. */\n triggerTargetConnected(): void {\n this.#resync();\n }\n\n /** Re-measures when a `trigger` target leaves the disclosure set. */\n triggerTargetDisconnected(): void {\n this.#resync();\n }\n\n /**\n * Hands `hidden` back when an element stops being a `collapsible` target.\n *\n * The marker is the source of truth for what may be collapsed, so an element\n * that loses it while the controller is live is an always-visible item again —\n * and rendering only walks the *current* targets, so nothing else would ever\n * clear the `hidden` this controller put there.\n *\n * The `#connected` guard is load-bearing, not defensive. Stimulus fires this\n * callback for **every** target during teardown as well (`Context.disconnect()`\n * stops the target observer after `disconnect()` has run), and there the\n * collapsed DOM must survive untouched so Turbo's cache restores the trail in\n * the state the user left it (see the ownership note in {@link BreadcrumbController}).\n *\n * @param element - the element that just left the `collapsible` target set\n */\n collapsibleTargetDisconnected(element: HTMLElement): void {\n if (this.#connected) element.hidden = false;\n this.#resync();\n }\n\n /**\n * Expands or re-collapses the trail and dispatches `toggle`.\n *\n * No-op while the trail fits: there is nothing collapsed to reveal, and the\n * (hidden) trigger would otherwise keep a stale `aria-expanded=\"true\"` that\n * surfaces pre-expanded the next time the trail overflows. No event is\n * dispatched in that case.\n */\n toggle(): void {\n if (!this.#connected || !this.#overflowing) return;\n this.#expanded = !this.#expanded;\n this.#render();\n this.dispatch(\"toggle\", { detail: { expanded: this.#expanded } });\n }\n\n /**\n * Re-measures overflow and re-renders. Wired to resizes and list mutations;\n * exposed as an action so consumers can re-measure after a change the\n * observers cannot see (e.g. a web font swap that only alters text width).\n */\n update(): void {\n if (!this.#connected) return;\n this.#overflowing = this.#measureOverflow();\n if (!this.#overflowing) this.#expanded = false;\n this.#render();\n }\n\n /** Re-attaches the list observation (targets may have been swapped) and re-measures. */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncListObservation();\n this.update();\n }\n\n /** Reads the expanded state back from its DOM home (the trigger's `aria-expanded`). */\n #initialExpanded(): boolean {\n return this.hasTriggerTarget && this.triggerTarget.getAttribute(\"aria-expanded\") === \"true\";\n }\n\n /**\n * Points the resize + mutation observation at the current `list` target.\n *\n * The list is re-resolved rather than captured at connect so a target swapped\n * at runtime (Turbo Stream replacement of the `<ol>`) is observed too. Only\n * `childList` / `subtree` / `characterData` are watched — deliberately **not**\n * `attributes`, which the controller's own `hidden` writes would re-trigger.\n */\n #syncListObservation(): void {\n const next = this.hasListTarget ? this.listTarget : null;\n if (next === this.#observedList) return;\n\n this.#stopObservingList();\n if (!next) return;\n\n this.#observedList = next;\n this.#layout.observe(next);\n if (typeof MutationObserver !== \"undefined\") {\n this.#listMutations = new MutationObserver(this.#onListMutation);\n this.#listMutations.observe(next, { childList: true, subtree: true, characterData: true });\n }\n }\n\n #stopObservingList(): void {\n if (this.#observedList) this.#layout.unobserve(this.#observedList);\n this.#observedList = null;\n this.#listMutations?.disconnect();\n this.#listMutations = null;\n }\n\n /**\n * Measures overflow against the **fully expanded** layout so hiding items does\n * not make the condition oscillate. Reveals every item, then compares the list's\n * own scroll width to its own client width; `#render` immediately re-applies\n * the correct hidden state afterward.\n *\n * Both widths are read from the list element itself (not the host `nav`) so the\n * check is independent of any padding/border on the host — comparing against the\n * host's `clientWidth` would over-report the available space by its padding and\n * miss real overflow in a padded container.\n *\n * With no `collapsible` items there is nothing to collapse, so the trail is\n * never reported as overflowing (which also keeps the disclosure out of the\n * tab order — a button controlling nothing).\n *\n * The full disclosure set is a precondition: collapsing without an `ellipsis`\n * *and* a `trigger` would hide items behind a control that does not exist, so\n * an incomplete set reports \"not overflowing\" and `#render` degrades the trail\n * to a plain breadcrumb. Losing layout is recoverable; losing the path is not.\n */\n #measureOverflow(): boolean {\n if (!this.hasListTarget || this.collapsibleTargets.length === 0) return false;\n if (!this.hasEllipsisTarget || !this.hasTriggerTarget) return false;\n for (const item of this.collapsibleTargets) item.hidden = false;\n // No `hasEllipsisTarget` check: the line above already returned without one.\n this.ellipsisTarget.hidden = true;\n return this.listTarget.scrollWidth > this.listTarget.clientWidth + OVERFLOW_EPSILON;\n }\n\n /**\n * Applies the collapsed/expanded state to the items, ellipsis, and trigger.\n */\n #render(): void {\n const collapsed = this.#overflowing && !this.#expanded;\n const showEllipsis = this.#overflowing && this.collapsibleTargets.length > 0;\n\n // Decided *before* the writes below: once a focused item is hidden the browser\n // drops focus to <body> and the information is gone.\n const rescueFocus =\n collapsed &&\n this.hasTriggerTarget &&\n this.collapsibleTargets.some((i) => this.#holdsFocus(i));\n\n for (const item of this.collapsibleTargets) item.hidden = collapsed;\n if (this.hasEllipsisTarget) this.#applyEllipsisVisibility(showEllipsis);\n if (this.hasTriggerTarget) {\n // Never report expanded while the trail fits: nothing is collapsed to reveal.\n const expanded = this.#overflowing && this.#expanded;\n this.triggerTarget.setAttribute(\"aria-expanded\", expanded ? \"true\" : \"false\");\n }\n if (rescueFocus) this.triggerTarget.focus();\n }\n\n /**\n * Shows or hides the ellipsis item, deferring a hide that would blur the user.\n *\n * Hiding the element that currently holds focus drops focus to `<body>`, so the\n * hide waits for that element's `blur` and is re-applied then (the shared\n * `BlurDeferral` registry).\n */\n #applyEllipsisVisibility(show: boolean): void {\n const ellipsis = this.ellipsisTarget;\n if (show) {\n this.#deferredHide.releaseAll();\n ellipsis.hidden = false;\n return;\n }\n\n const active = document.activeElement;\n if (active instanceof HTMLElement && ellipsis.contains(active)) {\n ellipsis.hidden = false;\n this.#deferredHide.deferOnly(active);\n return;\n }\n\n this.#deferredHide.releaseAll();\n ellipsis.hidden = true;\n }\n\n /** Whether `element` contains (or is) the currently focused element. */\n #holdsFocus(element: HTMLElement): boolean {\n const active = document.activeElement;\n return active instanceof HTMLElement && element.contains(active);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/blur_deferral.ts","../../src/utils/layout_observer.ts","../../src/controllers/breadcrumb_controller.ts"],"names":[],"mappings":";;AAgDO,IAAM,eAAN,MAAwD;AAAA,EAEpD,QAAA,uBAAe,GAAA,EAAmB;AAAA,EAElC,UAAA;AAAA,EAGT,YAAY,SAAA,EAAiC;AAC3C,IAAA,IAAA,CAAK,UAAA,GAAa,SAAA;AAAA,EACpB;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,QAAA,CAAS,IAAA;AAAA,EACvB;AAAA,EAGA,IAAI,QAAA,GAAgB;AAClB,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,QAAA,CAAS,MAAM,CAAA;AAAA,EACjC;AAAA,EAGA,IAAI,OAAA,EAAqB;AACvB,IAAA,OAAO,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA;AAAA,EAClC;AAAA,EAGA,MAAM,OAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA,EAAG;AAChC,IAAA,MAAM,SAAS,MAAY;AACzB,MAAA,IAAA,CAAK,QAAQ,OAAO,CAAA;AACpB,MAAA,IAAA,CAAK,WAAW,OAAO,CAAA;AAAA,IACzB,CAAA;AACA,IAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAA,EAAS,MAAM,CAAA;AACjC,IAAA,OAAA,CAAQ,gBAAA,CAAiB,QAAQ,MAAM,CAAA;AAAA,EACzC;AAAA,EAGA,UAAU,OAAA,EAAkB;AAC1B,IAAA,KAAA,MAAW,OAAA,IAAW,KAAK,QAAA,EAAU;AACnC,MAAA,IAAI,OAAA,KAAY,OAAA,EAAS,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAAA,IAC/C;AACA,IAAA,IAAA,CAAK,MAAM,OAAO,CAAA;AAAA,EACpB;AAAA,EAGA,QAAQ,OAAA,EAAkB;AACxB,IAAA,IAAA,CAAK,QAAQ,OAAO,CAAA;AAAA,EACtB;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,QAAQ,OAAO,CAAA;AAAA,EAC3D;AAAA,EAGA,QAAQ,OAAA,EAAkB;AACxB,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA;AACxC,IAAA,IAAI,MAAA,EAAQ,OAAA,CAAQ,mBAAA,CAAoB,MAAA,EAAQ,MAAM,CAAA;AACtD,IAAA,IAAA,CAAK,QAAA,CAAS,OAAO,OAAO,CAAA;AAAA,EAC9B;AACF,CAAA;;AC1DO,IAAM,iBAAN,MAAqB;AAAA,EACjB,SAAA;AAAA,EACA,sBAAA;AAAA,EACT,eAAA,GAAyC,IAAA;AAAA,EACzC,kBAAA,GAAqB,KAAA;AAAA,EACrB,cAAA,GAAiC,IAAA;AAAA,EAGxB,wBAAwB,MAAY;AAC3C,IAAA,IAAA,CAAK,SAAA,EAAU;AAAA,EACjB,CAAA;AAAA,EAGS,wBAAwB,MAAY;AAC3C,IAAA,IAAA,CAAK,SAAA,EAAU;AAAA,EACjB,CAAA;AAAA,EAEA,WAAA,CAAY,QAAA,EAA0B,OAAA,GAAiC,EAAC,EAAG;AACzE,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AACjB,IAAA,IAAA,CAAK,sBAAA,GACH,OAAA,CAAQ,qBAAA,KACP,OAAO,cAAA,KAAmB,WAAA,GAAc,IAAA,GAAO,CAAC,EAAA,KAAO,IAAI,cAAA,CAAe,EAAE,CAAA,CAAA;AAAA,EACjF;AAAA,EAOA,QAAQ,OAAA,EAAwB;AAC9B,IAAA,IAAI,CAAC,KAAK,sBAAA,EAAwB;AAClC,IAAA,IAAI,CAAC,KAAK,eAAA,EAAiB;AACzB,MAAA,IAAA,CAAK,eAAA,GAAkB,IAAA,CAAK,sBAAA,CAAuB,MAAM;AACvD,QAAA,IAAA,CAAK,SAAA,EAAU;AAAA,MACjB,CAAC,CAAA;AAAA,IACH;AACA,IAAA,IAAA,CAAK,eAAA,CAAgB,QAAQ,OAAO,CAAA;AAAA,EACtC;AAAA,EAGA,UAAU,OAAA,EAAwB;AAChC,IAAA,IAAA,CAAK,eAAA,EAAiB,UAAU,OAAO,CAAA;AAAA,EACzC;AAAA,EAGA,eAAA,GAAwB;AACtB,IAAA,IAAI,KAAK,kBAAA,EAAoB;AAC7B,IAAA,IAAA,CAAK,kBAAA,GAAqB,IAAA;AAC1B,IAAA,MAAA,CAAO,gBAAA,CAAiB,QAAA,EAAU,IAAA,CAAK,qBAAqB,CAAA;AAAA,EAC9D;AAAA,EAGA,iBAAA,GAA0B;AACxB,IAAA,IAAI,CAAC,KAAK,kBAAA,EAAoB;AAC9B,IAAA,IAAA,CAAK,kBAAA,GAAqB,KAAA;AAC1B,IAAA,MAAA,CAAO,mBAAA,CAAoB,QAAA,EAAU,IAAA,CAAK,qBAAqB,CAAA;AAAA,EACjE;AAAA,EAYA,uBAAuB,SAAA,EAA0B;AAC/C,IAAA,IAAA,CAAK,wBAAA,EAAyB;AAC9B,IAAA,IAAA,CAAK,cAAA,GAAiB,SAAA;AACtB,IAAA,SAAA,CAAU,gBAAA,CAAiB,MAAA,EAAQ,IAAA,CAAK,qBAAA,EAAuB,IAAI,CAAA;AAAA,EACrE;AAAA,EAGA,wBAAA,GAAiC;AAC/B,IAAA,IAAA,CAAK,cAAA,EAAgB,mBAAA,CAAoB,MAAA,EAAQ,IAAA,CAAK,uBAAuB,IAAI,CAAA;AACjF,IAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AAAA,EACxB;AAAA,EAOA,UAAA,GAAmB;AACjB,IAAA,IAAA,CAAK,iBAAiB,UAAA,EAAW;AACjC,IAAA,IAAA,CAAK,eAAA,GAAkB,IAAA;AACvB,IAAA,IAAA,CAAK,iBAAA,EAAkB;AACvB,IAAA,IAAA,CAAK,wBAAA,EAAyB;AAAA,EAChC;AACF,CAAA;;ACnIA,IAAM,gBAAA,GAAmB,CAAA;AAoFlB,IAAM,oBAAA,GAAN,cAAmC,UAAA,CAAwB;AAAA,EAChE,OAAgB,OAAA,GAAU,CAAC,MAAA,EAAQ,aAAA,EAAe,YAAY,SAAS,CAAA;AAAA,EACvE,OAAO,OAAA,GAAU,CAAC,QAAA,EAAU,QAAQ,CAAA;AAAA,EACpC,OAAO,MAAA,GAAS,CAAC,QAAQ,CAAA;AAAA,EAWzB,UAAA,GAAa,KAAA;AAAA,EAEb,YAAA,GAAe,KAAA;AAAA,EAEf,SAAA,GAAY,KAAA;AAAA,EAEZ,aAAA,GAAoC,IAAA;AAAA,EACpC,cAAA,GAA0C,IAAA;AAAA,EACjC,UAAU,IAAI,cAAA,CAAe,MAAM,IAAA,CAAK,QAAQ,CAAA;AAAA,EAEhD,kBAAkB,MAAY;AACrC,IAAA,IAAA,CAAK,MAAA,EAAO;AAAA,EACd,CAAA;AAAA,EAGS,aAAA,GAAgB,IAAI,YAAA,CAAa,MAAM;AAC9C,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf,CAAC,CAAA;AAAA,EAGQ,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAIlB,IAAA,IAAA,CAAK,SAAA,GAAY,KAAK,gBAAA,EAAiB;AACvC,IAAA,IAAA,CAAK,QAAQ,eAAA,EAAgB;AAC7B,IAAA,IAAA,CAAK,oBAAA,EAAqB;AAC1B,IAAA,IAAA,CAAK,MAAA,EAAO;AAAA,EACd;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,cAAc,UAAA,EAAW;AAC9B,IAAA,IAAA,CAAK,kBAAA,EAAmB;AACxB,IAAA,IAAA,CAAK,QAAQ,UAAA,EAAW;AAAA,EAC1B;AAAA,EAEA,mBAAA,GAA4B;AAC1B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EAEA,sBAAA,GAA+B;AAC7B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EAEA,0BAAA,GAAmC;AACjC,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EASA,uBAAA,GAAgC;AAC9B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EAGA,0BAAA,GAAmC;AACjC,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EAGA,sBAAA,GAA+B;AAC7B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EAGA,yBAAA,GAAkC;AAChC,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EAkBA,8BAA8B,OAAA,EAA4B;AACxD,IAAA,IAAI,IAAA,CAAK,UAAA,EAAY,OAAA,CAAQ,MAAA,GAAS,KAAA;AACtC,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EAUA,MAAA,GAAe;AACb,IAAA,IAAI,CAAC,IAAA,CAAK,UAAA,IAAc,CAAC,KAAK,YAAA,EAAc;AAC5C,IAAA,IAAA,CAAK,SAAA,GAAY,CAAC,IAAA,CAAK,SAAA;AACvB,IAAA,IAAA,CAAK,OAAA,EAAQ;AACb,IAAA,IAAA,CAAK,QAAA,CAAS,UAAU,EAAE,MAAA,EAAQ,EAAE,QAAA,EAAU,IAAA,CAAK,SAAA,EAAU,EAAG,CAAA;AAAA,EAClE;AAAA,EAOA,MAAA,GAAe;AACb,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,YAAA,GAAe,KAAK,gBAAA,EAAiB;AAC1C,IAAA,IAAI,CAAC,IAAA,CAAK,YAAA,EAAc,IAAA,CAAK,SAAA,GAAY,KAAA;AACzC,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EAGA,OAAA,GAAgB;AACd,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,oBAAA,EAAqB;AAC1B,IAAA,IAAA,CAAK,MAAA,EAAO;AAAA,EACd;AAAA,EAGA,gBAAA,GAA4B;AAC1B,IAAA,OAAO,KAAK,gBAAA,IAAoB,IAAA,CAAK,aAAA,CAAc,YAAA,CAAa,eAAe,CAAA,KAAM,MAAA;AAAA,EACvF;AAAA,EAUA,oBAAA,GAA6B;AAC3B,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,aAAA,GAAgB,IAAA,CAAK,UAAA,GAAa,IAAA;AACpD,IAAA,IAAI,IAAA,KAAS,KAAK,aAAA,EAAe;AAEjC,IAAA,IAAA,CAAK,kBAAA,EAAmB;AACxB,IAAA,IAAI,CAAC,IAAA,EAAM;AAEX,IAAA,IAAA,CAAK,aAAA,GAAgB,IAAA;AACrB,IAAA,IAAA,CAAK,OAAA,CAAQ,QAAQ,IAAI,CAAA;AACzB,IAAA,IAAI,OAAO,qBAAqB,WAAA,EAAa;AAC3C,MAAA,IAAA,CAAK,cAAA,GAAiB,IAAI,gBAAA,CAAiB,IAAA,CAAK,eAAe,CAAA;AAC/D,MAAA,IAAA,CAAK,cAAA,CAAe,OAAA,CAAQ,IAAA,EAAM,EAAE,SAAA,EAAW,MAAM,OAAA,EAAS,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,CAAA;AAAA,IAC3F;AAAA,EACF;AAAA,EAEA,kBAAA,GAA2B;AACzB,IAAA,IAAI,KAAK,aAAA,EAAe,IAAA,CAAK,OAAA,CAAQ,SAAA,CAAU,KAAK,aAAa,CAAA;AACjE,IAAA,IAAA,CAAK,aAAA,GAAgB,IAAA;AACrB,IAAA,IAAA,CAAK,gBAAgB,UAAA,EAAW;AAChC,IAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AAAA,EACxB;AAAA,EAsBA,gBAAA,GAA4B;AAC1B,IAAA,IAAI,CAAC,IAAA,CAAK,aAAA,IAAiB,KAAK,kBAAA,CAAmB,MAAA,KAAW,GAAG,OAAO,KAAA;AACxE,IAAA,IAAI,CAAC,IAAA,CAAK,iBAAA,IAAqB,CAAC,IAAA,CAAK,kBAAkB,OAAO,KAAA;AAC9D,IAAA,KAAA,MAAW,IAAA,IAAQ,IAAA,CAAK,kBAAA,EAAoB,IAAA,CAAK,MAAA,GAAS,KAAA;AAE1D,IAAA,IAAA,CAAK,eAAe,MAAA,GAAS,IAAA;AAC7B,IAAA,OAAO,IAAA,CAAK,UAAA,CAAW,WAAA,GAAc,IAAA,CAAK,WAAW,WAAA,GAAc,gBAAA;AAAA,EACrE;AAAA,EAKA,OAAA,GAAgB;AACd,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,YAAA,IAAgB,CAAC,IAAA,CAAK,SAAA;AAC7C,IAAA,MAAM,YAAA,GAAe,IAAA,CAAK,YAAA,IAAgB,IAAA,CAAK,mBAAmB,MAAA,GAAS,CAAA;AAI3E,IAAA,MAAM,WAAA,GACJ,SAAA,IACA,IAAA,CAAK,gBAAA,IACL,IAAA,CAAK,kBAAA,CAAmB,IAAA,CAAK,CAAC,CAAA,KAAM,IAAA,CAAK,WAAA,CAAY,CAAC,CAAC,CAAA;AAEzD,IAAA,KAAA,MAAW,IAAA,IAAQ,IAAA,CAAK,kBAAA,EAAoB,IAAA,CAAK,MAAA,GAAS,SAAA;AAC1D,IAAA,IAAI,IAAA,CAAK,iBAAA,EAAmB,IAAA,CAAK,wBAAA,CAAyB,YAAY,CAAA;AACtE,IAAA,IAAI,KAAK,gBAAA,EAAkB;AAEzB,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,YAAA,IAAgB,IAAA,CAAK,SAAA;AAC3C,MAAA,IAAA,CAAK,aAAA,CAAc,YAAA,CAAa,eAAA,EAAiB,QAAA,GAAW,SAAS,OAAO,CAAA;AAAA,IAC9E;AACA,IAAA,IAAI,WAAA,EAAa,IAAA,CAAK,aAAA,CAAc,KAAA,EAAM;AAAA,EAC5C;AAAA,EASA,yBAAyB,IAAA,EAAqB;AAC5C,IAAA,MAAM,WAAW,IAAA,CAAK,cAAA;AACtB,IAAA,IAAI,IAAA,EAAM;AACR,MAAA,IAAA,CAAK,cAAc,UAAA,EAAW;AAC9B,MAAA,QAAA,CAAS,MAAA,GAAS,KAAA;AAClB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AACxB,IAAA,IAAI,MAAA,YAAkB,WAAA,IAAe,QAAA,CAAS,QAAA,CAAS,MAAM,CAAA,EAAG;AAC9D,MAAA,QAAA,CAAS,MAAA,GAAS,KAAA;AAClB,MAAA,IAAA,CAAK,aAAA,CAAc,UAAU,MAAM,CAAA;AACnC,MAAA;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,cAAc,UAAA,EAAW;AAC9B,IAAA,QAAA,CAAS,MAAA,GAAS,IAAA;AAAA,EACpB;AAAA,EAGA,YAAY,OAAA,EAA+B;AACzC,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AACxB,IAAA,OAAO,MAAA,YAAkB,WAAA,IAAe,OAAA,CAAQ,QAAA,CAAS,MAAM,CAAA;AAAA,EACjE;AACF","file":"breadcrumb_controller.js","sourcesContent":["/**\n * Shared bookkeeping for updates that must wait until an element loses focus.\n *\n * A control that becomes redundant while the user is standing on it cannot be\n * removed from the page immediately: hiding a focused element (or turning it\n * into a native `disabled` one) drops focus to `<body>`, stranding a keyboard\n * user mid-widget. The fix every affected controller reached for is the same —\n * keep the element reachable, hold the destructive update, listen for `blur`,\n * and apply it then.\n *\n * {@link BlurDeferral} owns *only* the registry part of that: which elements are\n * currently holding an update back, attaching and detaching the `blur` listener,\n * and guaranteeing teardown. It is deliberately **policy-free** — what the\n * interim state looks like (`aria-disabled`, an ownership marker, nothing at\n * all), and what the deferred update actually is, stay in the controller, which\n * receives the element back through its release callback.\n *\n * Three contracts are worth stating up front, because each one is a hazard the\n * consumers would otherwise hit:\n *\n * - **The release callback fires only on a real `blur`.** `release()` /\n * `releaseAll()` *cancel* a deferral; they do not complete it. Conflating the\n * two makes `overflow-indicator` disable a button that just left the boundary.\n * - **`elements` is a snapshot.** Iterating the live key set while releasing\n * inside the loop is the failure this registry exists to prevent.\n * - **The entry is detached before the callback runs**, so a controller that\n * decides the update is still unsafe can simply defer again.\n *\n * @example\n * ```ts\n * readonly #deferred = new BlurDeferral<HTMLElement>((trigger) => {\n * if (this.#connected) this.#evaluate();\n * });\n *\n * #hide(trigger: HTMLElement) {\n * if (document.activeElement === trigger) {\n * this.#deferred.deferOnly(trigger); // stays visible until blur\n * return;\n * }\n * this.#deferred.releaseAll();\n * trigger.hidden = true;\n * }\n *\n * disconnect() {\n * this.#deferred.releaseAll();\n * }\n * ```\n */\nexport class BlurDeferral<T extends HTMLElement = HTMLElement> {\n /** Elements currently holding an update back, mapped to their `blur` listener. */\n readonly #pending = new Map<T, () => void>();\n /** Called after a pending element blurs and has been detached. */\n readonly #onRelease: (element: T) => void;\n\n /** @param onRelease - Invoked once `element` actually blurs; never on `release`. */\n constructor(onRelease: (element: T) => void) {\n this.#onRelease = onRelease;\n }\n\n /** Number of elements currently holding an update back. */\n get size(): number {\n return this.#pending.size;\n }\n\n /** Snapshot of the pending elements, safe to iterate while releasing them. */\n get elements(): T[] {\n return [...this.#pending.keys()];\n }\n\n /** Whether `element` is currently holding an update back. */\n has(element: T): boolean {\n return this.#pending.has(element);\n }\n\n /** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */\n defer(element: T): void {\n if (this.#pending.has(element)) return;\n const onBlur = (): void => {\n this.#detach(element);\n this.#onRelease(element);\n };\n this.#pending.set(element, onBlur);\n element.addEventListener(\"blur\", onBlur);\n }\n\n /** Defers `element` as the only pending entry, cancelling any others. */\n deferOnly(element: T): void {\n for (const pending of this.elements) {\n if (pending !== element) this.#detach(pending);\n }\n this.defer(element);\n }\n\n /** Cancels `element`'s deferral without completing it; no-ops when not pending. */\n release(element: T): void {\n this.#detach(element);\n }\n\n /** Cancels every deferral without completing any of them. */\n releaseAll(): void {\n for (const element of this.elements) this.#detach(element);\n }\n\n /** Removes the `blur` listener for `element` and forgets it. */\n #detach(element: T): void {\n const onBlur = this.#pending.get(element);\n if (onBlur) element.removeEventListener(\"blur\", onBlur);\n this.#pending.delete(element);\n }\n}\n","/**\n * Unified layout observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — have three sources that move the layout under them: their\n * *own* box changing — via {@link ResizeObserver} — the *viewport* changing — via the\n * `window` `resize` event — and a descendant *resource settling*, because an image or\n * a frame reports a height of zero until it has loaded. Wiring those three by hand in\n * every controller risks leaked listeners on `disconnect()`. {@link LayoutObserver}\n * owns all three behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * this.#layout.observeDescendantLoads(this.panelTarget);\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n #loadContainer: Element | null = null;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */\n readonly #handleDescendantLoad = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Starts reporting a `load` from anywhere inside `container` — an image or a\n * frame settling changes the box it sits in, and it measures as zero high until\n * then. `load` does not bubble, so the subscription is a capture-phase listener\n * on the container itself and nothing the caller spells.\n *\n * **One container at a time.** A further call moves the observation, so a widget\n * whose content element is swapped at runtime releases the element it let go by\n * naming the new one — there is no second place for the release to drift from.\n */\n observeDescendantLoads(container: Element): void {\n this.unobserveDescendantLoads();\n this.#loadContainer = container;\n container.addEventListener(\"load\", this.#handleDescendantLoad, true);\n }\n\n /** Stops reporting descendant loads without affecting element or viewport observation. */\n unobserveDescendantLoads(): void {\n this.#loadContainer?.removeEventListener(\"load\", this.#handleDescendantLoad, true);\n this.#loadContainer = null;\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and removes\n * the viewport and descendant-load listeners. Safe to call multiple times. Call\n * this from a controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n this.unobserveDescendantLoads();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BlurDeferral } from \"../utils/blur_deferral\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/**\n * Sub-pixel tolerance (px) applied to the overflow comparison.\n *\n * Fractional layout metrics (zoom, `border-box` rounding, fractional font\n * metrics) routinely make `scrollWidth` exceed `clientWidth` by a fraction of a\n * pixel on a trail that visually fits, which would collapse it for no reason.\n */\nconst OVERFLOW_EPSILON = 1;\n\n/**\n * Headless, accessible responsive breadcrumb behavior.\n *\n * Markup contract (identifier: `stimeo--breadcrumb`):\n * <nav data-controller=\"stimeo--breadcrumb\" aria-label=\"Breadcrumb\">\n * <ol data-stimeo--breadcrumb-target=\"list\">\n * <li><a href=\"/\">Home</a></li>\n * <li data-stimeo--breadcrumb-target=\"ellipsis\" hidden>\n * <button type=\"button\" aria-expanded=\"false\" aria-controls=\"bc-a bc-b\"\n * aria-label=\"Show full path\"\n * data-stimeo--breadcrumb-target=\"trigger\"\n * data-action=\"click->stimeo--breadcrumb#toggle\">…</button>\n * </li>\n * <li id=\"bc-a\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a\">Section A</a></li>\n * <li id=\"bc-b\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a/b\">Sub B</a></li>\n * <li><a href=\"/a/b/c\" aria-current=\"page\">Item C</a></li>\n * </ol>\n * </nav>\n *\n * Implements the WAI-ARIA APG **Breadcrumb** pattern. The base structure (`nav`\n * + `ol` + `aria-current=\"page\"`) lives in the markup; this controller adds the\n * responsive behavior: when the trail overflows its container it collapses the\n * author-marked middle items behind a disclosure (`…`) button, which expands\n * them back on demand.\n *\n * `toggle` dispatches `{ expanded: boolean }`.\n *\n * @remarks\n * Behavior only — the consumer owns separators (CSS) and the look. The items to\n * collapse are the author-marked `collapsible` targets (the source of truth),\n * placed between the leading and trailing items that must always stay visible.\n *\n * Behavior provided:\n * - Detects overflow via a shared layout observer (element + viewport resize) and a\n * `MutationObserver` on the list (content edits, Turbo Stream / morph item\n * swaps); the public `update` action re-measures on demand.\n * - While overflowing and not expanded, hides the `collapsible` items and shows\n * the `ellipsis` item; when it fits, shows everything, hides the ellipsis and\n * resets the expanded state (so a later collapse starts collapsed).\n * - The disclosure `trigger` toggles `aria-expanded` and the collapsed items,\n * dispatching `stimeo--breadcrumb:toggle`. `toggle` is a no-op (no state\n * change, no event) while the trail fits, so a hidden trigger can never be\n * left reporting `aria-expanded=\"true\"`.\n *\n * Layout requirements the behavior depends on:\n * - **The list must not wrap** (`white-space: nowrap` / `flex-wrap: nowrap`). A\n * wrapping list grows in height instead of scroll width, so\n * `scrollWidth === clientWidth` always holds and overflow is never detected.\n * - **The list width must follow its container** (no shrink-to-fit / intrinsic\n * width). Overflow is re-measured synchronously inside the resize\n * notification, so a list that widens to its content would never report\n * overflow. There is deliberately no debounce: the pass is one write → one\n * forced read → one write on a single element.\n * - Consumers must not neutralize `[hidden]` (e.g. `display: inline-flex` on the\n * items wins over the UA's `[hidden] { display: none }`); collapsing is\n * expressed exclusively through the `hidden` attribute.\n *\n * Ownership of `hidden`: this controller owns the attribute on an element **for as\n * long as that element is a `collapsible` target and the controller is connected**.\n * Outside that window it does not write it:\n * - lose the marker while connected → the element becomes an always-visible item\n * and the controller clears the `hidden` it owned;\n * - controller `disconnect()` → the collapsed state is left in the DOM on purpose,\n * because that DOM is what Turbo caches and restores.\n *\n * A consumer that also wants to own `hidden` on the same element (for a reason of\n * its own) is not supported: the overflow measurement clears it on every pass to\n * read the expanded width.\n *\n * Focus safety: hiding the element the user is standing on would drop focus to\n * `<body>`. When a collapse would hide the focused item, focus moves to the\n * disclosure `trigger` first; when the ellipsis would be hidden while it holds\n * focus, the hide waits for `blur`.\n *\n * **Fail-safe when the disclosure is incomplete.** Collapsing needs all of\n * `list`, one `collapsible`, `ellipsis`, and `trigger`; miss any and the trail\n * degrades to a plain APG breadcrumb (every item visible, ellipsis hidden,\n * `aria-expanded=\"false\"`, `toggle` a silent no-op). Hiding the middle items\n * with no control that can reveal them puts the path out of reach for good — a\n * too-wide trail is the cheaper failure. Watched at runtime, so a swap that\n * breaks the set releases the items and one that completes it starts collapsing.\n */\nexport class BreadcrumbController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"collapsible\", \"ellipsis\", \"trigger\"];\n static actions = [\"toggle\", \"update\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly collapsibleTargets: HTMLElement[];\n declare readonly ellipsisTarget: HTMLElement;\n declare readonly triggerTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasEllipsisTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n /** Guards target callbacks that can fire outside the connected window. */\n #connected = false;\n /** Whether the trail currently overflows its container. */\n #overflowing = false;\n /** Whether the user has expanded the collapsed items via the disclosure. */\n #expanded = false;\n /** The list element currently observed (resize + mutations), if any. */\n #observedList: HTMLElement | null = null;\n #listMutations: MutationObserver | null = null;\n readonly #layout = new LayoutObserver(() => this.update());\n\n readonly #onListMutation = (): void => {\n this.update();\n };\n\n /** Holds the ellipsis hide back while it contains focus; re-renders on blur. */\n readonly #deferredHide = new BlurDeferral(() => {\n this.#render();\n });\n\n /** Starts observing for overflow and renders the initial state. */\n override connect(): void {\n this.#connected = true;\n // The DOM is the source of truth on reconnect (Turbo cache restore / morph):\n // the trigger's `aria-expanded` is where the expanded state lives, so a trail\n // the user opened survives a back-navigation instead of silently re-collapsing.\n this.#expanded = this.#initialExpanded();\n this.#layout.observeViewport();\n this.#syncListObservation();\n this.update();\n }\n\n /** Releases the resize/mutation observation (Turbo navigation included). */\n override disconnect(): void {\n this.#connected = false;\n this.#deferredHide.releaseAll();\n this.#stopObservingList();\n this.#layout.disconnect();\n }\n\n listTargetConnected(): void {\n this.#resync();\n }\n\n listTargetDisconnected(): void {\n this.#resync();\n }\n\n collapsibleTargetConnected(): void {\n this.#resync();\n }\n\n /**\n * Re-measures when an `ellipsis` target arrives. The `ellipsis` and `trigger`\n * targets form the disclosure set, a precondition for collapsing at all, so they\n * need the same watching the list and items get: without their callbacks a swap\n * that breaks or completes the set would only take effect at the next resize or\n * list mutation, which may never come.\n */\n ellipsisTargetConnected(): void {\n this.#resync();\n }\n\n /** Re-measures when an `ellipsis` target leaves the disclosure set. */\n ellipsisTargetDisconnected(): void {\n this.#resync();\n }\n\n /** Re-measures when a `trigger` target joins the disclosure set. */\n triggerTargetConnected(): void {\n this.#resync();\n }\n\n /** Re-measures when a `trigger` target leaves the disclosure set. */\n triggerTargetDisconnected(): void {\n this.#resync();\n }\n\n /**\n * Hands `hidden` back when an element stops being a `collapsible` target.\n *\n * The marker is the source of truth for what may be collapsed, so an element\n * that loses it while the controller is live is an always-visible item again —\n * and rendering only walks the *current* targets, so nothing else would ever\n * clear the `hidden` this controller put there.\n *\n * The `#connected` guard is load-bearing, not defensive. Stimulus fires this\n * callback for **every** target during teardown as well (`Context.disconnect()`\n * stops the target observer after `disconnect()` has run), and there the\n * collapsed DOM must survive untouched so Turbo's cache restores the trail in\n * the state the user left it (see the ownership note in {@link BreadcrumbController}).\n *\n * @param element - the element that just left the `collapsible` target set\n */\n collapsibleTargetDisconnected(element: HTMLElement): void {\n if (this.#connected) element.hidden = false;\n this.#resync();\n }\n\n /**\n * Expands or re-collapses the trail and dispatches `toggle`.\n *\n * No-op while the trail fits: there is nothing collapsed to reveal, and the\n * (hidden) trigger would otherwise keep a stale `aria-expanded=\"true\"` that\n * surfaces pre-expanded the next time the trail overflows. No event is\n * dispatched in that case.\n */\n toggle(): void {\n if (!this.#connected || !this.#overflowing) return;\n this.#expanded = !this.#expanded;\n this.#render();\n this.dispatch(\"toggle\", { detail: { expanded: this.#expanded } });\n }\n\n /**\n * Re-measures overflow and re-renders. Wired to resizes and list mutations;\n * exposed as an action so consumers can re-measure after a change the\n * observers cannot see (e.g. a web font swap that only alters text width).\n */\n update(): void {\n if (!this.#connected) return;\n this.#overflowing = this.#measureOverflow();\n if (!this.#overflowing) this.#expanded = false;\n this.#render();\n }\n\n /** Re-attaches the list observation (targets may have been swapped) and re-measures. */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncListObservation();\n this.update();\n }\n\n /** Reads the expanded state back from its DOM home (the trigger's `aria-expanded`). */\n #initialExpanded(): boolean {\n return this.hasTriggerTarget && this.triggerTarget.getAttribute(\"aria-expanded\") === \"true\";\n }\n\n /**\n * Points the resize + mutation observation at the current `list` target.\n *\n * The list is re-resolved rather than captured at connect so a target swapped\n * at runtime (Turbo Stream replacement of the `<ol>`) is observed too. Only\n * `childList` / `subtree` / `characterData` are watched — deliberately **not**\n * `attributes`, which the controller's own `hidden` writes would re-trigger.\n */\n #syncListObservation(): void {\n const next = this.hasListTarget ? this.listTarget : null;\n if (next === this.#observedList) return;\n\n this.#stopObservingList();\n if (!next) return;\n\n this.#observedList = next;\n this.#layout.observe(next);\n if (typeof MutationObserver !== \"undefined\") {\n this.#listMutations = new MutationObserver(this.#onListMutation);\n this.#listMutations.observe(next, { childList: true, subtree: true, characterData: true });\n }\n }\n\n #stopObservingList(): void {\n if (this.#observedList) this.#layout.unobserve(this.#observedList);\n this.#observedList = null;\n this.#listMutations?.disconnect();\n this.#listMutations = null;\n }\n\n /**\n * Measures overflow against the **fully expanded** layout so hiding items does\n * not make the condition oscillate. Reveals every item, then compares the list's\n * own scroll width to its own client width; `#render` immediately re-applies\n * the correct hidden state afterward.\n *\n * Both widths are read from the list element itself (not the host `nav`) so the\n * check is independent of any padding/border on the host — comparing against the\n * host's `clientWidth` would over-report the available space by its padding and\n * miss real overflow in a padded container.\n *\n * With no `collapsible` items there is nothing to collapse, so the trail is\n * never reported as overflowing (which also keeps the disclosure out of the\n * tab order — a button controlling nothing).\n *\n * The full disclosure set is a precondition: collapsing without an `ellipsis`\n * *and* a `trigger` would hide items behind a control that does not exist, so\n * an incomplete set reports \"not overflowing\" and `#render` degrades the trail\n * to a plain breadcrumb. Losing layout is recoverable; losing the path is not.\n */\n #measureOverflow(): boolean {\n if (!this.hasListTarget || this.collapsibleTargets.length === 0) return false;\n if (!this.hasEllipsisTarget || !this.hasTriggerTarget) return false;\n for (const item of this.collapsibleTargets) item.hidden = false;\n // No `hasEllipsisTarget` check: the line above already returned without one.\n this.ellipsisTarget.hidden = true;\n return this.listTarget.scrollWidth > this.listTarget.clientWidth + OVERFLOW_EPSILON;\n }\n\n /**\n * Applies the collapsed/expanded state to the items, ellipsis, and trigger.\n */\n #render(): void {\n const collapsed = this.#overflowing && !this.#expanded;\n const showEllipsis = this.#overflowing && this.collapsibleTargets.length > 0;\n\n // Decided *before* the writes below: once a focused item is hidden the browser\n // drops focus to <body> and the information is gone.\n const rescueFocus =\n collapsed &&\n this.hasTriggerTarget &&\n this.collapsibleTargets.some((i) => this.#holdsFocus(i));\n\n for (const item of this.collapsibleTargets) item.hidden = collapsed;\n if (this.hasEllipsisTarget) this.#applyEllipsisVisibility(showEllipsis);\n if (this.hasTriggerTarget) {\n // Never report expanded while the trail fits: nothing is collapsed to reveal.\n const expanded = this.#overflowing && this.#expanded;\n this.triggerTarget.setAttribute(\"aria-expanded\", expanded ? \"true\" : \"false\");\n }\n if (rescueFocus) this.triggerTarget.focus();\n }\n\n /**\n * Shows or hides the ellipsis item, deferring a hide that would blur the user.\n *\n * Hiding the element that currently holds focus drops focus to `<body>`, so the\n * hide waits for that element's `blur` and is re-applied then (the shared\n * `BlurDeferral` registry).\n */\n #applyEllipsisVisibility(show: boolean): void {\n const ellipsis = this.ellipsisTarget;\n if (show) {\n this.#deferredHide.releaseAll();\n ellipsis.hidden = false;\n return;\n }\n\n const active = document.activeElement;\n if (active instanceof HTMLElement && ellipsis.contains(active)) {\n ellipsis.hidden = false;\n this.#deferredHide.deferOnly(active);\n return;\n }\n\n this.#deferredHide.releaseAll();\n ellipsis.hidden = true;\n }\n\n /** Whether `element` contains (or is) the currently focused element. */\n #holdsFocus(element: HTMLElement): boolean {\n const active = document.activeElement;\n return active instanceof HTMLElement && element.contains(active);\n }\n}\n"]}
|
|
@@ -76,10 +76,17 @@ declare class BulkSelectController extends Controller<HTMLElement> {
|
|
|
76
76
|
default: string;
|
|
77
77
|
};
|
|
78
78
|
};
|
|
79
|
+
static valueConstraints: {
|
|
80
|
+
totalCount: {
|
|
81
|
+
readonly finite: true;
|
|
82
|
+
readonly min: 0;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
79
85
|
static actions: readonly ["clear", "selectAllPages"];
|
|
80
86
|
static events: readonly ["change", "reconcile"];
|
|
81
87
|
readonly allTarget: HTMLInputElement;
|
|
82
88
|
readonly barTarget: HTMLElement;
|
|
89
|
+
readonly barTargets: HTMLElement[];
|
|
83
90
|
readonly countTarget: HTMLElement;
|
|
84
91
|
readonly itemTargets: HTMLInputElement[];
|
|
85
92
|
readonly hasAllTarget: boolean;
|
|
@@ -97,6 +104,17 @@ declare class BulkSelectController extends Controller<HTMLElement> {
|
|
|
97
104
|
allTargetConnected(): void;
|
|
98
105
|
/** Repairs the figures after the select-all box leaves. */
|
|
99
106
|
allTargetDisconnected(): void;
|
|
107
|
+
/** Shows or hides a bar that arrives at runtime as the selection calls for. */
|
|
108
|
+
barTargetConnected(): void;
|
|
109
|
+
/**
|
|
110
|
+
* Gives a bar that no longer resolves its own `hidden` back, even after `disconnect()`,
|
|
111
|
+
* and repaints the bar that stays.
|
|
112
|
+
*/
|
|
113
|
+
barTargetDisconnected(bar: HTMLElement): void;
|
|
114
|
+
/** Writes the shown figure into a count that arrives at runtime. */
|
|
115
|
+
countTargetConnected(): void;
|
|
116
|
+
/** Writes the shown figure into the count that stays after another leaves. */
|
|
117
|
+
countTargetDisconnected(): void;
|
|
100
118
|
/** Repaints the count for a total that changed at runtime. */
|
|
101
119
|
totalCountValueChanged(): void;
|
|
102
120
|
/** Repaints so wording changed at runtime is used by the next announcement. */
|