stimeo-ui 0.17.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 +106 -1
- package/README.md +19 -10
- package/dist/cable/index.d.ts +9 -8
- package/dist/cable/index.js +22 -14
- package/dist/cable/index.js.map +1 -1
- package/dist/coerce-JtZVNG5h.d.ts +20 -0
- package/dist/controllers/accordion_controller.js +14 -6
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.js +145 -118
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +5 -3
- package/dist/controllers/announcer_controller.js +113 -17
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js +185 -60
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +55 -43
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +3 -3
- package/dist/controllers/bulk_select_controller.js +64 -50
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +15 -12
- package/dist/controllers/calendar_controller.js +55 -53
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +9 -5
- package/dist/controllers/carousel_controller.js +124 -86
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +27 -6
- package/dist/controllers/character_counter_controller.js +291 -95
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.js +6 -0
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js +118 -27
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +52 -49
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.js +29 -7
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +1 -1
- package/dist/controllers/combobox_controller.js +210 -71
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +9 -9
- package/dist/controllers/command_palette_controller.js +289 -140
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js +145 -58
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +145 -118
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +49 -14
- package/dist/controllers/context_menu_controller.js +138 -73
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +2 -2
- package/dist/controllers/countdown_controller.js +109 -21
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +75 -11
- package/dist/controllers/currency_input_controller.js +446 -72
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +10 -8
- package/dist/controllers/data_grid_controller.js +134 -28
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +12 -9
- package/dist/controllers/date_range_picker_controller.js +93 -72
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +4 -5
- package/dist/controllers/dialog_controller.js +145 -118
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +1 -1
- package/dist/controllers/direct_upload_controller.js +24 -14
- 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 +68 -31
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js +64 -46
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js +145 -118
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.js +76 -63
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.js +199 -62
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +13 -6
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +3 -0
- package/dist/controllers/file_dropzone_controller.js +55 -48
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +9 -2
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +4 -4
- package/dist/controllers/flash_controller.js +66 -33
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +3 -3
- package/dist/controllers/focus_controller.js +39 -14
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +52 -43
- 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 +18 -11
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +13 -12
- package/dist/controllers/frame_loading_controller.js +55 -48
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +5 -4
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +4 -1
- package/dist/controllers/hover_card_controller.js +67 -61
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +6 -5
- package/dist/controllers/idle_controller.js +10 -6
- 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 +411 -24
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +3 -0
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +3 -2
- package/dist/controllers/lazy_frame_controller.js +15 -3
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +10 -7
- package/dist/controllers/listbox_controller.js +260 -79
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +8 -6
- package/dist/controllers/local_time_controller.js +48 -13
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.js +11 -4
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +24 -5
- package/dist/controllers/menu_controller.js +138 -82
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +49 -7
- package/dist/controllers/menubar_controller.js +65 -19
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +13 -20
- package/dist/controllers/meter_controller.js +270 -24
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +17 -15
- package/dist/controllers/multi_select_controller.js +251 -94
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +19 -5
- package/dist/controllers/navigation_menu_controller.js +67 -19
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js +98 -64
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.js +70 -52
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +24 -6
- package/dist/controllers/number_input_controller.js +344 -94
- 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 +29 -11
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +35 -4
- package/dist/controllers/otp_controller.js +502 -104
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.js +6 -0
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +10 -9
- package/dist/controllers/overflow_menu_controller.js +136 -21
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +3 -2
- package/dist/controllers/pagination_controller.js +47 -14
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +5 -4
- package/dist/controllers/password_reveal_controller.js +52 -49
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +1 -1
- package/dist/controllers/password_strength_controller.js +13 -6
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js +48 -128
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +19 -8
- package/dist/controllers/pointer_drag_controller.js +249 -23
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +3 -3
- package/dist/controllers/popover_controller.js +82 -65
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.js +6 -0
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.js +14 -3
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +9 -19
- package/dist/controllers/progress_controller.js +271 -27
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +11 -8
- package/dist/controllers/radio_group_controller.js +167 -80
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +4 -3
- package/dist/controllers/range_slider_controller.js +27 -7
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.js +93 -57
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.js +10 -6
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.js +22 -5
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +4 -4
- package/dist/controllers/relative_time_controller.js +75 -11
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_on_restore_controller.d.ts +3 -3
- package/dist/controllers/reset_on_restore_controller.js +6 -0
- package/dist/controllers/reset_on_restore_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +3 -0
- package/dist/controllers/resizable_controller.js +55 -22
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +3 -3
- package/dist/controllers/roving_controller.js +10 -4
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +129 -88
- 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 -2
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +93 -60
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js +11 -5
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +1 -1
- package/dist/controllers/separator_controller.js +59 -43
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +1 -1
- package/dist/controllers/sidebar_controller.js +150 -122
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js +55 -48
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +2 -1
- package/dist/controllers/slider_controller.js +27 -6
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +1 -1
- package/dist/controllers/smart_sticky_header_controller.js +13 -5
- 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 +27 -14
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +55 -48
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +4 -4
- package/dist/controllers/step_indicator_controller.js +6 -0
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +2 -3
- package/dist/controllers/stepper_controller.js +6 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +3 -1
- package/dist/controllers/stick_to_bottom_controller.js +19 -8
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.js +49 -43
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +2 -1
- package/dist/controllers/submit_once_controller.js +145 -85
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +7 -4
- package/dist/controllers/switch_controller.js +172 -28
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +7 -4
- package/dist/controllers/tabs_controller.js +6 -1
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +4 -3
- package/dist/controllers/tags_input_controller.js +157 -28
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +1 -1
- package/dist/controllers/textarea_autosize_controller.js +40 -19
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +3 -2
- package/dist/controllers/theme_controller.js +10 -4
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +38 -5
- package/dist/controllers/time_picker_controller.js +192 -54
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +4 -5
- package/dist/controllers/toast_controller.js +58 -27
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +2 -3
- package/dist/controllers/toggle_group_controller.js +158 -56
- 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 +150 -21
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +4 -1
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +1 -1
- package/dist/controllers/tree_view_controller.js +134 -25
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2451 -1312
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +15 -11
- package/dist/inspector/cli.js +16 -3
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +53 -8
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +13 -13
- package/dist/inspector/manifest.json +19 -5
- package/dist/positioning/index.d.ts +7 -7
- package/dist/positioning/index.js +6 -0
- package/dist/positioning/index.js.map +1 -1
- package/package.json +10 -9
|
@@ -13,19 +13,21 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
13
13
|
*
|
|
14
14
|
* The server emits UTC (cache-friendly — it never needs to know the viewer's
|
|
15
15
|
* timezone), and the controller reformats the visible text into the viewer's
|
|
16
|
-
* locale/zone on connect.
|
|
17
|
-
*
|
|
16
|
+
* locale/zone on connect. A `datetime` without a zone is read as UTC; an explicit
|
|
17
|
+
* `Z` or numeric offset keeps its meaning. This is the *absolute* localization axis,
|
|
18
|
+
* distinct from `stimeo--relative-time`'s "3 minutes ago".
|
|
18
19
|
*
|
|
19
20
|
* `format` dispatches `{ formatted }`.
|
|
20
21
|
*
|
|
21
22
|
* @remarks
|
|
22
|
-
* Behavior only. The machine-readable `datetime`
|
|
23
|
-
* assistive tech and crawlers keep the canonical value while only the display
|
|
23
|
+
* Behavior only. The machine-readable `datetime` is left untouched, exactly as written,
|
|
24
|
+
* so assistive tech and crawlers keep the canonical value while only the display
|
|
24
25
|
* text — and an optional `title` — change. Formatting is a pure function of
|
|
25
26
|
* `datetime` with no module-scope state or timers, so a Turbo Drive cache restore
|
|
26
27
|
* re-runs `connect()` and stays consistent. A `datetime` that does not parse, and
|
|
27
|
-
* a locale or time zone `Intl` rejects, leave the
|
|
28
|
-
* any other failure surfaces rather than being
|
|
28
|
+
* a locale or time zone `Intl` rejects, leave the text already shown in place — the
|
|
29
|
+
* authored text until the first format; any other failure surfaces rather than being
|
|
30
|
+
* absorbed.
|
|
29
31
|
*
|
|
30
32
|
* Render inputs are followed at runtime: a morph that swaps a Value or the
|
|
31
33
|
* `datetime` attribute on the live element — which keeps the element, so
|
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
var DATETIME = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
4
|
+
function readDatetime(element) {
|
|
5
|
+
const raw = element.getAttribute("datetime");
|
|
6
|
+
if (raw === null) return null;
|
|
7
|
+
const match = DATETIME.exec(raw.trim());
|
|
8
|
+
if (match === null) return null;
|
|
9
|
+
const [
|
|
10
|
+
,
|
|
11
|
+
yearText,
|
|
12
|
+
monthText,
|
|
13
|
+
dayText,
|
|
14
|
+
hourText = "00",
|
|
15
|
+
minuteText = "00",
|
|
16
|
+
secondText = "00",
|
|
17
|
+
fraction = "",
|
|
18
|
+
zone = "Z"
|
|
19
|
+
] = match;
|
|
20
|
+
const year = Number(yearText);
|
|
21
|
+
const month = Number(monthText);
|
|
22
|
+
const day = Number(dayText);
|
|
23
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
24
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
25
|
+
if (month < 1 || month > 12 || day < 1 || day > (days[month - 1] ?? 0)) return null;
|
|
26
|
+
const hour = Number(hourText);
|
|
27
|
+
const minute = Number(minuteText);
|
|
28
|
+
const second = Number(secondText);
|
|
29
|
+
if (hour === 24 && (minute !== 0 || second !== 0 || /[1-9]/.test(fraction))) return null;
|
|
30
|
+
let offset = zone;
|
|
31
|
+
if (zone !== "Z") {
|
|
32
|
+
const digits = zone.slice(1).replace(":", "");
|
|
33
|
+
const offsetHour = digits.slice(0, 2);
|
|
34
|
+
const offsetMinute = digits.slice(2);
|
|
35
|
+
offset = `${zone[0]}${offsetHour}:${offsetMinute}`;
|
|
36
|
+
}
|
|
37
|
+
const milliseconds = fraction.slice(0, 3).padEnd(3, "0");
|
|
38
|
+
const canonical = `${yearText}-${monthText}-${dayText}T${hourText}:${minuteText}:${secondText}.${milliseconds}${offset}`;
|
|
39
|
+
const instant = Date.parse(canonical);
|
|
40
|
+
return Number.isNaN(instant) ? null : instant;
|
|
41
|
+
}
|
|
42
|
+
|
|
3
43
|
var formatters = /* @__PURE__ */ new WeakMap();
|
|
4
44
|
function intlFormatter(factory, locale, options, fallbackLocale) {
|
|
5
45
|
const cache = cacheFor(factory);
|
|
@@ -59,6 +99,9 @@ var MicrotaskCoalescer = class {
|
|
|
59
99
|
this.#queued = false;
|
|
60
100
|
this.#generation += 1;
|
|
61
101
|
}
|
|
102
|
+
get pending() {
|
|
103
|
+
return this.#queued;
|
|
104
|
+
}
|
|
62
105
|
schedule() {
|
|
63
106
|
if (!this.#active || this.#queued) return;
|
|
64
107
|
this.#queued = true;
|
|
@@ -89,6 +132,9 @@ var MorphRenderWatcher = class {
|
|
|
89
132
|
schedule() {
|
|
90
133
|
this.#pass.schedule();
|
|
91
134
|
}
|
|
135
|
+
get pending() {
|
|
136
|
+
return this.#pass.pending;
|
|
137
|
+
}
|
|
92
138
|
disconnect() {
|
|
93
139
|
this.#listeners.dispose();
|
|
94
140
|
this.#pass.cancel();
|
|
@@ -161,19 +207,8 @@ var LocalTimeController = class extends Controller {
|
|
|
161
207
|
if (notify) this.dispatch("format", { detail: { formatted } });
|
|
162
208
|
}
|
|
163
209
|
#parse() {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
const ms = Date.parse(this.#asUtc(raw.trim()));
|
|
167
|
-
return Number.isNaN(ms) ? null : new Date(ms);
|
|
168
|
-
}
|
|
169
|
-
#asUtc(value) {
|
|
170
|
-
const isoLike = value.replace(
|
|
171
|
-
/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)$/,
|
|
172
|
-
"$1T$2"
|
|
173
|
-
);
|
|
174
|
-
const hasTime = /T\d{2}:\d{2}/.test(isoLike);
|
|
175
|
-
const hasZone = /(Z|[+-]\d{2}:?\d{2})$/.test(isoLike);
|
|
176
|
-
return hasTime && !hasZone ? `${isoLike}Z` : isoLike;
|
|
210
|
+
const instant = readDatetime(this.element);
|
|
211
|
+
return instant === null ? null : new Date(instant);
|
|
177
212
|
}
|
|
178
213
|
#title(date) {
|
|
179
214
|
if (this.titleFormatValue.length === 0) return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/intl_format.ts","../../src/utils/locale.ts","../../src/utils/listener_set.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/morph_render_watcher.ts","../../src/controllers/local_time_controller.ts"],"names":[],"mappings":"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","file":"local_time_controller.js","sourcesContent":["/**\n * Construction of `Intl` formatters for the controllers that take a locale, and\n * the options that go with it, from a declaration.\n *\n * Every `Intl` constructor resolves locale data on the way in, which makes it\n * too costly to rebuild on a hot path, and rejects a malformed language tag,\n * currency code, time zone or digit count with a `RangeError`. Both concerns are\n * handled here once. A formatter is built at most once per distinct request and\n * shared by every caller that asks for the same one. A `RangeError` is caught\n * and answered the way the caller asked: by rebuilding under a fallback locale,\n * or by returning `null` so the caller can leave its authored text alone. Any\n * other failure is a programming fault and propagates.\n */\n\n/** The constructor shape the `Intl` formatter classes share. */\nexport interface IntlFormatterConstructor<F, O extends object> {\n new (locales?: string | string[], options?: O): F;\n}\n\n/** Built formatters, and refusals, per constructor; see `cacheKey` for the key. */\nconst formatters = new WeakMap<object, Map<string, unknown>>();\n\n/**\n * A formatter for `locale` with `options`.\n *\n * With `fallbackLocale`, a `RangeError` from the constructor is answered by\n * building the same options under the fallback instead, so the result is never\n * `null`; the fallback's own failure propagates. Without it, a `RangeError`\n * yields `null` and the caller decides what to show. Errors of any other kind\n * propagate in both forms.\n *\n * Results are cached per constructor and `(locale, options, fallback)`, `null`\n * included, so asking again on every render costs one map lookup.\n */\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale: string,\n): F;\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n): F | null;\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale?: string,\n): F | null {\n const cache = cacheFor(factory);\n const key = cacheKey(locale, options, fallbackLocale);\n if (cache.has(key)) return cache.get(key) as F | null;\n const formatter = construct(factory, locale, options, fallbackLocale);\n cache.set(key, formatter);\n return formatter;\n}\n\n/** The cache for one constructor, created on first use. */\nfunction cacheFor(factory: object): Map<string, unknown> {\n const existing = formatters.get(factory);\n if (existing) return existing;\n const created = new Map<string, unknown>();\n formatters.set(factory, created);\n return created;\n}\n\n/**\n * One key per distinct request. `JSON.stringify` drops an option left\n * `undefined`, which is how the constructor reads it too. Two spellings of the\n * same options differing only in key order key separately; that costs one extra\n * formatter, never a wrong one, and call sites pass a fixed literal anyway.\n */\nfunction cacheKey(\n locale: string | undefined,\n options: object,\n fallbackLocale: string | undefined,\n): string {\n return JSON.stringify([locale, options, fallbackLocale]);\n}\n\n/** Builds the formatter under the `RangeError` contract described on `intlFormatter`. */\nfunction construct<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale: string | undefined,\n): F | null {\n try {\n return new factory(locale, options);\n } catch (error) {\n if (!(error instanceof RangeError)) throw error;\n if (fallbackLocale === undefined) return null;\n return new factory(fallbackLocale, options);\n }\n}\n","/**\n * The locale a component formats in: its own declaration, else the language of\n * the text around it, else the runtime default.\n *\n * `lang` is inherited in HTML, so the language that applies to an element is the\n * nearest one on the element itself or on an ancestor — not the document\n * element's, which is only the farthest of them. An empty `lang` there means the\n * language is unknown and stops the inheritance at that point, so it resolves to\n * the runtime default rather than to a `lang` further up.\n *\n * Resolution only, and the value is passed through as authored. Whether `Intl`\n * accepts the tag, and what to show when it does not, is the caller's contract.\n *\n * @example\n * ```ts\n * intlFormatter(Intl.DateTimeFormat, resolveLocale(this.element, this.localeValue), options);\n * ```\n */\nexport function resolveLocale(element: Element, declared = \"\"): string | undefined {\n return declared || element.closest(\"[lang]\")?.getAttribute(\"lang\") || undefined;\n}\n","/**\n * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt. {@link\n * ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, which places `dispose()` in its\n * cleanup sequence. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, or one that\n * lives for less than the connection, keeps its own pair of calls.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included, which has to match at release time and no\n * longer has a second place to drift from.\n */\n add(\n target: EventTarget,\n type: string,\n handler: EventListener,\n options?: Omit<AddEventListenerOptions, \"signal\">,\n ): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\n }\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { ListenerSet } from \"./listener_set\";\nimport { MicrotaskCoalescer } from \"./microtask_coalescer\";\n\n/**\n * Coalesces retained-element morphs and explicit lifecycle requests into one pass.\n *\n * Observe the controller's element after its initial state is settled. Morphs on\n * that element and bubbling morphs on its descendants share the same microtask\n * as {@link schedule}. The callback repairs derived output; the controller owns\n * any logical transition reporting. {@link disconnect} releases the listener and\n * cancels pending work, including work from an earlier connected lifetime.\n */\nexport class MorphRenderWatcher {\n readonly #listeners = new ListenerSet();\n readonly #pass: MicrotaskCoalescer;\n #element: Element | null = null;\n readonly #onMorph = (): void => this.#pass.schedule();\n\n /** @param run - the single pass that repairs the settled controller's output. */\n constructor(run: () => void) {\n this.#pass = new MicrotaskCoalescer(run);\n }\n\n /** Opens observation; observing the same element again preserves queued work. */\n observe(element: Element): void {\n if (this.#element === element) return;\n this.disconnect();\n this.#element = element;\n this.#pass.activate();\n this.#listeners.add(element, \"turbo:morph-element\", this.#onMorph);\n }\n\n /** Requests one pass; requests outside an observed lifetime are ignored. */\n schedule(): void {\n this.#pass.schedule();\n }\n\n /** Releases observation and pending work synchronously; safe to repeat. */\n disconnect(): void {\n this.#listeners.dispose();\n this.#pass.cancel();\n this.#element = null;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { intlFormatter } from \"../utils/intl_format\";\nimport { resolveLocale } from \"../utils/locale\";\nimport { MorphRenderWatcher } from \"../utils/morph_render_watcher\";\n\n/** A date/time style keyword accepted by `Intl` `dateStyle` / `timeStyle`. */\ntype DateTimeStyle = \"full\" | \"long\" | \"medium\" | \"short\";\n\n/** The valid `Intl` style keywords, used to validate the string-typed values. */\nconst STYLES = new Set<DateTimeStyle>([\"full\", \"long\", \"medium\", \"short\"]);\n\n/** Narrows an arbitrary string to a valid {@link DateTimeStyle}, or `undefined`. */\nfunction toStyle(value: string): DateTimeStyle | undefined {\n return (STYLES as Set<string>).has(value) ? (value as DateTimeStyle) : undefined;\n}\n\n/**\n * Headless local-time behavior: renders the UTC instant in a `<time datetime>`\n * as an absolute, viewer-localized string via `Intl.DateTimeFormat`. No\n * dedicated APG pattern; it follows the HTML `<time>` semantics.\n *\n * Markup contract (identifier: `stimeo--local-time`):\n * <time datetime=\"2026-06-08T12:30:00Z\"\n * data-controller=\"stimeo--local-time\"\n * data-stimeo--local-time-date-style-value=\"medium\"\n * data-stimeo--local-time-time-style-value=\"short\">2026-06-08 12:30 UTC</time>\n *\n * The server emits UTC (cache-friendly — it never needs to know the viewer's\n * timezone), and the controller reformats the visible text into the viewer's\n * locale/zone on connect. This is the *absolute* localization axis, distinct from\n * {@link RelativeTimeController}'s \"3 minutes ago\".\n *\n * `format` dispatches `{ formatted }`.\n *\n * @remarks\n * Behavior only. The machine-readable `datetime` (UTC) is left untouched so\n * assistive tech and crawlers keep the canonical value while only the display\n * text — and an optional `title` — change. Formatting is a pure function of\n * `datetime` with no module-scope state or timers, so a Turbo Drive cache restore\n * re-runs `connect()` and stays consistent. A `datetime` that does not parse, and\n * a locale or time zone `Intl` rejects, leave the authored absolute text in place;\n * any other failure surfaces rather than being absorbed.\n *\n * Render inputs are followed at runtime: a morph that swaps a Value or the\n * `datetime` attribute on the live element — which keeps the element, so\n * `connect()` never runs again — repaints through one coalesced pass\n * (`MorphRenderWatcher`) rather than leaving a stale reading on screen.\n */\nexport class LocalTimeController extends Controller<HTMLElement> {\n static override values = {\n locale: { type: String, default: \"\" },\n timeZone: { type: String, default: \"\" },\n dateStyle: { type: String, default: \"medium\" },\n timeStyle: { type: String, default: \"short\" },\n titleFormat: { type: String, default: \"\" },\n };\n static events = [\"format\"] as const;\n\n declare localeValue: string;\n declare timeZoneValue: string;\n declare dateStyleValue: string;\n declare timeStyleValue: string;\n declare titleFormatValue: string;\n\n /** Collapses a morph that swaps several render inputs at once into one repaint. */\n readonly #resync = new MorphRenderWatcher(() => {\n const datetimeChanged = this.#datetimeWatch.takeRecords().length > 0;\n const notify = this.#formatPending || datetimeChanged;\n this.#formatPending = false;\n this.#render(notify);\n });\n #formatPending = false;\n /**\n * Watches the one render input that is not a Value. Only `datetime` is filtered\n * in, so the text and `title` this controller writes cannot re-enter the pass.\n */\n readonly #datetimeWatch = new MutationObserver(() => {\n this.#formatPending = true;\n this.#resync.schedule();\n });\n\n override connect(): void {\n this.#resync.observe(this.element);\n this.#datetimeWatch.observe(this.element, { attributeFilter: [\"datetime\"] });\n this.#formatPending = false;\n this.#render();\n }\n\n override disconnect(): void {\n this.#resync.disconnect();\n this.#datetimeWatch.disconnect();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `locale` at runtime. */\n localeValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `timeZone` at runtime. */\n timeZoneValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `dateStyle` at runtime. */\n dateStyleValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `timeStyle` at runtime. */\n timeStyleValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `titleFormat` at runtime. */\n titleFormatValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /**\n * Formats the instant in `datetime` against the current Values and writes it out.\n *\n * Formatting from a changed input reports `format`; restoring output alone is\n * silent. A pass that cannot format preserves the authored fallback.\n *\n * @stimeoRenderRoot\n */\n #render(notify = true): void {\n const date = this.#parse();\n if (date === null) return;\n\n const formatted = this.#applyFormat(date, this.dateStyleValue, this.timeStyleValue);\n if (formatted === null) return;\n\n // Only the visible text (and optional title) change; `datetime` is the\n // immutable machine-readable source and is never rewritten.\n this.element.textContent = formatted;\n\n const title = this.#title(date);\n if (title !== null) this.element.setAttribute(\"title\", title);\n\n if (notify) this.dispatch(\"format\", { detail: { formatted } });\n }\n\n /**\n * Parses the UTC `datetime` attribute into a {@link Date}, or `null`. Whitespace\n * around the attribute value is tolerated.\n */\n #parse(): Date | null {\n const raw = this.element.getAttribute(\"datetime\");\n if (!raw) return null;\n const ms = Date.parse(this.#asUtc(raw.trim()));\n return Number.isNaN(ms) ? null : new Date(ms);\n }\n\n /**\n * Reads a timezone-less date-time as UTC — the input contract of this\n * controller — since `Date.parse` would otherwise read `\"2026-06-08T12:30:00\"` in\n * the *runtime's* local zone, contradicting \"the server emits UTC\". Values that\n * already carry `Z` or a `±hh:mm` offset (and bare `YYYY-MM-DD` dates, already\n * parsed as UTC) are returned unchanged.\n *\n * HTML accepts a space where ISO 8601 wants `T`, and `Date.parse` of that form is\n * left to each engine, so a whole value shaped that way is normalized to the `T`\n * separator first. The pattern is anchored: a value trailing anything else — a\n * zone word such as `\"2026-06-08 12:30:00 UTC\"` — is handed to `Date.parse` as\n * authored instead of being turned into a string nothing can parse.\n */\n #asUtc(value: string): string {\n const isoLike = value.replace(\n /^(\\d{4}-\\d{2}-\\d{2}) (\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d+)?)?(?:Z|[+-]\\d{2}:?\\d{2})?)$/,\n \"$1T$2\",\n );\n const hasTime = /T\\d{2}:\\d{2}/.test(isoLike);\n const hasZone = /(Z|[+-]\\d{2}:?\\d{2})$/.test(isoLike);\n return hasTime && !hasZone ? `${isoLike}Z` : isoLike;\n }\n\n /**\n * Builds the optional detailed `title`. `titleFormat` is an `Intl` style\n * keyword applied to *both* date and time; empty (the default) adds no title.\n */\n #title(date: Date): string | null {\n if (this.titleFormatValue.length === 0) return null;\n return this.#applyFormat(date, this.titleFormatValue, this.titleFormatValue);\n }\n\n /**\n * Formats `date` with `Intl.DateTimeFormat`, including each style only when it\n * is a valid keyword (so a consumer can show date-only or time-only by clearing\n * the other). Returns `null` when neither style is usable or the declaration\n * names a locale or time zone `Intl` rejects, so the caller can leave the\n * authored text untouched.\n */\n #applyFormat(date: Date, dateStyle: string, timeStyle: string): string | null {\n // `toStyle` yields `undefined` for an empty/invalid keyword; assigning it is\n // equivalent to omitting the option, so a consumer can show date- or time-only.\n const options: Intl.DateTimeFormatOptions = {\n dateStyle: toStyle(dateStyle),\n timeStyle: toStyle(timeStyle),\n };\n if (options.dateStyle === undefined && options.timeStyle === undefined) return null;\n if (this.timeZoneValue.length > 0) options.timeZone = this.timeZoneValue;\n\n // An invalid locale or timeZone must not break the page: the authored\n // absolute text remains as the graceful fallback. `date` is already known to\n // be a real instant (`#parse` rejects the rest), so formatting it cannot fail.\n const formatter = intlFormatter(\n Intl.DateTimeFormat,\n resolveLocale(this.element, this.localeValue),\n options,\n );\n return formatter === null ? null : formatter.format(date);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/datetime.ts","../../src/utils/intl_format.ts","../../src/utils/locale.ts","../../src/utils/listener_set.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/morph_render_watcher.ts","../../src/controllers/local_time_controller.ts"],"names":[],"mappings":"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","file":"local_time_controller.js","sourcesContent":["/** Complete Gregorian dates with an optional clock and numeric UTC offset. */\nconst DATETIME =\n /^([+-]\\d{6}|\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d+))?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/;\n\n/**\n * Reads a complete Gregorian `datetime` as epoch milliseconds, or `null`.\n *\n * Dates and clocks without a zone use UTC; explicit offsets retain their meaning.\n * Surrounding whitespace is tolerated, fractions are truncated to milliseconds,\n * and 24:00 is allowed only when every smaller unit is zero. Invalid civil dates,\n * incomplete fields, and instants outside the Date range return `null`, and the caller\n * keeps what the element shows.\n */\nexport function readDatetime(element: Element): number | null {\n const raw = element.getAttribute(\"datetime\");\n if (raw === null) return null;\n const match = DATETIME.exec(raw.trim());\n if (match === null) return null;\n const [\n ,\n yearText,\n monthText,\n dayText,\n hourText = \"00\",\n minuteText = \"00\",\n secondText = \"00\",\n fraction = \"\",\n zone = \"Z\",\n ] = match;\n const year = Number(yearText);\n const month = Number(monthText);\n const day = Number(dayText);\n const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);\n const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n if (month < 1 || month > 12 || day < 1 || day > (days[month - 1] ?? 0)) return null;\n const hour = Number(hourText);\n const minute = Number(minuteText);\n const second = Number(secondText);\n if (hour === 24 && (minute !== 0 || second !== 0 || /[1-9]/.test(fraction))) return null;\n let offset = zone;\n if (zone !== \"Z\") {\n const digits = zone.slice(1).replace(\":\", \"\");\n const offsetHour = digits.slice(0, 2);\n const offsetMinute = digits.slice(2);\n offset = `${zone[0]}${offsetHour}:${offsetMinute}`;\n }\n const milliseconds = fraction.slice(0, 3).padEnd(3, \"0\");\n const canonical = `${yearText}-${monthText}-${dayText}T${hourText}:${minuteText}:${secondText}.${milliseconds}${offset}`;\n const instant = Date.parse(canonical);\n return Number.isNaN(instant) ? null : instant;\n}\n","/**\n * Construction of `Intl` formatters for the controllers that take a locale, and\n * the options that go with it, from a declaration.\n *\n * Every `Intl` constructor resolves locale data on the way in, which makes it\n * too costly to rebuild on a hot path, and rejects a malformed language tag,\n * currency code, time zone or digit count with a `RangeError`. Both concerns are\n * handled here once. A formatter is built at most once per distinct request and\n * shared by every caller that asks for the same one. A `RangeError` is caught\n * and answered the way the caller asked: by rebuilding under a fallback locale,\n * or by returning `null` so the caller can leave its authored text alone. Any\n * other failure is a programming fault and propagates.\n */\n\n/** The constructor shape the `Intl` formatter classes share. */\nexport interface IntlFormatterConstructor<F, O extends object> {\n new (locales?: string | string[], options?: O): F;\n}\n\n/** Built formatters, and refusals, per constructor; see `cacheKey` for the key. */\nconst formatters = new WeakMap<object, Map<string, unknown>>();\n\n/**\n * A formatter for `locale` with `options`.\n *\n * With `fallbackLocale`, a `RangeError` from the constructor is answered by\n * building the same options under the fallback instead, so the result is never\n * `null`; the fallback's own failure propagates. Without it, a `RangeError`\n * yields `null` and the caller decides what to show. Errors of any other kind\n * propagate in both forms.\n *\n * Results are cached per constructor and `(locale, options, fallback)`, `null`\n * included, so asking again on every render costs one map lookup.\n */\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale: string,\n): F;\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n): F | null;\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale?: string,\n): F | null {\n const cache = cacheFor(factory);\n const key = cacheKey(locale, options, fallbackLocale);\n if (cache.has(key)) return cache.get(key) as F | null;\n const formatter = construct(factory, locale, options, fallbackLocale);\n cache.set(key, formatter);\n return formatter;\n}\n\n/** The cache for one constructor, created on first use. */\nfunction cacheFor(factory: object): Map<string, unknown> {\n const existing = formatters.get(factory);\n if (existing) return existing;\n const created = new Map<string, unknown>();\n formatters.set(factory, created);\n return created;\n}\n\n/**\n * One key per distinct request. `JSON.stringify` drops an option left\n * `undefined`, which is how the constructor reads it too. Two spellings of the\n * same options differing only in key order key separately; that costs one extra\n * formatter, never a wrong one, and call sites pass a fixed literal anyway.\n */\nfunction cacheKey(\n locale: string | undefined,\n options: object,\n fallbackLocale: string | undefined,\n): string {\n return JSON.stringify([locale, options, fallbackLocale]);\n}\n\n/** Builds the formatter under the `RangeError` contract described on `intlFormatter`. */\nfunction construct<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale: string | undefined,\n): F | null {\n try {\n return new factory(locale, options);\n } catch (error) {\n if (!(error instanceof RangeError)) throw error;\n if (fallbackLocale === undefined) return null;\n return new factory(fallbackLocale, options);\n }\n}\n","/**\n * The locale a component formats in: its own declaration, else the language of\n * the text around it, else the runtime default.\n *\n * `lang` is inherited in HTML, so the language that applies to an element is the\n * nearest one on the element itself or on an ancestor — not the document\n * element's, which is only the farthest of them. An empty `lang` there means the\n * language is unknown and stops the inheritance at that point, so it resolves to\n * the runtime default rather than to a `lang` further up.\n *\n * Resolution only, and the value is passed through as authored. Whether `Intl`\n * accepts the tag, and what to show when it does not, is the caller's contract.\n *\n * @example\n * ```ts\n * intlFormatter(Intl.DateTimeFormat, resolveLocale(this.element, this.localeValue), options);\n * ```\n */\nexport function resolveLocale(element: Element, declared = \"\"): string | undefined {\n return declared || element.closest(\"[lang]\")?.getAttribute(\"lang\") || undefined;\n}\n","/** What {@link ListenerSet.add} passes on besides the signal it supplies itself. */\nexport type ListenerOptions = Omit<AddEventListenerOptions, \"signal\">;\n\n/**\n * The event map a target's own `addEventListener` types its handlers with: the\n * window's, a document's, an HTML or SVG element's, any other element's, or a\n * media query list's. Other targets name none.\n */\ntype EventMapOf<T extends EventTarget> = T extends Window\n ? WindowEventMap\n : T extends Document\n ? DocumentEventMap\n : T extends HTMLElement\n ? HTMLElementEventMap\n : T extends SVGElement\n ? SVGElementEventMap\n : T extends Element\n ? ElementEventMap\n : T extends MediaQueryList\n ? MediaQueryListEventMap\n : Record<never, never>;\n\n/**\n * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt.\n * {@link ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, which places `dispose()` in its\n * cleanup sequence. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * A lifetime shorter than the connection — while a Value holds, until a first\n * load, while a subscription is open — gets a set of its own, which the end of\n * that lifetime disposes. A controller therefore holds one set per lifetime: the\n * connection's, and one for each swapped receiver or shorter span.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, listeners of\n * several elements that come and go one at a time, and those one gesture holds\n * keep their own pair of calls or a signal of their own.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included. A removal by hand would have to repeat that\n * flag; aborting the signal releases the listener without it.\n *\n * A `type` the target's own event map names types `handler` with that event,\n * as the target's `addEventListener` does, so a handler written for a\n * `KeyboardEvent` is passed as it is; any other `type` takes an\n * `EventListener`.\n */\n add<T extends EventTarget, K extends keyof EventMapOf<T> & string>(\n target: T,\n type: K,\n handler: (event: EventMapOf<T>[K]) => void,\n options?: ListenerOptions,\n ): void;\n add(target: EventTarget, type: string, handler: EventListener, options?: ListenerOptions): void;\n add(target: EventTarget, type: string, handler: EventListener, options?: ListenerOptions): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\n }\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** 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 { ListenerSet } from \"./listener_set\";\nimport { MicrotaskCoalescer } from \"./microtask_coalescer\";\n\n/**\n * Coalesces retained-element morphs and explicit lifecycle requests into one pass.\n *\n * Observe the controller's element after its initial state is settled. Morphs on\n * that element and bubbling morphs on its descendants share the same microtask\n * as {@link schedule}. The callback repairs derived output; the controller owns\n * any logical transition reporting. {@link disconnect} releases the listener and\n * cancels pending work, including work from an earlier connected lifetime.\n */\nexport class MorphRenderWatcher {\n readonly #listeners = new ListenerSet();\n readonly #pass: MicrotaskCoalescer;\n #element: Element | null = null;\n readonly #onMorph = (): void => this.#pass.schedule();\n\n /** @param run - the single pass that repairs the settled controller's output. */\n constructor(run: () => void) {\n this.#pass = new MicrotaskCoalescer(run);\n }\n\n /** Opens observation; observing the same element again preserves queued work. */\n observe(element: Element): void {\n if (this.#element === element) return;\n this.disconnect();\n this.#element = element;\n this.#pass.activate();\n this.#listeners.add(element, \"turbo:morph-element\", this.#onMorph);\n }\n\n /** Requests one pass; requests outside an observed lifetime are ignored. */\n schedule(): void {\n this.#pass.schedule();\n }\n\n /** Whether a pass requested by a morph or {@link schedule} has yet to run. */\n get pending(): boolean {\n return this.#pass.pending;\n }\n\n /** Releases observation and pending work synchronously; safe to repeat. */\n disconnect(): void {\n this.#listeners.dispose();\n this.#pass.cancel();\n this.#element = null;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { readDatetime } from \"../utils/datetime\";\nimport { intlFormatter } from \"../utils/intl_format\";\nimport { resolveLocale } from \"../utils/locale\";\nimport { MorphRenderWatcher } from \"../utils/morph_render_watcher\";\n\n/** A date/time style keyword accepted by `Intl` `dateStyle` / `timeStyle`. */\ntype DateTimeStyle = \"full\" | \"long\" | \"medium\" | \"short\";\n\n/** The valid `Intl` style keywords, used to validate the string-typed values. */\nconst STYLES = new Set<DateTimeStyle>([\"full\", \"long\", \"medium\", \"short\"]);\n\n/** Narrows an arbitrary string to a valid {@link DateTimeStyle}, or `undefined`. */\nfunction toStyle(value: string): DateTimeStyle | undefined {\n return (STYLES as Set<string>).has(value) ? (value as DateTimeStyle) : undefined;\n}\n\n/**\n * Headless local-time behavior: renders the UTC instant in a `<time datetime>`\n * as an absolute, viewer-localized string via `Intl.DateTimeFormat`. No\n * dedicated APG pattern; it follows the HTML `<time>` semantics.\n *\n * Markup contract (identifier: `stimeo--local-time`):\n * <time datetime=\"2026-06-08T12:30:00Z\"\n * data-controller=\"stimeo--local-time\"\n * data-stimeo--local-time-date-style-value=\"medium\"\n * data-stimeo--local-time-time-style-value=\"short\">2026-06-08 12:30 UTC</time>\n *\n * The server emits UTC (cache-friendly — it never needs to know the viewer's\n * timezone), and the controller reformats the visible text into the viewer's\n * locale/zone on connect. A `datetime` without a zone is read as UTC; an explicit\n * `Z` or numeric offset keeps its meaning. This is the *absolute* localization axis,\n * distinct from `stimeo--relative-time`'s \"3 minutes ago\".\n *\n * `format` dispatches `{ formatted }`.\n *\n * @remarks\n * Behavior only. The machine-readable `datetime` is left untouched, exactly as written,\n * so assistive tech and crawlers keep the canonical value while only the display\n * text — and an optional `title` — change. Formatting is a pure function of\n * `datetime` with no module-scope state or timers, so a Turbo Drive cache restore\n * re-runs `connect()` and stays consistent. A `datetime` that does not parse, and\n * a locale or time zone `Intl` rejects, leave the text already shown in place — the\n * authored text until the first format; any other failure surfaces rather than being\n * absorbed.\n *\n * Render inputs are followed at runtime: a morph that swaps a Value or the\n * `datetime` attribute on the live element — which keeps the element, so\n * `connect()` never runs again — repaints through one coalesced pass\n * (`MorphRenderWatcher`) rather than leaving a stale reading on screen.\n */\nexport class LocalTimeController extends Controller<HTMLElement> {\n static override values = {\n locale: { type: String, default: \"\" },\n timeZone: { type: String, default: \"\" },\n dateStyle: { type: String, default: \"medium\" },\n timeStyle: { type: String, default: \"short\" },\n titleFormat: { type: String, default: \"\" },\n };\n static events = [\"format\"] as const;\n\n declare localeValue: string;\n declare timeZoneValue: string;\n declare dateStyleValue: string;\n declare timeStyleValue: string;\n declare titleFormatValue: string;\n\n /** Collapses a morph that swaps several render inputs at once into one repaint. */\n readonly #resync = new MorphRenderWatcher(() => {\n const datetimeChanged = this.#datetimeWatch.takeRecords().length > 0;\n const notify = this.#formatPending || datetimeChanged;\n this.#formatPending = false;\n this.#render(notify);\n });\n #formatPending = false;\n /**\n * Watches the one render input that is not a Value. Only `datetime` is filtered\n * in, so the text and `title` this controller writes cannot re-enter the pass.\n */\n readonly #datetimeWatch = new MutationObserver(() => {\n this.#formatPending = true;\n this.#resync.schedule();\n });\n\n override connect(): void {\n this.#resync.observe(this.element);\n this.#datetimeWatch.observe(this.element, { attributeFilter: [\"datetime\"] });\n this.#formatPending = false;\n this.#render();\n }\n\n override disconnect(): void {\n this.#resync.disconnect();\n this.#datetimeWatch.disconnect();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `locale` at runtime. */\n localeValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `timeZone` at runtime. */\n timeZoneValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `dateStyle` at runtime. */\n dateStyleValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `timeStyle` at runtime. */\n timeStyleValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `titleFormat` at runtime. */\n titleFormatValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /**\n * Formats the instant in `datetime` against the current Values and writes it out.\n *\n * Formatting from a changed input reports `format`; restoring output alone is\n * silent. A pass that cannot format leaves the text already shown.\n *\n * @stimeoRenderRoot\n */\n #render(notify = true): void {\n const date = this.#parse();\n if (date === null) return;\n\n const formatted = this.#applyFormat(date, this.dateStyleValue, this.timeStyleValue);\n if (formatted === null) return;\n\n // Only the visible text (and optional title) change; `datetime` is the\n // immutable machine-readable source and is never rewritten.\n this.element.textContent = formatted;\n\n const title = this.#title(date);\n if (title !== null) this.element.setAttribute(\"title\", title);\n\n if (notify) this.dispatch(\"format\", { detail: { formatted } });\n }\n\n /**\n * Reads the `datetime` instant with the strict reader `stimeo--relative-time` also uses;\n * the attribute is never rewritten.\n */\n #parse(): Date | null {\n const instant = readDatetime(this.element);\n return instant === null ? null : new Date(instant);\n }\n\n /**\n * Builds the optional detailed `title`. `titleFormat` is an `Intl` style\n * keyword applied to *both* date and time; empty (the default) adds no title.\n */\n #title(date: Date): string | null {\n if (this.titleFormatValue.length === 0) return null;\n return this.#applyFormat(date, this.titleFormatValue, this.titleFormatValue);\n }\n\n /**\n * Formats `date` with `Intl.DateTimeFormat`, including each style only when it\n * is a valid keyword (so a consumer can show date-only or time-only by clearing\n * the other). Returns `null` when neither style is usable or the declaration\n * names a locale or time zone `Intl` rejects, so the caller can leave the text\n * already shown untouched.\n */\n #applyFormat(date: Date, dateStyle: string, timeStyle: string): string | null {\n // `toStyle` yields `undefined` for an empty/invalid keyword; assigning it is\n // equivalent to omitting the option, so a consumer can show date- or time-only.\n const options: Intl.DateTimeFormatOptions = {\n dateStyle: toStyle(dateStyle),\n timeStyle: toStyle(timeStyle),\n };\n if (options.dateStyle === undefined && options.timeStyle === undefined) return null;\n if (this.timeZoneValue.length > 0) options.timeZone = this.timeZoneValue;\n\n // An invalid locale or timeZone must not break the page: the text already shown\n // stays as the graceful fallback. `date` is already known to be a real instant\n // (`#parse` rejects the rest), so formatting it cannot fail.\n const formatter = intlFormatter(\n Intl.DateTimeFormat,\n resolveLocale(this.element, this.localeValue),\n options,\n );\n return formatter === null ? null : formatter.format(date);\n }\n}\n"]}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
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
|
+
}
|
|
9
|
+
|
|
3
10
|
var LayoutObserver = class {
|
|
4
11
|
#callback;
|
|
5
12
|
#resizeObserverFactory;
|
|
@@ -71,6 +78,9 @@ var MicrotaskCoalescer = class {
|
|
|
71
78
|
this.#queued = false;
|
|
72
79
|
this.#generation += 1;
|
|
73
80
|
}
|
|
81
|
+
get pending() {
|
|
82
|
+
return this.#queued;
|
|
83
|
+
}
|
|
74
84
|
schedule() {
|
|
75
85
|
if (!this.#active || this.#queued) return;
|
|
76
86
|
this.#queued = true;
|
|
@@ -205,10 +215,7 @@ var MasonryController = class _MasonryController extends Controller {
|
|
|
205
215
|
if ((heights[col] ?? 0) < (heights[shortest] ?? 0)) shortest = col;
|
|
206
216
|
}
|
|
207
217
|
const assigned = String(shortest);
|
|
208
|
-
if (item
|
|
209
|
-
item.setAttribute("data-column", assigned);
|
|
210
|
-
changed = true;
|
|
211
|
-
}
|
|
218
|
+
if (setAttributeIfChanged(item, "data-column", assigned)) changed = true;
|
|
212
219
|
heights[shortest] = (heights[shortest] ?? 0) + (boxes[index] ?? 0) + this.#gap;
|
|
213
220
|
});
|
|
214
221
|
this.element.style.setProperty(COLUMNS_PROPERTY, String(columns));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../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 * 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 /** 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/**\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 { 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 (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 /** 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"]}
|
|
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),
|
|
@@ -65,9 +65,18 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
65
65
|
* {@link connect}, a trigger or a menu that takes over, and {@link disconnect}
|
|
66
66
|
* are all silent.
|
|
67
67
|
*
|
|
68
|
-
* Roving focus skips `hidden` and natively `disabled` items. An
|
|
69
|
-
* `aria-disabled="true"
|
|
70
|
-
*
|
|
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.
|
|
71
80
|
*/
|
|
72
81
|
declare class MenuController extends Controller<HTMLElement> {
|
|
73
82
|
#private;
|
|
@@ -123,13 +132,23 @@ declare class MenuController extends Controller<HTMLElement> {
|
|
|
123
132
|
* Closes the menu after an item is activated. Bound via `data-action`, and
|
|
124
133
|
* also reached from the delegated listener.
|
|
125
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
|
+
*
|
|
126
145
|
* Markup that carries the per-element action *and* gets the delegate would run
|
|
127
146
|
* this twice for one gesture. The state hooks (`hidden`, `aria-expanded`) are
|
|
128
147
|
* written only when they change, so a second pass writes nothing on its own, but
|
|
129
148
|
* a page handler between the two paths — on an ancestor of the item — that opens
|
|
130
149
|
* the menu again would see the second pass close it. The event is therefore
|
|
131
150
|
* claimed: the path that gets there first does the work, the other one finds it
|
|
132
|
-
* claimed and returns.
|
|
151
|
+
* claimed and returns.
|
|
133
152
|
*/
|
|
134
153
|
activate(event?: Event): void;
|
|
135
154
|
}
|