stimeo-ui 0.16.0 → 0.18.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 +136 -0
- package/README.md +27 -11
- package/dist/cable/index.d.ts +88 -31
- package/dist/cable/index.js +384 -449
- package/dist/cable/index.js.map +1 -1
- package/dist/coerce-JtZVNG5h.d.ts +20 -0
- package/dist/controllers/accordion_controller.d.ts +4 -2
- package/dist/controllers/accordion_controller.js +174 -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 +903 -235
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +37 -5
- package/dist/controllers/announcer_controller.js +238 -205
- 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 +414 -145
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +185 -107
- 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 +21 -3
- package/dist/controllers/bulk_select_controller.js +282 -58
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +38 -16
- package/dist/controllers/calendar_controller.js +228 -260
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +43 -12
- package/dist/controllers/carousel_controller.js +536 -436
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +44 -7
- package/dist/controllers/character_counter_controller.js +493 -207
- 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 +38 -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 +261 -149
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +17 -5
- package/dist/controllers/collapsible_controller.js +304 -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 +178 -146
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +24 -6
- package/dist/controllers/combobox_controller.js +522 -156
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +51 -20
- package/dist/controllers/command_palette_controller.js +1156 -395
- 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 +300 -171
- 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 +980 -262
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +80 -18
- package/dist/controllers/context_menu_controller.js +346 -153
- 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 +47 -3
- package/dist/controllers/countdown_controller.js +281 -158
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +93 -10
- package/dist/controllers/currency_input_controller.js +575 -225
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +25 -12
- package/dist/controllers/data_grid_controller.js +248 -214
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +42 -14
- package/dist/controllers/date_range_picker_controller.js +338 -235
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +17 -6
- package/dist/controllers/dialog_controller.js +899 -237
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +25 -8
- package/dist/controllers/direct_upload_controller.js +97 -167
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +6 -1
- package/dist/controllers/dirty_form_controller.js +72 -69
- 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 +196 -24
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +39 -11
- package/dist/controllers/drawer_controller.js +974 -355
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +31 -1
- package/dist/controllers/dropdown_controller.js +339 -96
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +315 -88
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +79 -58
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +63 -18
- package/dist/controllers/file_dropzone_controller.js +322 -211
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +77 -42
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +35 -12
- package/dist/controllers/flash_controller.js +238 -347
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +15 -6
- package/dist/controllers/focus_controller.js +679 -240
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +156 -132
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.d.ts +3 -3
- package/dist/controllers/form_validation_controller.js +31 -97
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +47 -14
- package/dist/controllers/frame_loading_controller.js +275 -217
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +27 -6
- 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 +60 -4
- package/dist/controllers/hover_card_controller.js +327 -130
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +48 -6
- package/dist/controllers/idle_controller.js +271 -101
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +30 -13
- package/dist/controllers/input_mask_controller.js +483 -97
- 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 +150 -106
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +19 -2
- package/dist/controllers/lazy_frame_controller.js +24 -68
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +44 -9
- package/dist/controllers/listbox_controller.js +523 -240
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +9 -7
- package/dist/controllers/local_time_controller.js +102 -77
- 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 +80 -96
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +55 -11
- package/dist/controllers/menu_controller.js +302 -188
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +51 -9
- package/dist/controllers/menubar_controller.js +232 -345
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +33 -20
- package/dist/controllers/meter_controller.js +417 -87
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +48 -18
- package/dist/controllers/multi_select_controller.js +785 -393
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +27 -6
- package/dist/controllers/navigation_menu_controller.js +190 -250
- 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 +396 -192
- 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 +268 -62
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +50 -8
- package/dist/controllers/number_input_controller.js +610 -303
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.d.ts +8 -5
- package/dist/controllers/optimistic_controller.js +45 -87
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +55 -8
- package/dist/controllers/otp_controller.js +658 -255
- 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 +122 -113
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +58 -20
- package/dist/controllers/overflow_menu_controller.js +470 -295
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +37 -7
- package/dist/controllers/pagination_controller.js +332 -123
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +46 -8
- package/dist/controllers/password_reveal_controller.js +392 -121
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +15 -6
- package/dist/controllers/password_strength_controller.js +111 -158
- 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 +165 -111
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +35 -12
- package/dist/controllers/pointer_drag_controller.js +434 -188
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +41 -5
- package/dist/controllers/popover_controller.js +269 -113
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +44 -4
- package/dist/controllers/portal_controller.js +215 -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 +243 -116
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +20 -19
- package/dist/controllers/progress_controller.js +391 -78
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +21 -9
- package/dist/controllers/radio_group_controller.js +278 -211
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +47 -6
- package/dist/controllers/range_slider_controller.js +220 -113
- 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 +458 -211
- 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 +138 -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 +166 -137
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +20 -6
- package/dist/controllers/relative_time_controller.js +211 -112
- 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} +103 -38
- package/dist/controllers/reset_on_restore_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +31 -2
- package/dist/controllers/resizable_controller.js +299 -150
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +3 -3
- package/dist/controllers/roving_controller.js +49 -88
- 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 +490 -246
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +3 -3
- package/dist/controllers/scroll_restore_controller.js +14 -55
- 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 +432 -149
- 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 +193 -248
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +20 -5
- package/dist/controllers/separator_controller.js +285 -142
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +65 -8
- package/dist/controllers/sidebar_controller.js +1069 -404
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +23 -0
- package/dist/controllers/skeleton_controller.js +259 -101
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +29 -2
- package/dist/controllers/slider_controller.js +187 -86
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +22 -4
- package/dist/controllers/smart_sticky_header_controller.js +173 -66
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.d.ts +4 -4
- package/dist/controllers/sortable_controller.js +26 -125
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +39 -4
- package/dist/controllers/spinner_controller.js +288 -173
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +9 -4
- package/dist/controllers/step_indicator_controller.js +105 -46
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +14 -6
- package/dist/controllers/stepper_controller.js +120 -73
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +9 -1
- package/dist/controllers/stick_to_bottom_controller.js +275 -140
- 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 +220 -49
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +24 -8
- package/dist/controllers/submit_once_controller.js +376 -227
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +18 -4
- package/dist/controllers/switch_controller.js +201 -84
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +9 -6
- package/dist/controllers/tabs_controller.js +41 -30
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +16 -3
- package/dist/controllers/tags_input_controller.js +293 -173
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +11 -1
- package/dist/controllers/textarea_autosize_controller.js +108 -74
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +24 -11
- package/dist/controllers/theme_controller.js +140 -116
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +55 -5
- package/dist/controllers/time_picker_controller.js +331 -127
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +40 -16
- package/dist/controllers/toast_controller.js +244 -324
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +14 -6
- package/dist/controllers/toggle_group_controller.js +252 -191
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +8 -6
- package/dist/controllers/toolbar_controller.js +219 -120
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +49 -4
- 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 +22 -12
- package/dist/controllers/tree_view_controller.js +283 -369
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +8573 -10410
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +38 -14
- package/dist/inspector/cli.js +108 -40
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +145 -64
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +20 -20
- package/dist/inspector/manifest.json +802 -101
- package/dist/positioning/index.d.ts +15 -7
- package/dist/positioning/index.js +154 -47
- package/dist/positioning/index.js.map +1 -1
- package/package.json +11 -10
- package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
- package/dist/controllers/reset_before_cache_controller.js.map +0 -1
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function setAttributeIfChanged(element, name, value, beforeWrite) {
|
|
4
|
+
if (element.getAttribute(name) === value) return false;
|
|
5
|
+
if (value === null) element.removeAttribute(name);
|
|
6
|
+
else element.setAttribute(name, value);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
4
9
|
|
|
5
|
-
// src/utils/layout_observer.ts
|
|
6
10
|
var LayoutObserver = class {
|
|
7
11
|
#callback;
|
|
8
12
|
#resizeObserverFactory;
|
|
9
13
|
#resizeObserver = null;
|
|
10
14
|
#observingViewport = false;
|
|
11
15
|
#loadContainer = null;
|
|
12
|
-
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
13
16
|
#handleViewportResize = () => {
|
|
14
17
|
this.#callback();
|
|
15
18
|
};
|
|
16
|
-
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
17
19
|
#handleDescendantLoad = () => {
|
|
18
20
|
this.#callback();
|
|
19
21
|
};
|
|
@@ -21,11 +23,6 @@ var LayoutObserver = class {
|
|
|
21
23
|
this.#callback = callback;
|
|
22
24
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
23
25
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Starts observing an element's size. Repeated calls observe additional
|
|
26
|
-
* elements through the same shared observer. No-ops when no
|
|
27
|
-
* `ResizeObserver` implementation is available.
|
|
28
|
-
*/
|
|
29
26
|
observe(element) {
|
|
30
27
|
if (!this.#resizeObserverFactory) return;
|
|
31
28
|
if (!this.#resizeObserver) {
|
|
@@ -35,47 +32,28 @@ var LayoutObserver = class {
|
|
|
35
32
|
}
|
|
36
33
|
this.#resizeObserver.observe(element);
|
|
37
34
|
}
|
|
38
|
-
/** Stops observing a single element while leaving any others in place. */
|
|
39
35
|
unobserve(element) {
|
|
40
36
|
this.#resizeObserver?.unobserve(element);
|
|
41
37
|
}
|
|
42
|
-
/** Starts observing viewport resizes. Idempotent: the listener is added once. */
|
|
43
38
|
observeViewport() {
|
|
44
39
|
if (this.#observingViewport) return;
|
|
45
40
|
this.#observingViewport = true;
|
|
46
41
|
window.addEventListener("resize", this.#handleViewportResize);
|
|
47
42
|
}
|
|
48
|
-
/** Stops observing viewport resizes without affecting element observation. */
|
|
49
43
|
unobserveViewport() {
|
|
50
44
|
if (!this.#observingViewport) return;
|
|
51
45
|
this.#observingViewport = false;
|
|
52
46
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
53
47
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Starts reporting a `load` from anywhere inside `container` — an image or a
|
|
56
|
-
* frame settling changes the box it sits in, and it measures as zero high until
|
|
57
|
-
* then. `load` does not bubble, so the subscription is a capture-phase listener
|
|
58
|
-
* on the container itself and nothing the caller spells.
|
|
59
|
-
*
|
|
60
|
-
* **One container at a time.** A further call moves the observation, so a widget
|
|
61
|
-
* whose content element is swapped at runtime releases the element it let go by
|
|
62
|
-
* naming the new one — there is no second place for the release to drift from.
|
|
63
|
-
*/
|
|
64
48
|
observeDescendantLoads(container) {
|
|
65
49
|
this.unobserveDescendantLoads();
|
|
66
50
|
this.#loadContainer = container;
|
|
67
51
|
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
68
52
|
}
|
|
69
|
-
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
70
53
|
unobserveDescendantLoads() {
|
|
71
54
|
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
72
55
|
this.#loadContainer = null;
|
|
73
56
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Releases every observation: disconnects the {@link ResizeObserver} and removes
|
|
76
|
-
* the viewport and descendant-load listeners. Safe to call multiple times. Call
|
|
77
|
-
* this from a controller's `disconnect()`.
|
|
78
|
-
*/
|
|
79
57
|
disconnect() {
|
|
80
58
|
this.#resizeObserver?.disconnect();
|
|
81
59
|
this.#resizeObserver = null;
|
|
@@ -84,27 +62,25 @@ var LayoutObserver = class {
|
|
|
84
62
|
}
|
|
85
63
|
};
|
|
86
64
|
|
|
87
|
-
// src/utils/microtask_coalescer.ts
|
|
88
65
|
var MicrotaskCoalescer = class {
|
|
89
66
|
#run;
|
|
90
67
|
#queued = false;
|
|
91
68
|
#active = false;
|
|
92
69
|
#generation = 0;
|
|
93
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
94
70
|
constructor(run) {
|
|
95
71
|
this.#run = run;
|
|
96
72
|
}
|
|
97
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
98
73
|
activate() {
|
|
99
74
|
this.#active = true;
|
|
100
75
|
}
|
|
101
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
102
76
|
cancel() {
|
|
103
77
|
this.#active = false;
|
|
104
78
|
this.#queued = false;
|
|
105
79
|
this.#generation += 1;
|
|
106
80
|
}
|
|
107
|
-
|
|
81
|
+
get pending() {
|
|
82
|
+
return this.#queued;
|
|
83
|
+
}
|
|
108
84
|
schedule() {
|
|
109
85
|
if (!this.#active || this.#queued) return;
|
|
110
86
|
this.#queued = true;
|
|
@@ -117,69 +93,87 @@ var MicrotaskCoalescer = class {
|
|
|
117
93
|
}
|
|
118
94
|
};
|
|
119
95
|
|
|
120
|
-
|
|
96
|
+
var NUMBER_BOUNDS = {
|
|
97
|
+
finite: { finite: true }};
|
|
98
|
+
function matchesNumberBounds(value, bounds) {
|
|
99
|
+
if (!Number.isFinite(value)) {
|
|
100
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
101
|
+
if (direction === null) return false;
|
|
102
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
103
|
+
}
|
|
104
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
105
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
106
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
107
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
108
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readNumber(raw, fallback, bounds) {
|
|
113
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
var NumberValueReader = class {
|
|
117
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
118
|
+
read(owner, name, raw, fallback, bounds) {
|
|
119
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
120
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
121
|
+
this.#lastRejected.delete(name);
|
|
122
|
+
return resolved;
|
|
123
|
+
}
|
|
124
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
125
|
+
const literal = owner.element.getAttribute(attribute);
|
|
126
|
+
if (literal === null) {
|
|
127
|
+
this.#lastRejected.delete(name);
|
|
128
|
+
return resolved;
|
|
129
|
+
}
|
|
130
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
131
|
+
this.#lastRejected.set(name, literal);
|
|
132
|
+
console.warn(
|
|
133
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return resolved;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
121
140
|
var COLUMNS_PROPERTY = "--stimeo--masonry-columns";
|
|
122
141
|
var DEFAULT_MIN_COLUMN_WIDTH = 240;
|
|
123
142
|
var DEFAULT_GAP = 16;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
var MasonryController = class extends Controller {
|
|
143
|
+
var MasonryController = class _MasonryController extends Controller {
|
|
144
|
+
#numbers = new NumberValueReader();
|
|
128
145
|
static targets = ["item"];
|
|
129
146
|
static values = {
|
|
130
147
|
minColumnWidth: { type: Number, default: DEFAULT_MIN_COLUMN_WIDTH },
|
|
131
148
|
gap: { type: Number, default: DEFAULT_GAP }
|
|
132
149
|
};
|
|
150
|
+
static valueConstraints = {
|
|
151
|
+
minColumnWidth: NUMBER_BOUNDS.finite,
|
|
152
|
+
gap: NUMBER_BOUNDS.finite
|
|
153
|
+
};
|
|
133
154
|
static events = ["layout"];
|
|
134
|
-
/**
|
|
135
|
-
* The declared numbers after validation, so the layout path never sees a value
|
|
136
|
-
* it cannot compute with. Both are resolved once per declaration change rather
|
|
137
|
-
* than on every pass.
|
|
138
|
-
*/
|
|
139
155
|
#minColumnWidth = DEFAULT_MIN_COLUMN_WIDTH;
|
|
140
156
|
#gap = DEFAULT_GAP;
|
|
141
|
-
/**
|
|
142
|
-
* Collapses every re-layout trigger of one DOM mutation into a single pass, and
|
|
143
|
-
* refuses to run before `connect()` or after `disconnect()`.
|
|
144
|
-
*
|
|
145
|
-
* The triggers arrive in bursts — a resize stream, a morph that syncs several
|
|
146
|
-
* attributes, a batch of rows — and each pass measures every item, so folding
|
|
147
|
-
* them keeps the work proportional to the batch rather than to the events in it.
|
|
148
|
-
*/
|
|
149
157
|
#reconcile = new MicrotaskCoalescer(() => this.#relayout());
|
|
150
|
-
/** Items that left the target set and still carry the column hook. */
|
|
151
158
|
#released = /* @__PURE__ */ new Set();
|
|
152
159
|
#layout = new LayoutObserver(() => this.#reconcile.schedule());
|
|
153
160
|
#mutationObserver = null;
|
|
154
|
-
/** Last published column count, so `layout` fires only on real changes. */
|
|
155
161
|
#lastColumns = 0;
|
|
156
|
-
/** Resolves the declared column width once, falling back when it is unreadable. */
|
|
157
162
|
minColumnWidthValueChanged() {
|
|
158
|
-
this.#minColumnWidth =
|
|
163
|
+
this.#minColumnWidth = this.#safeMinColumnWidth;
|
|
159
164
|
this.#reconcile.schedule();
|
|
160
165
|
}
|
|
161
|
-
/** Resolves the declared gap once, falling back when it is unreadable. */
|
|
162
166
|
gapValueChanged() {
|
|
163
|
-
this.#gap =
|
|
167
|
+
this.#gap = this.#safeGap;
|
|
164
168
|
this.#reconcile.schedule();
|
|
165
169
|
}
|
|
166
|
-
/** Packs an element that became an item without moving in the DOM. */
|
|
167
170
|
itemTargetConnected() {
|
|
168
171
|
this.#reconcile.schedule();
|
|
169
172
|
}
|
|
170
|
-
/**
|
|
171
|
-
* Queues the column hook of an element that stopped being an item for removal.
|
|
172
|
-
*
|
|
173
|
-
* The removal is queued rather than immediate because teardown reports every
|
|
174
|
-
* target as disconnected: doing it here would strip the whole grid just before
|
|
175
|
-
* a Turbo snapshot is taken. The coalescer's `cancel` drops the queue
|
|
176
|
-
* with the pass, so only a genuine target change reaches it.
|
|
177
|
-
*/
|
|
178
173
|
itemTargetDisconnected(item) {
|
|
179
174
|
this.#released.add(item);
|
|
180
175
|
this.#reconcile.schedule();
|
|
181
176
|
}
|
|
182
|
-
/** Observes size/content changes and performs the first layout pass. */
|
|
183
177
|
connect() {
|
|
184
178
|
this.#layout.observe(this.element);
|
|
185
179
|
this.#layout.observeViewport();
|
|
@@ -191,31 +185,13 @@ var MasonryController = class extends Controller {
|
|
|
191
185
|
this.#relayout();
|
|
192
186
|
this.#reconcile.activate();
|
|
193
187
|
}
|
|
194
|
-
/** Releases every observation so nothing fires after detach. */
|
|
195
188
|
disconnect() {
|
|
196
189
|
this.#reconcile.cancel();
|
|
197
|
-
this.#released.clear();
|
|
198
190
|
this.#layout.disconnect();
|
|
199
191
|
this.#mutationObserver?.disconnect();
|
|
200
192
|
this.#mutationObserver = null;
|
|
201
193
|
this.#lastColumns = 0;
|
|
202
194
|
}
|
|
203
|
-
/**
|
|
204
|
-
* Recomputes the column count and assigns every item to the shortest column.
|
|
205
|
-
* Runs automatically on connect, on resize, on item add/remove, when a declared
|
|
206
|
-
* number changes, and when a descendant resource loads (private — there is no
|
|
207
|
-
* public action; the observers, the target callbacks and the capture-phase
|
|
208
|
-
* `load` listener drive it). Items are walked in DOM order; each lands in the
|
|
209
|
-
* column with the least accumulated height, which keeps the packing balanced
|
|
210
|
-
* without reordering the DOM.
|
|
211
|
-
*
|
|
212
|
-
* Every box is measured before anything is written. Interleaving the two would
|
|
213
|
-
* make a consumer's `data-column` rule invalidate style once per item, and the
|
|
214
|
-
* next measurement then has to settle layout again — once per item instead of
|
|
215
|
-
* once per pass. The assignment is independent of the measurement because the
|
|
216
|
-
* columns are uniform in width, so the order of the two passes does not change
|
|
217
|
-
* the result.
|
|
218
|
-
*/
|
|
219
195
|
#relayout() {
|
|
220
196
|
const items = this.itemTargets;
|
|
221
197
|
const columns = this.#columnCount();
|
|
@@ -239,10 +215,7 @@ var MasonryController = class extends Controller {
|
|
|
239
215
|
if ((heights[col] ?? 0) < (heights[shortest] ?? 0)) shortest = col;
|
|
240
216
|
}
|
|
241
217
|
const assigned = String(shortest);
|
|
242
|
-
if (item
|
|
243
|
-
item.setAttribute("data-column", assigned);
|
|
244
|
-
changed = true;
|
|
245
|
-
}
|
|
218
|
+
if (setAttributeIfChanged(item, "data-column", assigned)) changed = true;
|
|
246
219
|
heights[shortest] = (heights[shortest] ?? 0) + (boxes[index] ?? 0) + this.#gap;
|
|
247
220
|
});
|
|
248
221
|
this.element.style.setProperty(COLUMNS_PROPERTY, String(columns));
|
|
@@ -251,20 +224,31 @@ var MasonryController = class extends Controller {
|
|
|
251
224
|
this.dispatch("layout", { detail: { columns } });
|
|
252
225
|
}
|
|
253
226
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Derives how many columns fit: `floor((width + gap) / (minColumnWidth + gap))`,
|
|
256
|
-
* never fewer than one. When the width is unmeasurable (detached, or a layout
|
|
257
|
-
* engine that reports `0`), it falls back to a single column so every item still
|
|
258
|
-
* gets a valid `data-column`.
|
|
259
|
-
*/
|
|
260
227
|
#columnCount() {
|
|
261
228
|
const width = this.element.getBoundingClientRect().width;
|
|
262
229
|
const denominator = this.#minColumnWidth + this.#gap;
|
|
263
230
|
if (width <= 0 || denominator <= 0) return 1;
|
|
264
231
|
return Math.max(1, Math.floor((width + this.#gap) / denominator));
|
|
265
232
|
}
|
|
233
|
+
get #safeMinColumnWidth() {
|
|
234
|
+
return this.#numbers.read(
|
|
235
|
+
this,
|
|
236
|
+
"minColumnWidth",
|
|
237
|
+
this.minColumnWidthValue,
|
|
238
|
+
_MasonryController.values.minColumnWidth.default,
|
|
239
|
+
_MasonryController.valueConstraints.minColumnWidth
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
get #safeGap() {
|
|
243
|
+
return this.#numbers.read(
|
|
244
|
+
this,
|
|
245
|
+
"gap",
|
|
246
|
+
this.gapValue,
|
|
247
|
+
_MasonryController.values.gap.default,
|
|
248
|
+
_MasonryController.valueConstraints.gap
|
|
249
|
+
);
|
|
250
|
+
}
|
|
266
251
|
};
|
|
267
252
|
|
|
268
253
|
export { MasonryController };
|
|
269
|
-
//# sourceMappingURL=masonry_controller.js.map
|
|
270
254
|
//# sourceMappingURL=masonry_controller.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/layout_observer.ts","../../src/utils/microtask_coalescer.ts","../../src/controllers/masonry_controller.ts"],"names":[],"mappings":"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","file":"masonry_controller.js","sourcesContent":["/**\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","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/** CSS custom property exposing the current column count to consumer CSS. */\nconst COLUMNS_PROPERTY = \"--stimeo--masonry-columns\";\n\n/** Column width assumed when the declaration is absent or unreadable. */\nconst DEFAULT_MIN_COLUMN_WIDTH = 240;\n/** Item spacing assumed when the declaration is absent or unreadable. */\nconst DEFAULT_GAP = 16;\n\n/**\n * Returns `value` when it is a number the column arithmetic can use, else\n * `fallback`.\n *\n * A unit suffix is the ordinary authoring slip here (`\"240px\"`), and Stimulus'\n * Number reader answers `NaN` rather than raising — which would reach the column\n * count and make the column bookkeeping impossible to allocate, leaving the grid\n * with no hooks at all. An infinity is rejected for the same reason: it divides\n * into itself as `NaN`.\n */\nfunction usableNumber(value: number, fallback: number): number {\n return Number.isFinite(value) ? value : fallback;\n}\n\n/**\n * Headless **Masonry** layout helper: assigns each item to the shortest column so\n * variable-height cards pack without vertical gaps. There is no APG widget — this\n * is a layout-only utility that emits state hooks, never visual structure.\n *\n * Markup contract (identifier: `stimeo--masonry`):\n * <div data-controller=\"stimeo--masonry\"\n * data-stimeo--masonry-min-column-width-value=\"240\"\n * data-stimeo--masonry-gap-value=\"16\">\n * <div data-stimeo--masonry-target=\"item\">…</div>\n * <div data-stimeo--masonry-target=\"item\">…</div>\n * </div>\n *\n * The column count is derived responsively from the container width and\n * `minColumnWidth`; each item is then placed into whichever column is currently\n * shortest (measured from item heights). The count is published on the controller\n * element as the `--stimeo--masonry-columns` custom property and each item gets a\n * `data-column` index, so the consumer's CSS owns the actual placement.\n *\n * `layout` dispatches `{ columns: number }` whenever the published result moves —\n * the column count changed, or some item landed in a different column. A pass that\n * reproduces the previous result stays silent.\n *\n * @remarks\n * Behavior only. **DOM order is never changed** — reading order and focus order\n * stay the source markup order (WCAG 1.3.2). The visual packing is purely the\n * column assignment a consumer reads from `data-column`; this controller writes no\n * positioning styles. Use only for independent cards whose visual order carries no\n * meaning.\n *\n * Re-layout runs on connect, on resize (`LayoutObserver`), on item\n * add/remove ({@link MutationObserver}), on an item joining or leaving the target\n * set, when a declared number changes, and when a descendant resource loads.\n * Everything but the first pass is folded into one microtask, so a burst of\n * triggers costs one pass. The observers and any pending pass are released on\n * `disconnect()` (Turbo navigation included).\n *\n * Consumer contract:\n * - A declaration that cannot be read as a number (`\"240px\"`, an infinity) falls\n * back to that Value's default and the grid keeps working; `0` and negatives are\n * readable numbers and are used as declared; the count falls back to one column\n * only when `minColumnWidth + gap` is not positive, or the container has no\n * measurable width.\n * - `data-column` belongs to this controller: it is written on every item it owns\n * and taken back from an element that stops being one.\n */\nexport class MasonryController extends Controller<HTMLElement> {\n static override targets = [\"item\"];\n static override values = {\n minColumnWidth: { type: Number, default: DEFAULT_MIN_COLUMN_WIDTH },\n gap: { type: Number, default: DEFAULT_GAP },\n };\n static events = [\"layout\"] as const;\n\n declare readonly itemTargets: HTMLElement[];\n declare minColumnWidthValue: number;\n declare gapValue: number;\n\n /**\n * The declared numbers after validation, so the layout path never sees a value\n * it cannot compute with. Both are resolved once per declaration change rather\n * than on every pass.\n */\n #minColumnWidth = DEFAULT_MIN_COLUMN_WIDTH;\n #gap = DEFAULT_GAP;\n\n /**\n * Collapses every re-layout trigger of one DOM mutation into a single pass, and\n * refuses to run before `connect()` or after `disconnect()`.\n *\n * The triggers arrive in bursts — a resize stream, a morph that syncs several\n * attributes, a batch of rows — and each pass measures every item, so folding\n * them keeps the work proportional to the batch rather than to the events in it.\n */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#relayout());\n\n /** Items that left the target set and still carry the column hook. */\n readonly #released = new Set<HTMLElement>();\n\n readonly #layout = new LayoutObserver(() => this.#reconcile.schedule());\n #mutationObserver: MutationObserver | null = null;\n /** Last published column count, so `layout` fires only on real changes. */\n #lastColumns = 0;\n\n /** Resolves the declared column width once, falling back when it is unreadable. */\n minColumnWidthValueChanged(): void {\n this.#minColumnWidth = usableNumber(this.minColumnWidthValue, DEFAULT_MIN_COLUMN_WIDTH);\n this.#reconcile.schedule();\n }\n\n /** Resolves the declared gap once, falling back when it is unreadable. */\n gapValueChanged(): void {\n this.#gap = usableNumber(this.gapValue, DEFAULT_GAP);\n this.#reconcile.schedule();\n }\n\n /** Packs an element that became an item without moving in the DOM. */\n itemTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Queues the column hook of an element that stopped being an item for removal.\n *\n * The removal is queued rather than immediate because teardown reports every\n * target as disconnected: doing it here would strip the whole grid just before\n * a Turbo snapshot is taken. The coalescer's `cancel` drops the queue\n * with the pass, so only a genuine target change reaches it.\n */\n itemTargetDisconnected(item: HTMLElement): void {\n this.#released.add(item);\n this.#reconcile.schedule();\n }\n\n /** Observes size/content changes and performs the first layout pass. */\n override connect(): void {\n this.#layout.observe(this.element);\n this.#layout.observeViewport();\n\n if (typeof MutationObserver !== \"undefined\") {\n this.#mutationObserver = new MutationObserver(() => this.#reconcile.schedule());\n this.#mutationObserver.observe(this.element, { childList: true, subtree: true });\n }\n this.#layout.observeDescendantLoads(this.element);\n this.#relayout();\n this.#reconcile.activate();\n }\n\n /** Releases every observation so nothing fires after detach. */\n override disconnect(): void {\n this.#reconcile.cancel();\n this.#released.clear();\n this.#layout.disconnect();\n this.#mutationObserver?.disconnect();\n this.#mutationObserver = null;\n this.#lastColumns = 0;\n }\n\n /**\n * Recomputes the column count and assigns every item to the shortest column.\n * Runs automatically on connect, on resize, on item add/remove, when a declared\n * number changes, and when a descendant resource loads (private — there is no\n * public action; the observers, the target callbacks and the capture-phase\n * `load` listener drive it). Items are walked in DOM order; each lands in the\n * column with the least accumulated height, which keeps the packing balanced\n * without reordering the DOM.\n *\n * Every box is measured before anything is written. Interleaving the two would\n * make a consumer's `data-column` rule invalidate style once per item, and the\n * next measurement then has to settle layout again — once per item instead of\n * once per pass. The assignment is independent of the measurement because the\n * columns are uniform in width, so the order of the two passes does not change\n * the result.\n */\n #relayout(): void {\n const items = this.itemTargets;\n const columns = this.#columnCount();\n const boxes = items.map((item) => item.getBoundingClientRect().height);\n\n let changed = false;\n if (this.#released.size > 0) {\n // An element that left and rejoined the target set within one batch is\n // queued here while still being an item, so ownership is decided against\n // the set this pass sees rather than against the queue alone.\n const owned = new Set(items);\n for (const released of this.#released) {\n if (owned.has(released)) continue;\n if (released.hasAttribute(\"data-column\")) {\n released.removeAttribute(\"data-column\");\n changed = true;\n }\n }\n this.#released.clear();\n }\n\n const heights = new Array<number>(columns).fill(0);\n items.forEach((item, index) => {\n let shortest = 0;\n for (let col = 1; col < columns; col++) {\n if ((heights[col] ?? 0) < (heights[shortest] ?? 0)) shortest = col;\n }\n const assigned = String(shortest);\n // Writing a value the item already carries would publish a change that did\n // not happen, and the same comparison is what tells the event whether the\n // published layout actually moved.\n if (item.getAttribute(\"data-column\") !== assigned) {\n item.setAttribute(\"data-column\", assigned);\n changed = true;\n }\n heights[shortest] = (heights[shortest] ?? 0) + (boxes[index] ?? 0) + this.#gap;\n });\n\n this.element.style.setProperty(COLUMNS_PROPERTY, String(columns));\n\n if (columns !== this.#lastColumns || changed) {\n this.#lastColumns = columns;\n this.dispatch(\"layout\", { detail: { columns } });\n }\n }\n\n /**\n * Derives how many columns fit: `floor((width + gap) / (minColumnWidth + gap))`,\n * never fewer than one. When the width is unmeasurable (detached, or a layout\n * engine that reports `0`), it falls back to a single column so every item still\n * gets a valid `data-column`.\n */\n #columnCount(): number {\n const width = this.element.getBoundingClientRect().width;\n const denominator = this.#minColumnWidth + this.#gap;\n if (width <= 0 || denominator <= 0) return 1;\n return Math.max(1, Math.floor((width + this.#gap) / denominator));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/attribute_write.ts","../../src/utils/layout_observer.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/number_bounds.ts","../../src/utils/coerce.ts","../../src/utils/number_value.ts","../../src/controllers/masonry_controller.ts"],"names":[],"mappings":"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","file":"masonry_controller.js","sourcesContent":["/**\n * Reflects a serialized attribute value only when the current value differs.\n *\n * A same-value `setAttribute` still queues a mutation record for every observer, so an\n * unchanged value writes nothing.\n *\n * @param element - The element whose attribute is reflected.\n * @param name - The attribute name, using the non-namespaced DOM API.\n * @param value - The serialized value, or null to remove the attribute.\n * @param beforeWrite - Called only for a change, before the DOM mutation, so the\n * caller can record ownership or identify its own observer records.\n * @returns Whether an attribute was written or removed.\n */\nexport function setAttributeIfChanged(\n element: Element,\n name: string,\n value: string | null,\n beforeWrite?: () => void,\n): boolean {\n if (element.getAttribute(name) === value) return false;\n beforeWrite?.();\n if (value === null) element.removeAttribute(name);\n else element.setAttribute(name, value);\n return true;\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","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Whether a requested pass has yet to run. */\n get pending(): boolean {\n return this.#queued;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { MAX_TIMER_DELAY_MS } from \"./timer_bounds\";\n\n/** Semantic constraints on an already decoded number, independent of the DOM. */\nexport interface NumberBounds {\n /** Finite numbers are required, including when this field is omitted. */\n readonly finite?: true;\n /** Inclusive lower endpoint. */\n readonly min?: number;\n /** Inclusive upper endpoint. */\n readonly max?: number;\n /** Exclusive lower endpoint. */\n readonly exclusiveMin?: number;\n /** Require a number without a fractional part. */\n readonly integer?: boolean;\n /** Exact decoded numbers accepted by a discrete contract. */\n readonly allowedValues?: readonly number[];\n /** JSON-safe exceptions to finite-number checking for unbounded endpoints. */\n readonly allowInfinity?: \"negative\" | \"positive\" | \"both\";\n}\n\n/** One constraint for each Number Value, including shorthand declarations. */\nexport type NumberValueConstraints<Values> = {\n readonly [Key in keyof Values as Values[Key] extends\n | NumberConstructor\n | { type: NumberConstructor }\n ? Key\n : never]: NumberBounds;\n};\n\n/** Reusable semantic domains; fractional inputs remain fractional unless excluded. */\nexport const NUMBER_BOUNDS = {\n finite: { finite: true },\n nonNegative: { finite: true, min: 0 },\n positive: { finite: true, exclusiveMin: 0 },\n nonNegativeInteger: { finite: true, min: 0, integer: true },\n positiveInteger: { finite: true, exclusiveMin: 0, integer: true },\n lowerBound: { finite: true, allowInfinity: \"negative\" },\n upperBound: { finite: true, allowInfinity: \"positive\" },\n timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS },\n positiveTimer: { finite: true, exclusiveMin: 0, max: MAX_TIMER_DELAY_MS },\n} as const satisfies Record<string, NumberBounds>;\n\n/**\n * Checks all declared bounds without rounding, clamping, or coercing the input.\n * Infinity exceptions relax only finite checking; every other bound still applies.\n */\nexport function matchesNumberBounds(value: number, bounds: NumberBounds): boolean {\n if (!Number.isFinite(value)) {\n const direction = value === Infinity ? \"positive\" : value === -Infinity ? \"negative\" : null;\n if (direction === null) return false;\n if (bounds.allowInfinity !== \"both\" && bounds.allowInfinity !== direction) return false;\n }\n if (bounds.min !== undefined && value < bounds.min) return false;\n if (bounds.max !== undefined && value > bounds.max) return false;\n if (bounds.exclusiveMin !== undefined && value <= bounds.exclusiveMin) return false;\n if (bounds.integer && !Number.isInteger(value)) return false;\n if (bounds.allowedValues !== undefined && !bounds.allowedValues.includes(value)) return false;\n return true;\n}\n\n/** Decodes a Number Value literal; action params use their own JSON decoder. */\nexport function decodeNumberValue(raw: string): number {\n return Number(raw.replace(/_/g, \"\"));\n}\n","/**\n * Numeric coercion for controllers that accept a number through a Value, an action\n * param, or an event detail.\n *\n * Stimulus decodes action params as JSON when possible and leaves other text\n * untouched. CustomEvent detail can also supply a number or numeric string.\n * The reader accepts those two types while rejecting blank and non-finite input.\n */\n\nimport { matchesNumberBounds, type NumberBounds } from \"./number_bounds\";\n\n/** Numeric update input, accepting decoded action params before CustomEvent detail. */\nexport type NumericValueEvent = Event & {\n params?: { amount?: unknown };\n detail?: { value?: unknown };\n};\n\n/** Reads the nullish action-param/detail precedence without falling back from invalid input. */\nexport function eventNumber(event: NumericValueEvent): number | null {\n return toFiniteNumber(event.params?.amount ?? event.detail?.value);\n}\n\n/**\n * Reads a finite number or nonblank numeric string; other input returns `null`.\n * Blank strings cannot silently reset a value to zero.\n */\nexport function toFiniteNumber(raw: unknown): number | null {\n if (typeof raw !== \"number\" && typeof raw !== \"string\") return null;\n if (typeof raw === \"string\" && raw.trim().length === 0) return null;\n const value = Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n\n/**\n * Returns an accepted decoded Number Value or the caller's fallback unchanged.\n * The caller owns the fallback contract; this reader does not parse or write a Value.\n */\nexport function readNumber(raw: number, fallback: number, bounds: NumberBounds): number {\n return matchesNumberBounds(raw, bounds) ? raw : fallback;\n}\n","import { readNumber } from \"./coerce\";\nimport { matchesNumberBounds, type NumberBounds } from \"./number_bounds\";\n\n/** The instance identity carried by a Number Value read. */\nexport interface NumberValueOwner {\n readonly identifier: string;\n readonly element: Element;\n}\n\n/** Reads current declarations at the point of use without rewriting their attributes. */\nexport class NumberValueReader {\n /** The last rejected literal per Value; no history grows for an unchanged instance. */\n readonly #lastRejected = new Map<string, string>();\n\n /** Resolves the declared number using its own fallback and shared class contract. */\n read(\n owner: NumberValueOwner,\n name: string,\n raw: number,\n fallback: number,\n bounds: NumberBounds,\n ): number {\n const resolved = readNumber(raw, fallback, bounds);\n if (matchesNumberBounds(raw, bounds)) {\n this.#lastRejected.delete(name);\n return resolved;\n }\n const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;\n const literal = owner.element.getAttribute(attribute);\n if (literal === null) {\n this.#lastRejected.delete(name);\n return resolved;\n }\n if (this.#lastRejected.get(name) !== literal) {\n this.#lastRejected.set(name, literal);\n console.warn(\n `Stimeo UI: \"${owner.identifier}\" has an invalid number Value \"${name}\" declaration ${JSON.stringify(literal)}; using ${fallback}.`,\n );\n }\n return resolved;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { setAttributeIfChanged } from \"../utils/attribute_write\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { NUMBER_BOUNDS, type NumberValueConstraints } from \"../utils/number_bounds\";\nimport { NumberValueReader } from \"../utils/number_value\";\n\n/** CSS custom property exposing the current column count to consumer CSS. */\nconst COLUMNS_PROPERTY = \"--stimeo--masonry-columns\";\n\n/** Column width assumed when the declaration is absent or unreadable. */\nconst DEFAULT_MIN_COLUMN_WIDTH = 240;\n/** Item spacing assumed when the declaration is absent or unreadable. */\nconst DEFAULT_GAP = 16;\n\n/**\n * Headless **Masonry** layout helper: assigns each item to the shortest column so\n * variable-height cards pack without vertical gaps. There is no APG widget — this\n * is a layout-only utility that emits state hooks, never visual structure.\n *\n * Markup contract (identifier: `stimeo--masonry`):\n * <div data-controller=\"stimeo--masonry\"\n * data-stimeo--masonry-min-column-width-value=\"240\"\n * data-stimeo--masonry-gap-value=\"16\">\n * <div data-stimeo--masonry-target=\"item\">…</div>\n * <div data-stimeo--masonry-target=\"item\">…</div>\n * </div>\n *\n * The column count is derived responsively from the container width and\n * `minColumnWidth`; each item is then placed into whichever column is currently\n * shortest (measured from item heights). The count is published on the controller\n * element as the `--stimeo--masonry-columns` custom property and each item gets a\n * `data-column` index, so the consumer's CSS owns the actual placement.\n *\n * `layout` dispatches `{ columns: number }` whenever the published result moves —\n * the column count changed, or some item landed in a different column. A pass that\n * reproduces the previous result stays silent.\n *\n * @remarks\n * Behavior only. **DOM order is never changed** — reading order and focus order\n * stay the source markup order (WCAG 1.3.2). The visual packing is purely the\n * column assignment a consumer reads from `data-column`; this controller writes no\n * positioning styles. Use only for independent cards whose visual order carries no\n * meaning.\n *\n * Re-layout runs on connect, on resize (`LayoutObserver`), on item\n * add/remove ({@link MutationObserver}), on an item joining or leaving the target\n * set, when a declared number changes, and when a descendant resource loads.\n * Everything but the first pass is folded into one microtask, so a burst of\n * triggers costs one pass. The observers and any pending pass are released on\n * `disconnect()` (Turbo navigation included).\n *\n * Consumer contract:\n * - A declaration that cannot be read as a number (`\"240px\"`, an infinity) falls\n * back to that Value's default and the grid keeps working; `0` and negatives are\n * readable numbers and are used as declared; the count falls back to one column\n * only when `minColumnWidth + gap` is not positive, or the container has no\n * measurable width.\n * - `data-column` belongs to this controller: it is written on every item it owns\n * and taken back from an element that stops being one.\n */\nexport class MasonryController extends Controller<HTMLElement> {\n /** Numeric read boundaries share one reader for this controller instance. */\n readonly #numbers = new NumberValueReader();\n\n static override targets = [\"item\"];\n static override values = {\n minColumnWidth: { type: Number, default: DEFAULT_MIN_COLUMN_WIDTH },\n gap: { type: Number, default: DEFAULT_GAP },\n };\n\n static valueConstraints = {\n minColumnWidth: NUMBER_BOUNDS.finite,\n gap: NUMBER_BOUNDS.finite,\n } satisfies NumberValueConstraints<typeof MasonryController.values>;\n static events = [\"layout\"] as const;\n\n declare readonly itemTargets: HTMLElement[];\n declare minColumnWidthValue: number;\n declare gapValue: number;\n\n /**\n * The declared numbers after validation, so the layout path never sees a value\n * it cannot compute with. Both are resolved once per declaration change rather\n * than on every pass.\n */\n #minColumnWidth = DEFAULT_MIN_COLUMN_WIDTH;\n #gap = DEFAULT_GAP;\n\n /**\n * Collapses every re-layout trigger of one DOM mutation into a single pass, and\n * refuses to run before `connect()` or after `disconnect()`.\n *\n * The triggers arrive in bursts — a resize stream, a morph that syncs several\n * attributes, a batch of rows — and each pass measures every item, so folding\n * them keeps the work proportional to the batch rather than to the events in it.\n */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#relayout());\n\n /** Items that left the target set and still carry the column hook. */\n readonly #released = new Set<HTMLElement>();\n\n readonly #layout = new LayoutObserver(() => this.#reconcile.schedule());\n #mutationObserver: MutationObserver | null = null;\n /** Last published column count, so `layout` fires only on real changes. */\n #lastColumns = 0;\n\n /** Resolves the declared column width once, falling back when it is unreadable. */\n minColumnWidthValueChanged(): void {\n this.#minColumnWidth = this.#safeMinColumnWidth;\n this.#reconcile.schedule();\n }\n\n /** Resolves the declared gap once, falling back when it is unreadable. */\n gapValueChanged(): void {\n this.#gap = this.#safeGap;\n this.#reconcile.schedule();\n }\n\n /** Packs an element that became an item without moving in the DOM. */\n itemTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Queues the column hook of an element that stopped being an item for removal.\n *\n * The removal is queued rather than immediate because teardown reports every\n * target as disconnected. The pass that drains the queue skips an element\n * that is still an item, which drops what teardown queued, and keeps a departure\n * queued before a disconnect for the pass after the reconnect.\n */\n itemTargetDisconnected(item: HTMLElement): void {\n this.#released.add(item);\n this.#reconcile.schedule();\n }\n\n /** Observes size/content changes and performs the first layout pass. */\n override connect(): void {\n this.#layout.observe(this.element);\n this.#layout.observeViewport();\n\n if (typeof MutationObserver !== \"undefined\") {\n this.#mutationObserver = new MutationObserver(() => this.#reconcile.schedule());\n this.#mutationObserver.observe(this.element, { childList: true, subtree: true });\n }\n this.#layout.observeDescendantLoads(this.element);\n this.#relayout();\n this.#reconcile.activate();\n }\n\n /** Releases every observation so nothing fires after detach. */\n override disconnect(): void {\n this.#reconcile.cancel();\n this.#layout.disconnect();\n this.#mutationObserver?.disconnect();\n this.#mutationObserver = null;\n this.#lastColumns = 0;\n }\n\n /**\n * Recomputes the column count and assigns every item to the shortest column.\n * Runs automatically on connect, on resize, on item add/remove, when a declared\n * number changes, and when a descendant resource loads (private — there is no\n * public action; the observers, the target callbacks and the capture-phase\n * `load` listener drive it). Items are walked in DOM order; each lands in the\n * column with the least accumulated height, which keeps the packing balanced\n * without reordering the DOM.\n *\n * Every box is measured before anything is written. Interleaving the two would\n * make a consumer's `data-column` rule invalidate style once per item, and the\n * next measurement then has to settle layout again — once per item instead of\n * once per pass. The assignment is independent of the measurement because the\n * columns are uniform in width, so the order of the two passes does not change\n * the result.\n */\n #relayout(): void {\n const items = this.itemTargets;\n const columns = this.#columnCount();\n const boxes = items.map((item) => item.getBoundingClientRect().height);\n\n let changed = false;\n if (this.#released.size > 0) {\n // An element that left and rejoined the target set within one batch is\n // queued here while still being an item, so ownership is decided against\n // the set this pass sees rather than against the queue alone.\n const owned = new Set(items);\n for (const released of this.#released) {\n if (owned.has(released)) continue;\n if (released.hasAttribute(\"data-column\")) {\n released.removeAttribute(\"data-column\");\n changed = true;\n }\n }\n this.#released.clear();\n }\n\n const heights = new Array<number>(columns).fill(0);\n items.forEach((item, index) => {\n let shortest = 0;\n for (let col = 1; col < columns; col++) {\n if ((heights[col] ?? 0) < (heights[shortest] ?? 0)) shortest = col;\n }\n const assigned = String(shortest);\n // Writing a value the item already carries would publish a change that did\n // not happen, and the same comparison is what tells the event whether the\n // published layout actually moved.\n if (setAttributeIfChanged(item, \"data-column\", assigned)) changed = true;\n heights[shortest] = (heights[shortest] ?? 0) + (boxes[index] ?? 0) + this.#gap;\n });\n\n this.element.style.setProperty(COLUMNS_PROPERTY, String(columns));\n\n if (columns !== this.#lastColumns || changed) {\n this.#lastColumns = columns;\n this.dispatch(\"layout\", { detail: { columns } });\n }\n }\n\n /**\n * Derives how many columns fit: `floor((width + gap) / (minColumnWidth + gap))`,\n * never fewer than one. When the width is unmeasurable (detached, or a layout\n * engine that reports `0`), it falls back to a single column so every item still\n * gets a valid `data-column`.\n */\n #columnCount(): number {\n const width = this.element.getBoundingClientRect().width;\n const denominator = this.#minColumnWidth + this.#gap;\n if (width <= 0 || denominator <= 0) return 1;\n return Math.max(1, Math.floor((width + this.#gap) / denominator));\n }\n /** Current `minColumnWidth` declaration resolved against its numeric contract. */\n get #safeMinColumnWidth(): number {\n return this.#numbers.read(\n this,\n \"minColumnWidth\",\n this.minColumnWidthValue,\n MasonryController.values.minColumnWidth.default,\n MasonryController.valueConstraints.minColumnWidth,\n );\n }\n\n /** Current `gap` declaration resolved against its numeric contract. */\n get #safeGap(): number {\n return this.#numbers.read(\n this,\n \"gap\",\n this.gapValue,\n MasonryController.values.gap.default,\n MasonryController.valueConstraints.gap,\n );\n }\n}\n"]}
|
|
@@ -27,7 +27,7 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
27
27
|
* `keydown->stimeo--menu#onItemKeydown`) is supported alongside it. A
|
|
28
28
|
* focus-moving key runs once (the action claims it and the delegate stands
|
|
29
29
|
* down), and `activate`, reachable on both paths, claims the click so one
|
|
30
|
-
* gesture activates once.
|
|
30
|
+
* gesture activates once.
|
|
31
31
|
*
|
|
32
32
|
* Implements the WAI-ARIA APG **Menu Button** pattern (a button that opens a menu
|
|
33
33
|
* of commands). Unlike `stimeo--dropdown` (a disclosure for arbitrary content),
|
|
@@ -53,15 +53,30 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
53
53
|
* dismissed first.
|
|
54
54
|
* - A click outside the controller closes the menu without moving focus away
|
|
55
55
|
* from the clicked element.
|
|
56
|
+
* - A trigger or a menu that takes over — in one task, or after an earlier one
|
|
57
|
+
* leaves in a later task — carries the open state; focus stays where the swap
|
|
58
|
+
* left it. With no menu left the menu button reads closed and leaves the Escape
|
|
59
|
+
* stack. One that stops resolving as the target gets back what it carried before
|
|
60
|
+
* this controller wrote on it.
|
|
56
61
|
* - Each move of the open state is reported: `stimeo--menu:open` and
|
|
57
62
|
* `stimeo--menu:close` dispatch `{ reason: StateReason }`, after the state
|
|
58
63
|
* attributes are written. Both are informational, so neither is cancelable. A
|
|
59
64
|
* call that leaves the state where it already was, the normalization in
|
|
60
|
-
* {@link connect}, and {@link disconnect}
|
|
65
|
+
* {@link connect}, a trigger or a menu that takes over, and {@link disconnect}
|
|
66
|
+
* are all silent.
|
|
61
67
|
*
|
|
62
|
-
* Roving focus skips `hidden` and natively `disabled` items. An
|
|
63
|
-
* `aria-disabled="true"
|
|
64
|
-
*
|
|
68
|
+
* Roving focus skips `hidden` and natively `disabled` items. An item that carries
|
|
69
|
+
* `aria-disabled="true"`, or sits inside an element that does, remains discoverable by
|
|
70
|
+
* arrow-key focus; ARIA applies the state to every focusable descendant. None of these
|
|
71
|
+
* is activated on any path: {@link activate} does nothing for an event whose target
|
|
72
|
+
* one of them holds — items can nest, and any item holding the target that refuses
|
|
73
|
+
* refuses the event — and a capture listener on the controller element also stops a
|
|
74
|
+
* click an `aria-disabled` one holds before the handlers inside that element
|
|
75
|
+
* run. A click is judged once, in that capture pass, so a consumer handler that
|
|
76
|
+
* disables the item while the click runs does not take back the activation. The
|
|
77
|
+
* verdict holds only for an item that held the click's target when it was taken: an
|
|
78
|
+
* item it was not taken for — a handler moved the target into another one — is
|
|
79
|
+
* judged when the action runs, as is any other event.
|
|
65
80
|
*/
|
|
66
81
|
declare class MenuController extends Controller<HTMLElement> {
|
|
67
82
|
#private;
|
|
@@ -69,7 +84,9 @@ declare class MenuController extends Controller<HTMLElement> {
|
|
|
69
84
|
static actions: readonly ["activate", "close", "onItemKeydown", "onTriggerKeydown", "open", "toggle"];
|
|
70
85
|
static events: readonly ["close", "open"];
|
|
71
86
|
readonly triggerTarget: HTMLButtonElement;
|
|
87
|
+
readonly triggerTargets: HTMLButtonElement[];
|
|
72
88
|
readonly menuTarget: HTMLElement;
|
|
89
|
+
readonly menuTargets: HTMLElement[];
|
|
73
90
|
readonly itemTargets: HTMLButtonElement[];
|
|
74
91
|
readonly hasTriggerTarget: boolean;
|
|
75
92
|
readonly hasMenuTarget: boolean;
|
|
@@ -77,6 +94,23 @@ declare class MenuController extends Controller<HTMLElement> {
|
|
|
77
94
|
connect(): void;
|
|
78
95
|
/** Releases the listeners, stack membership, and any pending Tab-close task. */
|
|
79
96
|
disconnect(): void;
|
|
97
|
+
/** Brings a trigger that arrives after connect to the open state. */
|
|
98
|
+
triggerTargetConnected(): void;
|
|
99
|
+
/**
|
|
100
|
+
* Gives a trigger that no longer resolves as one its own `aria-expanded` back — after
|
|
101
|
+
* `disconnect()` too, since dropping the identifier leaves the element on the page — and
|
|
102
|
+
* brings the trigger that stays to the open state.
|
|
103
|
+
*/
|
|
104
|
+
triggerTargetDisconnected(trigger: HTMLButtonElement): void;
|
|
105
|
+
/** Applies the open state to a menu that arrives after connect in front of the others. */
|
|
106
|
+
menuTargetConnected(): void;
|
|
107
|
+
/**
|
|
108
|
+
* Applies the open state to the menu left, then gives a menu that no longer resolves as the
|
|
109
|
+
* target its own `hidden` back — after `disconnect()` too, since dropping the identifier
|
|
110
|
+
* leaves the element on the page. The open state is read off the departing menu first,
|
|
111
|
+
* while it still carries it.
|
|
112
|
+
*/
|
|
113
|
+
menuTargetDisconnected(menu: HTMLElement): void;
|
|
80
114
|
/** Toggles the menu open/closed. Bound via `data-action` (click). */
|
|
81
115
|
toggle(event?: Event): void;
|
|
82
116
|
/** Opens the menu and reflects the expanded state on the trigger. */
|
|
@@ -98,13 +132,23 @@ declare class MenuController extends Controller<HTMLElement> {
|
|
|
98
132
|
* Closes the menu after an item is activated. Bound via `data-action`, and
|
|
99
133
|
* also reached from the delegated listener.
|
|
100
134
|
*
|
|
135
|
+
* An event whose target lies in an item that is `hidden`, natively `disabled`, or
|
|
136
|
+
* `aria-disabled="true"` itself or through an ancestor does nothing: the menu stays
|
|
137
|
+
* open, nothing is dispatched, focus stays where it is, and the event is left
|
|
138
|
+
* unclaimed. Every item holding the target counts, so an item nested in another one
|
|
139
|
+
* refuses for both. A click is judged as its capture pass found the items, before
|
|
140
|
+
* the handlers inside the controller element ran, when the item this
|
|
141
|
+
* resolves was among them; an item that was not, and any other event, is judged
|
|
142
|
+
* when this runs. An event that resolves to no item closes the menu, and a
|
|
143
|
+
* programmatic call with no event always runs.
|
|
144
|
+
*
|
|
101
145
|
* Markup that carries the per-element action *and* gets the delegate would run
|
|
102
|
-
* this twice for one gesture.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
146
|
+
* this twice for one gesture. The state hooks (`hidden`, `aria-expanded`) are
|
|
147
|
+
* written only when they change, so a second pass writes nothing on its own, but
|
|
148
|
+
* a page handler between the two paths — on an ancestor of the item — that opens
|
|
149
|
+
* the menu again would see the second pass close it. The event is therefore
|
|
150
|
+
* claimed: the path that gets there first does the work, the other one finds it
|
|
151
|
+
* claimed and returns.
|
|
108
152
|
*/
|
|
109
153
|
activate(event?: Event): void;
|
|
110
154
|
}
|