stimeo-ui 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +209 -0
- package/README.md +128 -1
- package/dist/cable/index.d.ts +79 -23
- package/dist/cable/index.js +392 -417
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +34 -5
- package/dist/controllers/accordion_controller.js +228 -28
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +20 -9
- package/dist/controllers/alert_dialog_controller.js +862 -221
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +38 -7
- package/dist/controllers/announcer_controller.js +135 -191
- 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 +295 -110
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +172 -106
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +10 -7
- package/dist/controllers/breadcrumb_controller.js +0 -129
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +22 -4
- package/dist/controllers/bulk_select_controller.js +264 -53
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +109 -28
- package/dist/controllers/calendar_controller.js +346 -180
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +99 -27
- package/dist/controllers/carousel_controller.js +587 -300
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +18 -2
- package/dist/controllers/character_counter_controller.js +271 -143
- 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 +77 -75
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +44 -7
- package/dist/controllers/clipboard_controller.js +187 -108
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +41 -5
- package/dist/controllers/collapsible_controller.js +374 -108
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +47 -14
- package/dist/controllers/color_picker_controller.js +183 -127
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +33 -6
- package/dist/controllers/combobox_controller.js +426 -109
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +45 -14
- package/dist/controllers/command_palette_controller.js +994 -350
- 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 +212 -125
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +49 -12
- package/dist/controllers/confirm_controller.js +939 -245
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +36 -3
- package/dist/controllers/context_menu_controller.js +275 -127
- 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 +75 -2
- package/dist/controllers/countdown_controller.js +235 -97
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +93 -15
- package/dist/controllers/currency_input_controller.js +241 -118
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +78 -22
- package/dist/controllers/data_grid_controller.js +229 -135
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +64 -11
- package/dist/controllers/date_range_picker_controller.js +359 -165
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +22 -3
- package/dist/controllers/dialog_controller.js +887 -225
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +24 -7
- package/dist/controllers/direct_upload_controller.js +80 -142
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +2 -2
- package/dist/controllers/dirty_form_controller.js +26 -47
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +19 -0
- package/dist/controllers/dismissible_controller.js +175 -20
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +52 -16
- package/dist/controllers/drawer_controller.js +963 -336
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +39 -3
- package/dist/controllers/dropdown_controller.js +342 -85
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +182 -58
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +67 -53
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +60 -18
- package/dist/controllers/file_dropzone_controller.js +426 -229
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +10 -4
- package/dist/controllers/filter_controller.js +78 -36
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +57 -14
- package/dist/controllers/flash_controller.js +328 -211
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +12 -3
- package/dist/controllers/focus_controller.js +654 -239
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +153 -136
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +29 -96
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +41 -9
- package/dist/controllers/frame_loading_controller.js +263 -198
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +22 -2
- package/dist/controllers/highlight_controller.js +119 -70
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +65 -4
- package/dist/controllers/hover_card_controller.js +335 -118
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +58 -4
- package/dist/controllers/idle_controller.js +316 -65
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +5 -2
- package/dist/controllers/input_mask_controller.js +116 -61
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +32 -13
- package/dist/controllers/intersection_controller.js +147 -103
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +16 -0
- package/dist/controllers/lazy_frame_controller.js +9 -56
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +84 -10
- package/dist/controllers/listbox_controller.js +413 -153
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +1 -1
- package/dist/controllers/local_time_controller.js +54 -64
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +11 -3
- package/dist/controllers/masonry_controller.js +70 -95
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +40 -9
- package/dist/controllers/menu_controller.js +254 -167
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +15 -4
- package/dist/controllers/menubar_controller.js +206 -331
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +20 -0
- package/dist/controllers/meter_controller.js +159 -71
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +50 -7
- package/dist/controllers/multi_select_controller.js +797 -388
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +19 -1
- package/dist/controllers/navigation_menu_controller.js +156 -231
- 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 +373 -174
- 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 +241 -45
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +62 -10
- package/dist/controllers/number_input_controller.js +387 -227
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +28 -75
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +38 -7
- package/dist/controllers/otp_controller.js +344 -196
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +13 -12
- package/dist/controllers/overflow_indicator_controller.js +167 -93
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +66 -9
- package/dist/controllers/overflow_menu_controller.js +408 -277
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +52 -15
- package/dist/controllers/pagination_controller.js +342 -120
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +60 -9
- package/dist/controllers/password_reveal_controller.js +424 -99
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +14 -5
- package/dist/controllers/password_strength_controller.js +99 -153
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +11 -4
- package/dist/controllers/persist_controller.js +252 -96
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +19 -7
- package/dist/controllers/pointer_drag_controller.js +263 -164
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +46 -4
- package/dist/controllers/popover_controller.js +267 -94
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +44 -4
- package/dist/controllers/portal_controller.js +209 -72
- 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 -112
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +11 -0
- package/dist/controllers/progress_controller.js +132 -59
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +19 -8
- package/dist/controllers/radio_group_controller.js +138 -133
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +92 -4
- package/dist/controllers/range_slider_controller.js +245 -112
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +33 -6
- package/dist/controllers/rating_controller.js +429 -194
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +26 -2
- package/dist/controllers/read_more_controller.js +185 -47
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +1 -1
- package/dist/controllers/reading_progress_controller.js +147 -135
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +18 -4
- package/dist/controllers/relative_time_controller.js +145 -110
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
- package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- package/dist/controllers/reset_on_restore_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +44 -4
- package/dist/controllers/resizable_controller.js +294 -118
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +39 -84
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -3
- package/dist/controllers/scroll_area_controller.js +428 -221
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +0 -53
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
- package/dist/controllers/scroll_visibility_controller.js +395 -146
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +9 -3
- package/dist/controllers/scrollspy_controller.js +181 -236
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +59 -17
- package/dist/controllers/separator_controller.js +288 -132
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +81 -7
- package/dist/controllers/sidebar_controller.js +1071 -375
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +23 -0
- package/dist/controllers/skeleton_controller.js +248 -92
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +72 -8
- package/dist/controllers/slider_controller.js +211 -96
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
- package/dist/controllers/smart_sticky_header_controller.js +163 -54
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +13 -110
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +39 -4
- package/dist/controllers/spinner_controller.js +277 -161
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +24 -17
- package/dist/controllers/step_indicator_controller.js +107 -53
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +43 -9
- package/dist/controllers/stepper_controller.js +171 -48
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +25 -0
- package/dist/controllers/stick_to_bottom_controller.js +310 -116
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +11 -0
- package/dist/controllers/sticky_observer_controller.js +214 -49
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +22 -6
- package/dist/controllers/submit_once_controller.js +294 -192
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +40 -7
- package/dist/controllers/switch_controller.js +107 -43
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +13 -1
- package/dist/controllers/tabs_controller.js +50 -25
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +27 -3
- package/dist/controllers/tags_input_controller.js +290 -149
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
- package/dist/controllers/textarea_autosize_controller.js +71 -58
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +37 -8
- package/dist/controllers/theme_controller.js +148 -80
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +18 -1
- package/dist/controllers/time_picker_controller.js +162 -81
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +85 -23
- package/dist/controllers/toast_controller.js +365 -193
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +60 -9
- package/dist/controllers/toggle_group_controller.js +194 -99
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +92 -90
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +52 -2
- package/dist/controllers/tooltip_controller.js +185 -125
- 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 -115
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +59 -18
- package/dist/controllers/tree_view_controller.js +248 -290
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +33 -3
- package/dist/index.js +8649 -8572
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +95 -9
- package/dist/inspector/cli.js +343 -77
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +390 -96
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +45 -45
- package/dist/inspector/manifest.json +1308 -155
- package/dist/positioning/index.d.ts +8 -0
- package/dist/positioning/index.js +148 -45
- package/dist/positioning/index.js.map +1 -1
- package/package.json +7 -7
- 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,27 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
4
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
5
|
+
function stateReasonFor(event) {
|
|
6
|
+
if (!event) return "api";
|
|
7
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
8
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
9
|
+
return "user";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function actionSource(source) {
|
|
13
|
+
if (source instanceof HTMLElement) {
|
|
14
|
+
return { event: null, host: source, origin: source, reason: "api" };
|
|
15
|
+
}
|
|
16
|
+
const event = source ?? null;
|
|
17
|
+
return {
|
|
18
|
+
event,
|
|
19
|
+
host: event?.currentTarget instanceof Element ? event.currentTarget : null,
|
|
20
|
+
origin: event?.target instanceof Element ? event.target : null,
|
|
21
|
+
reason: stateReasonFor(event)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
4
24
|
|
|
5
|
-
// src/utils/announce.ts
|
|
6
25
|
function announce(message, options = {}) {
|
|
7
26
|
const text = message.trim();
|
|
8
27
|
if (text.length === 0) return;
|
|
@@ -19,13 +38,22 @@ function fillTemplate(template, values) {
|
|
|
19
38
|
});
|
|
20
39
|
}
|
|
21
40
|
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
function sharedRegistry(key, create) {
|
|
42
|
+
const symbol = Symbol.for(key);
|
|
43
|
+
const scope = globalThis;
|
|
44
|
+
const existing = scope[symbol];
|
|
45
|
+
if (existing !== void 0) return existing;
|
|
46
|
+
const registry = create();
|
|
47
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
48
|
+
return registry;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
var ids = sharedRegistry("stimeo-ui.aria-ids.registry.v1", () => ({ counter: 0 }));
|
|
24
52
|
function uniqueId(prefix = "stimeo") {
|
|
25
53
|
let candidate;
|
|
26
54
|
do {
|
|
27
|
-
counter += 1;
|
|
28
|
-
candidate = `${prefix}-${counter}`;
|
|
55
|
+
ids.counter += 1;
|
|
56
|
+
candidate = `${prefix}-${ids.counter}`;
|
|
29
57
|
} while (typeof document !== "undefined" && document.getElementById(candidate) !== null);
|
|
30
58
|
return candidate;
|
|
31
59
|
}
|
|
@@ -36,12 +64,10 @@ function ensureId(element, prefix = "stimeo") {
|
|
|
36
64
|
return id;
|
|
37
65
|
}
|
|
38
66
|
|
|
39
|
-
// src/utils/logical_scroll.ts
|
|
40
67
|
function isRtl(element) {
|
|
41
68
|
return window.getComputedStyle(element).direction === "rtl";
|
|
42
69
|
}
|
|
43
70
|
|
|
44
|
-
// src/utils/arrow_step.ts
|
|
45
71
|
function logicalArrowKey(key, element) {
|
|
46
72
|
if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
|
|
47
73
|
if (!isRtl(element)) return key;
|
|
@@ -52,31 +78,177 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
52
78
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
53
79
|
}
|
|
54
80
|
|
|
55
|
-
|
|
81
|
+
function isAttributeValue(value) {
|
|
82
|
+
return value === null || typeof value === "string";
|
|
83
|
+
}
|
|
84
|
+
function readLeaseRecord(element, record) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
87
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
88
|
+
const [original, written, ...rest] = parsed;
|
|
89
|
+
const linked = rest[0] !== false;
|
|
90
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
91
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
92
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
93
|
+
} catch {
|
|
94
|
+
return void 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
var AttributeLease = class _AttributeLease {
|
|
98
|
+
static #registry = sharedRegistry(
|
|
99
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
100
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
101
|
+
);
|
|
102
|
+
#attribute;
|
|
103
|
+
#record;
|
|
104
|
+
#base;
|
|
105
|
+
#held = /* @__PURE__ */ new Set();
|
|
106
|
+
constructor(attribute, owner, options = {}) {
|
|
107
|
+
this.#attribute = attribute;
|
|
108
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
109
|
+
this.#base = options.base === true;
|
|
110
|
+
const names = _AttributeLease.#registry.names;
|
|
111
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
112
|
+
}
|
|
113
|
+
write(element, value) {
|
|
114
|
+
const stack = this.#stack(element);
|
|
115
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
116
|
+
this.#held.add(element);
|
|
117
|
+
const shows = !this.#covered(element, stack, hold);
|
|
118
|
+
hold.written = value;
|
|
119
|
+
this.#mark(element, stack);
|
|
120
|
+
if (shows) this.#reflect(element, value);
|
|
121
|
+
}
|
|
122
|
+
return(element) {
|
|
123
|
+
const held = this.#held.delete(element);
|
|
124
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
125
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
126
|
+
const hold = this.#take(stack);
|
|
127
|
+
if (!hold) return false;
|
|
128
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
129
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
130
|
+
const index = stack.indexOf(hold);
|
|
131
|
+
stack.splice(index, 1);
|
|
132
|
+
const above = stack[index];
|
|
133
|
+
if (above) above.linked &&= hold.linked;
|
|
134
|
+
this.#mark(element, stack);
|
|
135
|
+
if (!owns) return true;
|
|
136
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
137
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
returnAll() {
|
|
141
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
142
|
+
}
|
|
143
|
+
#stack(element) {
|
|
144
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
145
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
146
|
+
stacks.set(element, byAttribute);
|
|
147
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
148
|
+
byAttribute.set(this.#attribute, stack);
|
|
149
|
+
return stack;
|
|
150
|
+
}
|
|
151
|
+
#seed(element) {
|
|
152
|
+
const found = [];
|
|
153
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
154
|
+
const read = readLeaseRecord(element, record);
|
|
155
|
+
if (read) found.push({ ...read, record });
|
|
156
|
+
}
|
|
157
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
158
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
159
|
+
lease: null,
|
|
160
|
+
record,
|
|
161
|
+
written,
|
|
162
|
+
original,
|
|
163
|
+
linked: linked && index > 0
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
#take(stack) {
|
|
167
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
168
|
+
if (hold) hold.lease = this;
|
|
169
|
+
return hold;
|
|
170
|
+
}
|
|
171
|
+
#acquire(element, stack) {
|
|
172
|
+
const current = element.getAttribute(this.#attribute);
|
|
173
|
+
const top = stack.at(-1);
|
|
174
|
+
const bottom = stack[0];
|
|
175
|
+
const over = top?.written === current ? top : void 0;
|
|
176
|
+
const from = this.#base ? bottom : over;
|
|
177
|
+
const hold = {
|
|
178
|
+
lease: this,
|
|
179
|
+
record: this.#record,
|
|
180
|
+
written: current,
|
|
181
|
+
original: from ? from.original : current,
|
|
182
|
+
linked: !this.#base && over !== void 0
|
|
183
|
+
};
|
|
184
|
+
if (this.#base && bottom) {
|
|
185
|
+
bottom.linked = true;
|
|
186
|
+
stack.unshift(hold);
|
|
187
|
+
} else stack.push(hold);
|
|
188
|
+
return hold;
|
|
189
|
+
}
|
|
190
|
+
#covered(element, stack, hold) {
|
|
191
|
+
const current = element.getAttribute(this.#attribute);
|
|
192
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
193
|
+
}
|
|
194
|
+
#mark(element, stack) {
|
|
195
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
196
|
+
stack.forEach((hold, index) => {
|
|
197
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
198
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
199
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
200
|
+
records.set(
|
|
201
|
+
hold.record,
|
|
202
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
for (const [name, recorded] of records) {
|
|
206
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
207
|
+
if (recorded === null) element.removeAttribute(name);
|
|
208
|
+
else element.setAttribute(name, recorded);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
#reflect(element, value) {
|
|
212
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
213
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
214
|
+
else element.setAttribute(this.#attribute, value);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
function matchingPart(root, selector) {
|
|
219
|
+
return root.matches(selector) ? root : root.querySelector(selector);
|
|
220
|
+
}
|
|
221
|
+
function writeLabel(slot, text) {
|
|
222
|
+
if (slot.childElementCount === 0) {
|
|
223
|
+
slot.textContent = text;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
let first = null;
|
|
227
|
+
for (const node of Array.from(slot.childNodes)) {
|
|
228
|
+
if (node.nodeType !== Node.TEXT_NODE) continue;
|
|
229
|
+
if (first) node.remove();
|
|
230
|
+
else first = node;
|
|
231
|
+
}
|
|
232
|
+
if (first) first.nodeValue = text;
|
|
233
|
+
else slot.prepend(text);
|
|
234
|
+
}
|
|
235
|
+
function readLabel(slot) {
|
|
236
|
+
if (!slot) return "";
|
|
237
|
+
let text = "";
|
|
238
|
+
for (const node of Array.from(slot.childNodes)) {
|
|
239
|
+
if (node.nodeType === Node.TEXT_NODE) text += node.nodeValue ?? "";
|
|
240
|
+
}
|
|
241
|
+
return text.trim();
|
|
242
|
+
}
|
|
243
|
+
|
|
56
244
|
var RovingTabindex = class {
|
|
57
|
-
/** Returns the current ordered item elements; called on every operation. */
|
|
58
245
|
#getItems;
|
|
59
|
-
/**
|
|
60
|
-
* @param getItems - Returns the current ordered item elements. Called on every
|
|
61
|
-
* operation so the live target list is always used.
|
|
62
|
-
*/
|
|
63
246
|
constructor(getItems) {
|
|
64
247
|
this.#getItems = getItems;
|
|
65
248
|
}
|
|
66
|
-
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
67
249
|
get activeIndex() {
|
|
68
250
|
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
69
251
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
72
|
-
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
73
|
-
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
74
|
-
* "nothing is currently tabbable".
|
|
75
|
-
*
|
|
76
|
-
* @param index - Position of the item to make tabbable.
|
|
77
|
-
* @param options - Pass `{ focus: true }` to also move DOM focus to that item,
|
|
78
|
-
* and `items` to reuse an event-scoped collection snapshot.
|
|
79
|
-
*/
|
|
80
252
|
setActive(index, options = {}) {
|
|
81
253
|
const { focus = false } = options;
|
|
82
254
|
const items = options.items ?? this.#getItems();
|
|
@@ -87,7 +259,6 @@ var RovingTabindex = class {
|
|
|
87
259
|
}
|
|
88
260
|
};
|
|
89
261
|
|
|
90
|
-
// src/utils/chip_row.ts
|
|
91
262
|
var ChipRow = class {
|
|
92
263
|
#directionElement;
|
|
93
264
|
#getItems;
|
|
@@ -99,14 +270,10 @@ var ChipRow = class {
|
|
|
99
270
|
constructor(options) {
|
|
100
271
|
this.#directionElement = options.directionElement;
|
|
101
272
|
this.#getItems = options.getItems;
|
|
102
|
-
this.#getButton = options.getButton ?? ((item) => item
|
|
273
|
+
this.#getButton = options.getButton ?? ((item) => matchingPart(item, "button"));
|
|
103
274
|
this.#onRemove = options.onRemove;
|
|
104
275
|
this.#focusAfterEnd = options.focusAfterEnd;
|
|
105
276
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Binds delegation to `container`; any row currently bound is released first.
|
|
108
|
-
* Idempotent for the same node, so target callbacks may call it freely.
|
|
109
|
-
*/
|
|
110
277
|
connect(container) {
|
|
111
278
|
if (this.#container === container) return;
|
|
112
279
|
this.disconnect();
|
|
@@ -114,10 +281,6 @@ var ChipRow = class {
|
|
|
114
281
|
container.addEventListener("click", this.#onClick);
|
|
115
282
|
container.addEventListener("keydown", this.#onKeydown);
|
|
116
283
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Releases the current row. When `container` is supplied, a stale disconnect
|
|
119
|
-
* callback cannot tear listeners off a newer replacement target.
|
|
120
|
-
*/
|
|
121
284
|
disconnect(container) {
|
|
122
285
|
const current = this.#container;
|
|
123
286
|
if (!current || container !== void 0 && current !== container) return;
|
|
@@ -125,19 +288,15 @@ var ChipRow = class {
|
|
|
125
288
|
current.removeEventListener("keydown", this.#onKeydown);
|
|
126
289
|
this.#container = null;
|
|
127
290
|
}
|
|
128
|
-
/** Current remove buttons, at most one consumer-resolved button per chip item. */
|
|
129
291
|
get buttons() {
|
|
130
292
|
return this.#entries.map(({ button }) => button);
|
|
131
293
|
}
|
|
132
|
-
/** Number of currently navigable remove buttons. */
|
|
133
294
|
get length() {
|
|
134
295
|
return this.buttons.length;
|
|
135
296
|
}
|
|
136
|
-
/** Declared-item index of the last navigable chip, or `-1` for an empty row. */
|
|
137
297
|
get lastIndex() {
|
|
138
298
|
return this.#entries.at(-1)?.itemIndex ?? -1;
|
|
139
299
|
}
|
|
140
|
-
/** Keeps exactly one button tabbable, preferring the first authored Tab stop. */
|
|
141
300
|
ensureTabStop() {
|
|
142
301
|
const buttons = this.#entries.map(({ button }) => button);
|
|
143
302
|
const active = buttons.findIndex((button) => button.tabIndex === 0);
|
|
@@ -146,7 +305,6 @@ var ChipRow = class {
|
|
|
146
305
|
this.#roving.setActive(active === -1 ? 0 : active, { items: buttons });
|
|
147
306
|
}
|
|
148
307
|
}
|
|
149
|
-
/** Focuses the last chip, returning false for an empty row. */
|
|
150
308
|
focusLast() {
|
|
151
309
|
const buttons = this.#entries.map(({ button }) => button);
|
|
152
310
|
const last = buttons.length - 1;
|
|
@@ -154,10 +312,6 @@ var ChipRow = class {
|
|
|
154
312
|
this.#roving.setActive(last, { focus: true, items: buttons });
|
|
155
313
|
return true;
|
|
156
314
|
}
|
|
157
|
-
/**
|
|
158
|
-
* Focuses the chip that followed a removed index, or the new last chip.
|
|
159
|
-
* Returns false when removal emptied the row so the consumer can rescue focus.
|
|
160
|
-
*/
|
|
161
315
|
focusAfterRemoval(index) {
|
|
162
316
|
const entries = this.#entries;
|
|
163
317
|
if (entries.length === 0) return false;
|
|
@@ -168,14 +322,12 @@ var ChipRow = class {
|
|
|
168
322
|
});
|
|
169
323
|
return true;
|
|
170
324
|
}
|
|
171
|
-
/** Current remove-button entries, retaining each button's declared item index. */
|
|
172
325
|
get #entries() {
|
|
173
326
|
return this.#getItems().flatMap((item, itemIndex) => {
|
|
174
327
|
const button = this.#getButton(item);
|
|
175
328
|
return button ? [{ button, itemIndex }] : [];
|
|
176
329
|
});
|
|
177
330
|
}
|
|
178
|
-
/** Resolves a delegated event to the remove button owned by a declared chip. */
|
|
179
331
|
#entry(event) {
|
|
180
332
|
const target = event.target;
|
|
181
333
|
if (!(target instanceof Element)) return null;
|
|
@@ -186,13 +338,11 @@ var ChipRow = class {
|
|
|
186
338
|
const entry = entries[buttonIndex];
|
|
187
339
|
return { ...entry, buttonIndex, buttons: entries.map(({ button: button2 }) => button2) };
|
|
188
340
|
}
|
|
189
|
-
/** Delegates removal clicks without waiting for Stimulus to wire a new chip. */
|
|
190
341
|
#onClick = (event) => {
|
|
191
342
|
const entry = this.#entry(event);
|
|
192
343
|
if (!entry) return;
|
|
193
344
|
this.#onRemove(entry.itemIndex);
|
|
194
345
|
};
|
|
195
|
-
/** Applies the shared logical-arrow and removal policy within the chip row. */
|
|
196
346
|
#onKeydown = (event) => {
|
|
197
347
|
if (event.defaultPrevented || isReservedArrowChord(event)) return;
|
|
198
348
|
const entry = this.#entry(event);
|
|
@@ -223,74 +373,121 @@ var ChipRow = class {
|
|
|
223
373
|
};
|
|
224
374
|
};
|
|
225
375
|
|
|
226
|
-
|
|
376
|
+
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
377
|
+
"insertCompositionText",
|
|
378
|
+
"insertFromComposition",
|
|
379
|
+
"deleteCompositionText",
|
|
380
|
+
"deleteByComposition"
|
|
381
|
+
]);
|
|
227
382
|
var CompositionTracker = class {
|
|
228
383
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
229
384
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
230
385
|
#onStart;
|
|
231
386
|
#onEnd;
|
|
387
|
+
#confirmedTarget = null;
|
|
232
388
|
constructor(options = {}) {
|
|
233
389
|
this.#onStart = options.onStart;
|
|
234
390
|
this.#onEnd = options.onEnd;
|
|
235
391
|
}
|
|
236
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
237
392
|
observe(target) {
|
|
238
393
|
if (this.#observedTargets.has(target)) return;
|
|
239
394
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
240
395
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
396
|
+
target.addEventListener("keydown", this.#handleKeydown);
|
|
241
397
|
this.#observedTargets.add(target);
|
|
242
398
|
}
|
|
243
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
244
399
|
unobserve(target) {
|
|
245
400
|
if (!this.#observedTargets.delete(target)) return;
|
|
246
401
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
247
402
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
403
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
248
404
|
this.#activeTargets.delete(target);
|
|
405
|
+
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
249
406
|
}
|
|
250
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
251
407
|
disconnect() {
|
|
252
408
|
for (const target of this.#observedTargets) {
|
|
253
409
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
254
410
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
411
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
255
412
|
}
|
|
256
413
|
this.#observedTargets.clear();
|
|
257
414
|
this.#activeTargets.clear();
|
|
415
|
+
this.#confirmedTarget = null;
|
|
258
416
|
}
|
|
259
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
260
417
|
isComposing(event) {
|
|
261
418
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
262
419
|
}
|
|
420
|
+
consumesConfirmedInput(event) {
|
|
421
|
+
const confirmed = this.#confirmedTarget;
|
|
422
|
+
this.#confirmedTarget = null;
|
|
423
|
+
if (confirmed === null || confirmed !== event.target) return false;
|
|
424
|
+
const inputType = event.inputType;
|
|
425
|
+
return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
|
|
426
|
+
}
|
|
263
427
|
#handleStart = (event) => {
|
|
428
|
+
this.#confirmedTarget = null;
|
|
264
429
|
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
265
430
|
this.#onStart?.(event);
|
|
266
431
|
};
|
|
267
432
|
#handleEnd = (event) => {
|
|
268
433
|
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
434
|
+
this.#confirmedTarget = event.target;
|
|
269
435
|
this.#onEnd?.(event);
|
|
270
436
|
};
|
|
437
|
+
#handleKeydown = () => {
|
|
438
|
+
this.#confirmedTarget = null;
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
function writeFields(container, values, { name, form = "" }) {
|
|
443
|
+
const current = [...container.children];
|
|
444
|
+
const submits = current.length === values.length && current.every(
|
|
445
|
+
(child, index) => child instanceof HTMLInputElement && child.type === "hidden" && child.value === values[index] && child.name === name && (child.getAttribute("form") ?? "") === form
|
|
446
|
+
);
|
|
447
|
+
if (submits) return false;
|
|
448
|
+
container.replaceChildren(
|
|
449
|
+
...values.map((value) => {
|
|
450
|
+
const input = document.createElement("input");
|
|
451
|
+
input.type = "hidden";
|
|
452
|
+
input.name = name;
|
|
453
|
+
input.value = value;
|
|
454
|
+
if (form !== "") input.setAttribute("form", form);
|
|
455
|
+
return input;
|
|
456
|
+
})
|
|
457
|
+
);
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
function commitField(target) {
|
|
461
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
var ListenerSet = class {
|
|
465
|
+
#abort = new AbortController();
|
|
466
|
+
add(target, type, handler, options) {
|
|
467
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
468
|
+
}
|
|
469
|
+
dispose() {
|
|
470
|
+
this.#abort.abort();
|
|
471
|
+
this.#abort = new AbortController();
|
|
472
|
+
}
|
|
271
473
|
};
|
|
272
474
|
|
|
273
|
-
// src/utils/microtask_coalescer.ts
|
|
274
475
|
var MicrotaskCoalescer = class {
|
|
275
476
|
#run;
|
|
276
477
|
#queued = false;
|
|
277
478
|
#active = false;
|
|
278
479
|
#generation = 0;
|
|
279
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
280
480
|
constructor(run) {
|
|
281
481
|
this.#run = run;
|
|
282
482
|
}
|
|
283
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
284
483
|
activate() {
|
|
285
484
|
this.#active = true;
|
|
286
485
|
}
|
|
287
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
288
486
|
cancel() {
|
|
289
487
|
this.#active = false;
|
|
290
488
|
this.#queued = false;
|
|
291
489
|
this.#generation += 1;
|
|
292
490
|
}
|
|
293
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
294
491
|
schedule() {
|
|
295
492
|
if (!this.#active || this.#queued) return;
|
|
296
493
|
this.#queued = true;
|
|
@@ -303,7 +500,86 @@ var MicrotaskCoalescer = class {
|
|
|
303
500
|
}
|
|
304
501
|
};
|
|
305
502
|
|
|
306
|
-
|
|
503
|
+
var MorphRenderWatcher = class {
|
|
504
|
+
#listeners = new ListenerSet();
|
|
505
|
+
#pass;
|
|
506
|
+
#element = null;
|
|
507
|
+
#onMorph = () => this.#pass.schedule();
|
|
508
|
+
constructor(run) {
|
|
509
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
510
|
+
}
|
|
511
|
+
observe(element) {
|
|
512
|
+
if (this.#element === element) return;
|
|
513
|
+
this.disconnect();
|
|
514
|
+
this.#element = element;
|
|
515
|
+
this.#pass.activate();
|
|
516
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
517
|
+
}
|
|
518
|
+
schedule() {
|
|
519
|
+
this.#pass.schedule();
|
|
520
|
+
}
|
|
521
|
+
disconnect() {
|
|
522
|
+
this.#listeners.dispose();
|
|
523
|
+
this.#pass.cancel();
|
|
524
|
+
this.#element = null;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
var MoveCounter = class {
|
|
529
|
+
#sequence = 0;
|
|
530
|
+
record() {
|
|
531
|
+
this.#sequence += 1;
|
|
532
|
+
return this.#sequence;
|
|
533
|
+
}
|
|
534
|
+
isLatest(token) {
|
|
535
|
+
return token === this.#sequence;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
var NUMBER_BOUNDS = {
|
|
540
|
+
finite: { finite: true }};
|
|
541
|
+
function matchesNumberBounds(value, bounds) {
|
|
542
|
+
if (!Number.isFinite(value)) {
|
|
543
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
544
|
+
if (direction === null) return false;
|
|
545
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
546
|
+
}
|
|
547
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
548
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
549
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
550
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
551
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function readNumber(raw, fallback, bounds) {
|
|
556
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
var NumberValueReader = class {
|
|
560
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
561
|
+
read(owner, name, raw, fallback, bounds) {
|
|
562
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
563
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
564
|
+
this.#lastRejected.delete(name);
|
|
565
|
+
return resolved;
|
|
566
|
+
}
|
|
567
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
568
|
+
const literal = owner.element.getAttribute(attribute);
|
|
569
|
+
if (literal === null) {
|
|
570
|
+
this.#lastRejected.delete(name);
|
|
571
|
+
return resolved;
|
|
572
|
+
}
|
|
573
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
574
|
+
this.#lastRejected.set(name, literal);
|
|
575
|
+
console.warn(
|
|
576
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
return resolved;
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
307
583
|
function scrollOptionIntoView(list, option) {
|
|
308
584
|
if (list.scrollHeight <= list.clientHeight) return;
|
|
309
585
|
const listRect = list.getBoundingClientRect();
|
|
@@ -315,68 +591,324 @@ function scrollOptionIntoView(list, option) {
|
|
|
315
591
|
}
|
|
316
592
|
}
|
|
317
593
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
this.#
|
|
594
|
+
var StateRegions = class _StateRegions {
|
|
595
|
+
static #registry = sharedRegistry(
|
|
596
|
+
"stimeo-ui.state-regions.registry.v1",
|
|
597
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
598
|
+
);
|
|
599
|
+
#whenTrue;
|
|
600
|
+
#whenFalse;
|
|
601
|
+
#record;
|
|
602
|
+
constructor(options, owner) {
|
|
603
|
+
this.#whenTrue = options.whenTrue;
|
|
604
|
+
this.#whenFalse = options.whenFalse ?? null;
|
|
605
|
+
this.#record = `data-${owner}-hidden-region`;
|
|
606
|
+
_StateRegions.#registry.names.add(this.#record);
|
|
607
|
+
}
|
|
608
|
+
reflect(host, isTrue) {
|
|
609
|
+
const shown = this.#inside(host, this.#whenTrue());
|
|
610
|
+
if (!this.#whenFalse) {
|
|
611
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const hiddenSide = this.#inside(host, this.#whenFalse());
|
|
615
|
+
if (shown.length === 0 || hiddenSide.length === 0) {
|
|
616
|
+
for (const region of shown) this.#give(region);
|
|
617
|
+
for (const region of hiddenSide) this.#give(region);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
621
|
+
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
622
|
+
}
|
|
623
|
+
release(host) {
|
|
624
|
+
for (const region of [host, ...host.querySelectorAll("*")]) {
|
|
625
|
+
if (!(region instanceof HTMLElement)) continue;
|
|
626
|
+
const stack = this.#held(region);
|
|
627
|
+
const hold = stack && this.#take(stack);
|
|
628
|
+
if (!stack || !hold) continue;
|
|
629
|
+
const owns = region.hidden === hold.written && !this.#covered(region, stack, hold);
|
|
630
|
+
const index = stack.indexOf(hold);
|
|
631
|
+
stack.splice(index, 1);
|
|
632
|
+
const above = stack[index];
|
|
633
|
+
if (above) above.linked &&= hold.linked;
|
|
634
|
+
if (owns) {
|
|
635
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
636
|
+
if (below) {
|
|
637
|
+
if (region.hidden !== below.written) region.hidden = below.written;
|
|
638
|
+
} else if (region.getAttribute("hidden") !== hold.authored) {
|
|
639
|
+
if (hold.authored === null) region.removeAttribute("hidden");
|
|
640
|
+
else region.setAttribute("hidden", hold.authored);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
this.#mark(region, stack);
|
|
644
|
+
}
|
|
330
645
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
646
|
+
#write(region, hidden) {
|
|
647
|
+
const stack = this.#stack(region);
|
|
648
|
+
const hold = this.#take(stack) ?? this.#acquire(region, stack);
|
|
649
|
+
const shows = !this.#covered(region, stack, hold);
|
|
650
|
+
hold.written = hidden;
|
|
651
|
+
if (shows && region.hidden !== hidden) region.hidden = hidden;
|
|
652
|
+
this.#mark(region, stack);
|
|
653
|
+
}
|
|
654
|
+
#give(region) {
|
|
655
|
+
const stack = this.#held(region);
|
|
656
|
+
if (stack && this.#take(stack)?.written) this.#write(region, false);
|
|
657
|
+
}
|
|
658
|
+
#held(region) {
|
|
659
|
+
return _StateRegions.#registry.stacks.has(region) || region.hasAttribute(this.#record) ? this.#stack(region) : void 0;
|
|
660
|
+
}
|
|
661
|
+
#stack(region) {
|
|
662
|
+
const stacks = _StateRegions.#registry.stacks;
|
|
663
|
+
const stack = stacks.get(region) ?? this.#seed(region);
|
|
664
|
+
stacks.set(region, stack);
|
|
665
|
+
return stack;
|
|
666
|
+
}
|
|
667
|
+
#seed(region) {
|
|
668
|
+
const found = [];
|
|
669
|
+
for (const record of _StateRegions.#registry.names) {
|
|
670
|
+
const read = this.#recorded(region, record);
|
|
671
|
+
if (read) found.push({ ...read, record });
|
|
672
|
+
}
|
|
673
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
674
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, authored, written, linked }, index) => ({
|
|
675
|
+
instance: null,
|
|
676
|
+
record,
|
|
677
|
+
authored,
|
|
678
|
+
written,
|
|
679
|
+
linked: linked && index > 0
|
|
680
|
+
}));
|
|
681
|
+
}
|
|
682
|
+
#take(stack) {
|
|
683
|
+
const hold = stack.find((held) => held.instance === this) ?? stack.find((held) => held.instance === null && held.record === this.#record);
|
|
684
|
+
if (hold) hold.instance = this;
|
|
685
|
+
return hold;
|
|
686
|
+
}
|
|
687
|
+
#covered(region, stack, hold) {
|
|
688
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === region.hidden);
|
|
689
|
+
}
|
|
690
|
+
#acquire(region, stack) {
|
|
691
|
+
const top = stack.at(-1);
|
|
692
|
+
const over = top && region.hidden === top.written ? top : void 0;
|
|
693
|
+
const hold = {
|
|
694
|
+
instance: this,
|
|
695
|
+
record: this.#record,
|
|
696
|
+
authored: over ? over.authored : region.getAttribute("hidden"),
|
|
697
|
+
written: false,
|
|
698
|
+
linked: over !== void 0
|
|
699
|
+
};
|
|
700
|
+
stack.push(hold);
|
|
701
|
+
return hold;
|
|
702
|
+
}
|
|
703
|
+
#mark(region, stack) {
|
|
704
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
705
|
+
stack.forEach((hold, index) => {
|
|
706
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
707
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
708
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
709
|
+
records.set(
|
|
710
|
+
hold.record,
|
|
711
|
+
region.getAttribute("hidden") === hold.authored && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.authored, hold.written, ...marker, ...beneath])
|
|
712
|
+
);
|
|
713
|
+
});
|
|
714
|
+
for (const [name, recorded] of records) {
|
|
715
|
+
if (region.getAttribute(name) === recorded) continue;
|
|
716
|
+
if (recorded === null) region.removeAttribute(name);
|
|
717
|
+
else region.setAttribute(name, recorded);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
#recorded(region, name) {
|
|
721
|
+
try {
|
|
722
|
+
const parsed = JSON.parse(region.getAttribute(name) ?? "null");
|
|
723
|
+
if (!Array.isArray(parsed)) return null;
|
|
724
|
+
const [authored, written, ...rest] = parsed;
|
|
725
|
+
if (authored !== null && typeof authored !== "string") return null;
|
|
726
|
+
const linked = rest[0] !== false;
|
|
727
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
728
|
+
if (!beneath.every((under) => typeof under === "string")) return null;
|
|
729
|
+
return typeof written === "boolean" ? { authored, written, linked, beneath } : null;
|
|
730
|
+
} catch {
|
|
731
|
+
return null;
|
|
337
732
|
}
|
|
338
733
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
_BeforeCacheReset.#subscribers.delete(this);
|
|
342
|
-
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
343
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
734
|
+
#inside(host, regions) {
|
|
735
|
+
return regions.filter((region) => host.contains(region));
|
|
344
736
|
}
|
|
345
737
|
};
|
|
346
738
|
|
|
347
|
-
|
|
348
|
-
|
|
739
|
+
function queryOpenDescendants(root, selector) {
|
|
740
|
+
const matches = [];
|
|
741
|
+
const roots = [root];
|
|
742
|
+
for (const scope of roots) {
|
|
743
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
744
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
745
|
+
const shadow = scope.shadowRoot;
|
|
746
|
+
if (shadow) roots.push(shadow);
|
|
747
|
+
}
|
|
748
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
749
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return matches;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
var TabindexLoan = class _TabindexLoan {
|
|
756
|
+
static #registry = sharedRegistry(
|
|
757
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
758
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
759
|
+
);
|
|
349
760
|
#value;
|
|
761
|
+
#record;
|
|
350
762
|
#lent = /* @__PURE__ */ new Set();
|
|
351
|
-
|
|
352
|
-
#beforeCache = new BeforeCacheReset(() => this.returnAll());
|
|
353
|
-
/**
|
|
354
|
-
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
355
|
-
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
356
|
-
* which a scroll region with no focusable content of its own needs.
|
|
357
|
-
*/
|
|
358
|
-
constructor(value = "-1") {
|
|
763
|
+
constructor(value, owner) {
|
|
359
764
|
this.#value = value;
|
|
765
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
766
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
360
767
|
}
|
|
361
|
-
/** Lends `element` the value; no-ops when it already carries a `tabindex`. */
|
|
362
768
|
lend(element) {
|
|
363
|
-
|
|
364
|
-
element.
|
|
769
|
+
const stack = this.#stack(element);
|
|
770
|
+
const current = element.getAttribute("tabindex");
|
|
771
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
772
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
773
|
+
);
|
|
774
|
+
const linked = stack.at(-1)?.value === current;
|
|
775
|
+
if (!held && current !== null && !linked) return;
|
|
365
776
|
this.#lent.add(element);
|
|
366
|
-
|
|
777
|
+
if (held) held.loan = this;
|
|
778
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
779
|
+
this.#mark(element, stack);
|
|
780
|
+
if (current === null || !held && current !== this.#value) {
|
|
781
|
+
element.setAttribute("tabindex", this.#value);
|
|
782
|
+
}
|
|
367
783
|
}
|
|
368
|
-
/** Takes back every loan whose value is still the one that was lent. */
|
|
369
784
|
returnAll() {
|
|
370
785
|
for (const element of this.#lent) {
|
|
371
|
-
|
|
786
|
+
const stack = this.#stack(element);
|
|
787
|
+
const hold = stack.find((held) => held.loan === this);
|
|
788
|
+
if (hold) this.#release(element, stack, hold);
|
|
372
789
|
}
|
|
373
790
|
this.#lent.clear();
|
|
374
|
-
|
|
791
|
+
}
|
|
792
|
+
reclaim(element) {
|
|
793
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
794
|
+
const stack = this.#stack(element);
|
|
795
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
796
|
+
if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
|
|
797
|
+
}
|
|
798
|
+
reclaimWithin(root) {
|
|
799
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
800
|
+
this.reclaim(element);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
#stack(element) {
|
|
804
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
805
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
806
|
+
stacks.set(element, stack);
|
|
807
|
+
return stack;
|
|
808
|
+
}
|
|
809
|
+
#seed(element) {
|
|
810
|
+
const current = element.getAttribute("tabindex");
|
|
811
|
+
const holds = [];
|
|
812
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
813
|
+
const value = element.getAttribute(record);
|
|
814
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
815
|
+
}
|
|
816
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
817
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
818
|
+
return holds;
|
|
819
|
+
}
|
|
820
|
+
#release(element, stack, hold) {
|
|
821
|
+
const current = element.getAttribute("tabindex");
|
|
822
|
+
const index = stack.indexOf(hold);
|
|
823
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
824
|
+
stack.splice(index, 1);
|
|
825
|
+
const above = stack[index];
|
|
826
|
+
if (above) above.linked &&= hold.linked;
|
|
827
|
+
this.#mark(element, stack);
|
|
828
|
+
if (!owns) return;
|
|
829
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
830
|
+
if (!below) element.removeAttribute("tabindex");
|
|
831
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
832
|
+
}
|
|
833
|
+
#mark(element, stack) {
|
|
834
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
835
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
836
|
+
for (const [name, recorded] of records) {
|
|
837
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
838
|
+
if (recorded === null) element.removeAttribute(name);
|
|
839
|
+
else element.setAttribute(name, recorded);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
function targetSelector(identifier, name) {
|
|
845
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function cloneTemplateRoot(template) {
|
|
849
|
+
const root = template.content.firstElementChild;
|
|
850
|
+
return root instanceof HTMLElement ? root.cloneNode(true) : null;
|
|
851
|
+
}
|
|
852
|
+
var TemplateRow = class {
|
|
853
|
+
#options;
|
|
854
|
+
#warned = false;
|
|
855
|
+
constructor(options) {
|
|
856
|
+
this.#options = options;
|
|
857
|
+
}
|
|
858
|
+
selector(name) {
|
|
859
|
+
return targetSelector(this.#options.identifier, name);
|
|
860
|
+
}
|
|
861
|
+
connect() {
|
|
862
|
+
this.#warned = false;
|
|
863
|
+
}
|
|
864
|
+
report(missing) {
|
|
865
|
+
return this.#say(`lacks ${missing}`);
|
|
866
|
+
}
|
|
867
|
+
#say(problem) {
|
|
868
|
+
if (this.#warned) return null;
|
|
869
|
+
this.#warned = true;
|
|
870
|
+
const { identifier, outcome, noun } = this.#options;
|
|
871
|
+
console.warn(`Stimeo UI: "${identifier}" ${outcome} because its ${noun} ${problem}.`);
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
instantiate(template, values) {
|
|
875
|
+
const { root: rootName, required, optional, button: buttonName } = this.#options;
|
|
876
|
+
const root = cloneTemplateRoot(template);
|
|
877
|
+
if (!root?.matches(this.selector(rootName))) {
|
|
878
|
+
return this.report(`${this.#article(rootName)} "${rootName}" root`);
|
|
879
|
+
}
|
|
880
|
+
const count = template.content.children.length;
|
|
881
|
+
if (count !== 1) {
|
|
882
|
+
return this.#say(`holds ${count} elements, and the row has to be its only one`);
|
|
883
|
+
}
|
|
884
|
+
const slots = {};
|
|
885
|
+
for (const name2 of required) {
|
|
886
|
+
const part = this.#resolve(root, name2);
|
|
887
|
+
if (!part) return this.report(`${this.#article(name2)} "${name2}" target`);
|
|
888
|
+
slots[name2] = part;
|
|
889
|
+
}
|
|
890
|
+
for (const name2 of optional ?? []) slots[name2] = this.#resolve(root, name2);
|
|
891
|
+
const named = this.#resolve(root, buttonName);
|
|
892
|
+
if (!(named instanceof HTMLButtonElement)) {
|
|
893
|
+
return this.report(`${this.#article(buttonName)} "${buttonName}" target <button>`);
|
|
894
|
+
}
|
|
895
|
+
const button = named;
|
|
896
|
+
const name = button.getAttribute("aria-label")?.trim() ?? "";
|
|
897
|
+
if (name === "") return this.report(`a non-empty aria-label on its "${buttonName}" target`);
|
|
898
|
+
button.setAttribute("aria-label", fillTemplate(name, values));
|
|
899
|
+
return { root, slots, button };
|
|
900
|
+
}
|
|
901
|
+
#resolve(root, name) {
|
|
902
|
+
return matchingPart(root, this.selector(name));
|
|
903
|
+
}
|
|
904
|
+
#article(name) {
|
|
905
|
+
return /^[aeiou]/i.test(name) ? "an" : "a";
|
|
375
906
|
}
|
|
376
907
|
};
|
|
377
908
|
|
|
378
|
-
|
|
379
|
-
|
|
909
|
+
var MultiSelectController = class _MultiSelectController extends Controller {
|
|
910
|
+
#numbers = new NumberValueReader();
|
|
911
|
+
#moves = new MoveCounter();
|
|
380
912
|
static targets = [
|
|
381
913
|
"input",
|
|
382
914
|
"list",
|
|
@@ -386,7 +918,8 @@ var MultiSelectController = class extends Controller {
|
|
|
386
918
|
"tagTemplate",
|
|
387
919
|
"label",
|
|
388
920
|
"remove",
|
|
389
|
-
"fields"
|
|
921
|
+
"fields",
|
|
922
|
+
"empty"
|
|
390
923
|
];
|
|
391
924
|
static values = {
|
|
392
925
|
max: { type: Number, default: 0 },
|
|
@@ -395,46 +928,46 @@ var MultiSelectController = class extends Controller {
|
|
|
395
928
|
announceText: { type: String, default: "" },
|
|
396
929
|
announceRemovedText: { type: String, default: "" }
|
|
397
930
|
};
|
|
931
|
+
static valueConstraints = {
|
|
932
|
+
max: NUMBER_BOUNDS.finite
|
|
933
|
+
};
|
|
398
934
|
static actions = ["close", "filter", "onKeydown", "open", "toggleOption"];
|
|
399
935
|
static events = ["change", "filter", "reconcile"];
|
|
400
|
-
/** Stable id of the active option; the current target is resolved from the DOM. */
|
|
401
936
|
#activeOptionId = null;
|
|
402
|
-
|
|
403
|
-
#tabindex = new TabindexLoan();
|
|
404
|
-
/** Last reconciled selection, used to distinguish state changes from derived-DOM repair. */
|
|
937
|
+
#tabindex = new TabindexLoan("-1", this.identifier);
|
|
405
938
|
#selectionValues = [];
|
|
406
|
-
/** Whether initial normalization finished, so a fields callback cannot mirror stale state. */
|
|
407
939
|
#connected = false;
|
|
408
|
-
|
|
409
|
-
#
|
|
410
|
-
|
|
411
|
-
#
|
|
412
|
-
|
|
940
|
+
#emptyRegion = new StateRegions({ whenTrue: () => this.emptyTargets }, this.identifier);
|
|
941
|
+
#listHidden = new AttributeLease("hidden", this.identifier);
|
|
942
|
+
#expanded = new AttributeLease("aria-expanded", this.identifier);
|
|
943
|
+
#activeDescendant = new AttributeLease(
|
|
944
|
+
"aria-activedescendant",
|
|
945
|
+
this.identifier
|
|
946
|
+
);
|
|
947
|
+
#open = false;
|
|
948
|
+
#openList = null;
|
|
949
|
+
#reconcile = new MorphRenderWatcher(() => this.#reconcileOptions());
|
|
413
950
|
#composition = new CompositionTracker({
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
951
|
+
onEnd: () => this.filter()
|
|
952
|
+
});
|
|
953
|
+
#rows = new TemplateRow({
|
|
954
|
+
identifier: this.identifier,
|
|
955
|
+
root: "tag",
|
|
956
|
+
required: ["label"],
|
|
957
|
+
button: "remove",
|
|
958
|
+
outcome: "changed no selection",
|
|
959
|
+
noun: "chip template"
|
|
424
960
|
});
|
|
425
|
-
/** Whether this connection already reported its unusable chip template. */
|
|
426
|
-
#warnedTemplate = false;
|
|
427
|
-
/** Shared delegated interaction for the replaceable row of removable chips. */
|
|
428
961
|
#chipRow = new ChipRow({
|
|
429
962
|
directionElement: this.element,
|
|
430
963
|
getItems: () => this.tagTargets,
|
|
431
|
-
getButton: (tag) => tag.
|
|
964
|
+
getButton: (tag) => matchingPart(tag, `button${this.#rows.selector("remove")}`),
|
|
432
965
|
onRemove: (index) => this.#removeTagAt(index),
|
|
433
966
|
focusAfterEnd: () => this.#focusInput()
|
|
434
967
|
});
|
|
435
|
-
/** Starts closed, syncs chips for any pre-selected options, and listens out. */
|
|
436
968
|
connect() {
|
|
437
|
-
this.#
|
|
969
|
+
this.#tabindex.reclaim(this.element);
|
|
970
|
+
this.#rows.connect();
|
|
438
971
|
this.#normalizeSelection([]);
|
|
439
972
|
this.close();
|
|
440
973
|
if (this.hasTagsTarget) {
|
|
@@ -445,86 +978,81 @@ var MultiSelectController = class extends Controller {
|
|
|
445
978
|
this.#syncFields();
|
|
446
979
|
document.addEventListener("click", this.#onOutsideClick, true);
|
|
447
980
|
this.#connected = true;
|
|
448
|
-
this.#reconcile.
|
|
981
|
+
this.#reconcile.observe(this.element);
|
|
449
982
|
}
|
|
450
|
-
/**
|
|
451
|
-
* Derives the chips from the selected options, idempotently: a Turbo Drive cache
|
|
452
|
-
* restore or morph can re-connect with chips already in the DOM, so they are
|
|
453
|
-
* cleared before deriving afresh to avoid duplicates.
|
|
454
|
-
*/
|
|
455
983
|
#rebuildTags() {
|
|
456
984
|
if (!this.hasTagsTarget || !this.hasTagTemplateTarget) return;
|
|
457
|
-
const
|
|
985
|
+
const chips = [];
|
|
458
986
|
for (const option of this.#selectedOptions) {
|
|
459
|
-
const
|
|
460
|
-
if (!
|
|
461
|
-
|
|
987
|
+
const chip = this.#buildTag(option);
|
|
988
|
+
if (!chip) return;
|
|
989
|
+
chips.push(chip);
|
|
462
990
|
}
|
|
463
991
|
for (const tag of this.tagTargets) tag.remove();
|
|
464
|
-
this.tagsTarget.append(...
|
|
992
|
+
this.tagsTarget.append(...chips);
|
|
465
993
|
this.#chipRow.ensureTabStop();
|
|
466
994
|
}
|
|
467
|
-
/** Tears down document and chip listeners on disconnect (Turbo included). */
|
|
468
995
|
disconnect() {
|
|
469
996
|
this.#connected = false;
|
|
470
|
-
this.#reconcile.
|
|
997
|
+
this.#reconcile.disconnect();
|
|
471
998
|
this.#composition.disconnect();
|
|
472
|
-
this.#ignorePostCompositionInput = false;
|
|
473
999
|
this.#chipRow.disconnect();
|
|
474
1000
|
document.removeEventListener("click", this.#onOutsideClick, true);
|
|
475
1001
|
this.#releaseTabindex();
|
|
476
1002
|
}
|
|
477
|
-
/** Reconciles active state after an option target is added at runtime. */
|
|
478
1003
|
optionTargetConnected() {
|
|
479
1004
|
this.#scheduleOptionReconcile();
|
|
480
1005
|
}
|
|
481
|
-
/** Cleans a removed target and reconciles active state against the surviving DOM. */
|
|
482
1006
|
optionTargetDisconnected(option) {
|
|
483
1007
|
option.removeAttribute("data-active");
|
|
484
1008
|
this.#scheduleOptionReconcile();
|
|
485
1009
|
}
|
|
486
|
-
|
|
1010
|
+
emptyTargetConnected() {
|
|
1011
|
+
if (this.#connected) this.#reflectEmpty();
|
|
1012
|
+
}
|
|
487
1013
|
tagsTargetConnected(tags) {
|
|
488
1014
|
this.#chipRow.connect(tags);
|
|
489
1015
|
this.#scheduleOptionReconcile();
|
|
490
1016
|
}
|
|
491
|
-
/** Releases only the row that actually disconnected, never a newer replacement. */
|
|
492
1017
|
tagsTargetDisconnected(tags) {
|
|
493
1018
|
this.#chipRow.disconnect(tags);
|
|
494
1019
|
this.#scheduleOptionReconcile();
|
|
495
1020
|
}
|
|
496
|
-
/** Seeds a fields target inserted after connect from the current selection. */
|
|
497
1021
|
fieldsTargetConnected() {
|
|
498
1022
|
if (this.#connected) this.#syncFields();
|
|
499
1023
|
}
|
|
500
|
-
|
|
1024
|
+
fieldsTargetDisconnected() {
|
|
1025
|
+
this.#scheduleOptionReconcile();
|
|
1026
|
+
}
|
|
1027
|
+
listTargetConnected(_list) {
|
|
1028
|
+
if (!this.#connected) return;
|
|
1029
|
+
this.#reconcileOpenState();
|
|
1030
|
+
this.#scheduleOptionReconcile();
|
|
1031
|
+
}
|
|
1032
|
+
listTargetDisconnected(list) {
|
|
1033
|
+
if (!this.listTargets.includes(list)) {
|
|
1034
|
+
if (list === this.#openList) {
|
|
1035
|
+
this.#adoptPageClose();
|
|
1036
|
+
this.#openList = null;
|
|
1037
|
+
}
|
|
1038
|
+
this.#listHidden.return(list);
|
|
1039
|
+
}
|
|
1040
|
+
if (!this.#connected) return;
|
|
1041
|
+
this.#reconcileOpenState();
|
|
1042
|
+
this.#scheduleOptionReconcile();
|
|
1043
|
+
}
|
|
501
1044
|
maxValueChanged() {
|
|
502
1045
|
this.#scheduleOptionReconcile();
|
|
503
1046
|
}
|
|
504
|
-
/** Rebuilds submitted fields when their public name changes at runtime. */
|
|
505
1047
|
nameValueChanged() {
|
|
506
1048
|
this.#scheduleOptionReconcile();
|
|
507
1049
|
}
|
|
508
|
-
/** Rebuilds submitted fields when their associated form changes at runtime. */
|
|
509
1050
|
formValueChanged() {
|
|
510
1051
|
this.#scheduleOptionReconcile();
|
|
511
1052
|
}
|
|
512
|
-
/** Schedules one reconciliation after all callbacks in the mutation batch. */
|
|
513
1053
|
#scheduleOptionReconcile() {
|
|
514
1054
|
this.#reconcile.schedule();
|
|
515
1055
|
}
|
|
516
|
-
/**
|
|
517
|
-
* Keeps a surviving/same-id active target, otherwise falls back to the first
|
|
518
|
-
* visible one — and brings the derived state back in line with the new option set.
|
|
519
|
-
*
|
|
520
|
-
* The baseline pass fills in any missing `aria-selected`, and the chips and
|
|
521
|
-
* hidden fields are re-derived from it, because the options are the truth source
|
|
522
|
-
* for the selection. The chips are rebuilt **only when the selected value set
|
|
523
|
-
* actually moved**: the rebuild removes and recreates every chip, so running it
|
|
524
|
-
* for an unrelated option would drop focus from a chip's remove button to
|
|
525
|
-
* `<body>`, losing the keyboard user's place for something that did not concern
|
|
526
|
-
* them.
|
|
527
|
-
*/
|
|
528
1056
|
#reconcileOptions() {
|
|
529
1057
|
const visible = this.#visibleOptions;
|
|
530
1058
|
const active = this.#activeOption;
|
|
@@ -535,7 +1063,7 @@ var MultiSelectController = class extends Controller {
|
|
|
535
1063
|
const previousLabels = new Map(
|
|
536
1064
|
this.tagTargets.map((tag) => [
|
|
537
1065
|
tag.dataset.value ?? "",
|
|
538
|
-
(tag.
|
|
1066
|
+
readLabel(matchingPart(tag, this.#rows.selector("label")))
|
|
539
1067
|
])
|
|
540
1068
|
);
|
|
541
1069
|
const priority = [
|
|
@@ -553,24 +1081,23 @@ var MultiSelectController = class extends Controller {
|
|
|
553
1081
|
const changed = !this.#sameSelection(previous, values);
|
|
554
1082
|
this.#selectionValues = values;
|
|
555
1083
|
if (!changed) return;
|
|
1084
|
+
const token = this.#moves.record();
|
|
556
1085
|
const options = new Map(selected.map((option) => [this.#optionValue(option), option]));
|
|
557
1086
|
for (const value of previous) {
|
|
1087
|
+
if (!this.#moves.isLatest(token)) return;
|
|
558
1088
|
if (!values.includes(value)) {
|
|
559
1089
|
this.#announceTransition(false, previousLabels.get(value) || value, value, values.length);
|
|
560
1090
|
}
|
|
561
1091
|
}
|
|
562
1092
|
for (const value of values) {
|
|
1093
|
+
if (!this.#moves.isLatest(token)) return;
|
|
563
1094
|
if (previous.includes(value)) continue;
|
|
564
1095
|
const option = options.get(value);
|
|
565
1096
|
this.#announceTransition(true, this.#optionLabel(option), value, values.length);
|
|
566
1097
|
}
|
|
1098
|
+
if (!this.#moves.isLatest(token)) return;
|
|
567
1099
|
this.dispatch("reconcile", { detail: { values } });
|
|
568
1100
|
}
|
|
569
|
-
/**
|
|
570
|
-
* Gives every option an explicit `aria-selected` and enforces the current cap.
|
|
571
|
-
* Priority preserves existing chip order at runtime; a fresh connection passes
|
|
572
|
-
* no priority, so deterministic option DOM order chooses the initial survivors.
|
|
573
|
-
*/
|
|
574
1101
|
#normalizeSelection(priority) {
|
|
575
1102
|
const selected = this.optionTargets.filter(
|
|
576
1103
|
(option) => option.getAttribute("aria-selected") === "true"
|
|
@@ -596,32 +1123,39 @@ var MultiSelectController = class extends Controller {
|
|
|
596
1123
|
option.setAttribute("aria-selected", String(kept.has(option)));
|
|
597
1124
|
}
|
|
598
1125
|
}
|
|
599
|
-
/**
|
|
600
|
-
* Tracks an input added initially or after connect, and makes it describe the
|
|
601
|
-
* widget that is actually on screen: a swapped-in input arrives with the
|
|
602
|
-
* authored ARIA of a fresh node while this controller still holds the popup
|
|
603
|
-
* state, and the open path cannot repair that (it seeds an active option only
|
|
604
|
-
* when there is none), so a live list would go unannounced.
|
|
605
|
-
*/
|
|
606
1126
|
inputTargetConnected(input) {
|
|
607
1127
|
this.#composition.observe(input);
|
|
608
|
-
|
|
609
|
-
const active = this.#activeOption;
|
|
610
|
-
if (active) input.setAttribute("aria-activedescendant", ensureId(active, "stimeo-ms-opt"));
|
|
611
|
-
else input.removeAttribute("aria-activedescendant");
|
|
1128
|
+
this.#describeInput(input);
|
|
612
1129
|
}
|
|
613
|
-
/** Removes composition listeners when the active input is replaced or removed. */
|
|
614
1130
|
inputTargetDisconnected(input) {
|
|
615
1131
|
this.#composition.unobserve(input);
|
|
616
|
-
this
|
|
1132
|
+
if (!this.inputTargets.includes(input)) {
|
|
1133
|
+
this.#expanded.return(input);
|
|
1134
|
+
this.#activeDescendant.return(input);
|
|
1135
|
+
}
|
|
1136
|
+
if (this.#connected && this.hasInputTarget) this.#describeInput(this.inputTarget);
|
|
1137
|
+
}
|
|
1138
|
+
#describeInput(input) {
|
|
1139
|
+
this.#expanded.write(input, String(!this.#isClosed));
|
|
1140
|
+
const active = this.#activeOption;
|
|
1141
|
+
this.#activeDescendant.write(input, active ? ensureId(active, "stimeo-ms-opt") : null);
|
|
1142
|
+
}
|
|
1143
|
+
#reconcileOpenState() {
|
|
1144
|
+
if (!this.hasListTarget) return;
|
|
1145
|
+
this.#adoptPageClose();
|
|
1146
|
+
const list = this.listTarget;
|
|
1147
|
+
if (list !== this.#openList) {
|
|
1148
|
+
this.#showList(list, this.#open);
|
|
1149
|
+
this.#openList = list;
|
|
1150
|
+
}
|
|
1151
|
+
if (this.hasInputTarget) this.#expanded.write(this.inputTarget, String(!list.hidden));
|
|
1152
|
+
}
|
|
1153
|
+
#adoptPageClose() {
|
|
1154
|
+
if (this.#openList?.hidden) this.#open = false;
|
|
617
1155
|
}
|
|
618
|
-
/** Filters confirmed input text, opens, and re-seeds the active option. */
|
|
619
1156
|
filter(event) {
|
|
620
1157
|
if (!this.hasInputTarget) return;
|
|
621
|
-
if (event && this.#
|
|
622
|
-
this.#ignorePostCompositionInput = false;
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
1158
|
+
if (event && this.#composition.consumesConfirmedInput(event)) return;
|
|
625
1159
|
if (this.#composition.isComposing(event)) return;
|
|
626
1160
|
const query = this.inputTarget.value.trim().toLowerCase();
|
|
627
1161
|
for (const option of this.optionTargets) {
|
|
@@ -630,45 +1164,31 @@ var MultiSelectController = class extends Controller {
|
|
|
630
1164
|
}
|
|
631
1165
|
this.open();
|
|
632
1166
|
const visible = this.#visibleOptions;
|
|
633
|
-
this.#reflectEmpty();
|
|
634
1167
|
this.#setActive(visible[0] ?? null);
|
|
635
1168
|
this.dispatch("filter", { detail: { query } });
|
|
636
1169
|
}
|
|
637
|
-
/**
|
|
638
|
-
* Opens the list and activates the first visible option when none is active.
|
|
639
|
-
*
|
|
640
|
-
* Needs the input, which owns `aria-expanded` and `aria-activedescendant`: a
|
|
641
|
-
* list shown without one is a popup no assistive technology is told about. So
|
|
642
|
-
* opening is skipped entirely, where {@link close} still closes.
|
|
643
|
-
*/
|
|
644
1170
|
open() {
|
|
645
1171
|
if (!this.hasListTarget || !this.hasInputTarget) {
|
|
646
1172
|
this.#reflectEmpty();
|
|
647
1173
|
return;
|
|
648
1174
|
}
|
|
649
|
-
this.listTarget
|
|
650
|
-
this
|
|
1175
|
+
this.#showList(this.listTarget, true);
|
|
1176
|
+
this.#open = true;
|
|
1177
|
+
this.#openList = this.listTarget;
|
|
1178
|
+
this.#expanded.write(this.inputTarget, "true");
|
|
651
1179
|
if (!this.#activeOption) this.#setActive(this.#visibleOptions[0] ?? null);
|
|
652
1180
|
this.#reflectEmpty();
|
|
653
1181
|
}
|
|
654
|
-
/**
|
|
655
|
-
* Closes the list and clears the active option.
|
|
656
|
-
*
|
|
657
|
-
* Survives a missing input in both directions. `connect()` calls this second,
|
|
658
|
-
* so dereferencing the input here would throw before the chips, the roving
|
|
659
|
-
* seed, the chip listeners, the hidden fields and the outside-click listener —
|
|
660
|
-
* and Stimulus keeps the controller alive after that throw, so none of them
|
|
661
|
-
* would ever run and the selection would silently stop submitting. An input
|
|
662
|
-
* removed while the list is open must still let it come down *and* forget its
|
|
663
|
-
* active option, so only the `aria-expanded` write is guarded.
|
|
664
|
-
*/
|
|
665
1182
|
close() {
|
|
666
|
-
|
|
1183
|
+
this.#open = false;
|
|
1184
|
+
if (this.hasListTarget) {
|
|
1185
|
+
this.#showList(this.listTarget, false);
|
|
1186
|
+
this.#openList = this.listTarget;
|
|
1187
|
+
}
|
|
667
1188
|
this.#setActive(null);
|
|
668
|
-
if (this.hasInputTarget) this.inputTarget
|
|
1189
|
+
if (this.hasInputTarget) this.#expanded.write(this.inputTarget, "false");
|
|
669
1190
|
this.#reflectEmpty();
|
|
670
1191
|
}
|
|
671
|
-
/** Routes input keyboard interaction per the multi-select combobox model. */
|
|
672
1192
|
onKeydown(event) {
|
|
673
1193
|
if (event.defaultPrevented) return;
|
|
674
1194
|
if (isReservedArrowChord(event)) return;
|
|
@@ -733,39 +1253,20 @@ var MultiSelectController = class extends Controller {
|
|
|
733
1253
|
break;
|
|
734
1254
|
}
|
|
735
1255
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
* otherwise go dead until the user clicks back in ("focus stays on the input").
|
|
742
|
-
*/
|
|
743
|
-
toggleOption(event) {
|
|
744
|
-
const option = event.currentTarget.closest('[role="option"]');
|
|
1256
|
+
toggleOption(source) {
|
|
1257
|
+
const { event, host, origin, reason } = actionSource(source);
|
|
1258
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
1259
|
+
return;
|
|
1260
|
+
const option = host?.closest(targetSelector(this.identifier, "option"));
|
|
745
1261
|
if (!option || !this.optionTargets.includes(option)) return;
|
|
746
|
-
this
|
|
747
|
-
this.#
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
*
|
|
752
|
-
* All three callers run *after* an option or a chip already took focus, and all
|
|
753
|
-
* three are reachable without an input — options and chips carry their own
|
|
754
|
-
* `data-action`. Throwing here would leave the chip removed but focus stranded
|
|
755
|
-
* on a detached button.
|
|
756
|
-
*/
|
|
1262
|
+
const focus = event !== null || this.element.contains(document.activeElement);
|
|
1263
|
+
if (!this.#toggleSelection(option, reason)) return;
|
|
1264
|
+
if (focus && (event !== null || this.element.contains(document.activeElement)))
|
|
1265
|
+
this.#focusInput();
|
|
1266
|
+
}
|
|
757
1267
|
#focusInput() {
|
|
758
1268
|
if (this.hasInputTarget) this.inputTarget.focus();
|
|
759
1269
|
}
|
|
760
|
-
/**
|
|
761
|
-
* Re-homes focus after the last chip was removed: to the input, else the root.
|
|
762
|
-
*
|
|
763
|
-
* Unlike the other {@link #focusInput} callers, the element that held focus has
|
|
764
|
-
* just left the DOM, so "leave it alone" is not an option — the browser already
|
|
765
|
-
* dropped it to `<body>`. The root borrows a `tabindex="-1"` just-in-time (not a
|
|
766
|
-
* Tab stop, handed back before Turbo caches or on teardown). Focus that landed
|
|
767
|
-
* on a real element is left alone, so a chip removed out of band never steals it.
|
|
768
|
-
*/
|
|
769
1270
|
#focusAfterLastTag() {
|
|
770
1271
|
if (this.hasInputTarget) {
|
|
771
1272
|
this.inputTarget.focus();
|
|
@@ -779,15 +1280,9 @@ var MultiSelectController = class extends Controller {
|
|
|
779
1280
|
this.#tabindex.lend(this.element);
|
|
780
1281
|
this.element.focus();
|
|
781
1282
|
}
|
|
782
|
-
/**
|
|
783
|
-
* Returns the borrowed tab stop. Owning the borrow is not enough — the value
|
|
784
|
-
* has to still be the one this instance wrote, since a consumer that changed it
|
|
785
|
-
* afterwards owns it now.
|
|
786
|
-
*/
|
|
787
1283
|
#releaseTabindex() {
|
|
788
1284
|
this.#tabindex.returnAll();
|
|
789
1285
|
}
|
|
790
|
-
/** Moves the active option by `delta` among visible options, wrapping. */
|
|
791
1286
|
#moveActive(delta) {
|
|
792
1287
|
const visible = this.#visibleOptions;
|
|
793
1288
|
const active = this.#activeOption;
|
|
@@ -796,128 +1291,70 @@ var MultiSelectController = class extends Controller {
|
|
|
796
1291
|
const next = (candidate + visible.length) % visible.length;
|
|
797
1292
|
this.#setActive(visible[next] ?? null);
|
|
798
1293
|
}
|
|
799
|
-
|
|
800
|
-
#toggleSelection(option) {
|
|
1294
|
+
#toggleSelection(option, reason = "user") {
|
|
801
1295
|
const selected = option.getAttribute("aria-selected") === "true";
|
|
802
1296
|
const limit = this.#selectionLimit;
|
|
803
1297
|
if (!selected && limit > 0 && this.#selectedOptions.length >= limit) {
|
|
804
|
-
return;
|
|
1298
|
+
return true;
|
|
805
1299
|
}
|
|
806
1300
|
if (selected) {
|
|
807
1301
|
option.setAttribute("aria-selected", "false");
|
|
808
1302
|
this.#removeTagFor(option);
|
|
809
1303
|
} else {
|
|
810
|
-
if (this.hasTagTemplateTarget && !this.#appendTag(option)) return;
|
|
1304
|
+
if (this.hasTagTemplateTarget && !this.#appendTag(option)) return true;
|
|
811
1305
|
option.setAttribute("aria-selected", "true");
|
|
812
1306
|
}
|
|
813
1307
|
this.#refreshRoving();
|
|
814
|
-
this.#
|
|
815
|
-
const values = this.#values;
|
|
816
|
-
this.#selectionValues = values;
|
|
817
|
-
this.#announceTransition(
|
|
1308
|
+
return this.#publishSelection(
|
|
818
1309
|
!selected,
|
|
819
1310
|
this.#optionLabel(option),
|
|
820
1311
|
this.#optionValue(option),
|
|
821
|
-
|
|
1312
|
+
reason
|
|
822
1313
|
);
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
/**
|
|
826
|
-
* Re-reads each chip's label from its option, in place.
|
|
827
|
-
*
|
|
828
|
-
* The value order alone does not say the chips are still correct: a server can
|
|
829
|
-
* re-render the same candidate with a new label ("Apple" → "Green Apple"), and
|
|
830
|
-
* the chip text and its `Remove {label}` name are both derived from the option.
|
|
831
|
-
* Updating them here keeps the rebuild — which would drop focus — for the case
|
|
832
|
-
* that actually needs it, a changed selection.
|
|
833
|
-
*/
|
|
1314
|
+
}
|
|
834
1315
|
#refreshTagLabels(selected) {
|
|
835
1316
|
const options = new Map(selected.map((option) => [this.#optionValue(option), option]));
|
|
836
1317
|
for (const tag of this.tagTargets) {
|
|
837
1318
|
const option = options.get(tag.dataset.value ?? "");
|
|
838
1319
|
const text = this.#optionLabel(option);
|
|
839
|
-
const label = tag.
|
|
840
|
-
const button = tag.
|
|
841
|
-
'button[data-stimeo--multi-select-target~="remove"]'
|
|
842
|
-
);
|
|
1320
|
+
const label = matchingPart(tag, this.#rows.selector("label"));
|
|
1321
|
+
const button = matchingPart(tag, `button${this.#rows.selector("remove")}`);
|
|
843
1322
|
const name = this.#removeName(text, this.#optionValue(option));
|
|
844
1323
|
if (!label || !button || !name) continue;
|
|
845
|
-
if (label
|
|
1324
|
+
if (readLabel(label) !== text) writeLabel(label, text);
|
|
846
1325
|
if (button.getAttribute("aria-label") !== name) button.setAttribute("aria-label", name);
|
|
847
1326
|
}
|
|
848
1327
|
}
|
|
849
|
-
/** Builds one chip from the template for `option`. */
|
|
850
1328
|
#appendTag(option) {
|
|
851
1329
|
if (!this.hasTagsTarget) {
|
|
852
|
-
this.#
|
|
1330
|
+
this.#rows.report('a "tags" target to append the chip to');
|
|
853
1331
|
return false;
|
|
854
1332
|
}
|
|
855
|
-
const
|
|
856
|
-
if (!
|
|
857
|
-
this.tagsTarget.appendChild(
|
|
1333
|
+
const chip = this.#buildTag(option);
|
|
1334
|
+
if (!chip) return false;
|
|
1335
|
+
this.tagsTarget.appendChild(chip);
|
|
858
1336
|
return true;
|
|
859
1337
|
}
|
|
860
|
-
/**
|
|
861
|
-
* Builds one fully named chip without mutating the live tag row, or `null`
|
|
862
|
-
* when the authored template cannot produce one. Both callers establish the
|
|
863
|
-
* template first: a field authored without `tagTemplate` renders no chips at
|
|
864
|
-
* all — a supported configuration — and never reaches here.
|
|
865
|
-
*/
|
|
866
1338
|
#buildTag(option) {
|
|
867
|
-
const fragment = this.tagTemplateTarget.content.cloneNode(true);
|
|
868
|
-
const tag = fragment.querySelector('[data-stimeo--multi-select-target~="tag"]');
|
|
869
|
-
const label = fragment.querySelector(
|
|
870
|
-
'[data-stimeo--multi-select-target~="label"]'
|
|
871
|
-
);
|
|
872
|
-
const button = fragment.querySelector(
|
|
873
|
-
'button[data-stimeo--multi-select-target~="remove"]'
|
|
874
|
-
);
|
|
875
1339
|
const text = this.#optionLabel(option);
|
|
876
1340
|
const value = this.#optionValue(option);
|
|
877
|
-
const
|
|
878
|
-
if (!
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
}
|
|
884
|
-
tag.dataset.value = value;
|
|
885
|
-
label.textContent = text;
|
|
886
|
-
button.setAttribute("aria-label", fillTemplate(removeName, { label: text, value }));
|
|
887
|
-
button.tabIndex = -1;
|
|
888
|
-
return fragment;
|
|
889
|
-
}
|
|
890
|
-
/**
|
|
891
|
-
* Reports an unusable chip template to the author, once per connection.
|
|
892
|
-
*
|
|
893
|
-
* The selection itself stays untouched — no `aria-selected`, chip, hidden
|
|
894
|
-
* field, announcement, or event moves. Without this line the only symptom is
|
|
895
|
-
* a listbox whose options refuse to select, and the two causes the Inspector
|
|
896
|
-
* cannot see statically (a name that renders empty from a missing
|
|
897
|
-
* translation, a server-rendered template) would have no diagnostic anywhere.
|
|
898
|
-
*/
|
|
899
|
-
#warnTemplate(missing) {
|
|
900
|
-
if (!this.#warnedTemplate) {
|
|
901
|
-
this.#warnedTemplate = true;
|
|
902
|
-
console.warn(
|
|
903
|
-
`Stimeo UI: "${this.identifier}" changed no selection because its chip template lacks ${missing}.`
|
|
904
|
-
);
|
|
905
|
-
}
|
|
906
|
-
return null;
|
|
1341
|
+
const row = this.#rows.instantiate(this.tagTemplateTarget, { label: text, value });
|
|
1342
|
+
if (!row) return null;
|
|
1343
|
+
row.root.dataset.value = value;
|
|
1344
|
+
writeLabel(row.slots.label, text);
|
|
1345
|
+
row.button.tabIndex = -1;
|
|
1346
|
+
return row.root;
|
|
907
1347
|
}
|
|
908
|
-
/** Expands the current template's localized remove-button name. */
|
|
909
1348
|
#removeName(label, value) {
|
|
910
1349
|
if (!this.hasTagTemplateTarget) return null;
|
|
911
|
-
const template = this.tagTemplateTarget.content.querySelector(
|
|
1350
|
+
const template = this.tagTemplateTarget.content.querySelector(`button${this.#rows.selector("remove")}`)?.getAttribute("aria-label")?.trim();
|
|
912
1351
|
return template ? fillTemplate(template, { label, value }) : null;
|
|
913
1352
|
}
|
|
914
|
-
/** Removes the chip mirroring `option`, if present. */
|
|
915
1353
|
#removeTagFor(option) {
|
|
916
1354
|
const value = this.#optionValue(option);
|
|
917
1355
|
const tag = this.tagTargets.find((candidate) => candidate.dataset.value === value);
|
|
918
1356
|
tag?.remove();
|
|
919
1357
|
}
|
|
920
|
-
/** Removes chip `index` and deselects its option, re-homing focus. */
|
|
921
1358
|
#removeTagAt(index, focus = "neighbor") {
|
|
922
1359
|
const tag = this.tagTargets[index];
|
|
923
1360
|
if (!tag) return;
|
|
@@ -926,31 +1363,13 @@ var MultiSelectController = class extends Controller {
|
|
|
926
1363
|
if (option) option.setAttribute("aria-selected", "false");
|
|
927
1364
|
tag.remove();
|
|
928
1365
|
this.#refreshRoving();
|
|
929
|
-
this.#
|
|
930
|
-
const values = this.#values;
|
|
931
|
-
this.#selectionValues = values;
|
|
932
|
-
this.#announceTransition(
|
|
933
|
-
false,
|
|
934
|
-
option ? this.#optionLabel(option) : value,
|
|
935
|
-
value,
|
|
936
|
-
values.length
|
|
937
|
-
);
|
|
938
|
-
this.dispatch("change", { detail: { values } });
|
|
1366
|
+
if (!this.#publishSelection(false, option ? this.#optionLabel(option) : value, value)) return;
|
|
939
1367
|
if (focus === "input") {
|
|
940
1368
|
this.#focusInput();
|
|
941
1369
|
return;
|
|
942
1370
|
}
|
|
943
1371
|
if (!this.#chipRow.focusAfterRemoval(index)) this.#focusAfterLastTag();
|
|
944
1372
|
}
|
|
945
|
-
/**
|
|
946
|
-
* Marks `option` active via `data-active` and the input's
|
|
947
|
-
* `aria-activedescendant` (the attribute is removed, not emptied, when null).
|
|
948
|
-
*
|
|
949
|
-
* The state half runs even with no input, so a `close()` that cannot touch ARIA
|
|
950
|
-
* still clears it: {@link open} seeds an active option only when there is none,
|
|
951
|
-
* so a stale one makes the next open skip the seeding and a replacement input
|
|
952
|
-
* gets no `aria-activedescendant` at all.
|
|
953
|
-
*/
|
|
954
1373
|
#setActive(option) {
|
|
955
1374
|
const activeId = option ? ensureId(option, "stimeo-ms-opt") : null;
|
|
956
1375
|
this.#activeOptionId = activeId;
|
|
@@ -958,14 +1377,8 @@ var MultiSelectController = class extends Controller {
|
|
|
958
1377
|
candidate.toggleAttribute("data-active", candidate === option);
|
|
959
1378
|
}
|
|
960
1379
|
if (option && this.hasListTarget) scrollOptionIntoView(this.listTarget, option);
|
|
961
|
-
if (
|
|
962
|
-
if (activeId !== null) {
|
|
963
|
-
this.inputTarget.setAttribute("aria-activedescendant", activeId);
|
|
964
|
-
} else {
|
|
965
|
-
this.inputTarget.removeAttribute("aria-activedescendant");
|
|
966
|
-
}
|
|
1380
|
+
if (this.hasInputTarget) this.#activeDescendant.write(this.inputTarget, activeId);
|
|
967
1381
|
}
|
|
968
|
-
/** Repairs only active identity before a key; the fallback waits for the target callback. */
|
|
969
1382
|
#reconcileActiveForInteraction() {
|
|
970
1383
|
const activeId = this.#activeOptionId;
|
|
971
1384
|
if (activeId === null) return;
|
|
@@ -977,88 +1390,84 @@ var MultiSelectController = class extends Controller {
|
|
|
977
1390
|
this.#setActive(active);
|
|
978
1391
|
}
|
|
979
1392
|
}
|
|
980
|
-
/** Reflects whether the open list currently has no visible option targets. */
|
|
981
1393
|
#reflectEmpty() {
|
|
982
|
-
this.
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
);
|
|
1394
|
+
const empty = !this.#isClosed && this.#visibleOptions.length === 0;
|
|
1395
|
+
this.element.toggleAttribute(`data-${this.identifier}-empty`, empty);
|
|
1396
|
+
this.#emptyRegion.reflect(this.element, empty);
|
|
986
1397
|
}
|
|
987
|
-
/**
|
|
988
|
-
* Mirrors the selected values into named hidden inputs under the `fields`
|
|
989
|
-
* target so the selection submits with a normal form (parity with tags-input).
|
|
990
|
-
* No-ops without a `fields` target. When the
|
|
991
|
-
* `form` value is set, each input gets a matching `form` attribute so the picker
|
|
992
|
-
* can submit with a `<form>` it lives outside of.
|
|
993
|
-
*/
|
|
994
1398
|
#syncFields() {
|
|
995
|
-
if (!this.hasFieldsTarget) return;
|
|
996
|
-
this.
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
);
|
|
1399
|
+
if (!this.hasFieldsTarget) return false;
|
|
1400
|
+
const options = { name: this.nameValue, form: this.formValue };
|
|
1401
|
+
return writeFields(this.fieldsTarget, this.#values, options);
|
|
1402
|
+
}
|
|
1403
|
+
#publishSelection(selected, label, value, reason = "user") {
|
|
1404
|
+
const values = this.#values;
|
|
1405
|
+
const changed = !this.#sameSelection(this.#selectionValues, values);
|
|
1406
|
+
const fieldChanged = this.#syncFields();
|
|
1407
|
+
this.#selectionValues = values;
|
|
1408
|
+
if (!changed) return true;
|
|
1409
|
+
const token = this.#moves.record();
|
|
1410
|
+
if (fieldChanged) commitField(this.fieldsTarget);
|
|
1411
|
+
if (!this.#moves.isLatest(token)) return false;
|
|
1412
|
+
this.#announceTransition(selected, label, value, values.length);
|
|
1413
|
+
if (!this.#moves.isLatest(token)) return false;
|
|
1414
|
+
this.dispatch("change", { detail: { values, reason } });
|
|
1415
|
+
return this.#moves.isLatest(token);
|
|
1006
1416
|
}
|
|
1007
|
-
/** Keeps exactly one chip remove button tabbable after the set changes. */
|
|
1008
1417
|
#refreshRoving() {
|
|
1009
1418
|
this.#chipRow.ensureTabStop();
|
|
1010
1419
|
}
|
|
1011
|
-
/** Sends one localized selection transition through the page's shared announcer. */
|
|
1012
1420
|
#announceTransition(selected, label, value, count) {
|
|
1013
1421
|
const template = selected ? this.announceTextValue : this.announceRemovedTextValue;
|
|
1014
1422
|
announce(fillTemplate(template, { label, value, count }));
|
|
1015
1423
|
}
|
|
1016
|
-
/** Whether two arrays represent the same selection set, independent of DOM order. */
|
|
1017
1424
|
#sameSelection(left, right) {
|
|
1018
1425
|
if (left.length !== right.length) return false;
|
|
1019
1426
|
const a = [...left].sort();
|
|
1020
1427
|
const b = [...right].sort();
|
|
1021
1428
|
return a.every((value, index) => value === b[index]);
|
|
1022
1429
|
}
|
|
1023
|
-
/** Closes the list on a click outside the controller element. */
|
|
1024
1430
|
#onOutsideClick = (event) => {
|
|
1025
1431
|
if (!this.#isClosed && !this.element.contains(event.target)) this.close();
|
|
1026
1432
|
};
|
|
1027
|
-
/** Trimmed visible label of an option. */
|
|
1028
1433
|
#optionLabel(option) {
|
|
1029
1434
|
return (option.textContent ?? "").trim();
|
|
1030
1435
|
}
|
|
1031
|
-
/** An option's stable value: its `data-value`, else its display label. */
|
|
1032
1436
|
#optionValue(option) {
|
|
1033
1437
|
return option.dataset.value ?? this.#optionLabel(option);
|
|
1034
1438
|
}
|
|
1035
|
-
/** Options not hidden by the current filter. */
|
|
1036
1439
|
get #visibleOptions() {
|
|
1037
1440
|
return this.optionTargets.filter((option) => !option.hidden);
|
|
1038
1441
|
}
|
|
1039
|
-
/** Current active target resolved by stable id, never a detached node reference. */
|
|
1040
1442
|
get #activeOption() {
|
|
1041
1443
|
return this.optionTargets.find((option) => option.id === this.#activeOptionId) ?? null;
|
|
1042
1444
|
}
|
|
1043
|
-
/** Options currently selected. */
|
|
1044
1445
|
get #selectedOptions() {
|
|
1045
1446
|
return this.optionTargets.filter((option) => option.getAttribute("aria-selected") === "true");
|
|
1046
1447
|
}
|
|
1047
|
-
/** Selected values in option order. */
|
|
1048
1448
|
get #values() {
|
|
1049
1449
|
return this.#selectedOptions.map((option) => this.#optionValue(option));
|
|
1050
1450
|
}
|
|
1051
|
-
/** Normalized cardinality cap: zero and below are unlimited; a positive value floors, never below 1. */
|
|
1052
1451
|
get #selectionLimit() {
|
|
1053
|
-
if (
|
|
1054
|
-
return Math.max(1, Math.floor(this
|
|
1452
|
+
if (this.#safeMax <= 0) return 0;
|
|
1453
|
+
return Math.max(1, Math.floor(this.#safeMax));
|
|
1454
|
+
}
|
|
1455
|
+
#showList(list, open) {
|
|
1456
|
+
this.#listHidden.write(list, open ? null : "");
|
|
1055
1457
|
}
|
|
1056
|
-
/** Whether the list is currently hidden. */
|
|
1057
1458
|
get #isClosed() {
|
|
1058
1459
|
return !this.hasListTarget || this.listTarget.hidden !== false;
|
|
1059
1460
|
}
|
|
1461
|
+
get #safeMax() {
|
|
1462
|
+
return this.#numbers.read(
|
|
1463
|
+
this,
|
|
1464
|
+
"max",
|
|
1465
|
+
this.maxValue,
|
|
1466
|
+
_MultiSelectController.values.max.default,
|
|
1467
|
+
_MultiSelectController.valueConstraints.max
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1060
1470
|
};
|
|
1061
1471
|
|
|
1062
1472
|
export { MultiSelectController };
|
|
1063
|
-
//# sourceMappingURL=multi_select_controller.js.map
|
|
1064
1473
|
//# sourceMappingURL=multi_select_controller.js.map
|