stimeo-ui 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +136 -0
- package/README.md +27 -11
- package/dist/cable/index.d.ts +88 -31
- package/dist/cable/index.js +384 -449
- package/dist/cable/index.js.map +1 -1
- package/dist/coerce-JtZVNG5h.d.ts +20 -0
- package/dist/controllers/accordion_controller.d.ts +4 -2
- package/dist/controllers/accordion_controller.js +174 -55
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +12 -2
- package/dist/controllers/alert_dialog_controller.js +903 -235
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +37 -5
- package/dist/controllers/announcer_controller.js +238 -205
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.js +0 -10
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +30 -6
- package/dist/controllers/auto_submit_controller.js +414 -145
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +185 -107
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +0 -130
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +21 -3
- package/dist/controllers/bulk_select_controller.js +282 -58
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +38 -16
- package/dist/controllers/calendar_controller.js +228 -260
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +43 -12
- package/dist/controllers/carousel_controller.js +536 -436
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +44 -7
- package/dist/controllers/character_counter_controller.js +493 -207
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +7 -2
- package/dist/controllers/checkbox_controller.js +38 -74
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +16 -5
- package/dist/controllers/clipboard_controller.js +261 -149
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +17 -5
- package/dist/controllers/collapsible_controller.js +304 -120
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +18 -6
- package/dist/controllers/color_picker_controller.js +178 -146
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +24 -6
- package/dist/controllers/combobox_controller.js +522 -156
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +51 -20
- package/dist/controllers/command_palette_controller.js +1156 -395
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +15 -2
- package/dist/controllers/conditional_fields_controller.js +300 -171
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +49 -12
- package/dist/controllers/confirm_controller.js +980 -262
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +80 -18
- package/dist/controllers/context_menu_controller.js +346 -153
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +13 -1
- package/dist/controllers/count_up_controller.js +73 -21
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +47 -3
- package/dist/controllers/countdown_controller.js +281 -158
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +93 -10
- package/dist/controllers/currency_input_controller.js +575 -225
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +25 -12
- package/dist/controllers/data_grid_controller.js +248 -214
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +42 -14
- package/dist/controllers/date_range_picker_controller.js +338 -235
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +17 -6
- package/dist/controllers/dialog_controller.js +899 -237
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +25 -8
- package/dist/controllers/direct_upload_controller.js +97 -167
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +6 -1
- package/dist/controllers/dirty_form_controller.js +72 -69
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +19 -0
- package/dist/controllers/dismissible_controller.js +196 -24
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +39 -11
- package/dist/controllers/drawer_controller.js +974 -355
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +31 -1
- package/dist/controllers/dropdown_controller.js +339 -96
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +315 -88
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +79 -58
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +63 -18
- package/dist/controllers/file_dropzone_controller.js +322 -211
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +77 -42
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +35 -12
- package/dist/controllers/flash_controller.js +238 -347
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +15 -6
- package/dist/controllers/focus_controller.js +679 -240
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +156 -132
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.d.ts +3 -3
- package/dist/controllers/form_validation_controller.js +31 -97
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +47 -14
- package/dist/controllers/frame_loading_controller.js +275 -217
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +27 -6
- package/dist/controllers/highlight_controller.js +107 -75
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +60 -4
- package/dist/controllers/hover_card_controller.js +327 -130
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +48 -6
- package/dist/controllers/idle_controller.js +271 -101
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +30 -13
- package/dist/controllers/input_mask_controller.js +483 -97
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +32 -13
- package/dist/controllers/intersection_controller.js +150 -106
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +19 -2
- package/dist/controllers/lazy_frame_controller.js +24 -68
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +44 -9
- package/dist/controllers/listbox_controller.js +523 -240
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +9 -7
- package/dist/controllers/local_time_controller.js +102 -77
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +11 -3
- package/dist/controllers/masonry_controller.js +80 -96
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +55 -11
- package/dist/controllers/menu_controller.js +302 -188
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +51 -9
- package/dist/controllers/menubar_controller.js +232 -345
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +33 -20
- package/dist/controllers/meter_controller.js +417 -87
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +48 -18
- package/dist/controllers/multi_select_controller.js +785 -393
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +27 -6
- package/dist/controllers/navigation_menu_controller.js +190 -250
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +10 -0
- package/dist/controllers/nested_form_controller.js +396 -192
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +23 -0
- package/dist/controllers/network_status_controller.js +268 -62
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +50 -8
- package/dist/controllers/number_input_controller.js +610 -303
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.d.ts +8 -5
- package/dist/controllers/optimistic_controller.js +45 -87
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +55 -8
- package/dist/controllers/otp_controller.js +658 -255
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
- package/dist/controllers/overflow_indicator_controller.js +122 -113
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +58 -20
- package/dist/controllers/overflow_menu_controller.js +470 -295
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +37 -7
- package/dist/controllers/pagination_controller.js +332 -123
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +46 -8
- package/dist/controllers/password_reveal_controller.js +392 -121
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +15 -6
- package/dist/controllers/password_strength_controller.js +111 -158
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +11 -4
- package/dist/controllers/persist_controller.js +165 -111
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +35 -12
- package/dist/controllers/pointer_drag_controller.js +434 -188
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +41 -5
- package/dist/controllers/popover_controller.js +269 -113
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +44 -4
- package/dist/controllers/portal_controller.js +215 -76
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +10 -4
- package/dist/controllers/preview_guard_controller.js +243 -116
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +20 -19
- package/dist/controllers/progress_controller.js +391 -78
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +21 -9
- package/dist/controllers/radio_group_controller.js +278 -211
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +47 -6
- package/dist/controllers/range_slider_controller.js +220 -113
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +20 -4
- package/dist/controllers/rating_controller.js +458 -211
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +2 -0
- package/dist/controllers/read_more_controller.js +138 -63
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +1 -1
- package/dist/controllers/reading_progress_controller.js +166 -137
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +20 -6
- package/dist/controllers/relative_time_controller.js +211 -112
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
- package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
- package/dist/controllers/reset_on_restore_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +31 -2
- package/dist/controllers/resizable_controller.js +299 -150
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +3 -3
- package/dist/controllers/roving_controller.js +49 -88
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -3
- package/dist/controllers/scroll_area_controller.js +490 -246
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +3 -3
- package/dist/controllers/scroll_restore_controller.js +14 -55
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
- package/dist/controllers/scroll_visibility_controller.js +432 -149
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +6 -1
- package/dist/controllers/scrollspy_controller.js +193 -248
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +20 -5
- package/dist/controllers/separator_controller.js +285 -142
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +65 -8
- package/dist/controllers/sidebar_controller.js +1069 -404
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +23 -0
- package/dist/controllers/skeleton_controller.js +259 -101
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +29 -2
- package/dist/controllers/slider_controller.js +187 -86
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +22 -4
- package/dist/controllers/smart_sticky_header_controller.js +173 -66
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.d.ts +4 -4
- package/dist/controllers/sortable_controller.js +26 -125
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +39 -4
- package/dist/controllers/spinner_controller.js +288 -173
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +9 -4
- package/dist/controllers/step_indicator_controller.js +105 -46
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +14 -6
- package/dist/controllers/stepper_controller.js +120 -73
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +9 -1
- package/dist/controllers/stick_to_bottom_controller.js +275 -140
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +11 -0
- package/dist/controllers/sticky_observer_controller.js +220 -49
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +24 -8
- package/dist/controllers/submit_once_controller.js +376 -227
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +18 -4
- package/dist/controllers/switch_controller.js +201 -84
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +9 -6
- package/dist/controllers/tabs_controller.js +41 -30
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +16 -3
- package/dist/controllers/tags_input_controller.js +293 -173
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +11 -1
- package/dist/controllers/textarea_autosize_controller.js +108 -74
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +24 -11
- package/dist/controllers/theme_controller.js +140 -116
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +55 -5
- package/dist/controllers/time_picker_controller.js +331 -127
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +40 -16
- package/dist/controllers/toast_controller.js +244 -324
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +14 -6
- package/dist/controllers/toggle_group_controller.js +252 -191
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +8 -6
- package/dist/controllers/toolbar_controller.js +219 -120
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +49 -4
- package/dist/controllers/tooltip_controller.js +158 -124
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +22 -4
- package/dist/controllers/transition_controller.js +123 -119
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +22 -12
- package/dist/controllers/tree_view_controller.js +283 -369
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +8573 -10410
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +38 -14
- package/dist/inspector/cli.js +108 -40
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +145 -64
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +20 -20
- package/dist/inspector/manifest.json +802 -101
- package/dist/positioning/index.d.ts +15 -7
- package/dist/positioning/index.js +154 -47
- package/dist/positioning/index.js.map +1 -1
- package/package.json +11 -10
- package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
- package/dist/controllers/reset_before_cache_controller.js.map +0 -1
|
@@ -1,121 +1,258 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function setAttributeIfChanged(element, name, value, beforeWrite) {
|
|
4
|
+
if (element.getAttribute(name) === value) return false;
|
|
5
|
+
if (value === null) element.removeAttribute(name);
|
|
6
|
+
else element.setAttribute(name, value);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
function sharedRegistry(key, create) {
|
|
11
|
+
const symbol = Symbol.for(key);
|
|
12
|
+
const scope = globalThis;
|
|
13
|
+
const existing = scope[symbol];
|
|
14
|
+
if (existing !== void 0) return existing;
|
|
15
|
+
const registry2 = create();
|
|
16
|
+
Object.defineProperty(globalThis, symbol, { value: registry2 });
|
|
17
|
+
return registry2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var registry = sharedRegistry(
|
|
21
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
22
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
23
|
+
);
|
|
24
|
+
function isAttributeValue(value) {
|
|
25
|
+
return value === null || typeof value === "string";
|
|
26
|
+
}
|
|
27
|
+
function readLeaseRecord(element, record) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
30
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
31
|
+
const [original, written, ...rest] = parsed;
|
|
32
|
+
const linked = rest[0] !== false;
|
|
33
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
34
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
35
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
36
|
+
} catch {
|
|
37
|
+
return void 0;
|
|
17
38
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
}
|
|
40
|
+
function attributeStack(element, attribute) {
|
|
41
|
+
const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
42
|
+
registry.stacks.set(element, byAttribute);
|
|
43
|
+
const known = byAttribute.get(attribute);
|
|
44
|
+
if (known) return known;
|
|
45
|
+
const found = [];
|
|
46
|
+
for (const record of registry.names.get(attribute) ?? []) {
|
|
47
|
+
const read = readLeaseRecord(element, record);
|
|
48
|
+
if (read) found.push({ ...read, record });
|
|
49
|
+
}
|
|
50
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
51
|
+
const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
52
|
+
lease: null,
|
|
53
|
+
record,
|
|
54
|
+
written,
|
|
55
|
+
original,
|
|
56
|
+
linked: linked && index > 0
|
|
57
|
+
}));
|
|
58
|
+
byAttribute.set(attribute, stack);
|
|
59
|
+
return stack;
|
|
60
|
+
}
|
|
61
|
+
function findHold(stack, lease, record) {
|
|
62
|
+
return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
|
|
63
|
+
}
|
|
64
|
+
var AttributeLease = class {
|
|
65
|
+
#attribute;
|
|
66
|
+
#record;
|
|
67
|
+
#base;
|
|
68
|
+
#preserveAuthor;
|
|
69
|
+
#held = /* @__PURE__ */ new Set();
|
|
70
|
+
constructor(attribute, owner, options = {}) {
|
|
71
|
+
this.#attribute = attribute;
|
|
72
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
73
|
+
this.#base = options.base === true;
|
|
74
|
+
this.#preserveAuthor = options.preserveAuthor === true;
|
|
75
|
+
const names = registry.names;
|
|
76
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
77
|
+
}
|
|
78
|
+
write(element, value) {
|
|
79
|
+
const stack = attributeStack(element, this.#attribute);
|
|
80
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
81
|
+
this.#held.add(element);
|
|
82
|
+
const shows = !this.#covered(element, stack, hold);
|
|
83
|
+
hold.written = value;
|
|
84
|
+
this.#mark(element, stack);
|
|
85
|
+
if (shows) this.#reflect(element, value);
|
|
86
|
+
}
|
|
87
|
+
return(element) {
|
|
88
|
+
const held = this.#held.delete(element);
|
|
89
|
+
const known = registry.stacks.get(element)?.has(this.#attribute);
|
|
90
|
+
const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
|
|
91
|
+
const hold = this.#take(stack);
|
|
92
|
+
if (!hold) return false;
|
|
93
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
94
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
95
|
+
const index = stack.indexOf(hold);
|
|
96
|
+
stack.splice(index, 1);
|
|
97
|
+
const above = stack[index];
|
|
98
|
+
if (above) above.linked &&= hold.linked;
|
|
99
|
+
this.#mark(element, stack);
|
|
100
|
+
if (!owns) return true;
|
|
101
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
102
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
returnAll() {
|
|
106
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
107
|
+
}
|
|
108
|
+
#take(stack) {
|
|
109
|
+
const hold = findHold(stack, this, this.#record);
|
|
110
|
+
if (hold) hold.lease = this;
|
|
111
|
+
return hold;
|
|
112
|
+
}
|
|
113
|
+
#acquire(element, stack) {
|
|
114
|
+
const current = element.getAttribute(this.#attribute);
|
|
115
|
+
const top = stack.at(-1);
|
|
116
|
+
const bottom = stack[0];
|
|
117
|
+
const over = top?.written === current ? top : void 0;
|
|
118
|
+
const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
|
|
119
|
+
const hold = {
|
|
120
|
+
lease: this,
|
|
121
|
+
record: this.#record,
|
|
122
|
+
written: current,
|
|
123
|
+
original: from ? from.original : current,
|
|
124
|
+
linked: !this.#base && over !== void 0
|
|
125
|
+
};
|
|
126
|
+
if (this.#base && bottom) {
|
|
127
|
+
bottom.linked = true;
|
|
128
|
+
stack.unshift(hold);
|
|
129
|
+
} else stack.push(hold);
|
|
130
|
+
return hold;
|
|
131
|
+
}
|
|
132
|
+
#covered(element, stack, hold) {
|
|
133
|
+
const current = element.getAttribute(this.#attribute);
|
|
134
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
135
|
+
}
|
|
136
|
+
#mark(element, stack) {
|
|
137
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
138
|
+
stack.forEach((hold, index) => {
|
|
139
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
140
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
141
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
142
|
+
records.set(
|
|
143
|
+
hold.record,
|
|
144
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
for (const [name, recorded] of records) {
|
|
148
|
+
setAttributeIfChanged(element, name, recorded);
|
|
24
149
|
}
|
|
25
150
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
151
|
+
#reflect(element, value) {
|
|
152
|
+
setAttributeIfChanged(element, this.#attribute, value);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
var DetachGate = class _DetachGate {
|
|
157
|
+
#pending = false;
|
|
158
|
+
get pending() {
|
|
159
|
+
return this.#pending;
|
|
160
|
+
}
|
|
161
|
+
static isDetached(host) {
|
|
162
|
+
return !host.element.isConnected || !_DetachGate.#listed(host);
|
|
163
|
+
}
|
|
164
|
+
disconnected(host, teardown) {
|
|
165
|
+
const carried = _DetachGate.#carried(host);
|
|
166
|
+
if (!carried && _DetachGate.isDetached(host)) {
|
|
167
|
+
this.#pending = false;
|
|
168
|
+
teardown();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.#pending = true;
|
|
172
|
+
const probe = () => {
|
|
173
|
+
if (!this.#pending) return;
|
|
174
|
+
this.#pending = false;
|
|
175
|
+
teardown();
|
|
176
|
+
};
|
|
177
|
+
if (carried) setTimeout(probe, 0);
|
|
178
|
+
else queueMicrotask(probe);
|
|
179
|
+
}
|
|
180
|
+
static #carried(host) {
|
|
181
|
+
const element = host.element;
|
|
182
|
+
return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
|
|
183
|
+
}
|
|
184
|
+
static #listed(host) {
|
|
185
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
186
|
+
return tokens.includes(host.identifier);
|
|
187
|
+
}
|
|
188
|
+
cancel() {
|
|
189
|
+
this.#pending = false;
|
|
31
190
|
}
|
|
32
191
|
};
|
|
33
192
|
|
|
34
|
-
// src/utils/escape_layer.ts
|
|
35
193
|
var EscapeLayer = class _EscapeLayer {
|
|
36
|
-
static #registries =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
#claims = null;
|
|
42
|
-
/**
|
|
43
|
-
* Activates this layer at the top of its document's Escape stack, installing
|
|
44
|
-
* the document's shared resolver listener if this is its first layer.
|
|
45
|
-
* Re-activating an already-active layer moves it to the top.
|
|
46
|
-
*/
|
|
194
|
+
static #registries = sharedRegistry(
|
|
195
|
+
"stimeo-ui.escape-layer.registry.v1",
|
|
196
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
197
|
+
);
|
|
198
|
+
#active = null;
|
|
47
199
|
activate(ownerDocument = document, options) {
|
|
48
200
|
this.deactivate();
|
|
49
|
-
let
|
|
50
|
-
if (!
|
|
51
|
-
|
|
52
|
-
_EscapeLayer.#registries.set(ownerDocument,
|
|
53
|
-
ownerDocument.addEventListener("keydown",
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.#
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Removes this layer from its document's Escape stack, uninstalling the
|
|
62
|
-
* shared listener when the stack empties. Safe to call when inactive.
|
|
63
|
-
*/
|
|
201
|
+
let registry2 = _EscapeLayer.#registries.get(ownerDocument);
|
|
202
|
+
if (!registry2) {
|
|
203
|
+
registry2 = _EscapeLayer.#createRegistry();
|
|
204
|
+
_EscapeLayer.#registries.set(ownerDocument, registry2);
|
|
205
|
+
ownerDocument.addEventListener("keydown", registry2.onKeydown);
|
|
206
|
+
}
|
|
207
|
+
const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
|
|
208
|
+
registry2.stack.push(entry);
|
|
209
|
+
this.#active = { document: ownerDocument, entry };
|
|
210
|
+
}
|
|
64
211
|
deactivate() {
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
const index =
|
|
70
|
-
if (index >= 0)
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
_EscapeLayer.#registries.delete(
|
|
212
|
+
const active = this.#active;
|
|
213
|
+
if (!active) return;
|
|
214
|
+
const registry2 = _EscapeLayer.#registries.get(active.document);
|
|
215
|
+
if (registry2) {
|
|
216
|
+
const index = registry2.stack.lastIndexOf(active.entry);
|
|
217
|
+
if (index >= 0) registry2.stack.splice(index, 1);
|
|
218
|
+
if (registry2.stack.length === 0) {
|
|
219
|
+
active.document.removeEventListener("keydown", registry2.onKeydown);
|
|
220
|
+
_EscapeLayer.#registries.delete(active.document);
|
|
74
221
|
}
|
|
75
222
|
}
|
|
76
|
-
this.#
|
|
77
|
-
this.#onDismiss = null;
|
|
78
|
-
this.#claims = null;
|
|
223
|
+
this.#active = null;
|
|
79
224
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Whether this active layer would own a press right now: it is the topmost
|
|
82
|
-
* layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
|
|
83
|
-
* and diagnostics — production dismissal goes through the shared listener.
|
|
84
|
-
*/
|
|
85
225
|
get ownsEscape() {
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
90
|
-
return _EscapeLayer.#resolveOwner(
|
|
226
|
+
const active = this.#active;
|
|
227
|
+
if (!active) return false;
|
|
228
|
+
const registry2 = _EscapeLayer.#registries.get(active.document);
|
|
229
|
+
if (!registry2) return false;
|
|
230
|
+
return _EscapeLayer.#resolveOwner(registry2.stack) === active.entry;
|
|
91
231
|
}
|
|
92
|
-
/** Builds a document's registry with its shared resolver listener. */
|
|
93
232
|
static #createRegistry() {
|
|
94
|
-
const
|
|
233
|
+
const registry2 = {
|
|
95
234
|
stack: [],
|
|
96
235
|
onKeydown: (event) => {
|
|
97
236
|
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
98
|
-
const owner = _EscapeLayer.#resolveOwner(
|
|
237
|
+
const owner = _EscapeLayer.#resolveOwner(registry2.stack);
|
|
99
238
|
if (!owner) return;
|
|
100
239
|
event.preventDefault();
|
|
101
|
-
owner
|
|
240
|
+
owner.onDismiss();
|
|
102
241
|
}
|
|
103
242
|
};
|
|
104
|
-
return
|
|
243
|
+
return registry2;
|
|
105
244
|
}
|
|
106
|
-
/** The topmost stack layer whose claims predicate passes, or `null`. */
|
|
107
245
|
static #resolveOwner(stack) {
|
|
108
246
|
for (let index = stack.length - 1; index >= 0; index--) {
|
|
109
247
|
const layer = stack[index];
|
|
110
248
|
if (!layer) continue;
|
|
111
|
-
if (layer
|
|
249
|
+
if (layer.claims && !layer.claims()) continue;
|
|
112
250
|
return layer;
|
|
113
251
|
}
|
|
114
252
|
return null;
|
|
115
253
|
}
|
|
116
254
|
};
|
|
117
255
|
|
|
118
|
-
// src/utils/focus_candidate.ts
|
|
119
256
|
function inheritsFieldsetDisabled(control) {
|
|
120
257
|
let fieldset = control.closest("fieldset[disabled]");
|
|
121
258
|
while (fieldset) {
|
|
@@ -125,8 +262,34 @@ function inheritsFieldsetDisabled(control) {
|
|
|
125
262
|
}
|
|
126
263
|
return false;
|
|
127
264
|
}
|
|
265
|
+
function unslotted(element) {
|
|
266
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
267
|
+
}
|
|
268
|
+
function flatTreeParent(element) {
|
|
269
|
+
if (unslotted(element)) return null;
|
|
270
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
271
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
272
|
+
return parent instanceof Element ? parent : null;
|
|
273
|
+
}
|
|
274
|
+
function flatTreeContains(ancestor, element) {
|
|
275
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
276
|
+
if (node === ancestor) return true;
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
function closestInFlatTree(element, selector) {
|
|
281
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
282
|
+
if (node.matches(selector)) return node;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
function flatTreeChildren(element) {
|
|
287
|
+
return ownedScope(element) ?? Array.from(element.children);
|
|
288
|
+
}
|
|
128
289
|
function canTakeFocus(element) {
|
|
129
|
-
|
|
290
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
291
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
292
|
+
}
|
|
130
293
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
131
294
|
if (!("disabled" in element)) return true;
|
|
132
295
|
if (element.disabled) return false;
|
|
@@ -183,190 +346,670 @@ function isTabStop(element) {
|
|
|
183
346
|
if (tabindex !== null) return tabindex >= 0;
|
|
184
347
|
return hasNativeTabStop(element) || hasEditableTabStop(element);
|
|
185
348
|
}
|
|
186
|
-
function
|
|
187
|
-
return
|
|
188
|
-
|
|
349
|
+
function isRadio(element) {
|
|
350
|
+
return element instanceof HTMLInputElement && element.type === "radio";
|
|
351
|
+
}
|
|
352
|
+
function sameRadioGroup(a, b) {
|
|
353
|
+
return a.name !== "" && a.name === b.name && a.form === b.form && a.getRootNode() === b.getRootNode();
|
|
354
|
+
}
|
|
355
|
+
function isInEditableContent(element) {
|
|
356
|
+
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
357
|
+
if (node.getAttribute("contenteditable")?.toLowerCase() === "false") return false;
|
|
358
|
+
if (hasEditableTabStop(node)) return true;
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
var XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
|
|
363
|
+
function isSvgLink(element) {
|
|
364
|
+
return element instanceof SVGElement && element.localName === "a" && (element.hasAttribute("href") || element.hasAttributeNS(XLINK_NAMESPACE, "href"));
|
|
365
|
+
}
|
|
366
|
+
function hasRenderedImage(area) {
|
|
367
|
+
const map = area.closest("map");
|
|
368
|
+
const key = map?.name || map?.id;
|
|
369
|
+
if (!map || !key) return false;
|
|
370
|
+
const root = map.getRootNode();
|
|
371
|
+
return Array.from(root.querySelectorAll("img[usemap]")).some(
|
|
372
|
+
(image) => image.getAttribute("usemap") === `#${key}` && isRenderedForFocus(image)
|
|
189
373
|
);
|
|
190
374
|
}
|
|
375
|
+
function isFocusableInOrder(element) {
|
|
376
|
+
const tabindex = authoredTabindex(element);
|
|
377
|
+
return (tabindex === null || tabindex >= 0) && canTakeFocus(element);
|
|
378
|
+
}
|
|
379
|
+
function extraStop(element) {
|
|
380
|
+
if (element instanceof HTMLAreaElement) {
|
|
381
|
+
const focusable = element.hasAttribute("href") || authoredTabindex(element) !== null;
|
|
382
|
+
return focusable && isFocusableInOrder(element) && hasRenderedImage(element);
|
|
383
|
+
}
|
|
384
|
+
if (element instanceof HTMLObjectElement) {
|
|
385
|
+
return isFocusableInOrder(element) && isRenderedForFocus(element) && element.contentWindow !== null;
|
|
386
|
+
}
|
|
387
|
+
if (isSvgLink(element))
|
|
388
|
+
return isFocusableInOrder(element) && isRenderedForFocus(element);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
function isSequentialStop(element) {
|
|
392
|
+
const stop = extraStop(element) ?? (element.matches(TAB_STOP_CANDIDATE_SELECTOR) && isTabStop(element));
|
|
393
|
+
if (!stop) return false;
|
|
394
|
+
const link = element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement || isSvgLink(element);
|
|
395
|
+
return !(link && authoredTabindex(element) === null && isInEditableContent(element));
|
|
396
|
+
}
|
|
397
|
+
function ownedScope(element) {
|
|
398
|
+
if (element.shadowRoot) return Array.from(element.shadowRoot.children);
|
|
399
|
+
if (element instanceof HTMLSlotElement) {
|
|
400
|
+
const assigned = element.assignedElements();
|
|
401
|
+
return assigned.length > 0 ? assigned : Array.from(element.children);
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
function orderScope(elements, walk) {
|
|
406
|
+
const entries = [];
|
|
407
|
+
const visit = (element) => {
|
|
408
|
+
if (element === walk.from) walk.fromAt = walk.tree.length;
|
|
409
|
+
const tabindex = authoredTabindex(element) ?? 0;
|
|
410
|
+
const stop = (element instanceof HTMLElement || element instanceof SVGElement) && isSequentialStop(element) ? element : null;
|
|
411
|
+
if (stop) walk.tree.push(stop);
|
|
412
|
+
const owned = ownedScope(element);
|
|
413
|
+
if (owned) {
|
|
414
|
+
if (tabindex >= 0) {
|
|
415
|
+
entries.push({ tabindex, stops: [...stop ? [stop] : [], ...orderScope(owned, walk)] });
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (stop) entries.push({ tabindex, stops: [stop] });
|
|
420
|
+
for (const child of Array.from(element.children)) visit(child);
|
|
421
|
+
};
|
|
422
|
+
for (const element of elements) visit(element);
|
|
423
|
+
const positive = entries.filter((entry) => entry.tabindex > 0);
|
|
424
|
+
positive.sort((a, b) => a.tabindex - b.tabindex);
|
|
425
|
+
const rest = entries.filter((entry) => entry.tabindex === 0);
|
|
426
|
+
return [...positive, ...rest].flatMap((entry) => entry.stops);
|
|
427
|
+
}
|
|
428
|
+
function passesRadioRule(element, tree, focusedRadio) {
|
|
429
|
+
if (!isRadio(element) || element.checked) return true;
|
|
430
|
+
const group = tree.filter(
|
|
431
|
+
(stop) => stop === element || isRadio(stop) && sameRadioGroup(stop, element)
|
|
432
|
+
);
|
|
433
|
+
if (group.some((radio) => radio.checked)) return false;
|
|
434
|
+
if (group[0] !== element) return false;
|
|
435
|
+
return focusedRadio === null || !sameRadioGroup(focusedRadio, element);
|
|
436
|
+
}
|
|
437
|
+
function startIndex(order, walk, backward, isStop) {
|
|
438
|
+
if (walk.fromAt < 0) return backward ? order.length - 1 : 0;
|
|
439
|
+
const at = order.indexOf(walk.from);
|
|
440
|
+
if (at >= 0) return backward ? at - 1 : at + 1;
|
|
441
|
+
const neighbour = backward ? walk.tree.slice(0, walk.fromAt).reverse().find(isStop) : walk.tree.slice(walk.fromAt).find(isStop);
|
|
442
|
+
if (neighbour) return order.indexOf(neighbour);
|
|
443
|
+
return backward ? order.length - 1 : 0;
|
|
444
|
+
}
|
|
445
|
+
function sequentialTabStops(root, from = null, backward = false) {
|
|
446
|
+
const walk = { from, tree: [], fromAt: from === root ? 0 : -1 };
|
|
447
|
+
const order = orderScope(ownedScope(root) ?? Array.from(root.children), walk);
|
|
448
|
+
const focusedRadio = walk.fromAt >= 0 && isRadio(from) ? from : null;
|
|
449
|
+
const isStop = (element) => passesRadioRule(element, walk.tree, focusedRadio);
|
|
450
|
+
const start = startIndex(order, walk, backward, isStop);
|
|
451
|
+
const visited = backward ? [...order.slice(0, start + 1).reverse(), ...order.slice(start + 1).reverse()] : [...order.slice(start), ...order.slice(0, start)];
|
|
452
|
+
return visited.filter((element) => element !== from && isStop(element));
|
|
453
|
+
}
|
|
454
|
+
function deepActiveElement(ownerDocument) {
|
|
455
|
+
let active = ownerDocument.activeElement;
|
|
456
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
457
|
+
return active;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
var MicrotaskCoalescer = class {
|
|
461
|
+
#run;
|
|
462
|
+
#queued = false;
|
|
463
|
+
#active = false;
|
|
464
|
+
#generation = 0;
|
|
465
|
+
constructor(run) {
|
|
466
|
+
this.#run = run;
|
|
467
|
+
}
|
|
468
|
+
activate() {
|
|
469
|
+
this.#active = true;
|
|
470
|
+
}
|
|
471
|
+
cancel() {
|
|
472
|
+
this.#active = false;
|
|
473
|
+
this.#queued = false;
|
|
474
|
+
this.#generation += 1;
|
|
475
|
+
}
|
|
476
|
+
get pending() {
|
|
477
|
+
return this.#queued;
|
|
478
|
+
}
|
|
479
|
+
schedule() {
|
|
480
|
+
if (!this.#active || this.#queued) return;
|
|
481
|
+
this.#queued = true;
|
|
482
|
+
const generation = this.#generation;
|
|
483
|
+
queueMicrotask(() => {
|
|
484
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
485
|
+
this.#queued = false;
|
|
486
|
+
this.#run();
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
function queryOpenDescendants(root, selector) {
|
|
492
|
+
const matches = [];
|
|
493
|
+
const roots = [root];
|
|
494
|
+
for (const scope of roots) {
|
|
495
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
496
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
497
|
+
const shadow = scope.shadowRoot;
|
|
498
|
+
if (shadow) roots.push(shadow);
|
|
499
|
+
}
|
|
500
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
501
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return matches;
|
|
505
|
+
}
|
|
191
506
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
#
|
|
202
|
-
|
|
507
|
+
function holdsFocus(element) {
|
|
508
|
+
return element.matches(":focus") || deepActiveElement(element.ownerDocument) === element;
|
|
509
|
+
}
|
|
510
|
+
var TabindexLoan = class _TabindexLoan {
|
|
511
|
+
static #registry = sharedRegistry(
|
|
512
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
513
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
514
|
+
);
|
|
515
|
+
#value;
|
|
516
|
+
#record;
|
|
517
|
+
#preserveFocus;
|
|
518
|
+
#lent = /* @__PURE__ */ new Set();
|
|
519
|
+
constructor(value, owner, options = {}) {
|
|
520
|
+
this.#value = value;
|
|
521
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
522
|
+
this.#preserveFocus = options.preserveFocus === true;
|
|
523
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
524
|
+
}
|
|
525
|
+
lend(element, options = {}) {
|
|
526
|
+
const stack = this.#stack(element);
|
|
527
|
+
const current = element.getAttribute("tabindex");
|
|
528
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
529
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
530
|
+
);
|
|
531
|
+
const linked = stack.at(-1)?.value === current;
|
|
532
|
+
if (!held && options.onlyIfUnfocusable && (current !== null || element.tabIndex >= 0)) return;
|
|
533
|
+
if (!held && current !== null && !linked) return;
|
|
534
|
+
this.#lent.add(element);
|
|
535
|
+
if (held) held.loan = this;
|
|
536
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
537
|
+
this.#mark(element, stack);
|
|
538
|
+
if (current === null || !held && current !== this.#value) {
|
|
539
|
+
element.setAttribute("tabindex", this.#value);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return(element) {
|
|
543
|
+
const stack = this.#stack(element);
|
|
544
|
+
const hold = stack.find((held) => held.loan === this) ?? stack.find(
|
|
545
|
+
(held) => held.loan === null && held.record === this.#record && held.value === this.#value
|
|
546
|
+
);
|
|
547
|
+
if (!hold) return false;
|
|
548
|
+
if (!this.#release(element, stack, hold, this.#preserveFocus)) {
|
|
549
|
+
hold.loan = this;
|
|
550
|
+
this.#lent.add(element);
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
this.#lent.delete(element);
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
returnAll() {
|
|
557
|
+
for (const element of Array.from(this.#lent)) this.return(element);
|
|
558
|
+
}
|
|
559
|
+
reclaim(element) {
|
|
560
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
561
|
+
const stack = this.#stack(element);
|
|
562
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
563
|
+
if (hold && !holdsFocus(element)) this.#release(element, stack, hold);
|
|
564
|
+
}
|
|
565
|
+
reclaimWithin(root) {
|
|
566
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
567
|
+
this.reclaim(element);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
#stack(element) {
|
|
571
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
572
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
573
|
+
stacks.set(element, stack);
|
|
574
|
+
return stack;
|
|
575
|
+
}
|
|
576
|
+
#seed(element) {
|
|
577
|
+
const current = element.getAttribute("tabindex");
|
|
578
|
+
const holds = [];
|
|
579
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
580
|
+
const value = element.getAttribute(record);
|
|
581
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
582
|
+
}
|
|
583
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
584
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
585
|
+
return holds;
|
|
586
|
+
}
|
|
587
|
+
#release(element, stack, hold, preserveFocus = false) {
|
|
588
|
+
const current = element.getAttribute("tabindex");
|
|
589
|
+
const index = stack.indexOf(hold);
|
|
590
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
591
|
+
if (owns && preserveFocus && holdsFocus(element)) return false;
|
|
592
|
+
stack.splice(index, 1);
|
|
593
|
+
const above = stack[index];
|
|
594
|
+
if (above) above.linked &&= hold.linked;
|
|
595
|
+
this.#mark(element, stack);
|
|
596
|
+
if (!owns) return true;
|
|
597
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
598
|
+
if (!below) element.removeAttribute("tabindex");
|
|
599
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
#mark(element, stack) {
|
|
603
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
604
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
605
|
+
for (const [name, recorded] of records) {
|
|
606
|
+
setAttributeIfChanged(element, name, recorded);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
var FOCUS_TRAP_OWNER = "stimeo-focus-trap";
|
|
612
|
+
var INERT_MARK = `data-${FOCUS_TRAP_OWNER}-inert`;
|
|
613
|
+
var OVERFLOW_MARK = `data-${FOCUS_TRAP_OWNER}-overflow`;
|
|
614
|
+
var FocusTrap = class _FocusTrap {
|
|
615
|
+
static #registries = sharedRegistry(
|
|
616
|
+
"stimeo-ui.focus-trap.registry.v2",
|
|
617
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
618
|
+
);
|
|
619
|
+
static #TEXT_ENTRY_TYPES = /* @__PURE__ */ new Set([
|
|
620
|
+
"text",
|
|
621
|
+
"search",
|
|
622
|
+
"email",
|
|
623
|
+
"url",
|
|
624
|
+
"tel",
|
|
625
|
+
"password",
|
|
626
|
+
"number"
|
|
627
|
+
]);
|
|
628
|
+
#openers = [];
|
|
629
|
+
#entry;
|
|
630
|
+
#tabindexLoans = /* @__PURE__ */ new Map();
|
|
203
631
|
#activeState = false;
|
|
204
|
-
/** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */
|
|
205
632
|
#escapeLayer = new EscapeLayer();
|
|
206
|
-
|
|
633
|
+
#gate = new DetachGate();
|
|
207
634
|
#getContainer;
|
|
208
|
-
/** Closing/focus hooks; see {@link FocusTrapOptions}. */
|
|
209
635
|
#options;
|
|
210
|
-
/**
|
|
211
|
-
* @param getContainer - Returns the trapped element. Called on every operation
|
|
212
|
-
* so the live target is always used.
|
|
213
|
-
* @param options - Closing/focus hooks; see {@link FocusTrapOptions}.
|
|
214
|
-
*/
|
|
215
636
|
constructor(getContainer, options = {}) {
|
|
216
637
|
this.#getContainer = getContainer;
|
|
217
638
|
this.#options = options;
|
|
639
|
+
this.#entry = {
|
|
640
|
+
container: () => this.#readContainer(),
|
|
641
|
+
moveFocus: (backward) => this.#moveFocus(backward),
|
|
642
|
+
refocus: () => this.#refocus(),
|
|
643
|
+
isolated: false,
|
|
644
|
+
lockedBody: null,
|
|
645
|
+
openedFrom: null,
|
|
646
|
+
handedBack: []
|
|
647
|
+
};
|
|
218
648
|
}
|
|
219
|
-
/** Whether the trap is currently active. */
|
|
220
649
|
get active() {
|
|
221
650
|
return this.#activeState;
|
|
222
651
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Applies the trap: records the current focus, optionally locks background scroll
|
|
225
|
-
* and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless
|
|
226
|
-
* `autoFocus` is off) moves focus inside. No-ops if already active.
|
|
227
|
-
*/
|
|
228
652
|
activate() {
|
|
229
653
|
if (this.#activeState) return;
|
|
654
|
+
_FocusTrap.#sweep();
|
|
230
655
|
this.#activeState = true;
|
|
231
|
-
|
|
232
|
-
this.#
|
|
233
|
-
if (this.#flag(this.#options.lockScroll, true))
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
|
|
239
|
-
document.addEventListener("keydown", this.#onKeydown);
|
|
240
|
-
this.#beforeCache.activate();
|
|
656
|
+
this.#openers = _FocusTrap.#focusPath();
|
|
657
|
+
this.#entry.openedFrom = _FocusTrap.#trapHolding(this.#openers[0] ?? null);
|
|
658
|
+
if (this.#flag(this.#options.lockScroll, true)) this.#lockScroll();
|
|
659
|
+
this.#entry.isolated = this.#flag(this.#options.isolate, true);
|
|
660
|
+
this.#joinStack();
|
|
661
|
+
_FocusTrap.#reconcileBackground(document);
|
|
241
662
|
const onEscape = this.#options.onEscape;
|
|
242
663
|
if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });
|
|
243
664
|
if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
|
|
244
665
|
}
|
|
245
|
-
/**
|
|
246
|
-
* Reverts every side effect applied by {@link activate}. No-ops if inactive, so
|
|
247
|
-
* a controller can call it defensively from both `close()` and `disconnect()`.
|
|
248
|
-
*
|
|
249
|
-
* @param restoreFocus - Move focus back to the opener (default `true`). Pass
|
|
250
|
-
* `false` on teardown (`disconnect`), where yanking focus is undesirable.
|
|
251
|
-
*/
|
|
252
666
|
deactivate({ restoreFocus = true } = {}) {
|
|
253
|
-
if (!this.#activeState)
|
|
667
|
+
if (!this.#activeState) {
|
|
668
|
+
this.#returnTabindexes();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
254
671
|
this.#activeState = false;
|
|
255
672
|
this.#escapeLayer.deactivate();
|
|
256
|
-
|
|
257
|
-
this.#
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
673
|
+
const above = this.#leaveStack();
|
|
674
|
+
this.#unlockScroll();
|
|
675
|
+
_FocusTrap.#reconcileBackground(document);
|
|
676
|
+
const wayBack = this.#handOver(above);
|
|
677
|
+
if (restoreFocus) this.#restore(wayBack, above);
|
|
678
|
+
this.#returnTabindexes();
|
|
679
|
+
}
|
|
680
|
+
connect() {
|
|
681
|
+
this.#gate.cancel();
|
|
682
|
+
_FocusTrap.#sweep();
|
|
683
|
+
new TabindexLoan("-1", FOCUS_TRAP_OWNER).reclaimWithin(document);
|
|
684
|
+
}
|
|
685
|
+
disconnect(host) {
|
|
686
|
+
if (this.#activeState) this.deactivate({ restoreFocus: false });
|
|
687
|
+
this.#gate.disconnected(host, () => this.#returnTabindexes());
|
|
688
|
+
}
|
|
689
|
+
refreshIsolation() {
|
|
690
|
+
if (!this.#activeState) return;
|
|
691
|
+
const isolate = this.#flag(this.#options.isolate, true);
|
|
692
|
+
if (isolate === this.#entry.isolated) return;
|
|
693
|
+
this.#entry.isolated = isolate;
|
|
694
|
+
const stranded = _FocusTrap.#reconcileBackground(document);
|
|
695
|
+
const active = deepActiveElement(document);
|
|
696
|
+
if (stranded.some((element) => flatTreeContains(element, active))) this.#focusInitial();
|
|
697
|
+
}
|
|
698
|
+
refreshContainer() {
|
|
699
|
+
if (!this.#activeState) return;
|
|
700
|
+
_FocusTrap.#reconcileBackground(document);
|
|
701
|
+
this.#returnTabindexes();
|
|
702
|
+
if (this.#ownsTab()) this.#refocus();
|
|
267
703
|
}
|
|
268
|
-
/** Resolves a boolean-or-getter option, defaulting when it was not provided. */
|
|
269
704
|
#flag(option, fallback) {
|
|
270
705
|
if (option === void 0) return fallback;
|
|
271
706
|
return typeof option === "function" ? option() : option;
|
|
272
707
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
* is left alone mid-navigation. The listener lives only while active.
|
|
280
|
-
*/
|
|
281
|
-
#beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));
|
|
282
|
-
/**
|
|
283
|
-
* Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the
|
|
284
|
-
* shared {@link EscapeLayer} resolver, so Tab trapping stays independent of
|
|
285
|
-
* which layer currently owns Escape.
|
|
286
|
-
*/
|
|
287
|
-
#onKeydown = (event) => {
|
|
288
|
-
if (event.key === "Tab") this.#trapTab(event);
|
|
289
|
-
};
|
|
290
|
-
/** Keeps `Tab` focus cycling within the container's focusable elements. */
|
|
291
|
-
#trapTab(event) {
|
|
292
|
-
const focusable = this.#focusableElements();
|
|
293
|
-
if (focusable.length === 0) {
|
|
294
|
-
event.preventDefault();
|
|
295
|
-
return;
|
|
708
|
+
#joinStack() {
|
|
709
|
+
const registry2 = _FocusTrap.#registry(document);
|
|
710
|
+
if (registry2.stack.length === 0) {
|
|
711
|
+
document.addEventListener("keydown", registry2.onKeydown);
|
|
712
|
+
document.addEventListener("turbo:morph", registry2.onMorph);
|
|
713
|
+
document.addEventListener("turbo:morph-element", registry2.onMorph);
|
|
296
714
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
715
|
+
registry2.stack.push(this.#entry);
|
|
716
|
+
}
|
|
717
|
+
#leaveStack() {
|
|
718
|
+
const registry2 = _FocusTrap.#registry(document);
|
|
719
|
+
const index = registry2.stack.indexOf(this.#entry);
|
|
720
|
+
const above = registry2.stack.slice(index + 1);
|
|
721
|
+
registry2.stack.splice(index, 1);
|
|
722
|
+
if (registry2.stack.length === 0) {
|
|
723
|
+
document.removeEventListener("keydown", registry2.onKeydown);
|
|
724
|
+
document.removeEventListener("turbo:morph", registry2.onMorph);
|
|
725
|
+
document.removeEventListener("turbo:morph-element", registry2.onMorph);
|
|
304
726
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
727
|
+
return above;
|
|
728
|
+
}
|
|
729
|
+
static #focusPath() {
|
|
730
|
+
const path = [];
|
|
731
|
+
for (let active = document.activeElement; active; active = active.shadowRoot?.activeElement ?? null) {
|
|
732
|
+
if (active instanceof HTMLElement && active !== document.body) path.unshift(active);
|
|
733
|
+
}
|
|
734
|
+
return path;
|
|
735
|
+
}
|
|
736
|
+
static #trapHolding(element) {
|
|
737
|
+
for (const trap of [..._FocusTrap.#registry(document).stack].reverse()) {
|
|
738
|
+
const container = trap.container();
|
|
739
|
+
if (container && flatTreeContains(container, element)) return trap;
|
|
740
|
+
}
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
#handOver(above) {
|
|
744
|
+
const entry = this.#entry;
|
|
745
|
+
const wayBack = [
|
|
746
|
+
...this.#openers,
|
|
747
|
+
this.#options.fallbackFocus?.() ?? null,
|
|
748
|
+
...entry.handedBack
|
|
749
|
+
].filter((element) => element !== null);
|
|
750
|
+
for (const trap of above) {
|
|
751
|
+
if (trap.openedFrom !== entry) continue;
|
|
752
|
+
trap.handedBack = [...trap.handedBack, ...wayBack];
|
|
753
|
+
trap.openedFrom = entry.openedFrom;
|
|
754
|
+
}
|
|
755
|
+
this.#openers = [];
|
|
756
|
+
entry.openedFrom = null;
|
|
757
|
+
entry.handedBack = [];
|
|
758
|
+
return wayBack;
|
|
759
|
+
}
|
|
760
|
+
#restore(wayBack, above) {
|
|
761
|
+
const active = deepActiveElement(document);
|
|
762
|
+
const released = this.#readContainer();
|
|
763
|
+
const holds = (trap) => {
|
|
764
|
+
const container = trap.container();
|
|
765
|
+
if (!container || !_FocusTrap.#takesTab(trap) || !flatTreeContains(container, active)) {
|
|
766
|
+
return false;
|
|
335
767
|
}
|
|
336
|
-
|
|
768
|
+
return !released || !flatTreeContains(released, active) || flatTreeContains(released, container);
|
|
769
|
+
};
|
|
770
|
+
if (above.some(holds)) return;
|
|
771
|
+
for (const element of wayBack) {
|
|
772
|
+
if (this.#activeState) return;
|
|
773
|
+
if (!element.isConnected || !canTakeFocus(element) || !isRenderedForFocus(element)) continue;
|
|
774
|
+
const before = deepActiveElement(document);
|
|
775
|
+
element.focus();
|
|
776
|
+
const after = deepActiveElement(document);
|
|
777
|
+
if (after === element || after !== before) return;
|
|
778
|
+
}
|
|
779
|
+
_FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack)?.refocus();
|
|
780
|
+
}
|
|
781
|
+
#refocus() {
|
|
782
|
+
if (!this.#flag(this.#options.autoFocus, true)) return;
|
|
783
|
+
if (flatTreeContains(this.#getContainer(), deepActiveElement(document))) return;
|
|
784
|
+
this.#focusInitial();
|
|
785
|
+
}
|
|
786
|
+
#lendTabindex(container) {
|
|
787
|
+
const loan = this.#tabindexLoans.get(container) ?? new TabindexLoan("-1", FOCUS_TRAP_OWNER);
|
|
788
|
+
loan.lend(container);
|
|
789
|
+
this.#tabindexLoans.set(container, loan);
|
|
790
|
+
}
|
|
791
|
+
#returnTabindexes() {
|
|
792
|
+
const focused = deepActiveElement(document);
|
|
793
|
+
for (const [element, loan] of [...this.#tabindexLoans]) {
|
|
794
|
+
if (element === focused) continue;
|
|
795
|
+
loan.returnAll();
|
|
796
|
+
this.#tabindexLoans.delete(element);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
#lockScroll() {
|
|
800
|
+
const registry2 = _FocusTrap.#registry(document);
|
|
801
|
+
const body = document.body;
|
|
802
|
+
if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
|
|
803
|
+
registry2.scrollLocks.add(this.#entry);
|
|
804
|
+
this.#entry.lockedBody = body;
|
|
805
|
+
body.style.overflow = "hidden";
|
|
806
|
+
}
|
|
807
|
+
#unlockScroll() {
|
|
808
|
+
const registry2 = _FocusTrap.#registry(document);
|
|
809
|
+
const body = this.#entry.lockedBody;
|
|
810
|
+
this.#entry.lockedBody = null;
|
|
811
|
+
if (!registry2.scrollLocks.delete(this.#entry) || !body) return;
|
|
812
|
+
if (!_FocusTrap.#isLocked(registry2, body)) _FocusTrap.#unlock(body);
|
|
813
|
+
}
|
|
814
|
+
static #isLocked(registry2, body) {
|
|
815
|
+
return [...registry2.scrollLocks].some((trap) => trap.lockedBody === body);
|
|
816
|
+
}
|
|
817
|
+
static #unlock(body) {
|
|
818
|
+
const saved = body.getAttribute(OVERFLOW_MARK);
|
|
819
|
+
if (saved === null) return;
|
|
820
|
+
body.removeAttribute(OVERFLOW_MARK);
|
|
821
|
+
body.style.overflow = saved;
|
|
822
|
+
}
|
|
823
|
+
static #sweep() {
|
|
824
|
+
const registry2 = _FocusTrap.#registry(document);
|
|
825
|
+
for (const element of queryOpenDescendants(document, `[${INERT_MARK}]`)) {
|
|
826
|
+
if (registry2.inerted.has(element)) continue;
|
|
827
|
+
element.removeAttribute(INERT_MARK);
|
|
828
|
+
element.inert = false;
|
|
337
829
|
}
|
|
830
|
+
if (!_FocusTrap.#isLocked(registry2, document.body)) _FocusTrap.#unlock(document.body);
|
|
831
|
+
}
|
|
832
|
+
static #registry(ownerDocument) {
|
|
833
|
+
const existing = _FocusTrap.#registries.get(ownerDocument);
|
|
834
|
+
if (existing) return existing;
|
|
835
|
+
const morphs = new MicrotaskCoalescer(() => _FocusTrap.#reapply(registry2));
|
|
836
|
+
morphs.activate();
|
|
837
|
+
const registry2 = {
|
|
838
|
+
stack: [],
|
|
839
|
+
onKeydown: (event) => _FocusTrap.#resolveTab(registry2.stack, event),
|
|
840
|
+
onMorph: () => morphs.schedule(),
|
|
841
|
+
inerted: /* @__PURE__ */ new Set(),
|
|
842
|
+
scrollLocks: /* @__PURE__ */ new Set()
|
|
843
|
+
};
|
|
844
|
+
_FocusTrap.#registries.set(ownerDocument, registry2);
|
|
845
|
+
return registry2;
|
|
338
846
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
847
|
+
static #reapply(registry2) {
|
|
848
|
+
_FocusTrap.#reconcileBackground(document);
|
|
849
|
+
const body = document.body;
|
|
850
|
+
if (!_FocusTrap.#isLocked(registry2, body)) return;
|
|
851
|
+
if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
|
|
852
|
+
body.style.overflow = "hidden";
|
|
853
|
+
}
|
|
854
|
+
static #reconcileBackground(ownerDocument) {
|
|
855
|
+
const registry2 = _FocusTrap.#registry(ownerDocument);
|
|
856
|
+
for (const element of registry2.inerted) {
|
|
857
|
+
if (!element.hasAttribute(INERT_MARK)) registry2.inerted.delete(element);
|
|
858
|
+
}
|
|
859
|
+
const background = _FocusTrap.#backgroundOf(registry2);
|
|
860
|
+
for (const element of registry2.inerted) {
|
|
861
|
+
if (background.has(element)) continue;
|
|
862
|
+
element.removeAttribute(INERT_MARK);
|
|
863
|
+
element.inert = false;
|
|
864
|
+
registry2.inerted.delete(element);
|
|
865
|
+
}
|
|
866
|
+
const added = [];
|
|
867
|
+
for (const element of background) {
|
|
868
|
+
if (registry2.inerted.has(element)) continue;
|
|
869
|
+
element.setAttribute(INERT_MARK, "");
|
|
870
|
+
element.inert = true;
|
|
871
|
+
registry2.inerted.add(element);
|
|
872
|
+
added.push(element);
|
|
873
|
+
}
|
|
874
|
+
return added;
|
|
875
|
+
}
|
|
876
|
+
static #backgroundOf(registry2) {
|
|
877
|
+
const containers = registry2.stack.map((trap) => trap.container());
|
|
878
|
+
let top = -1;
|
|
879
|
+
registry2.stack.forEach((trap, index) => {
|
|
880
|
+
if (trap.isolated && containers[index]) top = index;
|
|
881
|
+
});
|
|
882
|
+
const background = /* @__PURE__ */ new Set();
|
|
883
|
+
if (top < 0) return background;
|
|
884
|
+
const kept = /* @__PURE__ */ new Set();
|
|
885
|
+
const keep = (path) => {
|
|
886
|
+
for (const node of path) {
|
|
887
|
+
kept.add(node);
|
|
888
|
+
background.delete(node);
|
|
889
|
+
}
|
|
890
|
+
for (const node of path) {
|
|
891
|
+
const parent = flatTreeParent(node);
|
|
892
|
+
for (const sibling of parent ? flatTreeChildren(parent) : []) {
|
|
893
|
+
if (!(sibling instanceof HTMLElement) || kept.has(sibling)) continue;
|
|
894
|
+
if (sibling.inert && !registry2.inerted.has(sibling)) continue;
|
|
895
|
+
background.add(sibling);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
containers.slice(top).forEach((container, index) => {
|
|
900
|
+
const path = [];
|
|
901
|
+
for (let node = container; node && node !== document.body && !kept.has(node); ) {
|
|
902
|
+
path.push(node);
|
|
903
|
+
node = flatTreeParent(node);
|
|
904
|
+
}
|
|
905
|
+
if (index === 0 || path.some((node) => background.has(node))) keep(path);
|
|
906
|
+
});
|
|
907
|
+
return background;
|
|
908
|
+
}
|
|
909
|
+
#readContainer() {
|
|
910
|
+
try {
|
|
911
|
+
return this.#getContainer();
|
|
912
|
+
} catch {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
static #resolveTab(stack, event) {
|
|
917
|
+
if (event.key !== "Tab" || event.defaultPrevented) return;
|
|
918
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
919
|
+
if (event.ctrlKey || event.metaKey) return;
|
|
920
|
+
const owner = _FocusTrap.#tabOwner(stack);
|
|
921
|
+
if (!owner) return;
|
|
922
|
+
event.preventDefault();
|
|
923
|
+
owner.moveFocus(event.shiftKey);
|
|
924
|
+
}
|
|
925
|
+
static #tabOwner(stack) {
|
|
926
|
+
for (const trap of [...stack].reverse()) {
|
|
927
|
+
if (_FocusTrap.#takesTab(trap)) return trap;
|
|
928
|
+
}
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
static #takesTab(trap) {
|
|
932
|
+
const container = trap.container();
|
|
933
|
+
if (!container) return false;
|
|
934
|
+
return container.isConnected && !closestInFlatTree(container, "[hidden]") && isRenderedForFocus(container);
|
|
935
|
+
}
|
|
936
|
+
#ownsTab() {
|
|
937
|
+
return _FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack) === this.#entry;
|
|
938
|
+
}
|
|
939
|
+
#moveFocus(backward) {
|
|
940
|
+
const from = deepActiveElement(document);
|
|
941
|
+
for (const stop of sequentialTabStops(this.#getContainer(), from, backward)) {
|
|
942
|
+
if (!this.#ownsTab()) return;
|
|
943
|
+
const before = deepActiveElement(document);
|
|
944
|
+
stop.focus();
|
|
945
|
+
const after = deepActiveElement(document);
|
|
946
|
+
if (after === before) continue;
|
|
947
|
+
if (after === stop) _FocusTrap.#selectOnEntry(stop);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
static #selectOnEntry(stop) {
|
|
952
|
+
if (stop instanceof HTMLInputElement && _FocusTrap.#TEXT_ENTRY_TYPES.has(stop.type)) {
|
|
953
|
+
stop.select();
|
|
343
954
|
}
|
|
344
|
-
this.#inertedSiblings = [];
|
|
345
955
|
}
|
|
346
|
-
/** Moves focus to the initial target, the first focusable, or the container. */
|
|
347
956
|
#focusInitial() {
|
|
348
957
|
const preferred = this.#options.initialFocus?.();
|
|
349
958
|
if (preferred) {
|
|
350
959
|
preferred.focus();
|
|
351
960
|
return;
|
|
352
961
|
}
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
962
|
+
const container = this.#getContainer();
|
|
963
|
+
const [first] = sequentialTabStops(container);
|
|
964
|
+
if (first) {
|
|
965
|
+
first.focus();
|
|
356
966
|
return;
|
|
357
967
|
}
|
|
358
|
-
|
|
359
|
-
container.tabIndex = -1;
|
|
968
|
+
this.#lendTabindex(container);
|
|
360
969
|
container.focus();
|
|
361
970
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
var ListenerSet = class {
|
|
974
|
+
#abort = new AbortController();
|
|
975
|
+
add(target, type, handler, options) {
|
|
976
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
977
|
+
}
|
|
978
|
+
dispose() {
|
|
979
|
+
this.#abort.abort();
|
|
980
|
+
this.#abort = new AbortController();
|
|
365
981
|
}
|
|
366
982
|
};
|
|
367
983
|
|
|
368
|
-
|
|
369
|
-
|
|
984
|
+
var MorphRenderWatcher = class {
|
|
985
|
+
#listeners = new ListenerSet();
|
|
986
|
+
#pass;
|
|
987
|
+
#element = null;
|
|
988
|
+
#onMorph = () => this.#pass.schedule();
|
|
989
|
+
constructor(run) {
|
|
990
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
991
|
+
}
|
|
992
|
+
observe(element) {
|
|
993
|
+
if (this.#element === element) return;
|
|
994
|
+
this.disconnect();
|
|
995
|
+
this.#element = element;
|
|
996
|
+
this.#pass.activate();
|
|
997
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
998
|
+
}
|
|
999
|
+
schedule() {
|
|
1000
|
+
this.#pass.schedule();
|
|
1001
|
+
}
|
|
1002
|
+
get pending() {
|
|
1003
|
+
return this.#pass.pending;
|
|
1004
|
+
}
|
|
1005
|
+
disconnect() {
|
|
1006
|
+
this.#listeners.dispose();
|
|
1007
|
+
this.#pass.cancel();
|
|
1008
|
+
this.#element = null;
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
var ConfirmController = class _ConfirmController extends Controller {
|
|
370
1013
|
static targets = ["dialog", "title", "message", "confirm", "cancel"];
|
|
371
1014
|
static values = {
|
|
372
1015
|
confirmLabel: { type: String, default: "OK" },
|
|
@@ -375,112 +1018,187 @@ var ConfirmController = class extends Controller {
|
|
|
375
1018
|
};
|
|
376
1019
|
static actions = ["confirm", "cancel", "request"];
|
|
377
1020
|
static events = ["open", "resolve"];
|
|
378
|
-
/** Resolver for the in-flight confirmation Promise (one dialog at a time). */
|
|
379
1021
|
#pending = null;
|
|
380
|
-
/** Turbo's forms config and its original confirm method, for restore. */
|
|
381
1022
|
#turboForms = null;
|
|
382
1023
|
#previousConfirm = void 0;
|
|
1024
|
+
#continuing = null;
|
|
1025
|
+
#connected = false;
|
|
1026
|
+
#asked = 0;
|
|
1027
|
+
#dialog = null;
|
|
1028
|
+
#hidden = new AttributeLease("hidden", this.identifier);
|
|
1029
|
+
#wording = null;
|
|
383
1030
|
#trap = new FocusTrap(() => this.dialogTarget, {
|
|
384
1031
|
onEscape: () => this.#resolve(false),
|
|
385
1032
|
initialFocus: () => this.#initialFocusElement()
|
|
386
1033
|
});
|
|
1034
|
+
#morphRender = new MorphRenderWatcher(() => this.#repair());
|
|
387
1035
|
connect() {
|
|
388
|
-
|
|
1036
|
+
this.#trap.connect();
|
|
1037
|
+
this.#connected = true;
|
|
1038
|
+
if (this.hasDialogTarget) this.#hidden.write(this.dialogTarget, "");
|
|
1039
|
+
this.#dialog = this.hasDialogTarget ? this.dialogTarget : null;
|
|
389
1040
|
this.#installTurboHook();
|
|
1041
|
+
this.#morphRender.observe(this.element);
|
|
1042
|
+
}
|
|
1043
|
+
#repair() {
|
|
1044
|
+
if (!this.hasDialogTarget) return;
|
|
1045
|
+
this.#hidden.write(this.dialogTarget, this.#pending ? null : "");
|
|
1046
|
+
this.#applyWording();
|
|
390
1047
|
}
|
|
391
1048
|
disconnect() {
|
|
1049
|
+
this.#connected = false;
|
|
392
1050
|
if (this.#turboForms) {
|
|
393
1051
|
this.#turboForms.confirm = this.#previousConfirm;
|
|
394
1052
|
this.#turboForms = null;
|
|
395
1053
|
}
|
|
396
1054
|
this.#resolve(false, false);
|
|
397
|
-
this.#trap.
|
|
1055
|
+
this.#trap.disconnect(this);
|
|
1056
|
+
this.#morphRender.disconnect();
|
|
1057
|
+
}
|
|
1058
|
+
dialogTargetConnected() {
|
|
1059
|
+
if (this.#connected) this.#adoptDialog();
|
|
1060
|
+
}
|
|
1061
|
+
dialogTargetDisconnected(dialog) {
|
|
1062
|
+
if (this.dialogTargets.includes(dialog)) return;
|
|
1063
|
+
this.#hidden.return(dialog);
|
|
1064
|
+
if (this.#connected) this.#adoptDialog();
|
|
1065
|
+
}
|
|
1066
|
+
messageTargetConnected() {
|
|
1067
|
+
this.#applyWording();
|
|
1068
|
+
}
|
|
1069
|
+
messageTargetDisconnected() {
|
|
1070
|
+
this.#applyWording();
|
|
1071
|
+
}
|
|
1072
|
+
confirmTargetConnected() {
|
|
1073
|
+
this.#applyWording();
|
|
1074
|
+
}
|
|
1075
|
+
confirmTargetDisconnected() {
|
|
1076
|
+
this.#applyWording();
|
|
1077
|
+
}
|
|
1078
|
+
cancelTargetConnected() {
|
|
1079
|
+
this.#applyWording();
|
|
1080
|
+
}
|
|
1081
|
+
cancelTargetDisconnected() {
|
|
1082
|
+
this.#applyWording();
|
|
398
1083
|
}
|
|
399
|
-
/** Confirms the pending request (resolves `true`). Bound via `data-action`. */
|
|
400
1084
|
confirm() {
|
|
401
1085
|
this.#resolve(true);
|
|
402
1086
|
}
|
|
403
|
-
/** Cancels the pending request (resolves `false`). Bound via `data-action`. */
|
|
404
1087
|
cancel() {
|
|
405
1088
|
this.#resolve(false);
|
|
406
1089
|
}
|
|
407
|
-
/**
|
|
408
|
-
* Intercepts a link/button click, shows the confirm dialog, and continues the
|
|
409
|
-
* original action (form submit or navigation) only when confirmed. The message
|
|
410
|
-
* comes from the `message` action param or the element's `data-turbo-confirm`.
|
|
411
|
-
*/
|
|
412
1090
|
request(event) {
|
|
413
1091
|
const element = event.currentTarget ?? event.target;
|
|
414
|
-
if (!element) return;
|
|
1092
|
+
if (!element || element === this.#continuing) return;
|
|
415
1093
|
event.preventDefault();
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
void this.#prompt(message).then((confirmed) => {
|
|
420
|
-
if (confirmed) this.#continue(element);
|
|
1094
|
+
const submitter = event.submitter ?? null;
|
|
1095
|
+
void this.#prompt(this.#messageOf(element, submitter)).then((confirmed) => {
|
|
1096
|
+
if (confirmed) this.#continue(element, submitter);
|
|
421
1097
|
});
|
|
422
1098
|
}
|
|
423
|
-
/**
|
|
424
|
-
* Opens the dialog for `message` and resolves once the user confirms or cancels.
|
|
425
|
-
* Degrades to native `window.confirm` when no dialog target is present.
|
|
426
|
-
*
|
|
427
|
-
* @stimeoRuntimeOnly The labels word the one dialog this call opens; every opening writes both
|
|
428
|
-
* labels again, and the dialog stays hidden between prompts.
|
|
429
|
-
*/
|
|
430
1099
|
#prompt(message) {
|
|
1100
|
+
if (!this.#connected) return Promise.resolve(false);
|
|
431
1101
|
if (!this.hasDialogTarget) return Promise.resolve(window.confirm(message));
|
|
1102
|
+
const asked = ++this.#asked;
|
|
432
1103
|
this.#resolve(false);
|
|
433
|
-
if (this
|
|
434
|
-
|
|
435
|
-
|
|
1104
|
+
if (asked !== this.#asked) return Promise.resolve(false);
|
|
1105
|
+
this.#wording = { message, confirm: this.confirmLabelValue, cancel: this.cancelLabelValue };
|
|
1106
|
+
this.#applyWording();
|
|
436
1107
|
return new Promise((resolve) => {
|
|
437
1108
|
this.#pending = resolve;
|
|
438
|
-
this.dialogTarget
|
|
1109
|
+
this.#hidden.write(this.dialogTarget, null);
|
|
439
1110
|
this.dispatch("open", { detail: { message } });
|
|
1111
|
+
if (this.#pending !== resolve) return;
|
|
440
1112
|
this.#trap.activate();
|
|
441
1113
|
});
|
|
442
1114
|
}
|
|
443
|
-
/**
|
|
444
|
-
* Settles the pending Promise, closes the dialog, and emits `resolve`.
|
|
445
|
-
* `restoreFocus` is forwarded to the trap so teardown (disconnect) can settle a
|
|
446
|
-
* pending confirmation without moving focus.
|
|
447
|
-
*/
|
|
448
1115
|
#resolve(confirmed, restoreFocus = true) {
|
|
449
1116
|
const pending = this.#pending;
|
|
450
1117
|
if (!pending) return;
|
|
451
1118
|
this.#pending = null;
|
|
452
|
-
|
|
1119
|
+
this.#wording = null;
|
|
1120
|
+
if (this.hasDialogTarget) this.#hidden.write(this.dialogTarget, "");
|
|
453
1121
|
this.#trap.deactivate({ restoreFocus });
|
|
454
1122
|
this.dispatch("resolve", { detail: { confirmed } });
|
|
455
1123
|
pending(confirmed);
|
|
456
1124
|
}
|
|
457
|
-
|
|
458
|
-
|
|
1125
|
+
#applyWording() {
|
|
1126
|
+
const wording = this.#wording;
|
|
1127
|
+
if (!wording) return;
|
|
1128
|
+
this.#word(this.messageTargets, wording.message);
|
|
1129
|
+
this.#word(this.confirmTargets, wording.confirm);
|
|
1130
|
+
this.#word(this.cancelTargets, wording.cancel);
|
|
1131
|
+
}
|
|
1132
|
+
#word(targets, text) {
|
|
1133
|
+
const slot = this.#inDialog(targets);
|
|
1134
|
+
if (slot) slot.textContent = text;
|
|
1135
|
+
}
|
|
1136
|
+
#inDialog(targets) {
|
|
1137
|
+
if (!this.hasDialogTarget) return null;
|
|
1138
|
+
const dialog = this.dialogTarget;
|
|
1139
|
+
return targets.find((element) => dialog.contains(element)) ?? null;
|
|
1140
|
+
}
|
|
1141
|
+
#adoptDialog() {
|
|
1142
|
+
const dialog = this.hasDialogTarget ? this.dialogTarget : null;
|
|
1143
|
+
if (dialog === this.#dialog) return;
|
|
1144
|
+
this.#dialog = dialog;
|
|
1145
|
+
if (!this.#pending) {
|
|
1146
|
+
if (dialog) this.#hidden.write(dialog, "");
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (!dialog) {
|
|
1150
|
+
this.#resolve(false);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
this.#hidden.write(dialog, null);
|
|
1154
|
+
this.#applyWording();
|
|
1155
|
+
this.#trap.refreshContainer();
|
|
1156
|
+
}
|
|
1157
|
+
#messageOf(element, submitter) {
|
|
1158
|
+
return element.getAttribute(`data-${this.identifier}-message-param`) || (submitter ?? element).getAttribute("data-turbo-confirm") || _ConfirmController.#formOf(element)?.getAttribute("data-turbo-confirm") || "";
|
|
1159
|
+
}
|
|
1160
|
+
#continue(element, submitter) {
|
|
459
1161
|
if (element instanceof HTMLAnchorElement && element.href) {
|
|
460
1162
|
window.location.href = element.href;
|
|
461
1163
|
return;
|
|
462
1164
|
}
|
|
463
|
-
const form = element
|
|
464
|
-
if (form)
|
|
465
|
-
|
|
1165
|
+
const form = _ConfirmController.#formOf(element);
|
|
1166
|
+
if (form) this.#submit(form, element === form ? submitter : element);
|
|
1167
|
+
}
|
|
1168
|
+
static #formOf(element) {
|
|
1169
|
+
if (element instanceof HTMLFormElement) return element;
|
|
1170
|
+
return _ConfirmController.#isSubmitButton(element) ? element.form : null;
|
|
1171
|
+
}
|
|
1172
|
+
#submit(form, submitter) {
|
|
1173
|
+
const button = _ConfirmController.#isSubmitButton(submitter) && submitter.form === form ? submitter : void 0;
|
|
1174
|
+
this.#continuing = form;
|
|
1175
|
+
try {
|
|
1176
|
+
form.requestSubmit(button);
|
|
1177
|
+
} finally {
|
|
1178
|
+
this.#continuing = null;
|
|
466
1179
|
}
|
|
467
1180
|
}
|
|
468
|
-
|
|
1181
|
+
static #isSubmitButton(element) {
|
|
1182
|
+
if (element instanceof HTMLButtonElement) return element.type === "submit";
|
|
1183
|
+
return element instanceof HTMLInputElement && (element.type === "submit" || element.type === "image");
|
|
1184
|
+
}
|
|
469
1185
|
#installTurboHook() {
|
|
470
1186
|
const turbo = window.Turbo;
|
|
471
1187
|
const forms = turbo?.config?.forms;
|
|
472
1188
|
if (!forms) return;
|
|
473
1189
|
this.#turboForms = forms;
|
|
474
1190
|
this.#previousConfirm = forms.confirm;
|
|
475
|
-
forms.confirm = (message) => this.#
|
|
1191
|
+
forms.confirm = (message, form) => this.#askForTurbo(message, form);
|
|
1192
|
+
}
|
|
1193
|
+
#askForTurbo(message, form) {
|
|
1194
|
+
if (this.#continuing && form === this.#continuing) return Promise.resolve(true);
|
|
1195
|
+
return this.#prompt(message);
|
|
476
1196
|
}
|
|
477
|
-
/** The button to focus on open — the least-destructive one (cancel) by default. */
|
|
478
1197
|
#initialFocusElement() {
|
|
479
|
-
|
|
480
|
-
return
|
|
1198
|
+
const confirm = this.initialFocusValue === "confirm" ? this.#inDialog(this.confirmTargets) : null;
|
|
1199
|
+
return confirm ?? this.#inDialog(this.cancelTargets);
|
|
481
1200
|
}
|
|
482
1201
|
};
|
|
483
1202
|
|
|
484
1203
|
export { ConfirmController };
|
|
485
|
-
//# sourceMappingURL=confirm_controller.js.map
|
|
486
1204
|
//# sourceMappingURL=confirm_controller.js.map
|