stimeo-ui 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +8 -1
- package/dist/cable/index.d.ts +79 -23
- package/dist/cable/index.js +363 -436
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +4 -2
- package/dist/controllers/accordion_controller.js +166 -55
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +12 -2
- package/dist/controllers/alert_dialog_controller.js +862 -221
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +35 -5
- package/dist/controllers/announcer_controller.js +135 -198
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.js +0 -10
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +30 -6
- package/dist/controllers/auto_submit_controller.js +271 -127
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +171 -105
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +0 -130
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +18 -0
- package/dist/controllers/bulk_select_controller.js +262 -52
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +24 -5
- package/dist/controllers/calendar_controller.js +175 -209
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +34 -7
- package/dist/controllers/carousel_controller.js +461 -399
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +17 -1
- package/dist/controllers/character_counter_controller.js +246 -156
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +7 -2
- package/dist/controllers/checkbox_controller.js +32 -74
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +16 -5
- package/dist/controllers/clipboard_controller.js +168 -147
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +16 -4
- package/dist/controllers/collapsible_controller.js +301 -120
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +18 -6
- package/dist/controllers/color_picker_controller.js +149 -139
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +23 -5
- package/dist/controllers/combobox_controller.js +360 -133
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +45 -14
- package/dist/controllers/command_palette_controller.js +970 -358
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +15 -2
- package/dist/controllers/conditional_fields_controller.js +166 -124
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +49 -12
- package/dist/controllers/confirm_controller.js +939 -248
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +31 -4
- package/dist/controllers/context_menu_controller.js +256 -128
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +13 -1
- package/dist/controllers/count_up_controller.js +73 -21
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +45 -1
- package/dist/controllers/countdown_controller.js +183 -148
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +21 -2
- package/dist/controllers/currency_input_controller.js +134 -158
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +15 -4
- package/dist/controllers/data_grid_controller.js +114 -186
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +30 -5
- package/dist/controllers/date_range_picker_controller.js +288 -206
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +13 -1
- package/dist/controllers/dialog_controller.js +858 -223
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +24 -7
- package/dist/controllers/direct_upload_controller.js +73 -153
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.js +14 -48
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +19 -0
- package/dist/controllers/dismissible_controller.js +175 -21
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +39 -11
- package/dist/controllers/drawer_controller.js +933 -341
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +31 -1
- package/dist/controllers/dropdown_controller.js +312 -82
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +158 -68
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +67 -53
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +60 -18
- package/dist/controllers/file_dropzone_controller.js +311 -207
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +67 -39
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +31 -8
- package/dist/controllers/flash_controller.js +172 -314
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +12 -3
- package/dist/controllers/focus_controller.js +654 -240
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +147 -132
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +13 -86
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +41 -9
- package/dist/controllers/frame_loading_controller.js +264 -213
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +22 -2
- package/dist/controllers/highlight_controller.js +107 -75
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +56 -3
- package/dist/controllers/hover_card_controller.js +309 -118
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +42 -1
- package/dist/controllers/idle_controller.js +267 -101
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js +71 -72
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +32 -13
- package/dist/controllers/intersection_controller.js +147 -106
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +16 -0
- package/dist/controllers/lazy_frame_controller.js +9 -65
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +34 -2
- package/dist/controllers/listbox_controller.js +306 -204
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +1 -1
- package/dist/controllers/local_time_controller.js +54 -64
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +11 -3
- package/dist/controllers/masonry_controller.js +70 -93
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +32 -7
- package/dist/controllers/menu_controller.js +214 -156
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +4 -4
- package/dist/controllers/menubar_controller.js +170 -329
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +20 -0
- package/dist/controllers/meter_controller.js +151 -67
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +32 -4
- package/dist/controllers/multi_select_controller.js +593 -358
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +8 -1
- package/dist/controllers/navigation_menu_controller.js +124 -232
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +10 -0
- package/dist/controllers/nested_form_controller.js +352 -182
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +23 -0
- package/dist/controllers/network_status_controller.js +242 -54
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +26 -2
- package/dist/controllers/number_input_controller.js +320 -263
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +16 -76
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +20 -4
- package/dist/controllers/otp_controller.js +198 -193
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
- package/dist/controllers/overflow_indicator_controller.js +116 -113
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +48 -11
- package/dist/controllers/overflow_menu_controller.js +343 -283
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +35 -6
- package/dist/controllers/pagination_controller.js +295 -119
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +45 -8
- package/dist/controllers/password_reveal_controller.js +389 -121
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +14 -5
- package/dist/controllers/password_strength_controller.js +99 -153
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +11 -4
- package/dist/controllers/persist_controller.js +237 -103
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +19 -7
- package/dist/controllers/pointer_drag_controller.js +185 -165
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +38 -2
- package/dist/controllers/popover_controller.js +236 -97
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +44 -4
- package/dist/controllers/portal_controller.js +209 -76
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +10 -4
- package/dist/controllers/preview_guard_controller.js +232 -116
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +11 -0
- package/dist/controllers/progress_controller.js +125 -56
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +14 -5
- package/dist/controllers/radio_group_controller.js +117 -137
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +43 -3
- package/dist/controllers/range_slider_controller.js +192 -105
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +20 -4
- package/dist/controllers/rating_controller.js +418 -207
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +2 -0
- package/dist/controllers/read_more_controller.js +134 -63
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +1 -1
- package/dist/controllers/reading_progress_controller.js +147 -135
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +18 -4
- package/dist/controllers/relative_time_controller.js +145 -110
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
- package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- package/dist/controllers/reset_on_restore_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +28 -2
- package/dist/controllers/resizable_controller.js +255 -139
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +39 -84
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -3
- package/dist/controllers/scroll_area_controller.js +428 -225
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +0 -53
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
- package/dist/controllers/scroll_visibility_controller.js +395 -145
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +6 -1
- package/dist/controllers/scrollspy_controller.js +181 -242
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +19 -4
- package/dist/controllers/separator_controller.js +267 -140
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +64 -7
- package/dist/controllers/sidebar_controller.js +1023 -386
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +23 -0
- package/dist/controllers/skeleton_controller.js +248 -97
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +27 -1
- package/dist/controllers/slider_controller.js +159 -79
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
- package/dist/controllers/smart_sticky_header_controller.js +162 -63
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +0 -112
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +39 -4
- package/dist/controllers/spinner_controller.js +277 -169
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +5 -0
- package/dist/controllers/step_indicator_controller.js +99 -46
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +12 -3
- package/dist/controllers/stepper_controller.js +114 -73
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +6 -0
- package/dist/controllers/stick_to_bottom_controller.js +262 -138
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +11 -0
- package/dist/controllers/sticky_observer_controller.js +214 -49
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +23 -8
- package/dist/controllers/submit_once_controller.js +294 -205
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -1
- package/dist/controllers/switch_controller.js +33 -60
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +2 -2
- package/dist/controllers/tabs_controller.js +35 -29
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +12 -0
- package/dist/controllers/tags_input_controller.js +136 -145
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
- package/dist/controllers/textarea_autosize_controller.js +71 -58
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +21 -9
- package/dist/controllers/theme_controller.js +130 -112
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +18 -1
- package/dist/controllers/time_picker_controller.js +146 -80
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +36 -11
- package/dist/controllers/toast_controller.js +186 -297
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +12 -3
- package/dist/controllers/toggle_group_controller.js +95 -136
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +68 -98
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +45 -3
- package/dist/controllers/tooltip_controller.js +158 -124
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +22 -4
- package/dist/controllers/transition_controller.js +123 -119
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +21 -11
- package/dist/controllers/tree_view_controller.js +150 -345
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6370 -9346
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +23 -3
- package/dist/inspector/cli.js +92 -37
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +92 -56
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +8 -8
- package/dist/inspector/manifest.json +787 -100
- package/dist/positioning/index.d.ts +8 -0
- package/dist/positioning/index.js +148 -47
- package/dist/positioning/index.js.map +1 -1
- package/package.json +6 -6
- package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
- package/dist/controllers/reset_before_cache_controller.js.map +0 -1
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
4
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
5
|
+
function stateReasonFor(event) {
|
|
6
|
+
if (!event) return "api";
|
|
7
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
8
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
9
|
+
return "user";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function actionSource(source) {
|
|
13
|
+
if (source instanceof HTMLElement) {
|
|
14
|
+
return { event: null, host: source, origin: source, reason: "api" };
|
|
15
|
+
}
|
|
16
|
+
const event = source ?? null;
|
|
17
|
+
return {
|
|
18
|
+
event,
|
|
19
|
+
host: event?.currentTarget instanceof Element ? event.currentTarget : null,
|
|
20
|
+
origin: event?.target instanceof Element ? event.target : null,
|
|
21
|
+
reason: stateReasonFor(event)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
4
24
|
|
|
5
|
-
// src/utils/logical_scroll.ts
|
|
6
25
|
function isRtl(element) {
|
|
7
26
|
return window.getComputedStyle(element).direction === "rtl";
|
|
8
27
|
}
|
|
9
28
|
|
|
10
|
-
// src/utils/arrow_step.ts
|
|
11
29
|
function logicalArrowStep(key, element) {
|
|
12
30
|
if (key === "ArrowDown") return 1;
|
|
13
31
|
if (key === "ArrowUp") return -1;
|
|
@@ -20,182 +38,222 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
20
38
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
21
39
|
}
|
|
22
40
|
|
|
23
|
-
|
|
24
|
-
|
|
41
|
+
function sharedRegistry(key, create) {
|
|
42
|
+
const symbol = Symbol.for(key);
|
|
43
|
+
const scope = globalThis;
|
|
44
|
+
const existing = scope[symbol];
|
|
45
|
+
if (existing !== void 0) return existing;
|
|
46
|
+
const registry = create();
|
|
47
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
48
|
+
return registry;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isAttributeValue(value) {
|
|
52
|
+
return value === null || typeof value === "string";
|
|
53
|
+
}
|
|
54
|
+
function readLeaseRecord(element, record) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
57
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
58
|
+
const [original, written, ...rest] = parsed;
|
|
59
|
+
const linked = rest[0] !== false;
|
|
60
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
61
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
62
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
63
|
+
} catch {
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
var AttributeLease = class _AttributeLease {
|
|
68
|
+
static #registry = sharedRegistry(
|
|
69
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
70
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
71
|
+
);
|
|
25
72
|
#attribute;
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
73
|
+
#record;
|
|
74
|
+
#base;
|
|
75
|
+
#held = /* @__PURE__ */ new Set();
|
|
76
|
+
constructor(attribute, owner, options = {}) {
|
|
29
77
|
this.#attribute = attribute;
|
|
78
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
79
|
+
this.#base = options.base === true;
|
|
80
|
+
const names = _AttributeLease.#registry.names;
|
|
81
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
30
82
|
}
|
|
31
|
-
/** Writes or removes the leased attribute while preserving its authored value. */
|
|
32
83
|
write(element, value) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
this.#reflect(element, value);
|
|
84
|
+
const stack = this.#stack(element);
|
|
85
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
86
|
+
this.#held.add(element);
|
|
87
|
+
const shows = !this.#covered(element, stack, hold);
|
|
88
|
+
hold.written = value;
|
|
89
|
+
this.#mark(element, stack);
|
|
90
|
+
if (shows) this.#reflect(element, value);
|
|
43
91
|
}
|
|
44
|
-
/** Returns one lease without overwriting a value subsequently authored by a consumer. */
|
|
45
92
|
return(element) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
this.#
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
93
|
+
const held = this.#held.delete(element);
|
|
94
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
95
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
96
|
+
const hold = this.#take(stack);
|
|
97
|
+
if (!hold) return false;
|
|
98
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
99
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
100
|
+
const index = stack.indexOf(hold);
|
|
101
|
+
stack.splice(index, 1);
|
|
102
|
+
const above = stack[index];
|
|
103
|
+
if (above) above.linked &&= hold.linked;
|
|
104
|
+
this.#mark(element, stack);
|
|
105
|
+
if (!owns) return true;
|
|
106
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
107
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
returnAll() {
|
|
111
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
112
|
+
}
|
|
113
|
+
#stack(element) {
|
|
114
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
115
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
116
|
+
stacks.set(element, byAttribute);
|
|
117
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
118
|
+
byAttribute.set(this.#attribute, stack);
|
|
119
|
+
return stack;
|
|
120
|
+
}
|
|
121
|
+
#seed(element) {
|
|
122
|
+
const found = [];
|
|
123
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
124
|
+
const read = readLeaseRecord(element, record);
|
|
125
|
+
if (read) found.push({ ...read, record });
|
|
126
|
+
}
|
|
127
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
128
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
129
|
+
lease: null,
|
|
130
|
+
record,
|
|
131
|
+
written,
|
|
132
|
+
original,
|
|
133
|
+
linked: linked && index > 0
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
#take(stack) {
|
|
137
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
138
|
+
if (hold) hold.lease = this;
|
|
139
|
+
return hold;
|
|
140
|
+
}
|
|
141
|
+
#acquire(element, stack) {
|
|
142
|
+
const current = element.getAttribute(this.#attribute);
|
|
143
|
+
const top = stack.at(-1);
|
|
144
|
+
const bottom = stack[0];
|
|
145
|
+
const over = top?.written === current ? top : void 0;
|
|
146
|
+
const from = this.#base ? bottom : over;
|
|
147
|
+
const hold = {
|
|
148
|
+
lease: this,
|
|
149
|
+
record: this.#record,
|
|
150
|
+
written: current,
|
|
151
|
+
original: from ? from.original : current,
|
|
152
|
+
linked: !this.#base && over !== void 0
|
|
153
|
+
};
|
|
154
|
+
if (this.#base && bottom) {
|
|
155
|
+
bottom.linked = true;
|
|
156
|
+
stack.unshift(hold);
|
|
157
|
+
} else stack.push(hold);
|
|
158
|
+
return hold;
|
|
159
|
+
}
|
|
160
|
+
#covered(element, stack, hold) {
|
|
161
|
+
const current = element.getAttribute(this.#attribute);
|
|
162
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
163
|
+
}
|
|
164
|
+
#mark(element, stack) {
|
|
165
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
166
|
+
stack.forEach((hold, index) => {
|
|
167
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
168
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
169
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
170
|
+
records.set(
|
|
171
|
+
hold.record,
|
|
172
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
for (const [name, recorded] of records) {
|
|
176
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
177
|
+
if (recorded === null) element.removeAttribute(name);
|
|
178
|
+
else element.setAttribute(name, recorded);
|
|
179
|
+
}
|
|
51
180
|
}
|
|
52
|
-
/** Reflects only a real value transition, avoiding self-triggered mutation work. */
|
|
53
181
|
#reflect(element, value) {
|
|
54
182
|
if (element.getAttribute(this.#attribute) === value) return;
|
|
55
183
|
if (value === null) element.removeAttribute(this.#attribute);
|
|
56
184
|
else element.setAttribute(this.#attribute, value);
|
|
57
185
|
}
|
|
58
|
-
/** Returns every outstanding lease using the same ownership check as {@link return}. */
|
|
59
|
-
returnAll() {
|
|
60
|
-
for (const element of Array.from(this.#records.keys())) this.return(element);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// src/utils/before_cache_reset.ts
|
|
65
|
-
var BeforeCacheReset = class _BeforeCacheReset {
|
|
66
|
-
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
67
|
-
static #subscribers = /* @__PURE__ */ new Set();
|
|
68
|
-
/** The shared listener; installed while at least one instance is subscribed. */
|
|
69
|
-
static #onBeforeCache = () => {
|
|
70
|
-
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
71
|
-
};
|
|
72
|
-
#rewind;
|
|
73
|
-
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
74
|
-
constructor(rewind) {
|
|
75
|
-
this.#rewind = rewind;
|
|
76
|
-
}
|
|
77
|
-
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
78
|
-
activate() {
|
|
79
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
80
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
81
|
-
if (first) {
|
|
82
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
86
|
-
deactivate() {
|
|
87
|
-
_BeforeCacheReset.#subscribers.delete(this);
|
|
88
|
-
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
89
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
90
|
-
}
|
|
91
186
|
};
|
|
92
187
|
|
|
93
|
-
// src/utils/detach_gate.ts
|
|
94
188
|
var DetachGate = class _DetachGate {
|
|
95
|
-
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
96
189
|
#pending = false;
|
|
97
|
-
/**
|
|
98
|
-
* True while a probe is queued — the last disconnect was ambiguous and no
|
|
99
|
-
* reconnect has cancelled it yet. Read it from `connect()` to tell the
|
|
100
|
-
* reconnect half of an in-page move from a first connect: a controller whose
|
|
101
|
-
* initialisation restarts a measurement (a min-duration floor, an elapsed
|
|
102
|
-
* counter) must skip it for the move, where nothing actually restarted.
|
|
103
|
-
*/
|
|
104
190
|
get pending() {
|
|
105
191
|
return this.#pending;
|
|
106
192
|
}
|
|
107
|
-
/**
|
|
108
|
-
* True when the disconnect is definitely a real detach — the element left
|
|
109
|
-
* the document, or `data-controller` no longer lists the identifier. False
|
|
110
|
-
* means ambiguous (in-page move or observed-root exit), NOT "alive".
|
|
111
|
-
*/
|
|
112
193
|
static isDetached(host) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return !tokens.includes(host.identifier);
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Call from `disconnect()`: runs `teardown` synchronously on a definite
|
|
119
|
-
* detach (fast path), otherwise defers it one microtask — a reconnect
|
|
120
|
-
* ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
|
|
121
|
-
* One microtask is the whole probe window: Stimulus reconnects a moved
|
|
122
|
-
* element within the same mutation batch, before the checkpoint drains.
|
|
123
|
-
*/
|
|
194
|
+
return !host.element.isConnected || !_DetachGate.#listed(host);
|
|
195
|
+
}
|
|
124
196
|
disconnected(host, teardown) {
|
|
125
|
-
|
|
197
|
+
const carried = _DetachGate.#carried(host);
|
|
198
|
+
if (!carried && _DetachGate.isDetached(host)) {
|
|
126
199
|
this.#pending = false;
|
|
127
200
|
teardown();
|
|
128
201
|
return;
|
|
129
202
|
}
|
|
130
203
|
this.#pending = true;
|
|
131
|
-
|
|
204
|
+
const probe = () => {
|
|
132
205
|
if (!this.#pending) return;
|
|
133
206
|
this.#pending = false;
|
|
134
207
|
teardown();
|
|
135
|
-
}
|
|
208
|
+
};
|
|
209
|
+
if (carried) setTimeout(probe, 0);
|
|
210
|
+
else queueMicrotask(probe);
|
|
211
|
+
}
|
|
212
|
+
static #carried(host) {
|
|
213
|
+
const element = host.element;
|
|
214
|
+
return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
|
|
215
|
+
}
|
|
216
|
+
static #listed(host) {
|
|
217
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
218
|
+
return tokens.includes(host.identifier);
|
|
136
219
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Disarms a pending probe. Call from `connect()` (the reconnect that proves
|
|
139
|
-
* an in-page move) and from the head of any teardown path not routed through
|
|
140
|
-
* {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
|
|
141
|
-
* never run the teardown a second time.
|
|
142
|
-
*/
|
|
143
220
|
cancel() {
|
|
144
221
|
this.#pending = false;
|
|
145
222
|
}
|
|
146
223
|
};
|
|
147
224
|
|
|
148
|
-
// src/utils/event_owner.ts
|
|
149
225
|
function ownerIndex(candidates, node) {
|
|
150
226
|
if (!(node instanceof Node)) return -1;
|
|
151
227
|
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
152
228
|
}
|
|
153
229
|
|
|
154
|
-
// src/utils/listener_set.ts
|
|
155
230
|
var ListenerSet = class {
|
|
156
|
-
/** The generation every `add` joins until the next `dispose()`. */
|
|
157
231
|
#abort = new AbortController();
|
|
158
|
-
/**
|
|
159
|
-
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
160
|
-
*
|
|
161
|
-
* The set supplies the signal, so `options` carries everything else the DOM
|
|
162
|
-
* accepts — `capture` included, which has to match at release time and no
|
|
163
|
-
* longer has a second place to drift from.
|
|
164
|
-
*/
|
|
165
232
|
add(target, type, handler, options) {
|
|
166
233
|
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
167
234
|
}
|
|
168
|
-
/**
|
|
169
|
-
* Releases every listener of the open generation, synchronously, and opens the
|
|
170
|
-
* next one. Idempotent, and safe before anything has been added.
|
|
171
|
-
*/
|
|
172
235
|
dispose() {
|
|
173
236
|
this.#abort.abort();
|
|
174
237
|
this.#abort = new AbortController();
|
|
175
238
|
}
|
|
176
239
|
};
|
|
177
240
|
|
|
178
|
-
// src/utils/microtask_coalescer.ts
|
|
179
241
|
var MicrotaskCoalescer = class {
|
|
180
242
|
#run;
|
|
181
243
|
#queued = false;
|
|
182
244
|
#active = false;
|
|
183
245
|
#generation = 0;
|
|
184
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
185
246
|
constructor(run) {
|
|
186
247
|
this.#run = run;
|
|
187
248
|
}
|
|
188
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
189
249
|
activate() {
|
|
190
250
|
this.#active = true;
|
|
191
251
|
}
|
|
192
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
193
252
|
cancel() {
|
|
194
253
|
this.#active = false;
|
|
195
254
|
this.#queued = false;
|
|
196
255
|
this.#generation += 1;
|
|
197
256
|
}
|
|
198
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
199
257
|
schedule() {
|
|
200
258
|
if (!this.#active || this.#queued) return;
|
|
201
259
|
this.#queued = true;
|
|
@@ -208,36 +266,101 @@ var MicrotaskCoalescer = class {
|
|
|
208
266
|
}
|
|
209
267
|
};
|
|
210
268
|
|
|
211
|
-
|
|
269
|
+
var MorphRenderWatcher = class {
|
|
270
|
+
#listeners = new ListenerSet();
|
|
271
|
+
#pass;
|
|
272
|
+
#element = null;
|
|
273
|
+
#onMorph = () => this.#pass.schedule();
|
|
274
|
+
constructor(run) {
|
|
275
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
276
|
+
}
|
|
277
|
+
observe(element) {
|
|
278
|
+
if (this.#element === element) return;
|
|
279
|
+
this.disconnect();
|
|
280
|
+
this.#element = element;
|
|
281
|
+
this.#pass.activate();
|
|
282
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
283
|
+
}
|
|
284
|
+
schedule() {
|
|
285
|
+
this.#pass.schedule();
|
|
286
|
+
}
|
|
287
|
+
disconnect() {
|
|
288
|
+
this.#listeners.dispose();
|
|
289
|
+
this.#pass.cancel();
|
|
290
|
+
this.#element = null;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
var MoveCounter = class {
|
|
295
|
+
#sequence = 0;
|
|
296
|
+
record() {
|
|
297
|
+
this.#sequence += 1;
|
|
298
|
+
return this.#sequence;
|
|
299
|
+
}
|
|
300
|
+
isLatest(token) {
|
|
301
|
+
return token === this.#sequence;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
306
|
+
|
|
307
|
+
var NUMBER_BOUNDS = {
|
|
308
|
+
positiveTimer: { finite: true, exclusiveMin: 0, max: MAX_TIMER_DELAY_MS }
|
|
309
|
+
};
|
|
310
|
+
function matchesNumberBounds(value, bounds) {
|
|
311
|
+
if (!Number.isFinite(value)) {
|
|
312
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
313
|
+
if (direction === null) return false;
|
|
314
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
315
|
+
}
|
|
316
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
317
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
318
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
319
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
320
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function readNumber(raw, fallback, bounds) {
|
|
325
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
var NumberValueReader = class {
|
|
329
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
330
|
+
read(owner, name, raw, fallback, bounds) {
|
|
331
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
332
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
333
|
+
this.#lastRejected.delete(name);
|
|
334
|
+
return resolved;
|
|
335
|
+
}
|
|
336
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
337
|
+
const literal = owner.element.getAttribute(attribute);
|
|
338
|
+
if (literal === null) {
|
|
339
|
+
this.#lastRejected.delete(name);
|
|
340
|
+
return resolved;
|
|
341
|
+
}
|
|
342
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
343
|
+
this.#lastRejected.set(name, literal);
|
|
344
|
+
console.warn(
|
|
345
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return resolved;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
212
352
|
function prefersReducedMotion() {
|
|
213
353
|
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
214
354
|
}
|
|
215
355
|
|
|
216
|
-
// src/utils/roving_tabindex.ts
|
|
217
356
|
var RovingTabindex = class {
|
|
218
|
-
/** Returns the current ordered item elements; called on every operation. */
|
|
219
357
|
#getItems;
|
|
220
|
-
/**
|
|
221
|
-
* @param getItems - Returns the current ordered item elements. Called on every
|
|
222
|
-
* operation so the live target list is always used.
|
|
223
|
-
*/
|
|
224
358
|
constructor(getItems) {
|
|
225
359
|
this.#getItems = getItems;
|
|
226
360
|
}
|
|
227
|
-
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
228
361
|
get activeIndex() {
|
|
229
362
|
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
230
363
|
}
|
|
231
|
-
/**
|
|
232
|
-
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
233
|
-
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
234
|
-
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
235
|
-
* "nothing is currently tabbable".
|
|
236
|
-
*
|
|
237
|
-
* @param index - Position of the item to make tabbable.
|
|
238
|
-
* @param options - Pass `{ focus: true }` to also move DOM focus to that item,
|
|
239
|
-
* and `items` to reuse an event-scoped collection snapshot.
|
|
240
|
-
*/
|
|
241
364
|
setActive(index, options = {}) {
|
|
242
365
|
const { focus = false } = options;
|
|
243
366
|
const items = options.items ?? this.#getItems();
|
|
@@ -253,38 +376,24 @@ function rovingMove(current, length, delta, wrap) {
|
|
|
253
376
|
return (next + length) % length;
|
|
254
377
|
}
|
|
255
378
|
|
|
256
|
-
// src/utils/safe_timeout.ts
|
|
257
379
|
var TimerRegistry = class {
|
|
258
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
259
380
|
ids = /* @__PURE__ */ new Set();
|
|
260
|
-
/**
|
|
261
|
-
* Cancels a single tracked timer.
|
|
262
|
-
*
|
|
263
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
264
|
-
* registry), so callers can clear defensively without guarding.
|
|
265
|
-
*/
|
|
266
381
|
clear(id) {
|
|
267
382
|
if (this.ids.delete(id)) {
|
|
268
383
|
this.cancel(id);
|
|
269
384
|
}
|
|
270
385
|
}
|
|
271
|
-
/**
|
|
272
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
273
|
-
* to guarantee no timer outlives the element.
|
|
274
|
-
*/
|
|
275
386
|
clearAll() {
|
|
276
387
|
for (const id of this.ids) {
|
|
277
388
|
this.cancel(id);
|
|
278
389
|
}
|
|
279
390
|
this.ids.clear();
|
|
280
391
|
}
|
|
281
|
-
/** Number of timers currently tracked (pending). */
|
|
282
392
|
get size() {
|
|
283
393
|
return this.ids.size;
|
|
284
394
|
}
|
|
285
395
|
};
|
|
286
396
|
var SafeInterval = class extends TimerRegistry {
|
|
287
|
-
/** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
|
|
288
397
|
set(callback, delay) {
|
|
289
398
|
const id = this.schedule(callback, delay);
|
|
290
399
|
this.ids.add(id);
|
|
@@ -298,16 +407,20 @@ var SafeInterval = class extends TimerRegistry {
|
|
|
298
407
|
}
|
|
299
408
|
};
|
|
300
409
|
|
|
301
|
-
|
|
302
|
-
|
|
410
|
+
var StateRegions = class _StateRegions {
|
|
411
|
+
static #registry = sharedRegistry(
|
|
412
|
+
"stimeo-ui.state-regions.registry.v1",
|
|
413
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
414
|
+
);
|
|
303
415
|
#whenTrue;
|
|
304
416
|
#whenFalse;
|
|
305
|
-
#
|
|
306
|
-
constructor(options) {
|
|
417
|
+
#record;
|
|
418
|
+
constructor(options, owner) {
|
|
307
419
|
this.#whenTrue = options.whenTrue;
|
|
308
420
|
this.#whenFalse = options.whenFalse ?? null;
|
|
421
|
+
this.#record = `data-${owner}-hidden-region`;
|
|
422
|
+
_StateRegions.#registry.names.add(this.#record);
|
|
309
423
|
}
|
|
310
|
-
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
311
424
|
reflect(host, isTrue) {
|
|
312
425
|
const shown = this.#inside(host, this.#whenTrue());
|
|
313
426
|
if (!this.#whenFalse) {
|
|
@@ -323,26 +436,131 @@ var StateRegions = class {
|
|
|
323
436
|
for (const region of shown) this.#write(region, !isTrue);
|
|
324
437
|
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
325
438
|
}
|
|
326
|
-
|
|
439
|
+
release(host) {
|
|
440
|
+
for (const region of [host, ...host.querySelectorAll("*")]) {
|
|
441
|
+
if (!(region instanceof HTMLElement)) continue;
|
|
442
|
+
const stack = this.#held(region);
|
|
443
|
+
const hold = stack && this.#take(stack);
|
|
444
|
+
if (!stack || !hold) continue;
|
|
445
|
+
const owns = region.hidden === hold.written && !this.#covered(region, stack, hold);
|
|
446
|
+
const index = stack.indexOf(hold);
|
|
447
|
+
stack.splice(index, 1);
|
|
448
|
+
const above = stack[index];
|
|
449
|
+
if (above) above.linked &&= hold.linked;
|
|
450
|
+
if (owns) {
|
|
451
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
452
|
+
if (below) {
|
|
453
|
+
if (region.hidden !== below.written) region.hidden = below.written;
|
|
454
|
+
} else if (region.getAttribute("hidden") !== hold.authored) {
|
|
455
|
+
if (hold.authored === null) region.removeAttribute("hidden");
|
|
456
|
+
else region.setAttribute("hidden", hold.authored);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
this.#mark(region, stack);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
327
462
|
#write(region, hidden) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
463
|
+
const stack = this.#stack(region);
|
|
464
|
+
const hold = this.#take(stack) ?? this.#acquire(region, stack);
|
|
465
|
+
const shows = !this.#covered(region, stack, hold);
|
|
466
|
+
hold.written = hidden;
|
|
467
|
+
if (shows && region.hidden !== hidden) region.hidden = hidden;
|
|
468
|
+
this.#mark(region, stack);
|
|
331
469
|
}
|
|
332
|
-
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
333
470
|
#give(region) {
|
|
334
|
-
|
|
471
|
+
const stack = this.#held(region);
|
|
472
|
+
if (stack && this.#take(stack)?.written) this.#write(region, false);
|
|
473
|
+
}
|
|
474
|
+
#held(region) {
|
|
475
|
+
return _StateRegions.#registry.stacks.has(region) || region.hasAttribute(this.#record) ? this.#stack(region) : void 0;
|
|
476
|
+
}
|
|
477
|
+
#stack(region) {
|
|
478
|
+
const stacks = _StateRegions.#registry.stacks;
|
|
479
|
+
const stack = stacks.get(region) ?? this.#seed(region);
|
|
480
|
+
stacks.set(region, stack);
|
|
481
|
+
return stack;
|
|
482
|
+
}
|
|
483
|
+
#seed(region) {
|
|
484
|
+
const found = [];
|
|
485
|
+
for (const record of _StateRegions.#registry.names) {
|
|
486
|
+
const read = this.#recorded(region, record);
|
|
487
|
+
if (read) found.push({ ...read, record });
|
|
488
|
+
}
|
|
489
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
490
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, authored, written, linked }, index) => ({
|
|
491
|
+
instance: null,
|
|
492
|
+
record,
|
|
493
|
+
authored,
|
|
494
|
+
written,
|
|
495
|
+
linked: linked && index > 0
|
|
496
|
+
}));
|
|
497
|
+
}
|
|
498
|
+
#take(stack) {
|
|
499
|
+
const hold = stack.find((held) => held.instance === this) ?? stack.find((held) => held.instance === null && held.record === this.#record);
|
|
500
|
+
if (hold) hold.instance = this;
|
|
501
|
+
return hold;
|
|
502
|
+
}
|
|
503
|
+
#covered(region, stack, hold) {
|
|
504
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === region.hidden);
|
|
505
|
+
}
|
|
506
|
+
#acquire(region, stack) {
|
|
507
|
+
const top = stack.at(-1);
|
|
508
|
+
const over = top && region.hidden === top.written ? top : void 0;
|
|
509
|
+
const hold = {
|
|
510
|
+
instance: this,
|
|
511
|
+
record: this.#record,
|
|
512
|
+
authored: over ? over.authored : region.getAttribute("hidden"),
|
|
513
|
+
written: false,
|
|
514
|
+
linked: over !== void 0
|
|
515
|
+
};
|
|
516
|
+
stack.push(hold);
|
|
517
|
+
return hold;
|
|
518
|
+
}
|
|
519
|
+
#mark(region, stack) {
|
|
520
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
521
|
+
stack.forEach((hold, index) => {
|
|
522
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
523
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
524
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
525
|
+
records.set(
|
|
526
|
+
hold.record,
|
|
527
|
+
region.getAttribute("hidden") === hold.authored && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.authored, hold.written, ...marker, ...beneath])
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
for (const [name, recorded] of records) {
|
|
531
|
+
if (region.getAttribute(name) === recorded) continue;
|
|
532
|
+
if (recorded === null) region.removeAttribute(name);
|
|
533
|
+
else region.setAttribute(name, recorded);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
#recorded(region, name) {
|
|
537
|
+
try {
|
|
538
|
+
const parsed = JSON.parse(region.getAttribute(name) ?? "null");
|
|
539
|
+
if (!Array.isArray(parsed)) return null;
|
|
540
|
+
const [authored, written, ...rest] = parsed;
|
|
541
|
+
if (authored !== null && typeof authored !== "string") return null;
|
|
542
|
+
const linked = rest[0] !== false;
|
|
543
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
544
|
+
if (!beneath.every((under) => typeof under === "string")) return null;
|
|
545
|
+
return typeof written === "boolean" ? { authored, written, linked, beneath } : null;
|
|
546
|
+
} catch {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
335
549
|
}
|
|
336
|
-
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
337
550
|
#inside(host, regions) {
|
|
338
551
|
return regions.filter((region) => host.contains(region));
|
|
339
552
|
}
|
|
340
553
|
};
|
|
341
554
|
|
|
342
|
-
|
|
555
|
+
function targetSelector(identifier, name) {
|
|
556
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
557
|
+
}
|
|
558
|
+
|
|
343
559
|
var DEFAULT_INTERVAL = 5e3;
|
|
344
560
|
var OBSERVED_ATTRIBUTES = ["aria-selected", "data-state", "hidden"];
|
|
345
|
-
var CarouselController = class extends Controller {
|
|
561
|
+
var CarouselController = class _CarouselController extends Controller {
|
|
562
|
+
#numbers = new NumberValueReader();
|
|
563
|
+
#moves = new MoveCounter();
|
|
346
564
|
static targets = [
|
|
347
565
|
"slide",
|
|
348
566
|
"viewport",
|
|
@@ -358,6 +576,9 @@ var CarouselController = class extends Controller {
|
|
|
358
576
|
interval: { type: Number, default: DEFAULT_INTERVAL },
|
|
359
577
|
loop: { type: Boolean, default: true }
|
|
360
578
|
};
|
|
579
|
+
static valueConstraints = {
|
|
580
|
+
interval: NUMBER_BOUNDS.positiveTimer
|
|
581
|
+
};
|
|
361
582
|
static actions = [
|
|
362
583
|
"goto",
|
|
363
584
|
"next",
|
|
@@ -369,79 +590,37 @@ var CarouselController = class extends Controller {
|
|
|
369
590
|
];
|
|
370
591
|
static events = ["change", "pause", "play", "reconcile"];
|
|
371
592
|
#roving = new RovingTabindex(() => this.pickerTargets);
|
|
372
|
-
#reconcileTargets = new
|
|
593
|
+
#reconcileTargets = new MorphRenderWatcher(() => this.#reconcileTargetSet());
|
|
373
594
|
#intervals = new SafeInterval();
|
|
374
|
-
|
|
375
|
-
#
|
|
376
|
-
|
|
377
|
-
#
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
#beforeCache = new BeforeCacheReset(() => this.#returnLeases());
|
|
386
|
-
/** Tells an in-page move, which keeps the rotation, from a real detach. */
|
|
595
|
+
#ariaDisabled = new AttributeLease("aria-disabled", this.identifier);
|
|
596
|
+
#ariaPressed = new AttributeLease("aria-pressed", this.identifier);
|
|
597
|
+
#ariaLive = new AttributeLease("aria-live", this.identifier);
|
|
598
|
+
#ariaAtomic = new AttributeLease("aria-atomic", this.identifier);
|
|
599
|
+
#labels = new StateRegions(
|
|
600
|
+
{
|
|
601
|
+
whenTrue: () => this.onLabelTargets,
|
|
602
|
+
whenFalse: () => this.offLabelTargets
|
|
603
|
+
},
|
|
604
|
+
this.identifier
|
|
605
|
+
);
|
|
387
606
|
#gate = new DetachGate();
|
|
388
|
-
/** The next pointer movement, listened for while a pointer suspension waits on it. */
|
|
389
607
|
#pointer = new ListenerSet();
|
|
390
|
-
/**
|
|
391
|
-
* Events an authored action binding already took. The delegated listener runs
|
|
392
|
-
* later — it sits on the controller element, above every control — so it can
|
|
393
|
-
* consume the mark and stand down, letting the two wirings coexist without
|
|
394
|
-
* handling one interaction twice.
|
|
395
|
-
*/
|
|
396
608
|
#handledEvents = /* @__PURE__ */ new WeakSet();
|
|
397
|
-
/**
|
|
398
|
-
* Whether `connect()` has run. Scheduling is already inert outside that window
|
|
399
|
-
* (`MicrotaskCoalescer`), so this gates the Value callbacks Stimulus
|
|
400
|
-
* delivers ahead of `connect()`.
|
|
401
|
-
*/
|
|
402
609
|
#connected = false;
|
|
403
|
-
/** Index of the visible slide. */
|
|
404
610
|
#index = 0;
|
|
405
|
-
/** The visible slide element, which identifies it across a changing target set. */
|
|
406
611
|
#activeSlide = null;
|
|
407
|
-
/** Slide count at the last resolved state, so a changed total is reportable. */
|
|
408
612
|
#total = 0;
|
|
409
|
-
/**
|
|
410
|
-
* Pointer rests on the carousel: a suspension that lifts on `mouseleave`, or on the
|
|
411
|
-
* pointer movement that confirms a suspension a move or a connect left unconfirmed.
|
|
412
|
-
*/
|
|
413
613
|
#pointerPaused = false;
|
|
414
|
-
/** Focus is inside the carousel: a suspension that lifts when it leaves. */
|
|
415
614
|
#focusPaused = false;
|
|
416
|
-
/** The tab is in the background: a suspension that lifts when it returns. */
|
|
417
615
|
#hiddenPaused = false;
|
|
418
|
-
/** Reduced motion holds back the intent declared at connect, until a press or a new one. */
|
|
419
616
|
#motionHold = false;
|
|
420
|
-
/** Id of the live autoplay interval, or null when stopped. */
|
|
421
617
|
#timerId = null;
|
|
422
|
-
/** Delay the live interval was armed with, so an `interval` change re-arms it. */
|
|
423
618
|
#timerInterval = 0;
|
|
424
|
-
/** Follows state-attribute rewrites on retained slides and pickers. */
|
|
425
619
|
#observer = null;
|
|
426
|
-
/** Keeps a hidden tab from advancing behind the user's back. */
|
|
427
620
|
#onVisibilityChange = () => {
|
|
428
621
|
this.#hiddenPaused = document.visibilityState === "hidden";
|
|
429
622
|
this.#syncTimer();
|
|
430
623
|
};
|
|
431
|
-
/**
|
|
432
|
-
* Renders the initial slide, wires the delegated listeners, and starts autoplay
|
|
433
|
-
* when requested.
|
|
434
|
-
*
|
|
435
|
-
* Focus, the tab's visibility and the reduced-motion hold are read afresh. The
|
|
436
|
-
* pointer is not something the document can answer yet: no `mouseleave` reaches a
|
|
437
|
-
* node that moved, and `:hover` can answer from before the move until the pointer
|
|
438
|
-
* moves again. So an in-page move — which Stimulus delivers to the *same* controller
|
|
439
|
-
* instance as `disconnect()` then `connect()` — keeps the pointer suspension it had,
|
|
440
|
-
* `:hover` reading true adds one, and either waits for the next pointer movement to
|
|
441
|
-
* be confirmed or lifted. A rotation the move carried is still running, so the edges
|
|
442
|
-
* are judged against it. The attribute observer starts after the first render so the
|
|
443
|
-
* controller's own opening writes are not fed back to it.
|
|
444
|
-
*/
|
|
445
624
|
connect() {
|
|
446
625
|
const moved = this.#gate.pending;
|
|
447
626
|
this.#gate.cancel();
|
|
@@ -455,8 +634,7 @@ var CarouselController = class extends Controller {
|
|
|
455
634
|
this.#total = this.slideTargets.length;
|
|
456
635
|
this.#syncTimer();
|
|
457
636
|
this.#connected = true;
|
|
458
|
-
this.#reconcileTargets.
|
|
459
|
-
this.#beforeCache.activate();
|
|
637
|
+
this.#reconcileTargets.observe(this.element);
|
|
460
638
|
this.element.addEventListener("click", this.#onClick);
|
|
461
639
|
this.element.addEventListener("keydown", this.#onKeydown);
|
|
462
640
|
this.element.addEventListener("focusin", this.#onFocusin);
|
|
@@ -471,12 +649,6 @@ var CarouselController = class extends Controller {
|
|
|
471
649
|
attributeFilter: OBSERVED_ATTRIBUTES
|
|
472
650
|
});
|
|
473
651
|
}
|
|
474
|
-
/**
|
|
475
|
-
* Releases every listener and observer, returns the leased ARIA, drops the focus
|
|
476
|
-
* and tab suspensions. The rotation goes through the detach gate: a reconnect in
|
|
477
|
-
* the same batch — an in-page move — keeps it, and the pointer suspension with it;
|
|
478
|
-
* anything else ends it.
|
|
479
|
-
*/
|
|
480
652
|
disconnect() {
|
|
481
653
|
this.#connected = false;
|
|
482
654
|
this.#pointer.dispose();
|
|
@@ -489,129 +661,99 @@ var CarouselController = class extends Controller {
|
|
|
489
661
|
document.removeEventListener("visibilitychange", this.#onVisibilityChange);
|
|
490
662
|
this.#observer?.disconnect();
|
|
491
663
|
this.#observer = null;
|
|
492
|
-
this.#reconcileTargets.
|
|
664
|
+
this.#reconcileTargets.disconnect();
|
|
493
665
|
this.#focusPaused = false;
|
|
494
666
|
this.#hiddenPaused = false;
|
|
495
667
|
this.#activeSlide = null;
|
|
496
668
|
this.#returnLeases();
|
|
497
|
-
this.#beforeCache.deactivate();
|
|
498
669
|
this.#gate.disconnected(this, () => this.#endRun());
|
|
499
670
|
}
|
|
500
|
-
/**
|
|
501
|
-
* Re-establishes the single selected picker when one is added after connect.
|
|
502
|
-
*
|
|
503
|
-
* Without this an appended picker that arrives `aria-selected="true"` leaves two
|
|
504
|
-
* marked at once — the authored pre-selection is only read on connect, so
|
|
505
|
-
* nothing else ever resolves the conflict. The current slide is kept: the
|
|
506
|
-
* repaint re-derives every picker from `#index`, so a late arrival never steals
|
|
507
|
-
* the selection.
|
|
508
|
-
*/
|
|
509
671
|
pickerTargetConnected(picker) {
|
|
510
672
|
picker.tabIndex = -1;
|
|
511
673
|
this.#reconcileTargets.schedule();
|
|
512
674
|
}
|
|
513
|
-
/** Repairs selection and roving after a picker leaves a retained carousel. */
|
|
514
675
|
pickerTargetDisconnected() {
|
|
515
676
|
this.#reconcileTargets.schedule();
|
|
516
677
|
}
|
|
517
|
-
/** Reconciles a slide added in the same DOM batch as its picker. */
|
|
518
678
|
slideTargetConnected() {
|
|
519
679
|
this.#reconcileTargets.schedule();
|
|
520
680
|
}
|
|
521
|
-
/** Re-clamps the active index after a slide is removed. */
|
|
522
681
|
slideTargetDisconnected() {
|
|
523
682
|
this.#reconcileTargets.schedule();
|
|
524
683
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
684
|
+
viewportTargetConnected() {
|
|
685
|
+
this.#reconcileTargets.schedule();
|
|
686
|
+
}
|
|
687
|
+
viewportTargetDisconnected(viewport) {
|
|
688
|
+
if (!this.viewportTargets.includes(viewport)) {
|
|
689
|
+
this.#ariaLive.return(viewport);
|
|
690
|
+
this.#ariaAtomic.return(viewport);
|
|
691
|
+
}
|
|
692
|
+
this.#reconcileTargets.schedule();
|
|
693
|
+
}
|
|
694
|
+
playToggleTargetDisconnected(toggle) {
|
|
695
|
+
if (this.playToggleTargets.includes(toggle)) return;
|
|
696
|
+
this.#labels.release(toggle);
|
|
697
|
+
this.#ariaDisabled.return(toggle);
|
|
698
|
+
this.#ariaPressed.return(toggle);
|
|
699
|
+
}
|
|
700
|
+
prevTargetDisconnected(button) {
|
|
701
|
+
if (!this.prevTargets.includes(button)) this.#ariaDisabled.return(button);
|
|
702
|
+
}
|
|
703
|
+
nextTargetDisconnected(button) {
|
|
704
|
+
if (!this.nextTargets.includes(button)) this.#ariaDisabled.return(button);
|
|
705
|
+
}
|
|
532
706
|
playToggleTargetConnected() {
|
|
533
707
|
if (this.#connected) this.#syncTimer();
|
|
534
708
|
}
|
|
535
|
-
/**
|
|
536
|
-
* Publishes the rotation state on a label half that joins a connected toggle.
|
|
537
|
-
*
|
|
538
|
-
* A half arriving changes the toggle's subtree, not its attributes, so the
|
|
539
|
-
* attribute observer never sees it; where the arrival completes a pair that was
|
|
540
|
-
* standing alone, this is what makes that pair whole and reflectable.
|
|
541
|
-
*/
|
|
542
709
|
onLabelTargetConnected() {
|
|
543
710
|
if (this.#connected) this.#syncTimer();
|
|
544
711
|
}
|
|
545
|
-
/** Publishes the rotation state on a label half that joins a connected toggle. */
|
|
546
712
|
offLabelTargetConnected() {
|
|
547
713
|
if (this.#connected) this.#syncTimer();
|
|
548
714
|
}
|
|
549
|
-
/** Follows a rotation intent the page changed at runtime, lifting the reduced-motion hold. */
|
|
550
715
|
autoplayValueChanged() {
|
|
551
716
|
if (!this.#connected) return;
|
|
552
717
|
this.#motionHold = false;
|
|
553
718
|
this.#syncTimer();
|
|
554
719
|
}
|
|
555
|
-
/** Re-arms the live interval at the new delay without reporting a state change. */
|
|
556
720
|
intervalValueChanged() {
|
|
557
721
|
if (this.#connected) this.#syncTimer();
|
|
558
722
|
}
|
|
559
|
-
/**
|
|
560
|
-
* Re-publishes the step controls and re-evaluates the non-looping end through the
|
|
561
|
-
* reconcile pass, which reports a move only when the position changed.
|
|
562
|
-
*/
|
|
563
723
|
loopValueChanged() {
|
|
564
724
|
this.#reconcileTargets.schedule();
|
|
565
725
|
}
|
|
566
|
-
/** Advances to the next slide. Delegated; `data-action` wiring is optional. */
|
|
567
726
|
next(event) {
|
|
568
727
|
if (event?.defaultPrevented) return;
|
|
569
728
|
this.#markHandled(event);
|
|
570
|
-
this.#select(this.#step(1), { focus: false });
|
|
729
|
+
this.#select(this.#step(1), { focus: false }, stateReasonFor(event));
|
|
571
730
|
}
|
|
572
|
-
/** Returns to the previous slide. Delegated; `data-action` wiring is optional. */
|
|
573
731
|
prev(event) {
|
|
574
732
|
if (event?.defaultPrevented) return;
|
|
575
733
|
this.#markHandled(event);
|
|
576
|
-
this.#select(this.#step(-1), { focus: false });
|
|
734
|
+
this.#select(this.#step(-1), { focus: false }, stateReasonFor(event));
|
|
735
|
+
}
|
|
736
|
+
goto(source) {
|
|
737
|
+
const { event, host, origin, reason } = actionSource(source);
|
|
738
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
739
|
+
return;
|
|
740
|
+
if (event?.defaultPrevented) return;
|
|
741
|
+
const picker = host?.closest(targetSelector(this.identifier, "picker"));
|
|
742
|
+
if (!picker || !this.pickerTargets.includes(picker)) return;
|
|
743
|
+
this.#markHandled(event ?? void 0);
|
|
744
|
+
this.#select(this.pickerTargets.indexOf(picker), { focus: false }, reason);
|
|
577
745
|
}
|
|
578
|
-
/** Jumps to the slide whose picker was activated (click / Enter / Space). */
|
|
579
|
-
goto(event) {
|
|
580
|
-
if (event.defaultPrevented) return;
|
|
581
|
-
this.#markHandled(event);
|
|
582
|
-
const index = this.#pickerIndexFor(event.currentTarget);
|
|
583
|
-
if (index !== -1) this.#select(index, { focus: false });
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* Flips the rotation intent the toggle shows, on the user's explicit request.
|
|
587
|
-
*
|
|
588
|
-
* The result is written to the `autoplay` Value, which is where every other path
|
|
589
|
-
* reads it from. A carousel with nothing to advance to has no intent to flip: the
|
|
590
|
-
* toggle is marked `aria-disabled` and does nothing.
|
|
591
|
-
*/
|
|
592
746
|
togglePlay(event) {
|
|
593
747
|
if (event?.defaultPrevented) return;
|
|
594
748
|
this.#markHandled(event);
|
|
595
749
|
this.#togglePlay();
|
|
596
750
|
}
|
|
597
|
-
/**
|
|
598
|
-
* Suspends rotation. A `focus`-family event records that focus is inside the
|
|
599
|
-
* carousel; anything else — hover, or a bare programmatic call — records the
|
|
600
|
-
* pointer suspension. Both lift through the matching {@link resume}, and
|
|
601
|
-
* neither touches the rotation intent.
|
|
602
|
-
*/
|
|
603
751
|
pause(event) {
|
|
604
752
|
this.#markHandled(event);
|
|
605
753
|
if (isFocusEvent(event)) this.#focusPaused = true;
|
|
606
754
|
else this.#setPointerPaused(true);
|
|
607
755
|
this.#syncTimer();
|
|
608
756
|
}
|
|
609
|
-
/**
|
|
610
|
-
* Lifts the matching suspension. A `focusout` whose `relatedTarget` is still
|
|
611
|
-
* inside the carousel is focus moving between its own controls, which leaves the
|
|
612
|
-
* focus suspension in place — releasing it there would stop and restart the
|
|
613
|
-
* interval on every Tab press.
|
|
614
|
-
*/
|
|
615
757
|
resume(event) {
|
|
616
758
|
this.#markHandled(event);
|
|
617
759
|
if (isFocusEvent(event)) {
|
|
@@ -622,14 +764,12 @@ var CarouselController = class extends Controller {
|
|
|
622
764
|
}
|
|
623
765
|
this.#syncTimer();
|
|
624
766
|
}
|
|
625
|
-
/** Picker roving for authored bindings; the delegated path is `#onKeydown`. */
|
|
626
767
|
onPickerKeydown(event) {
|
|
627
768
|
const index = this.#pickerIndexFor(event.currentTarget);
|
|
628
769
|
if (index === -1) return;
|
|
629
770
|
this.#markHandled(event);
|
|
630
771
|
this.#handlePickerKeydown(event, index);
|
|
631
772
|
}
|
|
632
|
-
/** Delegated activation for pickers and step controls without authored actions. */
|
|
633
773
|
#onClick = (event) => this.#delegate(event, () => {
|
|
634
774
|
const index = this.#pickerIndexFor(event.target);
|
|
635
775
|
if (index !== -1) {
|
|
@@ -642,91 +782,60 @@ var CarouselController = class extends Controller {
|
|
|
642
782
|
this.#togglePlay();
|
|
643
783
|
}
|
|
644
784
|
});
|
|
645
|
-
/** Delegated picker roving for pickers without authored actions. */
|
|
646
785
|
#onKeydown = (event) => this.#delegate(event, () => {
|
|
647
786
|
const index = this.#pickerIndexFor(event.target);
|
|
648
787
|
if (index !== -1) this.#handlePickerKeydown(event, index);
|
|
649
788
|
});
|
|
650
|
-
/** Focus arriving anywhere inside suspends the rotation. */
|
|
651
789
|
#onFocusin = (event) => this.#delegate(event, () => {
|
|
652
790
|
this.#focusPaused = true;
|
|
653
791
|
this.#syncTimer();
|
|
654
792
|
});
|
|
655
|
-
/** Focus genuinely leaving lifts the suspension; moves between own controls do not. */
|
|
656
793
|
#onFocusout = (event) => this.#delegate(event, () => {
|
|
657
794
|
if (this.#focusStaysInside(event)) return;
|
|
658
795
|
this.#focusPaused = false;
|
|
659
796
|
this.#syncTimer();
|
|
660
797
|
});
|
|
661
|
-
/** Pointer entry suspends the rotation. */
|
|
662
798
|
#onMouseenter = (event) => this.#delegate(event, () => {
|
|
663
799
|
this.#setPointerPaused(true);
|
|
664
800
|
this.#syncTimer();
|
|
665
801
|
});
|
|
666
|
-
/** Pointer exit lifts the suspension. */
|
|
667
802
|
#onMouseleave = (event) => this.#delegate(event, () => {
|
|
668
803
|
this.#setPointerPaused(false);
|
|
669
804
|
this.#syncTimer();
|
|
670
805
|
});
|
|
671
|
-
/**
|
|
672
|
-
* Reads `:hover` once the pointer has moved, when the answer is current, and lifts
|
|
673
|
-
* the pointer suspension if the pointer is not over the carousel. Listens once.
|
|
674
|
-
*/
|
|
675
806
|
#onPointerMove = () => {
|
|
676
807
|
this.#pointer.dispose();
|
|
677
808
|
if (this.element.matches(":hover")) return;
|
|
678
809
|
this.#pointerPaused = false;
|
|
679
810
|
this.#syncTimer();
|
|
680
811
|
};
|
|
681
|
-
/** Leaves the pointer suspension to be confirmed or lifted on the next pointer movement. */
|
|
682
812
|
#awaitPointer() {
|
|
683
813
|
this.#pointer.add(document, "pointermove", this.#onPointerMove, {
|
|
684
814
|
capture: true,
|
|
685
815
|
passive: true
|
|
686
816
|
});
|
|
687
817
|
}
|
|
688
|
-
/** Records the pointer suspension; a newer answer ends any wait on the next movement. */
|
|
689
818
|
#setPointerPaused(paused) {
|
|
690
819
|
this.#pointer.dispose();
|
|
691
820
|
this.#pointerPaused = paused;
|
|
692
821
|
}
|
|
693
|
-
/**
|
|
694
|
-
* Schedules one reconciliation when a state attribute is rewritten in place —
|
|
695
|
-
* the only shape of change no target callback reports.
|
|
696
|
-
*
|
|
697
|
-
* The filter is the `attributeFilter` alone: those three attributes belong to
|
|
698
|
-
* the slides and pickers, and a coalesced pass over an unrelated one costs a
|
|
699
|
-
* repaint that writes nothing. Narrowing further here would add a branch no
|
|
700
|
-
* test could distinguish.
|
|
701
|
-
*/
|
|
702
822
|
#onAttributeMutations = () => {
|
|
703
823
|
this.#reconcileTargets.schedule();
|
|
704
824
|
};
|
|
705
|
-
/**
|
|
706
|
-
* Records that an action binding took this event.
|
|
707
|
-
*
|
|
708
|
-
* An authored binding sits on the control, so it runs while the event is still
|
|
709
|
-
* below the controller element and always precedes the delegated listener.
|
|
710
|
-
* Marking is the one signal that keeps the two wirings from both acting.
|
|
711
|
-
*/
|
|
712
825
|
#markHandled(event) {
|
|
713
826
|
if (event) this.#handledEvents.add(event);
|
|
714
827
|
}
|
|
715
|
-
/** Runs a delegated handler unless an action binding, or a descendant, took the event. */
|
|
716
828
|
#delegate(event, run) {
|
|
717
829
|
if (this.#handledEvents.delete(event) || event.defaultPrevented) return;
|
|
718
830
|
run();
|
|
719
831
|
}
|
|
720
|
-
/** Position of the picker that is or contains `node`, or `-1` when none does. */
|
|
721
832
|
#pickerIndexFor(node) {
|
|
722
833
|
return ownerIndex(this.pickerTargets, node);
|
|
723
834
|
}
|
|
724
|
-
/** Whether a focus transition lands on another control of this same carousel. */
|
|
725
835
|
#focusStaysInside(event) {
|
|
726
836
|
const next = event.relatedTarget;
|
|
727
837
|
return next instanceof Node && this.element.contains(next);
|
|
728
838
|
}
|
|
729
|
-
/** Flips the rotation intent unless there is nothing to rotate to; lifts the motion hold. */
|
|
730
839
|
#togglePlay() {
|
|
731
840
|
if (!this.#canAutoplay()) return;
|
|
732
841
|
const next = !this.#intent;
|
|
@@ -734,7 +843,6 @@ var CarouselController = class extends Controller {
|
|
|
734
843
|
this.autoplayValue = next;
|
|
735
844
|
this.#syncTimer();
|
|
736
845
|
}
|
|
737
|
-
/** Applies the APG picker key map: arrows, Home, and End all move focus only. */
|
|
738
846
|
#handlePickerKeydown(event, current) {
|
|
739
847
|
if (event.defaultPrevented) return;
|
|
740
848
|
if (isReservedArrowChord(event)) return;
|
|
@@ -749,8 +857,6 @@ var CarouselController = class extends Controller {
|
|
|
749
857
|
this.#roving.setActive(rovingMove(current, length, step), { focus: true });
|
|
750
858
|
return;
|
|
751
859
|
}
|
|
752
|
-
// A chorded Home/End is a document-level shortcut (`Control+Home` scrolls
|
|
753
|
-
// the page) that belongs to the browser, exactly as a chorded arrow does.
|
|
754
860
|
case "Home":
|
|
755
861
|
if (hasModifier(event)) return;
|
|
756
862
|
event.preventDefault();
|
|
@@ -763,54 +869,30 @@ var CarouselController = class extends Controller {
|
|
|
763
869
|
return;
|
|
764
870
|
}
|
|
765
871
|
}
|
|
766
|
-
/**
|
|
767
|
-
* Resolves the index one step away from the current one, honoring `loop`.
|
|
768
|
-
*
|
|
769
|
-
* Bounding is {@link CarouselController.#clampToSlides}'s job alone, so an
|
|
770
|
-
* empty set is allowed to fall out of the arithmetic here rather than being
|
|
771
|
-
* special-cased in two places that could disagree.
|
|
772
|
-
*/
|
|
773
872
|
#step(delta) {
|
|
774
873
|
const total = this.slideTargets.length;
|
|
775
|
-
const next = this.#
|
|
874
|
+
const next = this.#resolveIndex() + delta;
|
|
776
875
|
if (this.loopValue) return (next + total) % total;
|
|
777
876
|
return Math.min(total - 1, Math.max(0, next));
|
|
778
877
|
}
|
|
779
|
-
/**
|
|
780
|
-
* Confines an index to the slide range, so no index can hide every slide.
|
|
781
|
-
*
|
|
782
|
-
* The single place an index is bounded: an empty set collapses to the first
|
|
783
|
-
* position, which is also where a non-numeric step from that empty set lands.
|
|
784
|
-
*/
|
|
785
878
|
#clampToSlides(index) {
|
|
786
879
|
const last = this.slideTargets.length - 1;
|
|
787
880
|
if (last < 0) return 0;
|
|
788
881
|
return Math.min(last, Math.max(0, index));
|
|
789
882
|
}
|
|
790
|
-
|
|
791
|
-
* Changes the active slide, updates state hooks, and emits `change` — but only
|
|
792
|
-
* when the index actually changes, so a `next`/`prev` clamped at the end (or an
|
|
793
|
-
* autoplay tick at a non-looping boundary) re-renders without a spurious event.
|
|
794
|
-
*/
|
|
795
|
-
#select(index, { focus }) {
|
|
883
|
+
#select(index, { focus }, reason = "user") {
|
|
796
884
|
const target = this.#clampToSlides(index);
|
|
797
|
-
const
|
|
885
|
+
const total = this.slideTargets.length;
|
|
886
|
+
const changed = target !== this.#index || total !== this.#total;
|
|
798
887
|
this.#index = target;
|
|
888
|
+
this.#total = total;
|
|
889
|
+
const token = changed ? this.#moves.record() : null;
|
|
799
890
|
this.#render({ focus });
|
|
800
891
|
this.#syncTimer();
|
|
801
|
-
if (
|
|
802
|
-
this.dispatch("change", { detail: { index: target, total
|
|
892
|
+
if (token !== null && this.#moves.isLatest(token)) {
|
|
893
|
+
this.dispatch("change", { detail: { index: target, total, reason } });
|
|
803
894
|
}
|
|
804
895
|
}
|
|
805
|
-
/**
|
|
806
|
-
* Reflects `this.#index` onto the slides, the pickers, and the step controls.
|
|
807
|
-
*
|
|
808
|
-
* Every observed attribute is written only when its value actually changes: the
|
|
809
|
-
* same writes are watched by {@link CarouselController.#onAttributeMutations},
|
|
810
|
-
* and an unconditional rewrite would feed the controller its own output.
|
|
811
|
-
*
|
|
812
|
-
* @stimeoRenderRoot
|
|
813
|
-
*/
|
|
814
896
|
#render({ focus }) {
|
|
815
897
|
this.#activeSlide = this.slideTargets[this.#index] ?? null;
|
|
816
898
|
this.slideTargets.forEach((slide, i) => {
|
|
@@ -823,10 +905,9 @@ var CarouselController = class extends Controller {
|
|
|
823
905
|
this.pickerTargets.forEach((picker, i) => {
|
|
824
906
|
setAttributeIfChanged(picker, "aria-selected", i === pickerIndex ? "true" : "false");
|
|
825
907
|
});
|
|
826
|
-
this.#roving.setActive(pickerIndex, { focus });
|
|
827
908
|
this.#syncStepControls();
|
|
909
|
+
this.#roving.setActive(pickerIndex, { focus });
|
|
828
910
|
}
|
|
829
|
-
/** Marks the step control a non-looping carousel has no slide left to reach. */
|
|
830
911
|
#syncStepControls() {
|
|
831
912
|
const last = this.slideTargets.length - 1;
|
|
832
913
|
const atStart = !this.loopValue && this.#index <= 0;
|
|
@@ -838,16 +919,6 @@ var CarouselController = class extends Controller {
|
|
|
838
919
|
this.#ariaDisabled.write(button, atEnd ? "true" : null);
|
|
839
920
|
}
|
|
840
921
|
}
|
|
841
|
-
/**
|
|
842
|
-
* Resolves which slide is current from the strongest evidence available.
|
|
843
|
-
*
|
|
844
|
-
* A single `data-state="active"` is unambiguous and wins, which is what restores
|
|
845
|
-
* the visible slide after a Turbo cache restore even with no pickers present.
|
|
846
|
-
* Competing claims — a slide inserted already marked active — are settled by the
|
|
847
|
-
* element the last render actually showed, so a newcomer never displaces what the
|
|
848
|
-
* reader is looking at. With no claim at all the authored picker selection
|
|
849
|
-
* decides, first in DOM order; failing that the previous position is kept.
|
|
850
|
-
*/
|
|
851
922
|
#resolveIndex() {
|
|
852
923
|
const slides = this.slideTargets;
|
|
853
924
|
const claims = [];
|
|
@@ -865,45 +936,30 @@ var CarouselController = class extends Controller {
|
|
|
865
936
|
);
|
|
866
937
|
return this.#clampToSlides(selected === -1 ? this.#index : selected);
|
|
867
938
|
}
|
|
868
|
-
/** Re-resolves the active slide after the target set changed and reports the move. */
|
|
869
939
|
#reconcileTargetSet() {
|
|
870
940
|
const previousIndex = this.#index;
|
|
871
941
|
const previousTotal = this.#total;
|
|
872
942
|
this.#index = this.#resolveIndex();
|
|
873
|
-
this.#render({ focus: false });
|
|
874
943
|
this.#total = this.slideTargets.length;
|
|
944
|
+
const changed = this.#index !== previousIndex || this.#total !== previousTotal;
|
|
945
|
+
const token = changed ? this.#moves.record() : null;
|
|
946
|
+
this.#render({ focus: false });
|
|
875
947
|
this.#syncTimer();
|
|
876
|
-
if (
|
|
948
|
+
if (token !== null && this.#moves.isLatest(token)) {
|
|
877
949
|
this.dispatch("reconcile", { detail: { index: this.#index, total: this.#total } });
|
|
878
950
|
}
|
|
879
951
|
}
|
|
880
|
-
/** The rotation intent in effect: `autoplay`, unless reduced motion holds it back. */
|
|
881
952
|
get #intent() {
|
|
882
953
|
return this.autoplayValue && !this.#motionHold;
|
|
883
954
|
}
|
|
884
|
-
/** Whether autoplay has anywhere left to advance to. */
|
|
885
955
|
#canAutoplay() {
|
|
886
956
|
const total = this.slideTargets.length;
|
|
887
957
|
if (total <= 1) return false;
|
|
888
958
|
return this.loopValue || this.#index < total - 1;
|
|
889
959
|
}
|
|
890
|
-
/** The advance delay; a non-finite or non-positive declaration falls back. */
|
|
891
960
|
get #interval() {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
/**
|
|
896
|
-
* Drives the autoplay interval toward the desired state and publishes it.
|
|
897
|
-
*
|
|
898
|
-
* Rotation runs when the intent is on, nothing suspends it, and a slide is left
|
|
899
|
-
* to advance to. The toggle shows the intent only while a slide is left, so
|
|
900
|
-
* `aria-pressed` never claims a rotation that cannot happen. This body reads
|
|
901
|
-
* `autoplay` and never writes it, so the declared intent returns once a slide
|
|
902
|
-
* lies ahead again. Crossing the run/stop boundary emits `play`/`pause`;
|
|
903
|
-
* re-arming at a new `interval` is the same state and stays silent.
|
|
904
|
-
*
|
|
905
|
-
* @stimeoRenderRoot
|
|
906
|
-
*/
|
|
961
|
+
return this.#safeInterval;
|
|
962
|
+
}
|
|
907
963
|
#syncTimer() {
|
|
908
964
|
const canAutoplay = this.#canAutoplay();
|
|
909
965
|
const pressed = this.#intent && canAutoplay;
|
|
@@ -916,15 +972,14 @@ var CarouselController = class extends Controller {
|
|
|
916
972
|
}
|
|
917
973
|
if (shouldRun && this.#timerId === null) {
|
|
918
974
|
this.#timerInterval = interval;
|
|
919
|
-
this.#timerId = this.#intervals.set(
|
|
975
|
+
this.#timerId = this.#intervals.set(
|
|
976
|
+
() => this.#select(this.#step(1), { focus: false }, "timeout"),
|
|
977
|
+
interval
|
|
978
|
+
);
|
|
920
979
|
}
|
|
921
980
|
setAttributeIfChanged(this.element, "data-state", shouldRun ? "playing" : "paused");
|
|
922
|
-
if (shouldRun !== wasRunning) {
|
|
923
|
-
if (shouldRun) this.dispatch("play");
|
|
924
|
-
else this.dispatch("pause");
|
|
925
|
-
}
|
|
926
981
|
for (const toggle of this.playToggleTargets) {
|
|
927
|
-
|
|
982
|
+
this.#ariaPressed.write(toggle, pressed ? "true" : "false");
|
|
928
983
|
this.#ariaDisabled.write(toggle, canAutoplay ? null : "true");
|
|
929
984
|
this.#labels.reflect(toggle, pressed);
|
|
930
985
|
}
|
|
@@ -932,12 +987,11 @@ var CarouselController = class extends Controller {
|
|
|
932
987
|
this.#ariaLive.write(this.viewportTarget, shouldRun ? "off" : "polite");
|
|
933
988
|
this.#ariaAtomic.write(this.viewportTarget, "false");
|
|
934
989
|
}
|
|
990
|
+
if (shouldRun !== wasRunning) {
|
|
991
|
+
if (shouldRun) this.dispatch("play");
|
|
992
|
+
else this.dispatch("pause");
|
|
993
|
+
}
|
|
935
994
|
}
|
|
936
|
-
/**
|
|
937
|
-
* Ends the rotation once the controller has really left. A carousel still in
|
|
938
|
-
* the document hears the end — `pause`, and `data-state="paused"` — so no `play`
|
|
939
|
-
* it reported is left without one; a detached tree keeps the markup it had.
|
|
940
|
-
*/
|
|
941
995
|
#endRun() {
|
|
942
996
|
const wasRunning = this.#timerId !== null;
|
|
943
997
|
this.#intervals.clearAll();
|
|
@@ -946,12 +1000,21 @@ var CarouselController = class extends Controller {
|
|
|
946
1000
|
setAttributeIfChanged(this.element, "data-state", "paused");
|
|
947
1001
|
this.dispatch("pause");
|
|
948
1002
|
}
|
|
949
|
-
/** Hands every leased attribute back to the value the consumer authored. */
|
|
950
1003
|
#returnLeases() {
|
|
951
1004
|
this.#ariaDisabled.returnAll();
|
|
1005
|
+
this.#ariaPressed.returnAll();
|
|
952
1006
|
this.#ariaLive.returnAll();
|
|
953
1007
|
this.#ariaAtomic.returnAll();
|
|
954
1008
|
}
|
|
1009
|
+
get #safeInterval() {
|
|
1010
|
+
return this.#numbers.read(
|
|
1011
|
+
this,
|
|
1012
|
+
"interval",
|
|
1013
|
+
this.intervalValue,
|
|
1014
|
+
_CarouselController.values.interval.default,
|
|
1015
|
+
_CarouselController.valueConstraints.interval
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
955
1018
|
};
|
|
956
1019
|
function isFocusEvent(event) {
|
|
957
1020
|
return event?.type.startsWith("focus") === true;
|
|
@@ -967,5 +1030,4 @@ function setAttributeIfChanged(element, name, value) {
|
|
|
967
1030
|
}
|
|
968
1031
|
|
|
969
1032
|
export { CarouselController };
|
|
970
|
-
//# sourceMappingURL=carousel_controller.js.map
|
|
971
1033
|
//# sourceMappingURL=carousel_controller.js.map
|