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,8 +1,34 @@
|
|
|
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
|
+
}
|
|
24
|
+
|
|
25
|
+
function fillTemplate(template, values) {
|
|
26
|
+
return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
|
|
27
|
+
const replacement = Object.hasOwn(values, name) ? values[name] : void 0;
|
|
28
|
+
return replacement === void 0 ? match : String(replacement);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
4
31
|
|
|
5
|
-
// src/utils/announce.ts
|
|
6
32
|
function announce(message, options = {}) {
|
|
7
33
|
const text = message.trim();
|
|
8
34
|
if (text.length === 0) return;
|
|
@@ -12,20 +38,23 @@ function announce(message, options = {}) {
|
|
|
12
38
|
})
|
|
13
39
|
);
|
|
14
40
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
41
|
+
|
|
42
|
+
function sharedRegistry(key, create) {
|
|
43
|
+
const symbol = Symbol.for(key);
|
|
44
|
+
const scope = globalThis;
|
|
45
|
+
const existing = scope[symbol];
|
|
46
|
+
if (existing !== void 0) return existing;
|
|
47
|
+
const registry2 = create();
|
|
48
|
+
Object.defineProperty(globalThis, symbol, { value: registry2 });
|
|
49
|
+
return registry2;
|
|
20
50
|
}
|
|
21
51
|
|
|
22
|
-
|
|
23
|
-
var counter = 0;
|
|
52
|
+
var ids = sharedRegistry("stimeo-ui.aria-ids.registry.v1", () => ({ counter: 0 }));
|
|
24
53
|
function uniqueId(prefix = "stimeo") {
|
|
25
54
|
let candidate;
|
|
26
55
|
do {
|
|
27
|
-
counter += 1;
|
|
28
|
-
candidate = `${prefix}-${counter}`;
|
|
56
|
+
ids.counter += 1;
|
|
57
|
+
candidate = `${prefix}-${ids.counter}`;
|
|
29
58
|
} while (typeof document !== "undefined" && document.getElementById(candidate) !== null);
|
|
30
59
|
return candidate;
|
|
31
60
|
}
|
|
@@ -36,12 +65,10 @@ function ensureId(element, prefix = "stimeo") {
|
|
|
36
65
|
return id;
|
|
37
66
|
}
|
|
38
67
|
|
|
39
|
-
// src/utils/logical_scroll.ts
|
|
40
68
|
function isRtl(element) {
|
|
41
69
|
return window.getComputedStyle(element).direction === "rtl";
|
|
42
70
|
}
|
|
43
71
|
|
|
44
|
-
// src/utils/arrow_step.ts
|
|
45
72
|
function logicalArrowKey(key, element) {
|
|
46
73
|
if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
|
|
47
74
|
if (!isRtl(element)) return key;
|
|
@@ -51,8 +78,153 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
51
78
|
if (!event.key.startsWith("Arrow")) return false;
|
|
52
79
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
53
80
|
}
|
|
81
|
+
function hasModifierChord(event) {
|
|
82
|
+
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setAttributeIfChanged(element, name, value, beforeWrite) {
|
|
86
|
+
if (element.getAttribute(name) === value) return false;
|
|
87
|
+
if (value === null) element.removeAttribute(name);
|
|
88
|
+
else element.setAttribute(name, value);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var registry = sharedRegistry(
|
|
93
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
94
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
95
|
+
);
|
|
96
|
+
function isAttributeValue(value) {
|
|
97
|
+
return value === null || typeof value === "string";
|
|
98
|
+
}
|
|
99
|
+
function readLeaseRecord(element, record) {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
102
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
103
|
+
const [original, written, ...rest] = parsed;
|
|
104
|
+
const linked = rest[0] !== false;
|
|
105
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
106
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
107
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
108
|
+
} catch {
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function attributeStack(element, attribute) {
|
|
113
|
+
const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
114
|
+
registry.stacks.set(element, byAttribute);
|
|
115
|
+
const known = byAttribute.get(attribute);
|
|
116
|
+
if (known) return known;
|
|
117
|
+
const found = [];
|
|
118
|
+
for (const record of registry.names.get(attribute) ?? []) {
|
|
119
|
+
const read = readLeaseRecord(element, record);
|
|
120
|
+
if (read) found.push({ ...read, record });
|
|
121
|
+
}
|
|
122
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
123
|
+
const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
124
|
+
lease: null,
|
|
125
|
+
record,
|
|
126
|
+
written,
|
|
127
|
+
original,
|
|
128
|
+
linked: linked && index > 0
|
|
129
|
+
}));
|
|
130
|
+
byAttribute.set(attribute, stack);
|
|
131
|
+
return stack;
|
|
132
|
+
}
|
|
133
|
+
function findHold(stack, lease, record) {
|
|
134
|
+
return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
|
|
135
|
+
}
|
|
136
|
+
var AttributeLease = class {
|
|
137
|
+
#attribute;
|
|
138
|
+
#record;
|
|
139
|
+
#base;
|
|
140
|
+
#preserveAuthor;
|
|
141
|
+
#held = /* @__PURE__ */ new Set();
|
|
142
|
+
constructor(attribute, owner, options = {}) {
|
|
143
|
+
this.#attribute = attribute;
|
|
144
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
145
|
+
this.#base = options.base === true;
|
|
146
|
+
this.#preserveAuthor = options.preserveAuthor === true;
|
|
147
|
+
const names = registry.names;
|
|
148
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
149
|
+
}
|
|
150
|
+
write(element, value) {
|
|
151
|
+
const stack = attributeStack(element, this.#attribute);
|
|
152
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
153
|
+
this.#held.add(element);
|
|
154
|
+
const shows = !this.#covered(element, stack, hold);
|
|
155
|
+
hold.written = value;
|
|
156
|
+
this.#mark(element, stack);
|
|
157
|
+
if (shows) this.#reflect(element, value);
|
|
158
|
+
}
|
|
159
|
+
return(element) {
|
|
160
|
+
const held = this.#held.delete(element);
|
|
161
|
+
const known = registry.stacks.get(element)?.has(this.#attribute);
|
|
162
|
+
const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
|
|
163
|
+
const hold = this.#take(stack);
|
|
164
|
+
if (!hold) return false;
|
|
165
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
166
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
167
|
+
const index = stack.indexOf(hold);
|
|
168
|
+
stack.splice(index, 1);
|
|
169
|
+
const above = stack[index];
|
|
170
|
+
if (above) above.linked &&= hold.linked;
|
|
171
|
+
this.#mark(element, stack);
|
|
172
|
+
if (!owns) return true;
|
|
173
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
174
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
returnAll() {
|
|
178
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
179
|
+
}
|
|
180
|
+
#take(stack) {
|
|
181
|
+
const hold = findHold(stack, this, this.#record);
|
|
182
|
+
if (hold) hold.lease = this;
|
|
183
|
+
return hold;
|
|
184
|
+
}
|
|
185
|
+
#acquire(element, stack) {
|
|
186
|
+
const current = element.getAttribute(this.#attribute);
|
|
187
|
+
const top = stack.at(-1);
|
|
188
|
+
const bottom = stack[0];
|
|
189
|
+
const over = top?.written === current ? top : void 0;
|
|
190
|
+
const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
|
|
191
|
+
const hold = {
|
|
192
|
+
lease: this,
|
|
193
|
+
record: this.#record,
|
|
194
|
+
written: current,
|
|
195
|
+
original: from ? from.original : current,
|
|
196
|
+
linked: !this.#base && over !== void 0
|
|
197
|
+
};
|
|
198
|
+
if (this.#base && bottom) {
|
|
199
|
+
bottom.linked = true;
|
|
200
|
+
stack.unshift(hold);
|
|
201
|
+
} else stack.push(hold);
|
|
202
|
+
return hold;
|
|
203
|
+
}
|
|
204
|
+
#covered(element, stack, hold) {
|
|
205
|
+
const current = element.getAttribute(this.#attribute);
|
|
206
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
207
|
+
}
|
|
208
|
+
#mark(element, stack) {
|
|
209
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
210
|
+
stack.forEach((hold, index) => {
|
|
211
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
212
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
213
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
214
|
+
records.set(
|
|
215
|
+
hold.record,
|
|
216
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
for (const [name, recorded] of records) {
|
|
220
|
+
setAttributeIfChanged(element, name, recorded);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
#reflect(element, value) {
|
|
224
|
+
setAttributeIfChanged(element, this.#attribute, value);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
54
227
|
|
|
55
|
-
// src/utils/element_part.ts
|
|
56
228
|
function matchingPart(root, selector) {
|
|
57
229
|
return root.matches(selector) ? root : root.querySelector(selector);
|
|
58
230
|
}
|
|
@@ -79,31 +251,14 @@ function readLabel(slot) {
|
|
|
79
251
|
return text.trim();
|
|
80
252
|
}
|
|
81
253
|
|
|
82
|
-
// src/utils/roving_tabindex.ts
|
|
83
254
|
var RovingTabindex = class {
|
|
84
|
-
/** Returns the current ordered item elements; called on every operation. */
|
|
85
255
|
#getItems;
|
|
86
|
-
/**
|
|
87
|
-
* @param getItems - Returns the current ordered item elements. Called on every
|
|
88
|
-
* operation so the live target list is always used.
|
|
89
|
-
*/
|
|
90
256
|
constructor(getItems) {
|
|
91
257
|
this.#getItems = getItems;
|
|
92
258
|
}
|
|
93
|
-
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
94
259
|
get activeIndex() {
|
|
95
260
|
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
96
261
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
99
|
-
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
100
|
-
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
101
|
-
* "nothing is currently tabbable".
|
|
102
|
-
*
|
|
103
|
-
* @param index - Position of the item to make tabbable.
|
|
104
|
-
* @param options - Pass `{ focus: true }` to also move DOM focus to that item,
|
|
105
|
-
* and `items` to reuse an event-scoped collection snapshot.
|
|
106
|
-
*/
|
|
107
262
|
setActive(index, options = {}) {
|
|
108
263
|
const { focus = false } = options;
|
|
109
264
|
const items = options.items ?? this.#getItems();
|
|
@@ -114,9 +269,8 @@ var RovingTabindex = class {
|
|
|
114
269
|
}
|
|
115
270
|
};
|
|
116
271
|
|
|
117
|
-
// src/utils/chip_row.ts
|
|
118
272
|
var ChipRow = class {
|
|
119
|
-
#
|
|
273
|
+
#getDirectionElement;
|
|
120
274
|
#getItems;
|
|
121
275
|
#getButton;
|
|
122
276
|
#onRemove;
|
|
@@ -124,16 +278,12 @@ var ChipRow = class {
|
|
|
124
278
|
#roving = new RovingTabindex(() => this.buttons);
|
|
125
279
|
#container = null;
|
|
126
280
|
constructor(options) {
|
|
127
|
-
this.#
|
|
281
|
+
this.#getDirectionElement = options.getDirectionElement;
|
|
128
282
|
this.#getItems = options.getItems;
|
|
129
283
|
this.#getButton = options.getButton ?? ((item) => matchingPart(item, "button"));
|
|
130
284
|
this.#onRemove = options.onRemove;
|
|
131
285
|
this.#focusAfterEnd = options.focusAfterEnd;
|
|
132
286
|
}
|
|
133
|
-
/**
|
|
134
|
-
* Binds delegation to `container`; any row currently bound is released first.
|
|
135
|
-
* Idempotent for the same node, so target callbacks may call it freely.
|
|
136
|
-
*/
|
|
137
287
|
connect(container) {
|
|
138
288
|
if (this.#container === container) return;
|
|
139
289
|
this.disconnect();
|
|
@@ -141,10 +291,6 @@ var ChipRow = class {
|
|
|
141
291
|
container.addEventListener("click", this.#onClick);
|
|
142
292
|
container.addEventListener("keydown", this.#onKeydown);
|
|
143
293
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Releases the current row. When `container` is supplied, a stale disconnect
|
|
146
|
-
* callback cannot tear listeners off a newer replacement target.
|
|
147
|
-
*/
|
|
148
294
|
disconnect(container) {
|
|
149
295
|
const current = this.#container;
|
|
150
296
|
if (!current || container !== void 0 && current !== container) return;
|
|
@@ -152,19 +298,15 @@ var ChipRow = class {
|
|
|
152
298
|
current.removeEventListener("keydown", this.#onKeydown);
|
|
153
299
|
this.#container = null;
|
|
154
300
|
}
|
|
155
|
-
/** Current remove buttons, at most one consumer-resolved button per chip item. */
|
|
156
301
|
get buttons() {
|
|
157
302
|
return this.#entries.map(({ button }) => button);
|
|
158
303
|
}
|
|
159
|
-
/** Number of currently navigable remove buttons. */
|
|
160
304
|
get length() {
|
|
161
305
|
return this.buttons.length;
|
|
162
306
|
}
|
|
163
|
-
/** Declared-item index of the last navigable chip, or `-1` for an empty row. */
|
|
164
307
|
get lastIndex() {
|
|
165
308
|
return this.#entries.at(-1)?.itemIndex ?? -1;
|
|
166
309
|
}
|
|
167
|
-
/** Keeps exactly one button tabbable, preferring the first authored Tab stop. */
|
|
168
310
|
ensureTabStop() {
|
|
169
311
|
const buttons = this.#entries.map(({ button }) => button);
|
|
170
312
|
const active = buttons.findIndex((button) => button.tabIndex === 0);
|
|
@@ -173,7 +315,6 @@ var ChipRow = class {
|
|
|
173
315
|
this.#roving.setActive(active === -1 ? 0 : active, { items: buttons });
|
|
174
316
|
}
|
|
175
317
|
}
|
|
176
|
-
/** Focuses the last chip, returning false for an empty row. */
|
|
177
318
|
focusLast() {
|
|
178
319
|
const buttons = this.#entries.map(({ button }) => button);
|
|
179
320
|
const last = buttons.length - 1;
|
|
@@ -181,10 +322,6 @@ var ChipRow = class {
|
|
|
181
322
|
this.#roving.setActive(last, { focus: true, items: buttons });
|
|
182
323
|
return true;
|
|
183
324
|
}
|
|
184
|
-
/**
|
|
185
|
-
* Focuses the chip that followed a removed index, or the new last chip.
|
|
186
|
-
* Returns false when removal emptied the row so the consumer can rescue focus.
|
|
187
|
-
*/
|
|
188
325
|
focusAfterRemoval(index) {
|
|
189
326
|
const entries = this.#entries;
|
|
190
327
|
if (entries.length === 0) return false;
|
|
@@ -195,14 +332,12 @@ var ChipRow = class {
|
|
|
195
332
|
});
|
|
196
333
|
return true;
|
|
197
334
|
}
|
|
198
|
-
/** Current remove-button entries, retaining each button's declared item index. */
|
|
199
335
|
get #entries() {
|
|
200
336
|
return this.#getItems().flatMap((item, itemIndex) => {
|
|
201
337
|
const button = this.#getButton(item);
|
|
202
338
|
return button ? [{ button, itemIndex }] : [];
|
|
203
339
|
});
|
|
204
340
|
}
|
|
205
|
-
/** Resolves a delegated event to the remove button owned by a declared chip. */
|
|
206
341
|
#entry(event) {
|
|
207
342
|
const target = event.target;
|
|
208
343
|
if (!(target instanceof Element)) return null;
|
|
@@ -213,20 +348,18 @@ var ChipRow = class {
|
|
|
213
348
|
const entry = entries[buttonIndex];
|
|
214
349
|
return { ...entry, buttonIndex, buttons: entries.map(({ button: button2 }) => button2) };
|
|
215
350
|
}
|
|
216
|
-
/** Delegates removal clicks without waiting for Stimulus to wire a new chip. */
|
|
217
351
|
#onClick = (event) => {
|
|
218
352
|
const entry = this.#entry(event);
|
|
219
353
|
if (!entry) return;
|
|
220
354
|
this.#onRemove(entry.itemIndex);
|
|
221
355
|
};
|
|
222
|
-
/** Applies the shared logical-arrow and removal policy within the chip row. */
|
|
223
356
|
#onKeydown = (event) => {
|
|
224
357
|
if (event.defaultPrevented || isReservedArrowChord(event)) return;
|
|
225
358
|
const entry = this.#entry(event);
|
|
226
359
|
if (!entry) return;
|
|
227
360
|
const buttons = entry.buttons;
|
|
228
361
|
const index = entry.buttonIndex;
|
|
229
|
-
switch (logicalArrowKey(event.key, this.#
|
|
362
|
+
switch (logicalArrowKey(event.key, this.#getDirectionElement())) {
|
|
230
363
|
case "ArrowLeft":
|
|
231
364
|
if (index > 0) {
|
|
232
365
|
event.preventDefault();
|
|
@@ -250,86 +383,191 @@ var ChipRow = class {
|
|
|
250
383
|
};
|
|
251
384
|
};
|
|
252
385
|
|
|
253
|
-
// src/utils/composition_tracker.ts
|
|
254
386
|
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
255
387
|
"insertCompositionText",
|
|
256
388
|
"insertFromComposition",
|
|
257
389
|
"deleteCompositionText",
|
|
258
390
|
"deleteByComposition"
|
|
259
391
|
]);
|
|
392
|
+
function isCompositionInputType(inputType) {
|
|
393
|
+
return inputType !== void 0 && COMPOSITION_INPUT_TYPES.has(inputType);
|
|
394
|
+
}
|
|
395
|
+
var BEFORE_MORPH_ATTRIBUTE = "turbo:before-morph-attribute";
|
|
396
|
+
var textOf = (element) => element.value;
|
|
397
|
+
var holdsFocus = (element) => element instanceof Node && element.getRootNode().activeElement === element;
|
|
260
398
|
var CompositionTracker = class {
|
|
261
399
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
262
|
-
#
|
|
400
|
+
#composition = null;
|
|
263
401
|
#onStart;
|
|
264
402
|
#onEnd;
|
|
265
|
-
|
|
403
|
+
#onAbandon;
|
|
266
404
|
#confirmedTarget = null;
|
|
405
|
+
#keyComposing = false;
|
|
406
|
+
#connected = false;
|
|
407
|
+
#abandoned = null;
|
|
267
408
|
constructor(options = {}) {
|
|
268
409
|
this.#onStart = options.onStart;
|
|
269
410
|
this.#onEnd = options.onEnd;
|
|
411
|
+
this.#onAbandon = options.onAbandon;
|
|
270
412
|
}
|
|
271
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
272
413
|
observe(target) {
|
|
414
|
+
this.#connected = true;
|
|
273
415
|
if (this.#observedTargets.has(target)) return;
|
|
274
416
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
275
417
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
276
|
-
target.addEventListener("keydown", this.#handleKeydown);
|
|
418
|
+
target.addEventListener("keydown", this.#handleKeydown, true);
|
|
419
|
+
target.addEventListener("beforeinput", this.#handleBeforeInput, true);
|
|
420
|
+
target.addEventListener("input", this.#handleInput, true);
|
|
421
|
+
target.addEventListener("blur", this.#handleBlur, true);
|
|
422
|
+
target.addEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
|
|
277
423
|
this.#observedTargets.add(target);
|
|
278
424
|
}
|
|
279
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
280
425
|
unobserve(target) {
|
|
281
426
|
if (!this.#observedTargets.delete(target)) return;
|
|
282
|
-
|
|
283
|
-
target
|
|
284
|
-
target.removeEventListener("keydown", this.#handleKeydown);
|
|
285
|
-
this.#activeTargets.delete(target);
|
|
427
|
+
this.#release(target);
|
|
428
|
+
if (this.#composition?.target === target) this.#letGo();
|
|
286
429
|
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
287
430
|
}
|
|
288
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
289
431
|
disconnect() {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
target.removeEventListener("compositionend", this.#handleEnd);
|
|
293
|
-
target.removeEventListener("keydown", this.#handleKeydown);
|
|
294
|
-
}
|
|
432
|
+
this.#connected = false;
|
|
433
|
+
for (const target of this.#observedTargets) this.#release(target);
|
|
295
434
|
this.#observedTargets.clear();
|
|
296
|
-
this.#
|
|
435
|
+
this.#letGo();
|
|
297
436
|
this.#confirmedTarget = null;
|
|
437
|
+
this.#keyComposing = false;
|
|
438
|
+
this.#abandoned = null;
|
|
298
439
|
}
|
|
299
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
300
440
|
isComposing(event) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
441
|
+
this.#settle();
|
|
442
|
+
return event?.isComposing === true || this.#composition !== null;
|
|
443
|
+
}
|
|
444
|
+
composingElement() {
|
|
445
|
+
this.#settle();
|
|
446
|
+
return this.#composition?.element ?? null;
|
|
447
|
+
}
|
|
448
|
+
owns(event) {
|
|
449
|
+
const element = this.composingElement();
|
|
450
|
+
return event.isComposing === true || element !== null && element === event.target;
|
|
451
|
+
}
|
|
309
452
|
consumesConfirmedInput(event) {
|
|
310
453
|
const confirmed = this.#confirmedTarget;
|
|
311
454
|
this.#confirmedTarget = null;
|
|
312
455
|
if (confirmed === null || confirmed !== event.target) return false;
|
|
313
456
|
const inputType = event.inputType;
|
|
314
|
-
return !inputType ||
|
|
457
|
+
return !inputType || isCompositionInputType(inputType);
|
|
315
458
|
}
|
|
459
|
+
#release(target) {
|
|
460
|
+
target.removeEventListener("compositionstart", this.#handleStart);
|
|
461
|
+
target.removeEventListener("compositionend", this.#handleEnd);
|
|
462
|
+
target.removeEventListener("keydown", this.#handleKeydown, true);
|
|
463
|
+
target.removeEventListener("beforeinput", this.#handleBeforeInput, true);
|
|
464
|
+
target.removeEventListener("input", this.#handleInput, true);
|
|
465
|
+
target.removeEventListener("blur", this.#handleBlur, true);
|
|
466
|
+
target.removeEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
|
|
467
|
+
}
|
|
468
|
+
#letGo() {
|
|
469
|
+
const composition = this.#composition;
|
|
470
|
+
if (composition === null) return;
|
|
471
|
+
if (!holdsFocus(composition.element)) {
|
|
472
|
+
this.#composition = null;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
queueMicrotask(() => {
|
|
476
|
+
if (this.#composition !== composition || this.#observedTargets.has(composition.target)) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
this.#composition = null;
|
|
480
|
+
if (!this.#connected) return;
|
|
481
|
+
this.#abandoned = { element: composition.element, inPlace: true };
|
|
482
|
+
this.#runAbandon();
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
#settle() {
|
|
486
|
+
const composition = this.#composition;
|
|
487
|
+
if (composition === null) return;
|
|
488
|
+
const { target, element, text } = composition;
|
|
489
|
+
if (target instanceof Node && element instanceof Node && !target.contains(element)) {
|
|
490
|
+
this.#composition = null;
|
|
491
|
+
} else if (textOf(element) !== text) {
|
|
492
|
+
this.#abandon(false);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
#abandon(inPlace) {
|
|
496
|
+
this.#owe(inPlace);
|
|
497
|
+
this.#composition = null;
|
|
498
|
+
queueMicrotask(this.#runAbandon);
|
|
499
|
+
}
|
|
500
|
+
#owe(inPlace) {
|
|
501
|
+
const composition = this.#composition;
|
|
502
|
+
if (composition !== null) this.#abandoned = { element: composition.element, inPlace };
|
|
503
|
+
}
|
|
504
|
+
#runAbandon = () => {
|
|
505
|
+
const abandoned = this.#abandoned;
|
|
506
|
+
if (abandoned === null) return;
|
|
507
|
+
this.#abandoned = null;
|
|
508
|
+
this.#onAbandon?.(abandoned.element, abandoned.inPlace);
|
|
509
|
+
};
|
|
316
510
|
#handleStart = (event) => {
|
|
317
511
|
this.#confirmedTarget = null;
|
|
318
|
-
|
|
512
|
+
this.#settle();
|
|
513
|
+
this.#owe(true);
|
|
514
|
+
const target = event.currentTarget;
|
|
515
|
+
const element = event.target;
|
|
516
|
+
this.#composition = { target, element, text: textOf(element) };
|
|
517
|
+
this.#runAbandon();
|
|
319
518
|
this.#onStart?.(event);
|
|
320
519
|
};
|
|
321
520
|
#handleEnd = (event) => {
|
|
322
|
-
|
|
521
|
+
this.#runAbandon();
|
|
522
|
+
if (this.#composition?.target === event.currentTarget) this.#composition = null;
|
|
323
523
|
this.#confirmedTarget = event.target;
|
|
324
524
|
this.#onEnd?.(event);
|
|
325
525
|
};
|
|
326
|
-
|
|
327
|
-
|
|
526
|
+
#handleKeydown = (event) => {
|
|
527
|
+
this.#settle();
|
|
528
|
+
this.#runAbandon();
|
|
328
529
|
this.#confirmedTarget = null;
|
|
530
|
+
this.#keyComposing = event.isComposing === true;
|
|
531
|
+
};
|
|
532
|
+
#handleBeforeInput = (event) => {
|
|
533
|
+
const keyComposing = this.#keyComposing;
|
|
534
|
+
this.#keyComposing = false;
|
|
535
|
+
this.#settle();
|
|
536
|
+
const { isComposing, inputType } = event;
|
|
537
|
+
if (this.#composition !== null && !keyComposing && !isComposing && inputType && !isCompositionInputType(inputType)) {
|
|
538
|
+
this.#abandon(true);
|
|
539
|
+
}
|
|
540
|
+
this.#runAbandon();
|
|
541
|
+
};
|
|
542
|
+
#handleInput = (event) => {
|
|
543
|
+
const composition = this.#composition;
|
|
544
|
+
const { isTrusted, isComposing, inputType } = event;
|
|
545
|
+
if (composition !== null && event.target === composition.element && (isTrusted || isComposing || isCompositionInputType(inputType))) {
|
|
546
|
+
composition.text = textOf(composition.element);
|
|
547
|
+
} else {
|
|
548
|
+
this.#settle();
|
|
549
|
+
}
|
|
550
|
+
this.#runAbandon();
|
|
551
|
+
};
|
|
552
|
+
#handleBlur = (event) => {
|
|
553
|
+
this.#settle();
|
|
554
|
+
this.#runAbandon();
|
|
555
|
+
const composition = this.#composition;
|
|
556
|
+
if (composition === null || composition.target === composition.element) return;
|
|
557
|
+
const { element } = composition;
|
|
558
|
+
if (!(element instanceof Node) || event.target !== element) return;
|
|
559
|
+
const root = element.getRootNode();
|
|
560
|
+
if (root.activeElement !== element) this.#composition = null;
|
|
561
|
+
};
|
|
562
|
+
#handleBeforeMorphAttribute = (event) => {
|
|
563
|
+
this.#settle();
|
|
564
|
+
const composition = this.#composition;
|
|
565
|
+
if (composition?.target !== event.currentTarget || event.target !== composition.element) return;
|
|
566
|
+
const detail = event.detail;
|
|
567
|
+
if (detail?.attributeName === "value") event.preventDefault();
|
|
329
568
|
};
|
|
330
569
|
};
|
|
331
570
|
|
|
332
|
-
// src/utils/field_mirror.ts
|
|
333
571
|
function writeFields(container, values, { name, form = "" }) {
|
|
334
572
|
const current = [...container.children];
|
|
335
573
|
const submits = current.length === values.length && current.every(
|
|
@@ -352,27 +590,36 @@ function commitField(target) {
|
|
|
352
590
|
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
353
591
|
}
|
|
354
592
|
|
|
355
|
-
|
|
593
|
+
var ListenerSet = class {
|
|
594
|
+
#abort = new AbortController();
|
|
595
|
+
add(target, type, handler, options) {
|
|
596
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
597
|
+
}
|
|
598
|
+
dispose() {
|
|
599
|
+
this.#abort.abort();
|
|
600
|
+
this.#abort = new AbortController();
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
356
604
|
var MicrotaskCoalescer = class {
|
|
357
605
|
#run;
|
|
358
606
|
#queued = false;
|
|
359
607
|
#active = false;
|
|
360
608
|
#generation = 0;
|
|
361
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
362
609
|
constructor(run) {
|
|
363
610
|
this.#run = run;
|
|
364
611
|
}
|
|
365
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
366
612
|
activate() {
|
|
367
613
|
this.#active = true;
|
|
368
614
|
}
|
|
369
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
370
615
|
cancel() {
|
|
371
616
|
this.#active = false;
|
|
372
617
|
this.#queued = false;
|
|
373
618
|
this.#generation += 1;
|
|
374
619
|
}
|
|
375
|
-
|
|
620
|
+
get pending() {
|
|
621
|
+
return this.#queued;
|
|
622
|
+
}
|
|
376
623
|
schedule() {
|
|
377
624
|
if (!this.#active || this.#queued) return;
|
|
378
625
|
this.#queued = true;
|
|
@@ -385,7 +632,89 @@ var MicrotaskCoalescer = class {
|
|
|
385
632
|
}
|
|
386
633
|
};
|
|
387
634
|
|
|
388
|
-
|
|
635
|
+
var MorphRenderWatcher = class {
|
|
636
|
+
#listeners = new ListenerSet();
|
|
637
|
+
#pass;
|
|
638
|
+
#element = null;
|
|
639
|
+
#onMorph = () => this.#pass.schedule();
|
|
640
|
+
constructor(run) {
|
|
641
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
642
|
+
}
|
|
643
|
+
observe(element) {
|
|
644
|
+
if (this.#element === element) return;
|
|
645
|
+
this.disconnect();
|
|
646
|
+
this.#element = element;
|
|
647
|
+
this.#pass.activate();
|
|
648
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
649
|
+
}
|
|
650
|
+
schedule() {
|
|
651
|
+
this.#pass.schedule();
|
|
652
|
+
}
|
|
653
|
+
get pending() {
|
|
654
|
+
return this.#pass.pending;
|
|
655
|
+
}
|
|
656
|
+
disconnect() {
|
|
657
|
+
this.#listeners.dispose();
|
|
658
|
+
this.#pass.cancel();
|
|
659
|
+
this.#element = null;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
var MoveCounter = class {
|
|
664
|
+
#sequence = 0;
|
|
665
|
+
record() {
|
|
666
|
+
this.#sequence += 1;
|
|
667
|
+
return this.#sequence;
|
|
668
|
+
}
|
|
669
|
+
isLatest(token) {
|
|
670
|
+
return token === this.#sequence;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
var NUMBER_BOUNDS = {
|
|
675
|
+
finite: { finite: true }};
|
|
676
|
+
function matchesNumberBounds(value, bounds) {
|
|
677
|
+
if (!Number.isFinite(value)) {
|
|
678
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
679
|
+
if (direction === null) return false;
|
|
680
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
681
|
+
}
|
|
682
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
683
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
684
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
685
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
686
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function readNumber(raw, fallback, bounds) {
|
|
691
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
var NumberValueReader = class {
|
|
695
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
696
|
+
read(owner, name, raw, fallback, bounds) {
|
|
697
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
698
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
699
|
+
this.#lastRejected.delete(name);
|
|
700
|
+
return resolved;
|
|
701
|
+
}
|
|
702
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
703
|
+
const literal = owner.element.getAttribute(attribute);
|
|
704
|
+
if (literal === null) {
|
|
705
|
+
this.#lastRejected.delete(name);
|
|
706
|
+
return resolved;
|
|
707
|
+
}
|
|
708
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
709
|
+
this.#lastRejected.set(name, literal);
|
|
710
|
+
console.warn(
|
|
711
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
return resolved;
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
389
718
|
function scrollOptionIntoView(list, option) {
|
|
390
719
|
if (list.scrollHeight <= list.clientHeight) return;
|
|
391
720
|
const listRect = list.getBoundingClientRect();
|
|
@@ -397,16 +726,20 @@ function scrollOptionIntoView(list, option) {
|
|
|
397
726
|
}
|
|
398
727
|
}
|
|
399
728
|
|
|
400
|
-
|
|
401
|
-
|
|
729
|
+
var StateRegions = class _StateRegions {
|
|
730
|
+
static #registry = sharedRegistry(
|
|
731
|
+
"stimeo-ui.state-regions.registry.v1",
|
|
732
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
733
|
+
);
|
|
402
734
|
#whenTrue;
|
|
403
735
|
#whenFalse;
|
|
404
|
-
#
|
|
405
|
-
constructor(options) {
|
|
736
|
+
#record;
|
|
737
|
+
constructor(options, owner) {
|
|
406
738
|
this.#whenTrue = options.whenTrue;
|
|
407
739
|
this.#whenFalse = options.whenFalse ?? null;
|
|
740
|
+
this.#record = `data-${owner}-hidden-region`;
|
|
741
|
+
_StateRegions.#registry.names.add(this.#record);
|
|
408
742
|
}
|
|
409
|
-
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
410
743
|
reflect(host, isTrue) {
|
|
411
744
|
const shown = this.#inside(host, this.#whenTrue());
|
|
412
745
|
if (!this.#whenFalse) {
|
|
@@ -422,88 +755,248 @@ var StateRegions = class {
|
|
|
422
755
|
for (const region of shown) this.#write(region, !isTrue);
|
|
423
756
|
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
424
757
|
}
|
|
425
|
-
|
|
758
|
+
release(host) {
|
|
759
|
+
for (const region of [host, ...host.querySelectorAll("*")]) {
|
|
760
|
+
if (!(region instanceof HTMLElement)) continue;
|
|
761
|
+
const stack = this.#held(region);
|
|
762
|
+
const hold = stack && this.#take(stack);
|
|
763
|
+
if (!stack || !hold) continue;
|
|
764
|
+
const owns = region.hidden === hold.written && !this.#covered(region, stack, hold);
|
|
765
|
+
const index = stack.indexOf(hold);
|
|
766
|
+
stack.splice(index, 1);
|
|
767
|
+
const above = stack[index];
|
|
768
|
+
if (above) above.linked &&= hold.linked;
|
|
769
|
+
if (owns) {
|
|
770
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
771
|
+
if (below) {
|
|
772
|
+
if (region.hidden !== below.written) region.hidden = below.written;
|
|
773
|
+
} else {
|
|
774
|
+
setAttributeIfChanged(region, "hidden", hold.authored);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
this.#mark(region, stack);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
426
780
|
#write(region, hidden) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
781
|
+
const stack = this.#stack(region);
|
|
782
|
+
const hold = this.#take(stack) ?? this.#acquire(region, stack);
|
|
783
|
+
const shows = !this.#covered(region, stack, hold);
|
|
784
|
+
hold.written = hidden;
|
|
785
|
+
if (shows && region.hidden !== hidden) region.hidden = hidden;
|
|
786
|
+
this.#mark(region, stack);
|
|
430
787
|
}
|
|
431
|
-
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
432
788
|
#give(region) {
|
|
433
|
-
|
|
789
|
+
const stack = this.#held(region);
|
|
790
|
+
if (stack && this.#take(stack)?.written) this.#write(region, false);
|
|
791
|
+
}
|
|
792
|
+
#held(region) {
|
|
793
|
+
return _StateRegions.#registry.stacks.has(region) || region.hasAttribute(this.#record) ? this.#stack(region) : void 0;
|
|
794
|
+
}
|
|
795
|
+
#stack(region) {
|
|
796
|
+
const stacks = _StateRegions.#registry.stacks;
|
|
797
|
+
const stack = stacks.get(region) ?? this.#seed(region);
|
|
798
|
+
stacks.set(region, stack);
|
|
799
|
+
return stack;
|
|
800
|
+
}
|
|
801
|
+
#seed(region) {
|
|
802
|
+
const found = [];
|
|
803
|
+
for (const record of _StateRegions.#registry.names) {
|
|
804
|
+
const read = this.#recorded(region, record);
|
|
805
|
+
if (read) found.push({ ...read, record });
|
|
806
|
+
}
|
|
807
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
808
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, authored, written, linked }, index) => ({
|
|
809
|
+
instance: null,
|
|
810
|
+
record,
|
|
811
|
+
authored,
|
|
812
|
+
written,
|
|
813
|
+
linked: linked && index > 0
|
|
814
|
+
}));
|
|
815
|
+
}
|
|
816
|
+
#take(stack) {
|
|
817
|
+
const hold = stack.find((held) => held.instance === this) ?? stack.find((held) => held.instance === null && held.record === this.#record);
|
|
818
|
+
if (hold) hold.instance = this;
|
|
819
|
+
return hold;
|
|
820
|
+
}
|
|
821
|
+
#covered(region, stack, hold) {
|
|
822
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === region.hidden);
|
|
823
|
+
}
|
|
824
|
+
#acquire(region, stack) {
|
|
825
|
+
const top = stack.at(-1);
|
|
826
|
+
const over = top && region.hidden === top.written ? top : void 0;
|
|
827
|
+
const hold = {
|
|
828
|
+
instance: this,
|
|
829
|
+
record: this.#record,
|
|
830
|
+
authored: over ? over.authored : region.getAttribute("hidden"),
|
|
831
|
+
written: false,
|
|
832
|
+
linked: over !== void 0
|
|
833
|
+
};
|
|
834
|
+
stack.push(hold);
|
|
835
|
+
return hold;
|
|
836
|
+
}
|
|
837
|
+
#mark(region, stack) {
|
|
838
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
839
|
+
stack.forEach((hold, index) => {
|
|
840
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
841
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
842
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
843
|
+
records.set(
|
|
844
|
+
hold.record,
|
|
845
|
+
region.getAttribute("hidden") === hold.authored && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.authored, hold.written, ...marker, ...beneath])
|
|
846
|
+
);
|
|
847
|
+
});
|
|
848
|
+
for (const [name, recorded] of records) {
|
|
849
|
+
setAttributeIfChanged(region, name, recorded);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
#recorded(region, name) {
|
|
853
|
+
try {
|
|
854
|
+
const parsed = JSON.parse(region.getAttribute(name) ?? "null");
|
|
855
|
+
if (!Array.isArray(parsed)) return null;
|
|
856
|
+
const [authored, written, ...rest] = parsed;
|
|
857
|
+
if (authored !== null && typeof authored !== "string") return null;
|
|
858
|
+
const linked = rest[0] !== false;
|
|
859
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
860
|
+
if (!beneath.every((under) => typeof under === "string")) return null;
|
|
861
|
+
return typeof written === "boolean" ? { authored, written, linked, beneath } : null;
|
|
862
|
+
} catch {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
434
865
|
}
|
|
435
|
-
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
436
866
|
#inside(host, regions) {
|
|
437
867
|
return regions.filter((region) => host.contains(region));
|
|
438
868
|
}
|
|
439
869
|
};
|
|
870
|
+
function deepActiveElement(ownerDocument) {
|
|
871
|
+
let active = ownerDocument.activeElement;
|
|
872
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
873
|
+
return active;
|
|
874
|
+
}
|
|
440
875
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
this.#rewind = rewind;
|
|
453
|
-
}
|
|
454
|
-
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
455
|
-
activate() {
|
|
456
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
457
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
458
|
-
if (first) {
|
|
459
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
876
|
+
function queryOpenDescendants(root, selector) {
|
|
877
|
+
const matches = [];
|
|
878
|
+
const roots = [root];
|
|
879
|
+
for (const scope of roots) {
|
|
880
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
881
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
882
|
+
const shadow = scope.shadowRoot;
|
|
883
|
+
if (shadow) roots.push(shadow);
|
|
884
|
+
}
|
|
885
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
886
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
460
887
|
}
|
|
461
888
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
_BeforeCacheReset.#subscribers.delete(this);
|
|
465
|
-
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
466
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
467
|
-
}
|
|
468
|
-
};
|
|
889
|
+
return matches;
|
|
890
|
+
}
|
|
469
891
|
|
|
470
|
-
|
|
471
|
-
|
|
892
|
+
function holdsFocus2(element) {
|
|
893
|
+
return element.matches(":focus") || deepActiveElement(element.ownerDocument) === element;
|
|
894
|
+
}
|
|
895
|
+
var TabindexLoan = class _TabindexLoan {
|
|
896
|
+
static #registry = sharedRegistry(
|
|
897
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
898
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
899
|
+
);
|
|
472
900
|
#value;
|
|
901
|
+
#record;
|
|
902
|
+
#preserveFocus;
|
|
473
903
|
#lent = /* @__PURE__ */ new Set();
|
|
474
|
-
|
|
475
|
-
#beforeCache = new BeforeCacheReset(() => this.returnAll());
|
|
476
|
-
/**
|
|
477
|
-
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
478
|
-
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
479
|
-
* which a scroll region with no focusable content of its own needs.
|
|
480
|
-
*/
|
|
481
|
-
constructor(value = "-1") {
|
|
904
|
+
constructor(value, owner, options = {}) {
|
|
482
905
|
this.#value = value;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
906
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
907
|
+
this.#preserveFocus = options.preserveFocus === true;
|
|
908
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
909
|
+
}
|
|
910
|
+
lend(element, options = {}) {
|
|
911
|
+
const stack = this.#stack(element);
|
|
912
|
+
const current = element.getAttribute("tabindex");
|
|
913
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
914
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
915
|
+
);
|
|
916
|
+
const linked = stack.at(-1)?.value === current;
|
|
917
|
+
if (!held && options.onlyIfUnfocusable && (current !== null || element.tabIndex >= 0)) return;
|
|
918
|
+
if (!held && current !== null && !linked) return;
|
|
488
919
|
this.#lent.add(element);
|
|
489
|
-
|
|
920
|
+
if (held) held.loan = this;
|
|
921
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
922
|
+
this.#mark(element, stack);
|
|
923
|
+
if (current === null || !held && current !== this.#value) {
|
|
924
|
+
element.setAttribute("tabindex", this.#value);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return(element) {
|
|
928
|
+
const stack = this.#stack(element);
|
|
929
|
+
const hold = stack.find((held) => held.loan === this) ?? stack.find(
|
|
930
|
+
(held) => held.loan === null && held.record === this.#record && held.value === this.#value
|
|
931
|
+
);
|
|
932
|
+
if (!hold) return false;
|
|
933
|
+
if (!this.#release(element, stack, hold, this.#preserveFocus)) {
|
|
934
|
+
hold.loan = this;
|
|
935
|
+
this.#lent.add(element);
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
this.#lent.delete(element);
|
|
939
|
+
return true;
|
|
490
940
|
}
|
|
491
|
-
/** Takes back every loan whose value is still the one that was lent. */
|
|
492
941
|
returnAll() {
|
|
493
|
-
for (const element of this.#lent)
|
|
494
|
-
|
|
942
|
+
for (const element of Array.from(this.#lent)) this.return(element);
|
|
943
|
+
}
|
|
944
|
+
reclaim(element) {
|
|
945
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
946
|
+
const stack = this.#stack(element);
|
|
947
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
948
|
+
if (hold && !holdsFocus2(element)) this.#release(element, stack, hold);
|
|
949
|
+
}
|
|
950
|
+
reclaimWithin(root) {
|
|
951
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
952
|
+
this.reclaim(element);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
#stack(element) {
|
|
956
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
957
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
958
|
+
stacks.set(element, stack);
|
|
959
|
+
return stack;
|
|
960
|
+
}
|
|
961
|
+
#seed(element) {
|
|
962
|
+
const current = element.getAttribute("tabindex");
|
|
963
|
+
const holds = [];
|
|
964
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
965
|
+
const value = element.getAttribute(record);
|
|
966
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
967
|
+
}
|
|
968
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
969
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
970
|
+
return holds;
|
|
971
|
+
}
|
|
972
|
+
#release(element, stack, hold, preserveFocus = false) {
|
|
973
|
+
const current = element.getAttribute("tabindex");
|
|
974
|
+
const index = stack.indexOf(hold);
|
|
975
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
976
|
+
if (owns && preserveFocus && holdsFocus2(element)) return false;
|
|
977
|
+
stack.splice(index, 1);
|
|
978
|
+
const above = stack[index];
|
|
979
|
+
if (above) above.linked &&= hold.linked;
|
|
980
|
+
this.#mark(element, stack);
|
|
981
|
+
if (!owns) return true;
|
|
982
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
983
|
+
if (!below) element.removeAttribute("tabindex");
|
|
984
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
987
|
+
#mark(element, stack) {
|
|
988
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
989
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
990
|
+
for (const [name, recorded] of records) {
|
|
991
|
+
setAttributeIfChanged(element, name, recorded);
|
|
495
992
|
}
|
|
496
|
-
this.#lent.clear();
|
|
497
|
-
this.#beforeCache.deactivate();
|
|
498
993
|
}
|
|
499
994
|
};
|
|
500
995
|
|
|
501
|
-
// src/utils/target_selector.ts
|
|
502
996
|
function targetSelector(identifier, name) {
|
|
503
997
|
return `[data-${identifier}-target~="${name}"]`;
|
|
504
998
|
}
|
|
505
999
|
|
|
506
|
-
// src/utils/template_row.ts
|
|
507
1000
|
function cloneTemplateRoot(template) {
|
|
508
1001
|
const root = template.content.firstElementChild;
|
|
509
1002
|
return root instanceof HTMLElement ? root.cloneNode(true) : null;
|
|
@@ -514,22 +1007,15 @@ var TemplateRow = class {
|
|
|
514
1007
|
constructor(options) {
|
|
515
1008
|
this.#options = options;
|
|
516
1009
|
}
|
|
517
|
-
/** The attribute selector for one declared part, in this controller's namespace. */
|
|
518
1010
|
selector(name) {
|
|
519
1011
|
return targetSelector(this.#options.identifier, name);
|
|
520
1012
|
}
|
|
521
|
-
/** Re-arms the once-per-connection diagnostic. */
|
|
522
1013
|
connect() {
|
|
523
1014
|
this.#warned = false;
|
|
524
1015
|
}
|
|
525
|
-
/**
|
|
526
|
-
* Names one missing part on the console, at most once per connection, and
|
|
527
|
-
* returns `null` so a caller can hand it straight back.
|
|
528
|
-
*/
|
|
529
1016
|
report(missing) {
|
|
530
1017
|
return this.#say(`lacks ${missing}`);
|
|
531
1018
|
}
|
|
532
|
-
/** Writes one diagnostic per connection, saying what the template got wrong. */
|
|
533
1019
|
#say(problem) {
|
|
534
1020
|
if (this.#warned) return null;
|
|
535
1021
|
this.#warned = true;
|
|
@@ -537,10 +1023,6 @@ var TemplateRow = class {
|
|
|
537
1023
|
console.warn(`Stimeo UI: "${identifier}" ${outcome} because its ${noun} ${problem}.`);
|
|
538
1024
|
return null;
|
|
539
1025
|
}
|
|
540
|
-
/**
|
|
541
|
-
* Clones the row and resolves its parts, or names what the template got wrong
|
|
542
|
-
* and returns `null`. `values` fills the button's authored `aria-label`.
|
|
543
|
-
*/
|
|
544
1026
|
instantiate(template, values) {
|
|
545
1027
|
const { root: rootName, required, optional, button: buttonName } = this.#options;
|
|
546
1028
|
const root = cloneTemplateRoot(template);
|
|
@@ -568,7 +1050,6 @@ var TemplateRow = class {
|
|
|
568
1050
|
button.setAttribute("aria-label", fillTemplate(name, values));
|
|
569
1051
|
return { root, slots, button };
|
|
570
1052
|
}
|
|
571
|
-
/** The row may be its own part; otherwise the first descendant carrying the target. */
|
|
572
1053
|
#resolve(root, name) {
|
|
573
1054
|
return matchingPart(root, this.selector(name));
|
|
574
1055
|
}
|
|
@@ -577,8 +1058,9 @@ var TemplateRow = class {
|
|
|
577
1058
|
}
|
|
578
1059
|
};
|
|
579
1060
|
|
|
580
|
-
|
|
581
|
-
|
|
1061
|
+
var MultiSelectController = class _MultiSelectController extends Controller {
|
|
1062
|
+
#numbers = new NumberValueReader();
|
|
1063
|
+
#moves = new MoveCounter();
|
|
582
1064
|
static targets = [
|
|
583
1065
|
"input",
|
|
584
1066
|
"list",
|
|
@@ -598,28 +1080,28 @@ var MultiSelectController = class extends Controller {
|
|
|
598
1080
|
announceText: { type: String, default: "" },
|
|
599
1081
|
announceRemovedText: { type: String, default: "" }
|
|
600
1082
|
};
|
|
1083
|
+
static valueConstraints = {
|
|
1084
|
+
max: NUMBER_BOUNDS.finite
|
|
1085
|
+
};
|
|
601
1086
|
static actions = ["close", "filter", "onKeydown", "open", "toggleOption"];
|
|
602
1087
|
static events = ["change", "filter", "reconcile"];
|
|
603
|
-
/** Stable id of the active option; the current target is resolved from the DOM. */
|
|
604
1088
|
#activeOptionId = null;
|
|
605
|
-
|
|
606
|
-
#tabindex = new TabindexLoan();
|
|
607
|
-
/** Last reconciled selection, used to distinguish state changes from derived-DOM repair. */
|
|
1089
|
+
#tabindex = new TabindexLoan("-1", this.identifier);
|
|
608
1090
|
#selectionValues = [];
|
|
609
|
-
/** Whether initial normalization finished, so a fields callback cannot mirror stale state. */
|
|
610
1091
|
#connected = false;
|
|
611
|
-
|
|
612
|
-
#
|
|
613
|
-
|
|
614
|
-
#
|
|
615
|
-
|
|
1092
|
+
#emptyRegion = new StateRegions({ whenTrue: () => this.emptyTargets }, this.identifier);
|
|
1093
|
+
#listHidden = new AttributeLease("hidden", this.identifier);
|
|
1094
|
+
#expanded = new AttributeLease("aria-expanded", this.identifier);
|
|
1095
|
+
#activeDescendant = new AttributeLease(
|
|
1096
|
+
"aria-activedescendant",
|
|
1097
|
+
this.identifier
|
|
1098
|
+
);
|
|
1099
|
+
#open = false;
|
|
1100
|
+
#openList = null;
|
|
1101
|
+
#reconcile = new MorphRenderWatcher(() => this.#reconcileOptions());
|
|
616
1102
|
#composition = new CompositionTracker({
|
|
617
|
-
// Apply the confirmed query even in browsers that omit a final input event;
|
|
618
|
-
// where one follows, the tracker folds it so consumers receive one filter
|
|
619
|
-
// event for the confirmed text, not two.
|
|
620
1103
|
onEnd: () => this.filter()
|
|
621
1104
|
});
|
|
622
|
-
/** Builds one chip from the authored template and owns its diagnostic. */
|
|
623
1105
|
#rows = new TemplateRow({
|
|
624
1106
|
identifier: this.identifier,
|
|
625
1107
|
root: "tag",
|
|
@@ -628,16 +1110,16 @@ var MultiSelectController = class extends Controller {
|
|
|
628
1110
|
outcome: "changed no selection",
|
|
629
1111
|
noun: "chip template"
|
|
630
1112
|
});
|
|
631
|
-
/** Shared delegated interaction for the replaceable row of removable chips. */
|
|
632
1113
|
#chipRow = new ChipRow({
|
|
633
|
-
|
|
1114
|
+
getDirectionElement: () => this.#chipDirectionElement,
|
|
634
1115
|
getItems: () => this.tagTargets,
|
|
635
1116
|
getButton: (tag) => matchingPart(tag, `button${this.#rows.selector("remove")}`),
|
|
636
1117
|
onRemove: (index) => this.#removeTagAt(index),
|
|
637
1118
|
focusAfterEnd: () => this.#focusInput()
|
|
638
1119
|
});
|
|
639
|
-
|
|
1120
|
+
#listeners = new ListenerSet();
|
|
640
1121
|
connect() {
|
|
1122
|
+
this.#tabindex.reclaim(this.element);
|
|
641
1123
|
this.#rows.connect();
|
|
642
1124
|
this.#normalizeSelection([]);
|
|
643
1125
|
this.close();
|
|
@@ -647,15 +1129,10 @@ var MultiSelectController = class extends Controller {
|
|
|
647
1129
|
}
|
|
648
1130
|
this.#selectionValues = this.#values;
|
|
649
1131
|
this.#syncFields();
|
|
650
|
-
|
|
1132
|
+
this.#listeners.add(document, "click", this.#onOutsideClick, { capture: true });
|
|
651
1133
|
this.#connected = true;
|
|
652
|
-
this.#reconcile.
|
|
1134
|
+
this.#reconcile.observe(this.element);
|
|
653
1135
|
}
|
|
654
|
-
/**
|
|
655
|
-
* Derives the chips from the selected options, idempotently: a Turbo Drive cache
|
|
656
|
-
* restore or morph can re-connect with chips already in the DOM, so they are
|
|
657
|
-
* cleared before deriving afresh to avoid duplicates.
|
|
658
|
-
*/
|
|
659
1136
|
#rebuildTags() {
|
|
660
1137
|
if (!this.hasTagsTarget || !this.hasTagTemplateTarget) return;
|
|
661
1138
|
const chips = [];
|
|
@@ -668,72 +1145,67 @@ var MultiSelectController = class extends Controller {
|
|
|
668
1145
|
this.tagsTarget.append(...chips);
|
|
669
1146
|
this.#chipRow.ensureTabStop();
|
|
670
1147
|
}
|
|
671
|
-
/** Tears down document and chip listeners on disconnect (Turbo included). */
|
|
672
1148
|
disconnect() {
|
|
673
1149
|
this.#connected = false;
|
|
674
|
-
this.#reconcile.
|
|
1150
|
+
this.#reconcile.disconnect();
|
|
675
1151
|
this.#composition.disconnect();
|
|
676
1152
|
this.#chipRow.disconnect();
|
|
677
|
-
|
|
1153
|
+
this.#listeners.dispose();
|
|
678
1154
|
this.#releaseTabindex();
|
|
679
1155
|
}
|
|
680
|
-
/** Reconciles active state after an option target is added at runtime. */
|
|
681
1156
|
optionTargetConnected() {
|
|
682
1157
|
this.#scheduleOptionReconcile();
|
|
683
1158
|
}
|
|
684
|
-
/** Cleans a removed target and reconciles active state against the surviving DOM. */
|
|
685
1159
|
optionTargetDisconnected(option) {
|
|
686
1160
|
option.removeAttribute("data-active");
|
|
687
1161
|
this.#scheduleOptionReconcile();
|
|
688
1162
|
}
|
|
689
|
-
/** Settles a region inserted after connect on the side of the current state. */
|
|
690
1163
|
emptyTargetConnected() {
|
|
691
1164
|
if (this.#connected) this.#reflectEmpty();
|
|
692
1165
|
}
|
|
693
|
-
/** Rebinds delegated chip interaction when Turbo replaces the tags container. */
|
|
694
1166
|
tagsTargetConnected(tags) {
|
|
695
1167
|
this.#chipRow.connect(tags);
|
|
696
1168
|
this.#scheduleOptionReconcile();
|
|
697
1169
|
}
|
|
698
|
-
/** Releases only the row that actually disconnected, never a newer replacement. */
|
|
699
1170
|
tagsTargetDisconnected(tags) {
|
|
700
1171
|
this.#chipRow.disconnect(tags);
|
|
701
1172
|
this.#scheduleOptionReconcile();
|
|
702
1173
|
}
|
|
703
|
-
/** Seeds a fields target inserted after connect from the current selection. */
|
|
704
1174
|
fieldsTargetConnected() {
|
|
705
1175
|
if (this.#connected) this.#syncFields();
|
|
706
1176
|
}
|
|
707
|
-
|
|
1177
|
+
fieldsTargetDisconnected() {
|
|
1178
|
+
this.#scheduleOptionReconcile();
|
|
1179
|
+
}
|
|
1180
|
+
listTargetConnected(_list) {
|
|
1181
|
+
if (!this.#connected) return;
|
|
1182
|
+
this.#reconcileOpenState();
|
|
1183
|
+
this.#scheduleOptionReconcile();
|
|
1184
|
+
}
|
|
1185
|
+
listTargetDisconnected(list) {
|
|
1186
|
+
if (!this.listTargets.includes(list)) {
|
|
1187
|
+
if (list === this.#openList) {
|
|
1188
|
+
this.#adoptPageClose();
|
|
1189
|
+
this.#openList = null;
|
|
1190
|
+
}
|
|
1191
|
+
this.#listHidden.return(list);
|
|
1192
|
+
}
|
|
1193
|
+
if (!this.#connected) return;
|
|
1194
|
+
this.#reconcileOpenState();
|
|
1195
|
+
this.#scheduleOptionReconcile();
|
|
1196
|
+
}
|
|
708
1197
|
maxValueChanged() {
|
|
709
1198
|
this.#scheduleOptionReconcile();
|
|
710
1199
|
}
|
|
711
|
-
/** Rebuilds submitted fields when their public name changes at runtime. */
|
|
712
1200
|
nameValueChanged() {
|
|
713
1201
|
this.#scheduleOptionReconcile();
|
|
714
1202
|
}
|
|
715
|
-
/** Rebuilds submitted fields when their associated form changes at runtime. */
|
|
716
1203
|
formValueChanged() {
|
|
717
1204
|
this.#scheduleOptionReconcile();
|
|
718
1205
|
}
|
|
719
|
-
/** Schedules one reconciliation after all callbacks in the mutation batch. */
|
|
720
1206
|
#scheduleOptionReconcile() {
|
|
721
1207
|
this.#reconcile.schedule();
|
|
722
1208
|
}
|
|
723
|
-
/**
|
|
724
|
-
* Keeps a surviving/same-id active target, otherwise falls back to the first
|
|
725
|
-
* visible one — and brings the derived state back in line with the new option set.
|
|
726
|
-
*
|
|
727
|
-
* The baseline pass fills in any missing `aria-selected`, and the chips and
|
|
728
|
-
* hidden fields are re-derived from it, because the options are the truth source
|
|
729
|
-
* for the selection. The chips are rebuilt **only when the selected value set
|
|
730
|
-
* actually moved**: the rebuild removes and recreates every chip, so running it
|
|
731
|
-
* for an unrelated option would drop focus from a chip's remove button to
|
|
732
|
-
* `<body>`, losing the keyboard user's place for something that did not concern
|
|
733
|
-
* them.
|
|
734
|
-
*
|
|
735
|
-
* @stimeoRenderRoot
|
|
736
|
-
*/
|
|
737
1209
|
#reconcileOptions() {
|
|
738
1210
|
const visible = this.#visibleOptions;
|
|
739
1211
|
const active = this.#activeOption;
|
|
@@ -762,24 +1234,23 @@ var MultiSelectController = class extends Controller {
|
|
|
762
1234
|
const changed = !this.#sameSelection(previous, values);
|
|
763
1235
|
this.#selectionValues = values;
|
|
764
1236
|
if (!changed) return;
|
|
1237
|
+
const token = this.#moves.record();
|
|
765
1238
|
const options = new Map(selected.map((option) => [this.#optionValue(option), option]));
|
|
766
1239
|
for (const value of previous) {
|
|
1240
|
+
if (!this.#moves.isLatest(token)) return;
|
|
767
1241
|
if (!values.includes(value)) {
|
|
768
1242
|
this.#announceTransition(false, previousLabels.get(value) || value, value, values.length);
|
|
769
1243
|
}
|
|
770
1244
|
}
|
|
771
1245
|
for (const value of values) {
|
|
1246
|
+
if (!this.#moves.isLatest(token)) return;
|
|
772
1247
|
if (previous.includes(value)) continue;
|
|
773
1248
|
const option = options.get(value);
|
|
774
1249
|
this.#announceTransition(true, this.#optionLabel(option), value, values.length);
|
|
775
1250
|
}
|
|
1251
|
+
if (!this.#moves.isLatest(token)) return;
|
|
776
1252
|
this.dispatch("reconcile", { detail: { values } });
|
|
777
1253
|
}
|
|
778
|
-
/**
|
|
779
|
-
* Gives every option an explicit `aria-selected` and enforces the current cap.
|
|
780
|
-
* Priority preserves existing chip order at runtime; a fresh connection passes
|
|
781
|
-
* no priority, so deterministic option DOM order chooses the initial survivors.
|
|
782
|
-
*/
|
|
783
1254
|
#normalizeSelection(priority) {
|
|
784
1255
|
const selected = this.optionTargets.filter(
|
|
785
1256
|
(option) => option.getAttribute("aria-selected") === "true"
|
|
@@ -805,25 +1276,36 @@ var MultiSelectController = class extends Controller {
|
|
|
805
1276
|
option.setAttribute("aria-selected", String(kept.has(option)));
|
|
806
1277
|
}
|
|
807
1278
|
}
|
|
808
|
-
/**
|
|
809
|
-
* Tracks an input added initially or after connect, and makes it describe the
|
|
810
|
-
* widget that is actually on screen: a swapped-in input arrives with the
|
|
811
|
-
* authored ARIA of a fresh node while this controller still holds the popup
|
|
812
|
-
* state, and the open path cannot repair that (it seeds an active option only
|
|
813
|
-
* when there is none), so a live list would go unannounced.
|
|
814
|
-
*/
|
|
815
1279
|
inputTargetConnected(input) {
|
|
816
1280
|
this.#composition.observe(input);
|
|
817
|
-
|
|
818
|
-
const active = this.#activeOption;
|
|
819
|
-
if (active) input.setAttribute("aria-activedescendant", ensureId(active, "stimeo-ms-opt"));
|
|
820
|
-
else input.removeAttribute("aria-activedescendant");
|
|
1281
|
+
this.#describeInput(input);
|
|
821
1282
|
}
|
|
822
|
-
/** Removes composition listeners when the active input is replaced or removed. */
|
|
823
1283
|
inputTargetDisconnected(input) {
|
|
824
1284
|
this.#composition.unobserve(input);
|
|
1285
|
+
if (!this.inputTargets.includes(input)) {
|
|
1286
|
+
this.#expanded.return(input);
|
|
1287
|
+
this.#activeDescendant.return(input);
|
|
1288
|
+
}
|
|
1289
|
+
if (this.#connected && this.hasInputTarget) this.#describeInput(this.inputTarget);
|
|
1290
|
+
}
|
|
1291
|
+
#describeInput(input) {
|
|
1292
|
+
this.#expanded.write(input, String(!this.#isClosed));
|
|
1293
|
+
const active = this.#activeOption;
|
|
1294
|
+
this.#activeDescendant.write(input, active ? ensureId(active, "stimeo-ms-opt") : null);
|
|
1295
|
+
}
|
|
1296
|
+
#reconcileOpenState() {
|
|
1297
|
+
if (!this.hasListTarget) return;
|
|
1298
|
+
this.#adoptPageClose();
|
|
1299
|
+
const list = this.listTarget;
|
|
1300
|
+
if (list !== this.#openList) {
|
|
1301
|
+
this.#showList(list, this.#open);
|
|
1302
|
+
this.#openList = list;
|
|
1303
|
+
}
|
|
1304
|
+
if (this.hasInputTarget) this.#expanded.write(this.inputTarget, String(!list.hidden));
|
|
1305
|
+
}
|
|
1306
|
+
#adoptPageClose() {
|
|
1307
|
+
if (this.#openList?.hidden) this.#open = false;
|
|
825
1308
|
}
|
|
826
|
-
/** Filters confirmed input text, opens, and re-seeds the active option. */
|
|
827
1309
|
filter(event) {
|
|
828
1310
|
if (!this.hasInputTarget) return;
|
|
829
1311
|
if (event && this.#composition.consumesConfirmedInput(event)) return;
|
|
@@ -835,53 +1317,39 @@ var MultiSelectController = class extends Controller {
|
|
|
835
1317
|
}
|
|
836
1318
|
this.open();
|
|
837
1319
|
const visible = this.#visibleOptions;
|
|
838
|
-
this.#reflectEmpty();
|
|
839
1320
|
this.#setActive(visible[0] ?? null);
|
|
840
1321
|
this.dispatch("filter", { detail: { query } });
|
|
841
1322
|
}
|
|
842
|
-
/**
|
|
843
|
-
* Opens the list and activates the first visible option when none is active.
|
|
844
|
-
*
|
|
845
|
-
* Needs the input, which owns `aria-expanded` and `aria-activedescendant`: a
|
|
846
|
-
* list shown without one is a popup no assistive technology is told about. So
|
|
847
|
-
* opening is skipped entirely, where {@link MultiSelectController.close | close}
|
|
848
|
-
* still closes.
|
|
849
|
-
*/
|
|
850
1323
|
open() {
|
|
851
1324
|
if (!this.hasListTarget || !this.hasInputTarget) {
|
|
852
1325
|
this.#reflectEmpty();
|
|
853
1326
|
return;
|
|
854
1327
|
}
|
|
855
|
-
this.listTarget
|
|
856
|
-
this
|
|
1328
|
+
this.#showList(this.listTarget, true);
|
|
1329
|
+
this.#open = true;
|
|
1330
|
+
this.#openList = this.listTarget;
|
|
1331
|
+
this.#expanded.write(this.inputTarget, "true");
|
|
857
1332
|
if (!this.#activeOption) this.#setActive(this.#visibleOptions[0] ?? null);
|
|
858
1333
|
this.#reflectEmpty();
|
|
859
1334
|
}
|
|
860
|
-
/**
|
|
861
|
-
* Closes the list and clears the active option.
|
|
862
|
-
*
|
|
863
|
-
* Survives a missing input in both directions. `connect()` calls this second,
|
|
864
|
-
* so dereferencing the input here would throw before the chips, the roving
|
|
865
|
-
* seed, the chip listeners, the hidden fields and the outside-click listener —
|
|
866
|
-
* and Stimulus keeps the controller alive after that throw, so none of them
|
|
867
|
-
* would ever run and the selection would silently stop submitting. An input
|
|
868
|
-
* removed while the list is open must still let it come down *and* forget its
|
|
869
|
-
* active option, so only the `aria-expanded` write is guarded.
|
|
870
|
-
*/
|
|
871
1335
|
close() {
|
|
872
|
-
|
|
1336
|
+
this.#open = false;
|
|
1337
|
+
if (this.hasListTarget) {
|
|
1338
|
+
this.#showList(this.listTarget, false);
|
|
1339
|
+
this.#openList = this.listTarget;
|
|
1340
|
+
}
|
|
873
1341
|
this.#setActive(null);
|
|
874
|
-
if (this.hasInputTarget) this.inputTarget
|
|
1342
|
+
if (this.hasInputTarget) this.#expanded.write(this.inputTarget, "false");
|
|
875
1343
|
this.#reflectEmpty();
|
|
876
1344
|
}
|
|
877
|
-
/** Routes input keyboard interaction per the multi-select combobox model. */
|
|
878
1345
|
onKeydown(event) {
|
|
879
1346
|
if (event.defaultPrevented) return;
|
|
880
1347
|
if (isReservedArrowChord(event)) return;
|
|
1348
|
+
if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
|
|
881
1349
|
if (!this.hasInputTarget) return;
|
|
882
1350
|
if (this.#composition.isComposing(event)) return;
|
|
883
1351
|
this.#reconcileActiveForInteraction();
|
|
884
|
-
switch (logicalArrowKey(event.key, this
|
|
1352
|
+
switch (logicalArrowKey(event.key, this.#chipDirectionElement)) {
|
|
885
1353
|
case "ArrowDown":
|
|
886
1354
|
event.preventDefault();
|
|
887
1355
|
if (this.#isClosed) this.open();
|
|
@@ -939,39 +1407,23 @@ var MultiSelectController = class extends Controller {
|
|
|
939
1407
|
break;
|
|
940
1408
|
}
|
|
941
1409
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
* otherwise go dead until the user clicks back in ("focus stays on the input").
|
|
948
|
-
*/
|
|
949
|
-
toggleOption(event) {
|
|
950
|
-
const option = event.currentTarget.closest('[role="option"]');
|
|
1410
|
+
toggleOption(source) {
|
|
1411
|
+
const { event, host, origin, reason } = actionSource(source);
|
|
1412
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
1413
|
+
return;
|
|
1414
|
+
const option = host?.closest(targetSelector(this.identifier, "option"));
|
|
951
1415
|
if (!option || !this.optionTargets.includes(option)) return;
|
|
952
|
-
this
|
|
953
|
-
this.#
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
* `data-action`. Throwing here would leave the chip removed but focus stranded
|
|
961
|
-
* on a detached button.
|
|
962
|
-
*/
|
|
1416
|
+
const focus = event !== null || this.element.contains(document.activeElement);
|
|
1417
|
+
if (!this.#toggleSelection(option, reason)) return;
|
|
1418
|
+
if (focus && (event !== null || this.element.contains(document.activeElement)))
|
|
1419
|
+
this.#focusInput();
|
|
1420
|
+
}
|
|
1421
|
+
get #chipDirectionElement() {
|
|
1422
|
+
return this.hasTagsTarget ? this.tagsTarget : this.element;
|
|
1423
|
+
}
|
|
963
1424
|
#focusInput() {
|
|
964
1425
|
if (this.hasInputTarget) this.inputTarget.focus();
|
|
965
1426
|
}
|
|
966
|
-
/**
|
|
967
|
-
* Re-homes focus after the last chip was removed: to the input, else the root.
|
|
968
|
-
*
|
|
969
|
-
* Unlike the other {@link #focusInput} callers, the element that held focus has
|
|
970
|
-
* just left the DOM, so "leave it alone" is not an option — the browser already
|
|
971
|
-
* dropped it to `<body>`. The root borrows a `tabindex="-1"` just-in-time (not a
|
|
972
|
-
* Tab stop, handed back before Turbo caches or on teardown). Focus that landed
|
|
973
|
-
* on a real element is left alone, so a chip removed out of band never steals it.
|
|
974
|
-
*/
|
|
975
1427
|
#focusAfterLastTag() {
|
|
976
1428
|
if (this.hasInputTarget) {
|
|
977
1429
|
this.inputTarget.focus();
|
|
@@ -985,15 +1437,9 @@ var MultiSelectController = class extends Controller {
|
|
|
985
1437
|
this.#tabindex.lend(this.element);
|
|
986
1438
|
this.element.focus();
|
|
987
1439
|
}
|
|
988
|
-
/**
|
|
989
|
-
* Returns the borrowed tab stop. Owning the borrow is not enough — the value
|
|
990
|
-
* has to still be the one this instance wrote, since a consumer that changed it
|
|
991
|
-
* afterwards owns it now.
|
|
992
|
-
*/
|
|
993
1440
|
#releaseTabindex() {
|
|
994
1441
|
this.#tabindex.returnAll();
|
|
995
1442
|
}
|
|
996
|
-
/** Moves the active option by `delta` among visible options, wrapping. */
|
|
997
1443
|
#moveActive(delta) {
|
|
998
1444
|
const visible = this.#visibleOptions;
|
|
999
1445
|
const active = this.#activeOption;
|
|
@@ -1002,41 +1448,27 @@ var MultiSelectController = class extends Controller {
|
|
|
1002
1448
|
const next = (candidate + visible.length) % visible.length;
|
|
1003
1449
|
this.#setActive(visible[next] ?? null);
|
|
1004
1450
|
}
|
|
1005
|
-
|
|
1006
|
-
#toggleSelection(option) {
|
|
1451
|
+
#toggleSelection(option, reason = "user") {
|
|
1007
1452
|
const selected = option.getAttribute("aria-selected") === "true";
|
|
1008
1453
|
const limit = this.#selectionLimit;
|
|
1009
1454
|
if (!selected && limit > 0 && this.#selectedOptions.length >= limit) {
|
|
1010
|
-
return;
|
|
1455
|
+
return true;
|
|
1011
1456
|
}
|
|
1012
1457
|
if (selected) {
|
|
1013
1458
|
option.setAttribute("aria-selected", "false");
|
|
1014
1459
|
this.#removeTagFor(option);
|
|
1015
1460
|
} else {
|
|
1016
|
-
if (this.hasTagTemplateTarget && !this.#appendTag(option)) return;
|
|
1461
|
+
if (this.hasTagTemplateTarget && !this.#appendTag(option)) return true;
|
|
1017
1462
|
option.setAttribute("aria-selected", "true");
|
|
1018
1463
|
}
|
|
1019
1464
|
this.#refreshRoving();
|
|
1020
|
-
this.#
|
|
1021
|
-
const values = this.#values;
|
|
1022
|
-
this.#selectionValues = values;
|
|
1023
|
-
this.#announceTransition(
|
|
1465
|
+
return this.#publishSelection(
|
|
1024
1466
|
!selected,
|
|
1025
1467
|
this.#optionLabel(option),
|
|
1026
1468
|
this.#optionValue(option),
|
|
1027
|
-
|
|
1469
|
+
reason
|
|
1028
1470
|
);
|
|
1029
|
-
|
|
1030
|
-
}
|
|
1031
|
-
/**
|
|
1032
|
-
* Re-reads each chip's label from its option, in place.
|
|
1033
|
-
*
|
|
1034
|
-
* The value order alone does not say the chips are still correct: a server can
|
|
1035
|
-
* re-render the same candidate with a new label ("Apple" → "Green Apple"), and
|
|
1036
|
-
* the chip text and its `Remove {label}` name are both derived from the option.
|
|
1037
|
-
* Updating them here keeps the rebuild — which would drop focus — for the case
|
|
1038
|
-
* that actually needs it, a changed selection.
|
|
1039
|
-
*/
|
|
1471
|
+
}
|
|
1040
1472
|
#refreshTagLabels(selected) {
|
|
1041
1473
|
const options = new Map(selected.map((option) => [this.#optionValue(option), option]));
|
|
1042
1474
|
for (const tag of this.tagTargets) {
|
|
@@ -1047,10 +1479,9 @@ var MultiSelectController = class extends Controller {
|
|
|
1047
1479
|
const name = this.#removeName(text, this.#optionValue(option));
|
|
1048
1480
|
if (!label || !button || !name) continue;
|
|
1049
1481
|
if (readLabel(label) !== text) writeLabel(label, text);
|
|
1050
|
-
|
|
1482
|
+
setAttributeIfChanged(button, "aria-label", name);
|
|
1051
1483
|
}
|
|
1052
1484
|
}
|
|
1053
|
-
/** Builds one chip from the template for `option`. */
|
|
1054
1485
|
#appendTag(option) {
|
|
1055
1486
|
if (!this.hasTagsTarget) {
|
|
1056
1487
|
this.#rows.report('a "tags" target to append the chip to');
|
|
@@ -1061,14 +1492,6 @@ var MultiSelectController = class extends Controller {
|
|
|
1061
1492
|
this.tagsTarget.appendChild(chip);
|
|
1062
1493
|
return true;
|
|
1063
1494
|
}
|
|
1064
|
-
/**
|
|
1065
|
-
* Builds one fully named chip without mutating the live tag row, or `null`
|
|
1066
|
-
* when the authored template cannot produce one — in which case the selection
|
|
1067
|
-
* stays untouched and the row names the missing part once per connection.
|
|
1068
|
-
* Both callers establish the template first: a field authored without
|
|
1069
|
-
* `tagTemplate` renders no chips at all — a supported configuration — and
|
|
1070
|
-
* never reaches here.
|
|
1071
|
-
*/
|
|
1072
1495
|
#buildTag(option) {
|
|
1073
1496
|
const text = this.#optionLabel(option);
|
|
1074
1497
|
const value = this.#optionValue(option);
|
|
@@ -1079,19 +1502,16 @@ var MultiSelectController = class extends Controller {
|
|
|
1079
1502
|
row.button.tabIndex = -1;
|
|
1080
1503
|
return row.root;
|
|
1081
1504
|
}
|
|
1082
|
-
/** Expands the current template's localized remove-button name. */
|
|
1083
1505
|
#removeName(label, value) {
|
|
1084
1506
|
if (!this.hasTagTemplateTarget) return null;
|
|
1085
1507
|
const template = this.tagTemplateTarget.content.querySelector(`button${this.#rows.selector("remove")}`)?.getAttribute("aria-label")?.trim();
|
|
1086
1508
|
return template ? fillTemplate(template, { label, value }) : null;
|
|
1087
1509
|
}
|
|
1088
|
-
/** Removes the chip mirroring `option`, if present. */
|
|
1089
1510
|
#removeTagFor(option) {
|
|
1090
1511
|
const value = this.#optionValue(option);
|
|
1091
1512
|
const tag = this.tagTargets.find((candidate) => candidate.dataset.value === value);
|
|
1092
1513
|
tag?.remove();
|
|
1093
1514
|
}
|
|
1094
|
-
/** Removes chip `index` and deselects its option, re-homing focus. */
|
|
1095
1515
|
#removeTagAt(index, focus = "neighbor") {
|
|
1096
1516
|
const tag = this.tagTargets[index];
|
|
1097
1517
|
if (!tag) return;
|
|
@@ -1100,31 +1520,13 @@ var MultiSelectController = class extends Controller {
|
|
|
1100
1520
|
if (option) option.setAttribute("aria-selected", "false");
|
|
1101
1521
|
tag.remove();
|
|
1102
1522
|
this.#refreshRoving();
|
|
1103
|
-
this.#
|
|
1104
|
-
const values = this.#values;
|
|
1105
|
-
this.#selectionValues = values;
|
|
1106
|
-
this.#announceTransition(
|
|
1107
|
-
false,
|
|
1108
|
-
option ? this.#optionLabel(option) : value,
|
|
1109
|
-
value,
|
|
1110
|
-
values.length
|
|
1111
|
-
);
|
|
1112
|
-
this.dispatch("change", { detail: { values } });
|
|
1523
|
+
if (!this.#publishSelection(false, option ? this.#optionLabel(option) : value, value)) return;
|
|
1113
1524
|
if (focus === "input") {
|
|
1114
1525
|
this.#focusInput();
|
|
1115
1526
|
return;
|
|
1116
1527
|
}
|
|
1117
1528
|
if (!this.#chipRow.focusAfterRemoval(index)) this.#focusAfterLastTag();
|
|
1118
1529
|
}
|
|
1119
|
-
/**
|
|
1120
|
-
* Marks `option` active via `data-active` and the input's
|
|
1121
|
-
* `aria-activedescendant` (the attribute is removed, not emptied, when null).
|
|
1122
|
-
*
|
|
1123
|
-
* The state half runs even with no input, so a `close()` that cannot touch ARIA
|
|
1124
|
-
* still clears it: {@link MultiSelectController.open | open} seeds an active
|
|
1125
|
-
* option only when there is none, so a stale one makes the next open skip the
|
|
1126
|
-
* seeding and a replacement input gets no `aria-activedescendant` at all.
|
|
1127
|
-
*/
|
|
1128
1530
|
#setActive(option) {
|
|
1129
1531
|
const activeId = option ? ensureId(option, "stimeo-ms-opt") : null;
|
|
1130
1532
|
this.#activeOptionId = activeId;
|
|
@@ -1132,14 +1534,8 @@ var MultiSelectController = class extends Controller {
|
|
|
1132
1534
|
candidate.toggleAttribute("data-active", candidate === option);
|
|
1133
1535
|
}
|
|
1134
1536
|
if (option && this.hasListTarget) scrollOptionIntoView(this.listTarget, option);
|
|
1135
|
-
if (
|
|
1136
|
-
if (activeId !== null) {
|
|
1137
|
-
this.inputTarget.setAttribute("aria-activedescendant", activeId);
|
|
1138
|
-
} else {
|
|
1139
|
-
this.inputTarget.removeAttribute("aria-activedescendant");
|
|
1140
|
-
}
|
|
1537
|
+
if (this.hasInputTarget) this.#activeDescendant.write(this.inputTarget, activeId);
|
|
1141
1538
|
}
|
|
1142
|
-
/** Repairs only active identity before a key; the fallback waits for the target callback. */
|
|
1143
1539
|
#reconcileActiveForInteraction() {
|
|
1144
1540
|
const activeId = this.#activeOptionId;
|
|
1145
1541
|
if (activeId === null) return;
|
|
@@ -1151,88 +1547,84 @@ var MultiSelectController = class extends Controller {
|
|
|
1151
1547
|
this.#setActive(active);
|
|
1152
1548
|
}
|
|
1153
1549
|
}
|
|
1154
|
-
/**
|
|
1155
|
-
* Reflects whether the open list currently has no visible option targets, on
|
|
1156
|
-
* the root's state attribute and on the regions declared for that state.
|
|
1157
|
-
*/
|
|
1158
1550
|
#reflectEmpty() {
|
|
1159
1551
|
const empty = !this.#isClosed && this.#visibleOptions.length === 0;
|
|
1160
1552
|
this.element.toggleAttribute(`data-${this.identifier}-empty`, empty);
|
|
1161
1553
|
this.#emptyRegion.reflect(this.element, empty);
|
|
1162
1554
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
* target so the selection submits with a normal form (parity with tags-input).
|
|
1166
|
-
* No-ops without a `fields` target. When the
|
|
1167
|
-
* `form` value is set, each input gets a matching `form` attribute so the picker
|
|
1168
|
-
* can submit with a `<form>` it lives outside of.
|
|
1169
|
-
*/
|
|
1170
|
-
#syncFields(notify = false) {
|
|
1171
|
-
if (!this.hasFieldsTarget) return;
|
|
1555
|
+
#syncFields() {
|
|
1556
|
+
if (!this.hasFieldsTarget) return false;
|
|
1172
1557
|
const options = { name: this.nameValue, form: this.formValue };
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1558
|
+
return writeFields(this.fieldsTarget, this.#values, options);
|
|
1559
|
+
}
|
|
1560
|
+
#publishSelection(selected, label, value, reason = "user") {
|
|
1561
|
+
const values = this.#values;
|
|
1562
|
+
const changed = !this.#sameSelection(this.#selectionValues, values);
|
|
1563
|
+
const fieldChanged = this.#syncFields();
|
|
1564
|
+
this.#selectionValues = values;
|
|
1565
|
+
if (!changed) return true;
|
|
1566
|
+
const token = this.#moves.record();
|
|
1567
|
+
if (fieldChanged) commitField(this.fieldsTarget);
|
|
1568
|
+
if (!this.#moves.isLatest(token)) return false;
|
|
1569
|
+
this.#announceTransition(selected, label, value, values.length);
|
|
1570
|
+
if (!this.#moves.isLatest(token)) return false;
|
|
1571
|
+
this.dispatch("change", { detail: { values, reason } });
|
|
1572
|
+
return this.#moves.isLatest(token);
|
|
1176
1573
|
}
|
|
1177
|
-
/** Keeps exactly one chip remove button tabbable after the set changes. */
|
|
1178
1574
|
#refreshRoving() {
|
|
1179
1575
|
this.#chipRow.ensureTabStop();
|
|
1180
1576
|
}
|
|
1181
|
-
/**
|
|
1182
|
-
* Sends one localized selection transition through the page's shared announcer.
|
|
1183
|
-
*
|
|
1184
|
-
* @stimeoRuntimeOnly The texts word the one announcement of this change.
|
|
1185
|
-
*/
|
|
1186
1577
|
#announceTransition(selected, label, value, count) {
|
|
1187
1578
|
const template = selected ? this.announceTextValue : this.announceRemovedTextValue;
|
|
1188
1579
|
announce(fillTemplate(template, { label, value, count }));
|
|
1189
1580
|
}
|
|
1190
|
-
/** Whether two arrays represent the same selection set, independent of DOM order. */
|
|
1191
1581
|
#sameSelection(left, right) {
|
|
1192
1582
|
if (left.length !== right.length) return false;
|
|
1193
1583
|
const a = [...left].sort();
|
|
1194
1584
|
const b = [...right].sort();
|
|
1195
1585
|
return a.every((value, index) => value === b[index]);
|
|
1196
1586
|
}
|
|
1197
|
-
/** Closes the list on a click outside the controller element. */
|
|
1198
1587
|
#onOutsideClick = (event) => {
|
|
1199
1588
|
if (!this.#isClosed && !this.element.contains(event.target)) this.close();
|
|
1200
1589
|
};
|
|
1201
|
-
/** Trimmed visible label of an option. */
|
|
1202
1590
|
#optionLabel(option) {
|
|
1203
1591
|
return (option.textContent ?? "").trim();
|
|
1204
1592
|
}
|
|
1205
|
-
/** An option's stable value: its `data-value`, else its display label. */
|
|
1206
1593
|
#optionValue(option) {
|
|
1207
1594
|
return option.dataset.value ?? this.#optionLabel(option);
|
|
1208
1595
|
}
|
|
1209
|
-
/** Options not hidden by the current filter. */
|
|
1210
1596
|
get #visibleOptions() {
|
|
1211
1597
|
return this.optionTargets.filter((option) => !option.hidden);
|
|
1212
1598
|
}
|
|
1213
|
-
/** Current active target resolved by stable id, never a detached node reference. */
|
|
1214
1599
|
get #activeOption() {
|
|
1215
1600
|
return this.optionTargets.find((option) => option.id === this.#activeOptionId) ?? null;
|
|
1216
1601
|
}
|
|
1217
|
-
/** Options currently selected. */
|
|
1218
1602
|
get #selectedOptions() {
|
|
1219
1603
|
return this.optionTargets.filter((option) => option.getAttribute("aria-selected") === "true");
|
|
1220
1604
|
}
|
|
1221
|
-
/** Selected values in option order. */
|
|
1222
1605
|
get #values() {
|
|
1223
1606
|
return this.#selectedOptions.map((option) => this.#optionValue(option));
|
|
1224
1607
|
}
|
|
1225
|
-
/** Normalized cardinality cap: zero and below are unlimited; a positive value floors, never below 1. */
|
|
1226
1608
|
get #selectionLimit() {
|
|
1227
|
-
if (
|
|
1228
|
-
return Math.max(1, Math.floor(this
|
|
1609
|
+
if (this.#safeMax <= 0) return 0;
|
|
1610
|
+
return Math.max(1, Math.floor(this.#safeMax));
|
|
1611
|
+
}
|
|
1612
|
+
#showList(list, open) {
|
|
1613
|
+
this.#listHidden.write(list, open ? null : "");
|
|
1229
1614
|
}
|
|
1230
|
-
/** Whether the list is currently hidden. */
|
|
1231
1615
|
get #isClosed() {
|
|
1232
1616
|
return !this.hasListTarget || this.listTarget.hidden !== false;
|
|
1233
1617
|
}
|
|
1618
|
+
get #safeMax() {
|
|
1619
|
+
return this.#numbers.read(
|
|
1620
|
+
this,
|
|
1621
|
+
"max",
|
|
1622
|
+
this.maxValue,
|
|
1623
|
+
_MultiSelectController.values.max.default,
|
|
1624
|
+
_MultiSelectController.valueConstraints.max
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1234
1627
|
};
|
|
1235
1628
|
|
|
1236
1629
|
export { MultiSelectController };
|
|
1237
|
-
//# sourceMappingURL=multi_select_controller.js.map
|
|
1238
1630
|
//# sourceMappingURL=multi_select_controller.js.map
|