stimeo-ui 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +136 -0
- package/README.md +27 -11
- package/dist/cable/index.d.ts +88 -31
- package/dist/cable/index.js +384 -449
- package/dist/cable/index.js.map +1 -1
- package/dist/coerce-JtZVNG5h.d.ts +20 -0
- package/dist/controllers/accordion_controller.d.ts +4 -2
- package/dist/controllers/accordion_controller.js +174 -55
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +12 -2
- package/dist/controllers/alert_dialog_controller.js +903 -235
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +37 -5
- package/dist/controllers/announcer_controller.js +238 -205
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.js +0 -10
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +30 -6
- package/dist/controllers/auto_submit_controller.js +414 -145
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +185 -107
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +0 -130
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +21 -3
- package/dist/controllers/bulk_select_controller.js +282 -58
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +38 -16
- package/dist/controllers/calendar_controller.js +228 -260
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +43 -12
- package/dist/controllers/carousel_controller.js +536 -436
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +44 -7
- package/dist/controllers/character_counter_controller.js +493 -207
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +7 -2
- package/dist/controllers/checkbox_controller.js +38 -74
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +16 -5
- package/dist/controllers/clipboard_controller.js +261 -149
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +17 -5
- package/dist/controllers/collapsible_controller.js +304 -120
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +18 -6
- package/dist/controllers/color_picker_controller.js +178 -146
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +24 -6
- package/dist/controllers/combobox_controller.js +522 -156
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +51 -20
- package/dist/controllers/command_palette_controller.js +1156 -395
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +15 -2
- package/dist/controllers/conditional_fields_controller.js +300 -171
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +49 -12
- package/dist/controllers/confirm_controller.js +980 -262
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +80 -18
- package/dist/controllers/context_menu_controller.js +346 -153
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +13 -1
- package/dist/controllers/count_up_controller.js +73 -21
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +47 -3
- package/dist/controllers/countdown_controller.js +281 -158
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +93 -10
- package/dist/controllers/currency_input_controller.js +575 -225
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +25 -12
- package/dist/controllers/data_grid_controller.js +248 -214
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +42 -14
- package/dist/controllers/date_range_picker_controller.js +338 -235
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +17 -6
- package/dist/controllers/dialog_controller.js +899 -237
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +25 -8
- package/dist/controllers/direct_upload_controller.js +97 -167
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +6 -1
- package/dist/controllers/dirty_form_controller.js +72 -69
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +19 -0
- package/dist/controllers/dismissible_controller.js +196 -24
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +39 -11
- package/dist/controllers/drawer_controller.js +974 -355
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +31 -1
- package/dist/controllers/dropdown_controller.js +339 -96
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +315 -88
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +79 -58
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +63 -18
- package/dist/controllers/file_dropzone_controller.js +322 -211
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +77 -42
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +35 -12
- package/dist/controllers/flash_controller.js +238 -347
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +15 -6
- package/dist/controllers/focus_controller.js +679 -240
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +156 -132
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.d.ts +3 -3
- package/dist/controllers/form_validation_controller.js +31 -97
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +47 -14
- package/dist/controllers/frame_loading_controller.js +275 -217
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +27 -6
- package/dist/controllers/highlight_controller.js +107 -75
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +60 -4
- package/dist/controllers/hover_card_controller.js +327 -130
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +48 -6
- package/dist/controllers/idle_controller.js +271 -101
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +30 -13
- package/dist/controllers/input_mask_controller.js +483 -97
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +32 -13
- package/dist/controllers/intersection_controller.js +150 -106
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +19 -2
- package/dist/controllers/lazy_frame_controller.js +24 -68
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +44 -9
- package/dist/controllers/listbox_controller.js +523 -240
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +9 -7
- package/dist/controllers/local_time_controller.js +102 -77
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +11 -3
- package/dist/controllers/masonry_controller.js +80 -96
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +55 -11
- package/dist/controllers/menu_controller.js +302 -188
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +51 -9
- package/dist/controllers/menubar_controller.js +232 -345
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +33 -20
- package/dist/controllers/meter_controller.js +417 -87
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +48 -18
- package/dist/controllers/multi_select_controller.js +785 -393
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +27 -6
- package/dist/controllers/navigation_menu_controller.js +190 -250
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +10 -0
- package/dist/controllers/nested_form_controller.js +396 -192
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +23 -0
- package/dist/controllers/network_status_controller.js +268 -62
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +50 -8
- package/dist/controllers/number_input_controller.js +610 -303
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.d.ts +8 -5
- package/dist/controllers/optimistic_controller.js +45 -87
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +55 -8
- package/dist/controllers/otp_controller.js +658 -255
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
- package/dist/controllers/overflow_indicator_controller.js +122 -113
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +58 -20
- package/dist/controllers/overflow_menu_controller.js +470 -295
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +37 -7
- package/dist/controllers/pagination_controller.js +332 -123
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +46 -8
- package/dist/controllers/password_reveal_controller.js +392 -121
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +15 -6
- package/dist/controllers/password_strength_controller.js +111 -158
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +11 -4
- package/dist/controllers/persist_controller.js +165 -111
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +35 -12
- package/dist/controllers/pointer_drag_controller.js +434 -188
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +41 -5
- package/dist/controllers/popover_controller.js +269 -113
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +44 -4
- package/dist/controllers/portal_controller.js +215 -76
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +10 -4
- package/dist/controllers/preview_guard_controller.js +243 -116
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +20 -19
- package/dist/controllers/progress_controller.js +391 -78
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +21 -9
- package/dist/controllers/radio_group_controller.js +278 -211
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +47 -6
- package/dist/controllers/range_slider_controller.js +220 -113
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +20 -4
- package/dist/controllers/rating_controller.js +458 -211
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +2 -0
- package/dist/controllers/read_more_controller.js +138 -63
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +1 -1
- package/dist/controllers/reading_progress_controller.js +166 -137
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +20 -6
- package/dist/controllers/relative_time_controller.js +211 -112
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
- package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
- package/dist/controllers/reset_on_restore_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +31 -2
- package/dist/controllers/resizable_controller.js +299 -150
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +3 -3
- package/dist/controllers/roving_controller.js +49 -88
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -3
- package/dist/controllers/scroll_area_controller.js +490 -246
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +3 -3
- package/dist/controllers/scroll_restore_controller.js +14 -55
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
- package/dist/controllers/scroll_visibility_controller.js +432 -149
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +6 -1
- package/dist/controllers/scrollspy_controller.js +193 -248
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +20 -5
- package/dist/controllers/separator_controller.js +285 -142
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +65 -8
- package/dist/controllers/sidebar_controller.js +1069 -404
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +23 -0
- package/dist/controllers/skeleton_controller.js +259 -101
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +29 -2
- package/dist/controllers/slider_controller.js +187 -86
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +22 -4
- package/dist/controllers/smart_sticky_header_controller.js +173 -66
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.d.ts +4 -4
- package/dist/controllers/sortable_controller.js +26 -125
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +39 -4
- package/dist/controllers/spinner_controller.js +288 -173
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +9 -4
- package/dist/controllers/step_indicator_controller.js +105 -46
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +14 -6
- package/dist/controllers/stepper_controller.js +120 -73
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +9 -1
- package/dist/controllers/stick_to_bottom_controller.js +275 -140
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +11 -0
- package/dist/controllers/sticky_observer_controller.js +220 -49
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +24 -8
- package/dist/controllers/submit_once_controller.js +376 -227
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +18 -4
- package/dist/controllers/switch_controller.js +201 -84
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +9 -6
- package/dist/controllers/tabs_controller.js +41 -30
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +16 -3
- package/dist/controllers/tags_input_controller.js +293 -173
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +11 -1
- package/dist/controllers/textarea_autosize_controller.js +108 -74
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +24 -11
- package/dist/controllers/theme_controller.js +140 -116
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +55 -5
- package/dist/controllers/time_picker_controller.js +331 -127
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +40 -16
- package/dist/controllers/toast_controller.js +244 -324
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +14 -6
- package/dist/controllers/toggle_group_controller.js +252 -191
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +8 -6
- package/dist/controllers/toolbar_controller.js +219 -120
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +49 -4
- package/dist/controllers/tooltip_controller.js +158 -124
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +22 -4
- package/dist/controllers/transition_controller.js +123 -119
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +22 -12
- package/dist/controllers/tree_view_controller.js +283 -369
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +8573 -10410
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +38 -14
- package/dist/inspector/cli.js +108 -40
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +145 -64
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +20 -20
- package/dist/inspector/manifest.json +802 -101
- package/dist/positioning/index.d.ts +15 -7
- package/dist/positioning/index.js +154 -47
- package/dist/positioning/index.js.map +1 -1
- package/package.json +11 -10
- package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
- package/dist/controllers/reset_before_cache_controller.js.map +0 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function fillTemplate(template, values) {
|
|
4
|
+
return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
|
|
5
|
+
const replacement = Object.hasOwn(values, name) ? values[name] : void 0;
|
|
6
|
+
return replacement === void 0 ? match : String(replacement);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
4
9
|
|
|
5
|
-
// src/utils/announce.ts
|
|
6
10
|
function announce(message, options = {}) {
|
|
7
11
|
const text = message.trim();
|
|
8
12
|
if (text.length === 0) return;
|
|
@@ -12,39 +16,41 @@ function announce(message, options = {}) {
|
|
|
12
16
|
})
|
|
13
17
|
);
|
|
14
18
|
}
|
|
15
|
-
function fillTemplate(template, values) {
|
|
16
|
-
return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
|
|
17
|
-
const replacement = values[name];
|
|
18
|
-
return replacement === void 0 ? match : String(replacement);
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
var ListenerSet = class {
|
|
21
|
+
#abort = new AbortController();
|
|
22
|
+
add(target, type, handler, options) {
|
|
23
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
24
|
+
}
|
|
25
|
+
dispose() {
|
|
26
|
+
this.#abort.abort();
|
|
27
|
+
this.#abort = new AbortController();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
23
31
|
function isRtl(element) {
|
|
24
32
|
return window.getComputedStyle(element).direction === "rtl";
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
// src/utils/microtask_coalescer.ts
|
|
28
35
|
var MicrotaskCoalescer = class {
|
|
29
36
|
#run;
|
|
30
37
|
#queued = false;
|
|
31
38
|
#active = false;
|
|
32
39
|
#generation = 0;
|
|
33
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
34
40
|
constructor(run) {
|
|
35
41
|
this.#run = run;
|
|
36
42
|
}
|
|
37
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
38
43
|
activate() {
|
|
39
44
|
this.#active = true;
|
|
40
45
|
}
|
|
41
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
42
46
|
cancel() {
|
|
43
47
|
this.#active = false;
|
|
44
48
|
this.#queued = false;
|
|
45
49
|
this.#generation += 1;
|
|
46
50
|
}
|
|
47
|
-
|
|
51
|
+
get pending() {
|
|
52
|
+
return this.#queued;
|
|
53
|
+
}
|
|
48
54
|
schedule() {
|
|
49
55
|
if (!this.#active || this.#queued) return;
|
|
50
56
|
this.#queued = true;
|
|
@@ -57,19 +63,16 @@ var MicrotaskCoalescer = class {
|
|
|
57
63
|
}
|
|
58
64
|
};
|
|
59
65
|
|
|
60
|
-
// src/utils/transient_hooks.ts
|
|
61
66
|
var TransientHooks = class {
|
|
62
67
|
#attributes;
|
|
63
68
|
constructor(options) {
|
|
64
69
|
this.#attributes = options.attributes;
|
|
65
70
|
}
|
|
66
|
-
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
67
71
|
reset(element) {
|
|
68
72
|
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
69
73
|
}
|
|
70
74
|
};
|
|
71
75
|
|
|
72
|
-
// src/controllers/sortable_controller.ts
|
|
73
76
|
var TRANSIENT = new TransientHooks({ attributes: ["data-sortable-dragging"] });
|
|
74
77
|
var SortableController = class extends Controller {
|
|
75
78
|
static targets = ["list", "item"];
|
|
@@ -82,36 +85,22 @@ var SortableController = class extends Controller {
|
|
|
82
85
|
};
|
|
83
86
|
static events = ["reorder"];
|
|
84
87
|
#session = null;
|
|
85
|
-
/** Defers the lost-item check to after the mutation batch (see below). */
|
|
86
88
|
#settle = new MicrotaskCoalescer(() => this.#dropLostSession());
|
|
89
|
+
#listeners = new ListenerSet();
|
|
87
90
|
connect() {
|
|
88
91
|
TRANSIENT.reset(this.element);
|
|
89
92
|
this.#settle.activate();
|
|
90
|
-
this.element
|
|
91
|
-
this.element
|
|
92
|
-
this.element
|
|
93
|
-
this.element
|
|
93
|
+
this.#listeners.add(this.element, "stimeo--pointer-drag:start", this.#onDragStart);
|
|
94
|
+
this.#listeners.add(this.element, "stimeo--pointer-drag:move", this.#onDragMove);
|
|
95
|
+
this.#listeners.add(this.element, "stimeo--pointer-drag:end", this.#onDragEnd);
|
|
96
|
+
this.#listeners.add(this.element, "stimeo--pointer-drag:cancel", this.#onDragCancel);
|
|
94
97
|
}
|
|
95
98
|
disconnect() {
|
|
96
|
-
this.
|
|
97
|
-
this.element.removeEventListener("stimeo--pointer-drag:move", this.#onDragMove);
|
|
98
|
-
this.element.removeEventListener("stimeo--pointer-drag:end", this.#onDragEnd);
|
|
99
|
-
this.element.removeEventListener("stimeo--pointer-drag:cancel", this.#onDragCancel);
|
|
99
|
+
this.#listeners.dispose();
|
|
100
100
|
this.#settle.cancel();
|
|
101
101
|
this.#session = null;
|
|
102
102
|
this.element.removeAttribute("data-sortable-dragging");
|
|
103
103
|
}
|
|
104
|
-
/**
|
|
105
|
-
* Ends a session whose item left the item set.
|
|
106
|
-
*
|
|
107
|
-
* The controller's own reorder detaches and reattaches the item inside one
|
|
108
|
-
* mutation batch, so the loss is only real once the batch has settled — the
|
|
109
|
-
* check therefore runs a microtask later and asks whether the item is a target
|
|
110
|
-
* again. Without it a deleted row (a broadcast that drops it from the board)
|
|
111
|
-
* or a morph that strips the item's target attribute would hold the
|
|
112
|
-
* one-at-a-time session for the rest of the page's life, with the root hook
|
|
113
|
-
* stuck on and every later grab refused with no way out.
|
|
114
|
-
*/
|
|
115
104
|
itemTargetDisconnected(item) {
|
|
116
105
|
if (this.#session?.item !== item) return;
|
|
117
106
|
this.#settle.schedule();
|
|
@@ -125,7 +114,6 @@ var SortableController = class extends Controller {
|
|
|
125
114
|
this.element.removeAttribute("data-sortable-dragging");
|
|
126
115
|
if (session.item.isConnected) this.#restore(session, items);
|
|
127
116
|
}
|
|
128
|
-
/** Picks the item up: remembers its neighbours and announces the grab. */
|
|
129
117
|
#onDragStart = (event) => {
|
|
130
118
|
if (this.#session) return;
|
|
131
119
|
const items = this.#items();
|
|
@@ -153,7 +141,6 @@ var SortableController = class extends Controller {
|
|
|
153
141
|
this.#followPointer(session, detail, items);
|
|
154
142
|
}
|
|
155
143
|
};
|
|
156
|
-
/** Drops the item: announces, then reports `reorder` if the position changed. */
|
|
157
144
|
#onDragEnd = (event) => {
|
|
158
145
|
const session = this.#session;
|
|
159
146
|
if (!session) return;
|
|
@@ -168,7 +155,6 @@ var SortableController = class extends Controller {
|
|
|
168
155
|
this.dispatch("reorder", { detail: { item: session.item, from, to } });
|
|
169
156
|
}
|
|
170
157
|
};
|
|
171
|
-
/** Restores the pickup position (Escape / OS `pointercancel`). */
|
|
172
158
|
#onDragCancel = (event) => {
|
|
173
159
|
const session = this.#session;
|
|
174
160
|
if (!session) return;
|
|
@@ -179,16 +165,6 @@ var SortableController = class extends Controller {
|
|
|
179
165
|
this.#restore(session, items);
|
|
180
166
|
this.#announce("canceled", session.item);
|
|
181
167
|
};
|
|
182
|
-
/**
|
|
183
|
-
* The slot the item was picked up from, read back through the neighbours it
|
|
184
|
-
* had then — `null` when neither survives and the item itself is gone.
|
|
185
|
-
*
|
|
186
|
-
* The anchor is the item that followed it, so restoring means "before that one
|
|
187
|
-
* again". When the anchor was deleted mid-drag the predecessor answers the
|
|
188
|
-
* same question from the other side. With both gone the item's current slot is
|
|
189
|
-
* the honest answer: nothing is known to have moved, so no reorder is reported
|
|
190
|
-
* and a cancel leaves the item where it is.
|
|
191
|
-
*/
|
|
192
168
|
#pickupSlot(session, items) {
|
|
193
169
|
const others = items.filter((candidate) => candidate !== session.item);
|
|
194
170
|
if (session.anchor) {
|
|
@@ -202,7 +178,6 @@ var SortableController = class extends Controller {
|
|
|
202
178
|
const here = items.indexOf(session.item);
|
|
203
179
|
return here === -1 ? null : here;
|
|
204
180
|
}
|
|
205
|
-
/** Puts the item back where it was picked up from. */
|
|
206
181
|
#restore(session, items) {
|
|
207
182
|
const slot = this.#pickupSlot(session, items);
|
|
208
183
|
if (slot === null) return;
|
|
@@ -212,17 +187,6 @@ var SortableController = class extends Controller {
|
|
|
212
187
|
slot
|
|
213
188
|
);
|
|
214
189
|
}
|
|
215
|
-
/**
|
|
216
|
-
* Keyboard stepping: `pointer-drag` reports *cumulative* synthetic deltas, so
|
|
217
|
-
* the difference from the last consumed value is one arrow press — its sign is
|
|
218
|
-
* the direction. Cross-axis arrows never change the primary delta (no move).
|
|
219
|
-
*
|
|
220
|
-
* The sign is physical (`ArrowRight` is always `+dx`), so a right-to-left row
|
|
221
|
-
* has to invert it: there, moving the item rightward means moving it *earlier*
|
|
222
|
-
* in the DOM. Skipping this would also split the two halves of one keypress —
|
|
223
|
-
* `roving` already moves focus logically, so the same arrow would send the
|
|
224
|
-
* focus and the grabbed item opposite ways.
|
|
225
|
-
*/
|
|
226
190
|
#stepFromKeyboard(session, detail, items) {
|
|
227
191
|
const primary = Number(this.#isVertical ? detail.dy : detail.dx) || 0;
|
|
228
192
|
const delta = primary - session.lastPrimary;
|
|
@@ -239,18 +203,6 @@ var SortableController = class extends Controller {
|
|
|
239
203
|
);
|
|
240
204
|
this.#announce("moved", session.item);
|
|
241
205
|
}
|
|
242
|
-
/**
|
|
243
|
-
* Pointer following: the item moves to the slot whose siblings' midpoints the
|
|
244
|
-
* pointer has passed (per `orientation`).
|
|
245
|
-
*
|
|
246
|
-
* Only siblings that occupy space take part. A row with no layout box — a
|
|
247
|
-
* filtered-out item, a `display: none` ancestor, a collapsed `<details>` — is
|
|
248
|
-
* reported with an empty rect at the document origin, and its midpoint of `0`
|
|
249
|
-
* sits below every pointer position: counted, it would read as passed on the
|
|
250
|
-
* very first move and send the item across a slot the pointer never crossed.
|
|
251
|
-
* With no laid-out sibling at all there is nothing to compare against and the
|
|
252
|
-
* move is skipped entirely.
|
|
253
|
-
*/
|
|
254
206
|
#followPointer(session, detail, items) {
|
|
255
207
|
const pointer = Number(this.#isVertical ? detail.y : detail.x) || 0;
|
|
256
208
|
const vertical = this.#isVertical;
|
|
@@ -272,18 +224,6 @@ var SortableController = class extends Controller {
|
|
|
272
224
|
this.#insertAt(session.item, laidOut, target);
|
|
273
225
|
this.#announce("moved", session.item);
|
|
274
226
|
}
|
|
275
|
-
/**
|
|
276
|
-
* Reinserts `item` at `index` among `scope`, relative to the sibling already
|
|
277
|
-
* standing there.
|
|
278
|
-
*
|
|
279
|
-
* The reorder is defined against the items, not against a container: the
|
|
280
|
-
* neighbour's own parent is where the item belongs. A `list` target that is
|
|
281
|
-
* not the items' parent — or none at all, which the markup contract allows —
|
|
282
|
-
* therefore still lands the move in the right place instead of throwing, and
|
|
283
|
-
* the last slot is *after the last item* rather than after whatever else the
|
|
284
|
-
* container holds (a live region, a footer), which would put the row outside
|
|
285
|
-
* the reading order the list publishes.
|
|
286
|
-
*/
|
|
287
227
|
#insertAt(item, scope, index) {
|
|
288
228
|
const clamped = Math.max(0, Math.min(index, scope.length));
|
|
289
229
|
const ahead = scope[clamped] ?? null;
|
|
@@ -297,20 +237,6 @@ var SortableController = class extends Controller {
|
|
|
297
237
|
parent.insertBefore(item, ahead ?? neighbour.nextSibling);
|
|
298
238
|
if (hadFocus) active.focus();
|
|
299
239
|
}
|
|
300
|
-
/**
|
|
301
|
-
* Hands one step to the page's shared announcer.
|
|
302
|
-
*
|
|
303
|
-
* The library carries no live region and no English copy: the wording is the
|
|
304
|
-
* consumer's, written into `announceGrabbedText` / `announceMovedText` /
|
|
305
|
-
* `announceDroppedText` / `announceCanceledText` with `{name}` / `{position}` /
|
|
306
|
-
* `{total}` placeholders, and an unset one announces nothing.
|
|
307
|
-
*
|
|
308
|
-
* Only transitions reach here — the pickup, a step that actually changed the
|
|
309
|
-
* landing slot, and the single end of the session — so a pointer crossing the
|
|
310
|
-
* same slot twice or an arrow clamped at an end stays silent.
|
|
311
|
-
*
|
|
312
|
-
* @stimeoRuntimeOnly The texts word the steps of one reorder, spoken as they happen.
|
|
313
|
-
*/
|
|
314
240
|
#announce(key, item) {
|
|
315
241
|
const template = this.#announceTemplate(key);
|
|
316
242
|
if (template.length === 0) return;
|
|
@@ -323,7 +249,6 @@ var SortableController = class extends Controller {
|
|
|
323
249
|
})
|
|
324
250
|
);
|
|
325
251
|
}
|
|
326
|
-
/** The consumer's wording for one step, or `""` when they authored none. */
|
|
327
252
|
#announceTemplate(key) {
|
|
328
253
|
switch (key) {
|
|
329
254
|
case "grabbed":
|
|
@@ -336,52 +261,28 @@ var SortableController = class extends Controller {
|
|
|
336
261
|
return this.announceCanceledTextValue;
|
|
337
262
|
}
|
|
338
263
|
}
|
|
339
|
-
/** The announced item name: the authored override, else its collapsed text. */
|
|
340
264
|
#nameOf(item) {
|
|
341
265
|
const authored = item.getAttribute(`data-${this.identifier}-name`);
|
|
342
266
|
if (authored) return authored;
|
|
343
267
|
return (item.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
344
268
|
}
|
|
345
|
-
/**
|
|
346
|
-
* Resolves the sortable item owning a bubbled `pointer-drag` event.
|
|
347
|
-
*
|
|
348
|
-
* `pointer-drag` dispatches on its own element, and the markup contract puts
|
|
349
|
-
* one on each item, so the owner is the item that **is** the target. Matching
|
|
350
|
-
* an ancestor instead would make a card's own inner draggable — a knob, a
|
|
351
|
-
* split pane, anything the primitive is composed into — drive the card.
|
|
352
|
-
*/
|
|
353
269
|
#itemFor(target, items) {
|
|
354
270
|
if (!target) return null;
|
|
355
271
|
return items.find((item) => item === target) ?? null;
|
|
356
272
|
}
|
|
357
|
-
/** The items in live DOM order (targets re-query the DOM on every access). */
|
|
358
273
|
#items() {
|
|
359
274
|
return this.itemTargets;
|
|
360
275
|
}
|
|
361
|
-
/** The reorder container: the `list` target when present, else the element. */
|
|
362
276
|
get #list() {
|
|
363
277
|
return this.hasListTarget ? this.listTarget : this.element;
|
|
364
278
|
}
|
|
365
279
|
get #isVertical() {
|
|
366
280
|
return this.orientationValue !== "horizontal";
|
|
367
281
|
}
|
|
368
|
-
/**
|
|
369
|
-
* Whether DOM order runs opposite to the primary coordinate — true only for a
|
|
370
|
-
* horizontal row under `dir="rtl"`, where the first item sits at the largest
|
|
371
|
-
* `x`. Both drag paths reach this controller in physical terms (`pointer-drag`
|
|
372
|
-
* documents its deltas as physical and hands RTL to its consumer, which is
|
|
373
|
-
* this controller), so both have to be mapped back onto DOM order here.
|
|
374
|
-
*
|
|
375
|
-
* Read from the list, the element that lays the items out — never from the
|
|
376
|
-
* dragged item, which may carry its own `dir`. The list inherits the computed
|
|
377
|
-
* direction, so authoring `dir` on the root works too. A vertical list is
|
|
378
|
-
* unaffected: writing direction does not mirror the block axis.
|
|
379
|
-
*/
|
|
380
282
|
get #isReversed() {
|
|
381
283
|
return !this.#isVertical && isRtl(this.#list);
|
|
382
284
|
}
|
|
383
285
|
};
|
|
384
286
|
|
|
385
287
|
export { SortableController };
|
|
386
|
-
//# sourceMappingURL=sortable_controller.js.map
|
|
387
288
|
//# sourceMappingURL=sortable_controller.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/logical_scroll.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/transient_hooks.ts","../../src/controllers/sortable_controller.ts"],"names":[],"mappings":"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","file":"sortable_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Drops the transient state hooks a connection inherited, so the cycle it opens\n * starts from a DOM that claims nothing.\n *\n * A transient hook is an attribute a controller writes to say \"this is happening\n * right now\" — a drag in progress, a form with unsaved edits, a refused\n * subscription. Its lifetime belongs to the interaction, not to the element, and\n * the two come apart whenever the element outlives the instance that wrote it: a\n * Turbo cache snapshot taken mid-interaction restores the hook with the markup,\n * and an element re-inserted elsewhere in the page carries it along. Nothing\n * takes such a hook off, because the instance that owned it is gone; consumer\n * CSS then keeps claiming a state no one is in.\n *\n * **`turbo:before-cache` cannot do this job**, which is why the pass belongs in\n * `connect()`: that event fires for a navigation only, so an in-page move — the\n * other half of the population — never reaches it, and a hook that arrived with\n * a moved element would survive. `connect()` covers both, because both end in a\n * connection.\n *\n * Scope is the **drop** only. What the fresh cycle should say instead is the\n * consumer's: a value re-read from the live DOM, a roster refilled by the\n * stream, a measurement retaken from the current scroll position, a slot emptied\n * of the last result. Those answers differ per consumer and none of them is a\n * hook removal, so the pass stops where every consumer agrees.\n *\n * A declaration names one group of hooks and carries no per-instance state, so\n * it belongs at module scope, shared by every instance the page connects. A\n * consumer writes as many as it has groups — hooks it drops under different\n * conditions cannot share one. The exception is a hook whose name is only known\n * at runtime, such as one built from the registered identifier: that\n * declaration is a field, because the name is.\n *\n * **A drop is not always right.** The population splits on one question: can the\n * state a hook describes outlive the instance that wrote it? Where it cannot — a\n * drag ends with the pointer, a subscription dies with its socket — dropping on\n * every connection is correct. Where it can — a submission in flight is not\n * interrupted by an in-page move — only a connection that follows a real restore\n * may drop, and the consumer answers \"did a session survive?\" with the signal it\n * already holds for its own teardown (a live-session field, a `DetachGate`\n * probe). The drop then sits behind that answer, as the second example shows.\n *\n * @example Unconditional — the state cannot outlive the instance.\n * ```ts\n * const TRANSIENT = new TransientHooks({ attributes: [\"data-sortable-dragging\"] });\n *\n * connect(): void {\n * TRANSIENT.reset(this.element);\n * }\n * ```\n *\n * @example Gated — an in-page move must not drop a live state.\n * ```ts\n * const TRANSIENT = new TransientHooks({ attributes: [\"data-optimistic\"] });\n *\n * connect(): void {\n * const restored = !this.#connected && !this.#gate.pending;\n * this.#gate.cancel();\n * this.#connected = true;\n * if (restored) TRANSIENT.reset(this.element);\n * }\n * ```\n */\n\n/** What a consumer declares once about the hooks its interaction owns. */\nexport interface TransientHooksOptions {\n /**\n * The attribute names a connection may find written by an earlier one. Hooks\n * a consumer drops under different conditions belong to different\n * declarations, so each one can be reset on its own.\n */\n readonly attributes: readonly string[];\n}\n\n/**\n * One consumer's declaration of the hooks its interaction owns, dropped from an\n * element by {@link TransientHooks.reset}.\n *\n * Every member comment inside this class body is inlined into each consumer\n * bundle (`tsup` builds with `splitting: false`), so rationale belongs in this\n * file's own block, which `dist` drops, and only the contract belongs inside.\n */\nexport class TransientHooks {\n readonly #attributes: readonly string[];\n\n constructor(options: TransientHooksOptions) {\n this.#attributes = options.attributes;\n }\n\n /** Drops the declared hooks from `element`; what it carries besides stays. */\n reset(element: Element): void {\n for (const attribute of this.#attributes) element.removeAttribute(attribute);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { isRtl } from \"../utils/logical_scroll\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { TransientHooks } from \"../utils/transient_hooks\";\n\n/** The hook a connection may find written by an earlier, now-gone one. */\nconst TRANSIENT = new TransientHooks({ attributes: [\"data-sortable-dragging\"] });\n\n/** The four steps a consumer can give wording to. */\ntype AnnounceKey = \"grabbed\" | \"moved\" | \"dropped\" | \"canceled\";\n\n/** A reorder in flight: one item picked up by pointer or keyboard. */\ninterface SortSession {\n item: HTMLElement;\n /**\n * The item that followed it at pickup, and the one that preceded it.\n *\n * The pickup slot is remembered as nodes rather than as an index because the\n * list can change underneath a live drag — a broadcast board inserts and\n * deletes rows while someone is holding one — and an index taken at pickup\n * stops pointing at that place the moment a row appears above it.\n */\n anchor: HTMLElement | null;\n predecessor: HTMLElement | null;\n /** Last primary-axis cumulative delta consumed by keyboard stepping. */\n lastPrimary: number;\n}\n\n/**\n * Headless, accessible **sortable** (single-list reorder) — the a11y-first\n * drag-and-drop that is the flagship consumer of the\n * `stimeo--pointer-drag` primitive. The composition is markup-level and each\n * layer keeps its job: `pointer-drag` (on every item) emits the normalized drag\n * signal with its built-in keyboard alternative, `roving` (on the list) keeps\n * the handles a single Tab stop, and this controller interprets the signal —\n * live-reordering the DOM, handing each step to the page's shared announcer,\n * and reporting the final `reorder`. No dedicated APG pattern exists for\n * drag-and-drop; the keyboard model (grab → arrows → drop / Escape) comes from\n * `pointer-drag` and the announcements make it non-visually trackable\n * (WCAG 2.1.1 / 2.5.7 / 4.1.3). Core (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--sortable`):\n * <div data-controller=\"stimeo--sortable\">\n * <ul data-stimeo--sortable-target=\"list\" data-controller=\"stimeo--roving\"\n * data-stimeo--roving-orientation-value=\"vertical\">\n * <li data-stimeo--sortable-target=\"item\" data-stimeo--sortable-name=\"Card A\"\n * data-controller=\"stimeo--pointer-drag\"\n * data-stimeo--pointer-drag-axis-value=\"y\">\n * <span>Card A</span>\n * <button type=\"button\" aria-label=\"Reorder Card A\"\n * data-stimeo--pointer-drag-target=\"handle\"\n * data-stimeo--roving-target=\"item\">⠿</button>\n * </li>\n * …\n * </ul>\n * </div>\n *\n * The composed values must follow the sort axis: roving's `orientation`\n * (which defaults to `horizontal`, so a vertical list has to author\n * `vertical`) and pointer-drag's `axis`. `stimeo check` enforces both\n * alignments as composition rules (`composition-mismatch`).\n *\n * Pointer flow: dragging an item live-moves it whenever the pointer crosses a\n * sibling's midpoint (per `orientation`). Keyboard flow: each synthetic\n * `pointer-drag` move steps the item one position. A horizontal list under\n * `dir=\"rtl\"` reverses both, since DOM order then runs right-to-left while\n * `pointer-drag` reports physical coordinates. Dropping dispatches\n * `reorder` (`{ item, from, to }`, zero-based) when the position changed;\n * Escape / `pointercancel` restores the pickup position. Every step is handed to\n * the page's shared `stimeo--announcer` as the consumer's own wording —\n * `announceGrabbedText` / `announceMovedText` / `announceDroppedText` /\n * `announceCanceledText`, with `{name}` / `{position}` / `{total}` placeholders —\n * and an unset one announces nothing.\n *\n * @remarks\n * Behavior only — the ghost/placeholder/drop-hint visuals are the consumer's\n * CSS, keyed off `pointer-drag`'s `data-dragging`/`data-grabbed` on the item\n * and this controller's `data-sortable-dragging` on the root. Persistence is\n * the consumer's `reorder` listener (post the new position; a Turbo Stream\n * broadcast makes the board collaborative). The DOM order is the single source\n * of truth: items are read live on every event, so rows appended by Turbo need\n * no rewiring, and a Turbo cache restore reconnects idempotently (`connect()`\n * clears the transient root hook; a drag cannot survive a navigation). Only the\n * event listeners are held, and they are removed on `disconnect()`. Multi-list\n * / kanban, nested DnD, auto-scroll at the edges, and virtualized lists are\n * deliberately out of this single-list scope.\n */\nexport class SortableController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"item\"];\n static override values = {\n orientation: { type: String, default: \"vertical\" },\n announceGrabbedText: { type: String, default: \"\" },\n announceMovedText: { type: String, default: \"\" },\n announceDroppedText: { type: String, default: \"\" },\n announceCanceledText: { type: String, default: \"\" },\n };\n static events = [\"reorder\"] as const;\n\n declare readonly hasListTarget: boolean;\n declare readonly listTarget: HTMLElement;\n declare readonly itemTargets: HTMLElement[];\n declare orientationValue: string;\n declare announceGrabbedTextValue: string;\n declare announceMovedTextValue: string;\n declare announceDroppedTextValue: string;\n declare announceCanceledTextValue: string;\n\n #session: SortSession | null = null;\n /** Defers the lost-item check to after the mutation batch (see below). */\n readonly #settle = new MicrotaskCoalescer(() => this.#dropLostSession());\n\n override connect(): void {\n // A drag cannot survive a navigation: drop the hook a Turbo cache snapshot\n // may have preserved mid-drag (idempotent reconnect).\n TRANSIENT.reset(this.element);\n this.#settle.activate();\n this.element.addEventListener(\"stimeo--pointer-drag:start\", this.#onDragStart);\n this.element.addEventListener(\"stimeo--pointer-drag:move\", this.#onDragMove);\n this.element.addEventListener(\"stimeo--pointer-drag:end\", this.#onDragEnd);\n this.element.addEventListener(\"stimeo--pointer-drag:cancel\", this.#onDragCancel);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"stimeo--pointer-drag:start\", this.#onDragStart);\n this.element.removeEventListener(\"stimeo--pointer-drag:move\", this.#onDragMove);\n this.element.removeEventListener(\"stimeo--pointer-drag:end\", this.#onDragEnd);\n this.element.removeEventListener(\"stimeo--pointer-drag:cancel\", this.#onDragCancel);\n this.#settle.cancel();\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n }\n\n /**\n * Ends a session whose item left the item set.\n *\n * The controller's own reorder detaches and reattaches the item inside one\n * mutation batch, so the loss is only real once the batch has settled — the\n * check therefore runs a microtask later and asks whether the item is a target\n * again. Without it a deleted row (a broadcast that drops it from the board)\n * or a morph that strips the item's target attribute would hold the\n * one-at-a-time session for the rest of the page's life, with the root hook\n * stuck on and every later grab refused with no way out.\n */\n itemTargetDisconnected(item: HTMLElement): void {\n if (this.#session?.item !== item) return;\n this.#settle.schedule();\n }\n\n #dropLostSession(): void {\n const session = this.#session;\n if (!session) return;\n const items = this.#items();\n if (items.includes(session.item)) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n // An element that is still in the document (a morph took only its target\n // attribute) goes back where it was picked up. Nothing is announced either\n // way: the row is no longer one of the items, so there is no position to\n // read out — what matters is that the next grab is not refused.\n if (session.item.isConnected) this.#restore(session, items);\n }\n\n /** Picks the item up: remembers its neighbours and announces the grab. */\n readonly #onDragStart = (event: Event): void => {\n // One reorder at a time: a start from another item (each item has its own\n // pointer-drag instance) must not clobber the live session — its pickup\n // slot and cancel restore would be lost.\n if (this.#session) return;\n const items = this.#items();\n const item = this.#itemFor(event.target, items);\n if (!item) return;\n const index = items.indexOf(item);\n this.#session = {\n item,\n anchor: items[index + 1] ?? null,\n predecessor: items[index - 1] ?? null,\n lastPrimary: 0,\n };\n this.element.setAttribute(\"data-sortable-dragging\", \"true\");\n this.#announce(\"grabbed\", item);\n };\n\n readonly #onDragMove = (event: Event): void => {\n const session = this.#session;\n if (!session) return;\n const items = this.#items();\n if (this.#itemFor(event.target, items) !== session.item) return;\n\n const detail = (event as CustomEvent<Record<string, number | string>>).detail;\n if (detail.pointerType === \"keyboard\") {\n this.#stepFromKeyboard(session, detail, items);\n } else {\n this.#followPointer(session, detail, items);\n }\n };\n\n /** Drops the item: announces, then reports `reorder` if the position changed. */\n readonly #onDragEnd = (event: Event): void => {\n const session = this.#session;\n if (!session) return;\n const items = this.#items();\n if (this.#itemFor(event.target, items) !== session.item) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.#announce(\"dropped\", session.item);\n const to = items.indexOf(session.item);\n const from = this.#pickupSlot(session, items) ?? to;\n if (to !== from) {\n this.dispatch(\"reorder\", { detail: { item: session.item, from, to } });\n }\n };\n\n /** Restores the pickup position (Escape / OS `pointercancel`). */\n readonly #onDragCancel = (event: Event): void => {\n const session = this.#session;\n if (!session) return;\n const items = this.#items();\n if (this.#itemFor(event.target, items) !== session.item) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.#restore(session, items);\n this.#announce(\"canceled\", session.item);\n };\n\n /**\n * The slot the item was picked up from, read back through the neighbours it\n * had then — `null` when neither survives and the item itself is gone.\n *\n * The anchor is the item that followed it, so restoring means \"before that one\n * again\". When the anchor was deleted mid-drag the predecessor answers the\n * same question from the other side. With both gone the item's current slot is\n * the honest answer: nothing is known to have moved, so no reorder is reported\n * and a cancel leaves the item where it is.\n */\n #pickupSlot(session: SortSession, items: HTMLElement[]): number | null {\n const others = items.filter((candidate) => candidate !== session.item);\n if (session.anchor) {\n const at = others.indexOf(session.anchor);\n if (at !== -1) return at;\n }\n if (session.predecessor) {\n const at = others.indexOf(session.predecessor);\n if (at !== -1) return at + 1;\n }\n const here = items.indexOf(session.item);\n return here === -1 ? null : here;\n }\n\n /** Puts the item back where it was picked up from. */\n #restore(session: SortSession, items: HTMLElement[]): void {\n const slot = this.#pickupSlot(session, items);\n if (slot === null) return;\n this.#insertAt(\n session.item,\n items.filter((candidate) => candidate !== session.item),\n slot,\n );\n }\n\n /**\n * Keyboard stepping: `pointer-drag` reports *cumulative* synthetic deltas, so\n * the difference from the last consumed value is one arrow press — its sign is\n * the direction. Cross-axis arrows never change the primary delta (no move).\n *\n * The sign is physical (`ArrowRight` is always `+dx`), so a right-to-left row\n * has to invert it: there, moving the item rightward means moving it *earlier*\n * in the DOM. Skipping this would also split the two halves of one keypress —\n * `roving` already moves focus logically, so the same arrow would send the\n * focus and the grabbed item opposite ways.\n */\n #stepFromKeyboard(\n session: SortSession,\n detail: Record<string, number | string>,\n items: HTMLElement[],\n ): void {\n const primary = Number(this.#isVertical ? detail.dy : detail.dx) || 0;\n const delta = primary - session.lastPrimary;\n session.lastPrimary = primary;\n if (delta === 0) return;\n\n const index = items.indexOf(session.item);\n const step = (delta > 0 ? 1 : -1) * (this.#isReversed ? -1 : 1);\n const next = Math.max(0, Math.min(index + step, items.length - 1));\n if (next === index) return;\n this.#insertAt(\n session.item,\n items.filter((candidate) => candidate !== session.item),\n next,\n );\n this.#announce(\"moved\", session.item);\n }\n\n /**\n * Pointer following: the item moves to the slot whose siblings' midpoints the\n * pointer has passed (per `orientation`).\n *\n * Only siblings that occupy space take part. A row with no layout box — a\n * filtered-out item, a `display: none` ancestor, a collapsed `<details>` — is\n * reported with an empty rect at the document origin, and its midpoint of `0`\n * sits below every pointer position: counted, it would read as passed on the\n * very first move and send the item across a slot the pointer never crossed.\n * With no laid-out sibling at all there is nothing to compare against and the\n * move is skipped entirely.\n */\n #followPointer(\n session: SortSession,\n detail: Record<string, number | string>,\n items: HTMLElement[],\n ): void {\n const pointer = Number(this.#isVertical ? detail.y : detail.x) || 0;\n const vertical = this.#isVertical;\n // `target` counts the siblings that precede the pointer *in DOM order*, and\n // the two agree only while DOM order runs the same way as the coordinate. In\n // a right-to-left row it runs the other way, so the comparison flips;\n // leaving it physical would mix two orderings into one number and drop the\n // item at a slot the pointer never crossed. Resolved once: it cannot change\n // mid-loop, and reading it per sibling would re-run `getComputedStyle` on\n // every pointermove.\n const reversed = this.#isReversed;\n const here = items.indexOf(session.item);\n const laidOut: HTMLElement[] = [];\n let target = 0;\n // `current` is the item's own slot in that same laid-out space: the number\n // of laid-out siblings that come before it in DOM order.\n let current = 0;\n items.forEach((item, index) => {\n if (item === session.item) return;\n const rect = item.getBoundingClientRect();\n if (rect.width === 0 && rect.height === 0) return;\n if (index < here) current += 1;\n laidOut.push(item);\n const midpoint = vertical ? rect.top + rect.height / 2 : rect.left + rect.width / 2;\n if (reversed ? pointer < midpoint : pointer > midpoint) target += 1;\n });\n if (laidOut.length === 0 || target === current) return;\n\n this.#insertAt(session.item, laidOut, target);\n this.#announce(\"moved\", session.item);\n }\n\n /**\n * Reinserts `item` at `index` among `scope`, relative to the sibling already\n * standing there.\n *\n * The reorder is defined against the items, not against a container: the\n * neighbour's own parent is where the item belongs. A `list` target that is\n * not the items' parent — or none at all, which the markup contract allows —\n * therefore still lands the move in the right place instead of throwing, and\n * the last slot is *after the last item* rather than after whatever else the\n * container holds (a live region, a footer), which would put the row outside\n * the reading order the list publishes.\n */\n #insertAt(item: HTMLElement, scope: HTMLElement[], index: number): void {\n const clamped = Math.max(0, Math.min(index, scope.length));\n const ahead = scope[clamped] ?? null;\n const behind = ahead ? null : (scope[scope.length - 1] ?? null);\n const neighbour = ahead ?? behind;\n if (!neighbour) return;\n const parent = neighbour.parentNode;\n if (!parent) return;\n // Real browsers drop focus when the focused node is re-inserted (the move\n // is a remove+insert), which would strand the keyboard grab after one\n // arrow press — restore it so focus rides the moved item.\n const active = document.activeElement;\n const hadFocus = active instanceof HTMLElement && item.contains(active);\n parent.insertBefore(item, ahead ?? neighbour.nextSibling);\n if (hadFocus) active.focus();\n }\n\n /**\n * Hands one step to the page's shared announcer.\n *\n * The library carries no live region and no English copy: the wording is the\n * consumer's, written into `announceGrabbedText` / `announceMovedText` /\n * `announceDroppedText` / `announceCanceledText` with `{name}` / `{position}` /\n * `{total}` placeholders, and an unset one announces nothing.\n *\n * Only transitions reach here — the pickup, a step that actually changed the\n * landing slot, and the single end of the session — so a pointer crossing the\n * same slot twice or an arrow clamped at an end stays silent.\n *\n * @stimeoRuntimeOnly The texts word the steps of one reorder, spoken as they happen.\n */\n #announce(key: AnnounceKey, item: HTMLElement): void {\n const template = this.#announceTemplate(key);\n if (template.length === 0) return;\n const items = this.#items();\n announce(\n fillTemplate(template, {\n name: this.#nameOf(item),\n position: items.indexOf(item) + 1,\n total: items.length,\n }),\n );\n }\n\n /** The consumer's wording for one step, or `\"\"` when they authored none. */\n #announceTemplate(key: AnnounceKey): string {\n switch (key) {\n case \"grabbed\":\n return this.announceGrabbedTextValue;\n case \"moved\":\n return this.announceMovedTextValue;\n case \"dropped\":\n return this.announceDroppedTextValue;\n case \"canceled\":\n return this.announceCanceledTextValue;\n }\n }\n\n /** The announced item name: the authored override, else its collapsed text. */\n #nameOf(item: HTMLElement): string {\n const authored = item.getAttribute(`data-${this.identifier}-name`);\n if (authored) return authored;\n return (item.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n }\n\n /**\n * Resolves the sortable item owning a bubbled `pointer-drag` event.\n *\n * `pointer-drag` dispatches on its own element, and the markup contract puts\n * one on each item, so the owner is the item that **is** the target. Matching\n * an ancestor instead would make a card's own inner draggable — a knob, a\n * split pane, anything the primitive is composed into — drive the card.\n */\n #itemFor(target: EventTarget | null, items: HTMLElement[]): HTMLElement | null {\n if (!target) return null;\n return items.find((item) => item === target) ?? null;\n }\n\n /** The items in live DOM order (targets re-query the DOM on every access). */\n #items(): HTMLElement[] {\n return this.itemTargets;\n }\n\n /** The reorder container: the `list` target when present, else the element. */\n get #list(): HTMLElement {\n return this.hasListTarget ? this.listTarget : this.element;\n }\n\n get #isVertical(): boolean {\n return this.orientationValue !== \"horizontal\";\n }\n\n /**\n * Whether DOM order runs opposite to the primary coordinate — true only for a\n * horizontal row under `dir=\"rtl\"`, where the first item sits at the largest\n * `x`. Both drag paths reach this controller in physical terms (`pointer-drag`\n * documents its deltas as physical and hands RTL to its consumer, which is\n * this controller), so both have to be mapped back onto DOM order here.\n *\n * Read from the list, the element that lays the items out — never from the\n * dragged item, which may carry its own `dir`. The list inherits the computed\n * direction, so authoring `dir` on the root works too. A vertical list is\n * unaffected: writing direction does not mirror the block axis.\n */\n get #isReversed(): boolean {\n return !this.#isVertical && isRtl(this.#list);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/template.ts","../../src/utils/announce.ts","../../src/utils/listener_set.ts","../../src/utils/logical_scroll.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/transient_hooks.ts","../../src/controllers/sortable_controller.ts"],"names":[],"mappings":"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","file":"sortable_controller.js","sourcesContent":["/**\n * Fills `{name}` placeholders in a localized template from the own entries of `values`.\n *\n * Announcement and value-text templates share this one substitution, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n * Every occurrence is filled in one pass. Inserted strings stay literal, including\n * replacement-pattern characters and text that resembles another placeholder.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = Object.hasOwn(values, name) ? values[name] : undefined;\n return replacement === undefined ? match : String(replacement);\n });\n}\n","export { fillTemplate } from \"./template\";\n\n/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A message that has to reach assistive tech is not put into a live region created\n * or un-hidden along with it: a region only announces what changes *after* assistive\n * tech already knows about it, which such a region cannot satisfy. The announcer has\n * to be in the page before any message is handed to it — one sent while no announcer\n * is listening is not read — so state changes reach it as an event and it does the\n * reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — this helper adds no text of its own — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n","/** What {@link ListenerSet.add} passes on besides the signal it supplies itself. */\nexport type ListenerOptions = Omit<AddEventListenerOptions, \"signal\">;\n\n/**\n * The event map a target's own `addEventListener` types its handlers with: the\n * window's, a document's, an HTML or SVG element's, any other element's, or a\n * media query list's. Other targets name none.\n */\ntype EventMapOf<T extends EventTarget> = T extends Window\n ? WindowEventMap\n : T extends Document\n ? DocumentEventMap\n : T extends HTMLElement\n ? HTMLElementEventMap\n : T extends SVGElement\n ? SVGElementEventMap\n : T extends Element\n ? ElementEventMap\n : T extends MediaQueryList\n ? MediaQueryListEventMap\n : Record<never, never>;\n\n/**\n * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt.\n * {@link ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, which places `dispose()` in its\n * cleanup sequence. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * A lifetime shorter than the connection — while a Value holds, until a first\n * load, while a subscription is open — gets a set of its own, which the end of\n * that lifetime disposes. A controller therefore holds one set per lifetime: the\n * connection's, and one for each swapped receiver or shorter span.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, listeners of\n * several elements that come and go one at a time, and those one gesture holds\n * keep their own pair of calls or a signal of their own.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included. A removal by hand would have to repeat that\n * flag; aborting the signal releases the listener without it.\n *\n * A `type` the target's own event map names types `handler` with that event,\n * as the target's `addEventListener` does, so a handler written for a\n * `KeyboardEvent` is passed as it is; any other `type` takes an\n * `EventListener`.\n */\n add<T extends EventTarget, K extends keyof EventMapOf<T> & string>(\n target: T,\n type: K,\n handler: (event: EventMapOf<T>[K]) => void,\n options?: ListenerOptions,\n ): void;\n add(target: EventTarget, type: string, handler: EventListener, options?: ListenerOptions): void;\n add(target: EventTarget, type: string, handler: EventListener, options?: ListenerOptions): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\n }\n}\n","/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Whether a requested pass has yet to run. */\n get pending(): boolean {\n return this.#queued;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Drops the transient state hooks a connection inherited, so the cycle it opens\n * starts from a DOM that claims nothing.\n *\n * A transient hook is an attribute a controller writes to say \"this is happening\n * right now\" — a drag in progress, a form with unsaved edits, a refused\n * subscription. Its lifetime belongs to the interaction, not to the element, and\n * the two come apart whenever the element outlives the instance that wrote it: a\n * Turbo cache snapshot taken mid-interaction restores the hook with the markup,\n * and an element re-inserted elsewhere in the page carries it along. Nothing\n * takes such a hook off, because the instance that owned it is gone; consumer\n * CSS then keeps claiming a state no one is in.\n *\n * **`turbo:before-cache` cannot do this job**, which is why the pass belongs in\n * `connect()`: Turbo also dispatches that event on pages that stay, where the hook\n * is still true, and an in-page move — the other half of the population — never\n * reaches it, so a hook that arrived with a moved element would survive.\n * `connect()` covers both, because both end in a connection.\n *\n * Scope is the **drop** only. What the fresh cycle should say instead is the\n * consumer's: a value re-read from the live DOM, a roster refilled by the\n * stream, a measurement retaken from the current scroll position, a slot emptied\n * of the last result. Those answers differ per consumer and none of them is a\n * hook removal, so the pass stops where every consumer agrees.\n *\n * A declaration names one group of hooks and carries no per-instance state, so\n * it belongs at module scope, shared by every instance the page connects. A\n * consumer writes as many as it has groups — hooks it drops under different\n * conditions cannot share one. The exception is a hook whose name is only known\n * at runtime, such as one built from the registered identifier: that\n * declaration is a field, because the name is.\n *\n * **A drop is not always right.** The population splits on one question: can the\n * state a hook describes outlive the instance that wrote it? Where it cannot — a\n * drag ends with the pointer, a subscription dies with its socket — dropping on\n * every connection is correct. Where it can — a submission in flight is not\n * interrupted by an in-page move — only a connection that follows a real restore\n * may drop, and the consumer answers \"did a session survive?\" with the signal it\n * already holds for its own teardown (a live-session field, a `DetachGate`\n * probe). The drop then sits behind that answer, as the second example shows.\n *\n * @example Unconditional — the state cannot outlive the instance.\n * ```ts\n * const TRANSIENT = new TransientHooks({ attributes: [\"data-sortable-dragging\"] });\n *\n * connect(): void {\n * TRANSIENT.reset(this.element);\n * }\n * ```\n *\n * @example Gated — an in-page move must not drop a live state.\n * ```ts\n * const TRANSIENT = new TransientHooks({ attributes: [\"data-optimistic\"] });\n *\n * connect(): void {\n * const restored = !this.#connected && !this.#gate.pending;\n * this.#gate.cancel();\n * this.#connected = true;\n * if (restored) TRANSIENT.reset(this.element);\n * }\n * ```\n */\n\n/** What a consumer declares once about the hooks its interaction owns. */\nexport interface TransientHooksOptions {\n /**\n * The attribute names a connection may find written by an earlier one. Hooks\n * a consumer drops under different conditions belong to different\n * declarations, so each one can be reset on its own.\n */\n readonly attributes: readonly string[];\n}\n\n/**\n * One consumer's declaration of the hooks its interaction owns, dropped from an\n * element by {@link TransientHooks.reset}.\n */\nexport class TransientHooks {\n readonly #attributes: readonly string[];\n\n constructor(options: TransientHooksOptions) {\n this.#attributes = options.attributes;\n }\n\n /** Drops the declared hooks from `element`; what it carries besides stays. */\n reset(element: Element): void {\n for (const attribute of this.#attributes) element.removeAttribute(attribute);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { ListenerSet } from \"../utils/listener_set\";\nimport { isRtl } from \"../utils/logical_scroll\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { TransientHooks } from \"../utils/transient_hooks\";\n\n/** The hook a connection may find written by an earlier, now-gone one. */\nconst TRANSIENT = new TransientHooks({ attributes: [\"data-sortable-dragging\"] });\n\n/** The four steps a consumer can give wording to. */\ntype AnnounceKey = \"grabbed\" | \"moved\" | \"dropped\" | \"canceled\";\n\n/** A reorder in flight: one item picked up by pointer or keyboard. */\ninterface SortSession {\n item: HTMLElement;\n /**\n * The item that followed it at pickup, and the one that preceded it.\n *\n * The pickup slot is remembered as nodes rather than as an index because the\n * list can change underneath a live drag — a broadcast board inserts and\n * deletes rows while someone is holding one — and an index taken at pickup\n * stops pointing at that place the moment a row appears above it.\n */\n anchor: HTMLElement | null;\n predecessor: HTMLElement | null;\n /** Last primary-axis cumulative delta consumed by keyboard stepping. */\n lastPrimary: number;\n}\n\n/**\n * Headless, accessible **sortable** (single-list reorder) — the a11y-first\n * drag-and-drop that is the flagship consumer of the\n * `stimeo--pointer-drag` primitive. The composition is markup-level and each\n * layer keeps its job: `pointer-drag` (on every item) emits the normalized drag\n * signal with its built-in keyboard alternative, `roving` (on the list) keeps\n * the handles a single Tab stop, and this controller interprets the signal —\n * live-reordering the DOM, handing each step to the page's shared announcer,\n * and reporting the final `reorder`. No dedicated APG pattern exists for\n * drag-and-drop; the keyboard model (grab → arrows → drop / Escape) comes from\n * `pointer-drag` and the announcements make it non-visually trackable\n * (WCAG 2.1.1 / 2.5.7 / 4.1.3). Core (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--sortable`):\n * <div data-controller=\"stimeo--sortable\">\n * <ul data-stimeo--sortable-target=\"list\" data-controller=\"stimeo--roving\"\n * data-stimeo--roving-orientation-value=\"vertical\">\n * <li data-stimeo--sortable-target=\"item\" data-stimeo--sortable-name=\"Card A\"\n * data-controller=\"stimeo--pointer-drag\"\n * data-stimeo--pointer-drag-axis-value=\"y\">\n * <span>Card A</span>\n * <button type=\"button\" aria-label=\"Reorder Card A\"\n * data-stimeo--pointer-drag-target=\"handle\"\n * data-stimeo--roving-target=\"item\">⠿</button>\n * </li>\n * …\n * </ul>\n * </div>\n *\n * The composed values must follow the sort axis: roving's `orientation`\n * (which defaults to `horizontal`, so a vertical list has to author\n * `vertical`) and pointer-drag's `axis`. `stimeo check` enforces both\n * alignments as composition rules (`composition-mismatch`).\n *\n * Pointer flow: dragging an item live-moves it whenever the pointer crosses a\n * sibling's midpoint (per `orientation`). Keyboard flow: each synthetic\n * `pointer-drag` move steps the item one position. A horizontal list under\n * `dir=\"rtl\"` reverses both, since DOM order then runs right-to-left while\n * `pointer-drag` reports physical coordinates. Dropping dispatches\n * `reorder` (`{ item, from, to }`, zero-based) when the position changed;\n * Escape / `pointercancel` restores the pickup position. Every step is handed to\n * the page's shared `stimeo--announcer` as the consumer's own wording —\n * `announceGrabbedText` / `announceMovedText` / `announceDroppedText` /\n * `announceCanceledText`, with `{name}` / `{position}` / `{total}` placeholders —\n * and an unset one announces nothing.\n *\n * @remarks\n * Behavior only — the ghost/placeholder/drop-hint visuals are the consumer's\n * CSS, keyed off `pointer-drag`'s `data-dragging`/`data-grabbed` on the item\n * and this controller's `data-sortable-dragging` on the root. Persistence is\n * the consumer's `reorder` listener (post the new position; a Turbo Stream\n * broadcast makes the board collaborative). The DOM order is the single source\n * of truth: items are read live on every event, so rows appended by Turbo need\n * no rewiring, and a Turbo cache restore reconnects idempotently (`connect()`\n * clears the transient root hook; a drag cannot survive a navigation). The event\n * listeners and a pending lost-item check are all it holds, and `disconnect()`\n * releases both. Multi-list / kanban, nested DnD, auto-scroll at the edges, and\n * virtualized lists are deliberately out of this single-list scope.\n */\nexport class SortableController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"item\"];\n static override values = {\n orientation: { type: String, default: \"vertical\" },\n announceGrabbedText: { type: String, default: \"\" },\n announceMovedText: { type: String, default: \"\" },\n announceDroppedText: { type: String, default: \"\" },\n announceCanceledText: { type: String, default: \"\" },\n };\n static events = [\"reorder\"] as const;\n\n declare readonly hasListTarget: boolean;\n declare readonly listTarget: HTMLElement;\n declare readonly itemTargets: HTMLElement[];\n declare orientationValue: string;\n declare announceGrabbedTextValue: string;\n declare announceMovedTextValue: string;\n declare announceDroppedTextValue: string;\n declare announceCanceledTextValue: string;\n\n #session: SortSession | null = null;\n /** Defers the lost-item check to after the mutation batch (see below). */\n readonly #settle = new MicrotaskCoalescer(() => this.#dropLostSession());\n\n /** The listeners of one connection, released together on `disconnect()`. */\n readonly #listeners = new ListenerSet();\n\n override connect(): void {\n // A drag cannot survive a navigation: drop the hook a Turbo cache snapshot\n // may have preserved mid-drag (idempotent reconnect).\n TRANSIENT.reset(this.element);\n this.#settle.activate();\n this.#listeners.add(this.element, \"stimeo--pointer-drag:start\", this.#onDragStart);\n this.#listeners.add(this.element, \"stimeo--pointer-drag:move\", this.#onDragMove);\n this.#listeners.add(this.element, \"stimeo--pointer-drag:end\", this.#onDragEnd);\n this.#listeners.add(this.element, \"stimeo--pointer-drag:cancel\", this.#onDragCancel);\n }\n\n override disconnect(): void {\n this.#listeners.dispose();\n this.#settle.cancel();\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n }\n\n /**\n * Ends a session whose item left the item set.\n *\n * The controller's own reorder detaches and reattaches the item inside one\n * mutation batch, so the loss is only real once the batch has settled — the\n * check therefore runs a microtask later and asks whether the item is a target\n * again. Without it a deleted row (a broadcast that drops it from the board)\n * or a morph that strips the item's target attribute would hold the\n * one-at-a-time session for the rest of the page's life, with the root hook\n * stuck on and every later grab refused with no way out.\n */\n itemTargetDisconnected(item: HTMLElement): void {\n if (this.#session?.item !== item) return;\n this.#settle.schedule();\n }\n\n #dropLostSession(): void {\n const session = this.#session;\n if (!session) return;\n const items = this.#items();\n if (items.includes(session.item)) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n // An element that is still in the document (a morph took only its target\n // attribute) goes back where it was picked up. Nothing is announced either\n // way: the row is no longer one of the items, so there is no position to\n // read out — what matters is that the next grab is not refused.\n if (session.item.isConnected) this.#restore(session, items);\n }\n\n /** Picks the item up: remembers its neighbours and announces the grab. */\n readonly #onDragStart = (event: Event): void => {\n // One reorder at a time: a start from another item (each item has its own\n // pointer-drag instance) must not clobber the live session — its pickup\n // slot and cancel restore would be lost.\n if (this.#session) return;\n const items = this.#items();\n const item = this.#itemFor(event.target, items);\n if (!item) return;\n const index = items.indexOf(item);\n this.#session = {\n item,\n anchor: items[index + 1] ?? null,\n predecessor: items[index - 1] ?? null,\n lastPrimary: 0,\n };\n this.element.setAttribute(\"data-sortable-dragging\", \"true\");\n this.#announce(\"grabbed\", item);\n };\n\n readonly #onDragMove = (event: Event): void => {\n const session = this.#session;\n if (!session) return;\n const items = this.#items();\n if (this.#itemFor(event.target, items) !== session.item) return;\n\n const detail = (event as CustomEvent<Record<string, number | string>>).detail;\n if (detail.pointerType === \"keyboard\") {\n this.#stepFromKeyboard(session, detail, items);\n } else {\n this.#followPointer(session, detail, items);\n }\n };\n\n /** Drops the item: announces, then reports `reorder` if the position changed. */\n readonly #onDragEnd = (event: Event): void => {\n const session = this.#session;\n if (!session) return;\n const items = this.#items();\n if (this.#itemFor(event.target, items) !== session.item) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.#announce(\"dropped\", session.item);\n const to = items.indexOf(session.item);\n const from = this.#pickupSlot(session, items) ?? to;\n if (to !== from) {\n this.dispatch(\"reorder\", { detail: { item: session.item, from, to } });\n }\n };\n\n /** Restores the pickup position (Escape / OS `pointercancel`). */\n readonly #onDragCancel = (event: Event): void => {\n const session = this.#session;\n if (!session) return;\n const items = this.#items();\n if (this.#itemFor(event.target, items) !== session.item) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.#restore(session, items);\n this.#announce(\"canceled\", session.item);\n };\n\n /**\n * The slot the item was picked up from, read back through the neighbours it\n * had then — `null` when neither survives and the item itself is gone.\n *\n * The anchor is the item that followed it, so restoring means \"before that one\n * again\". When the anchor was deleted mid-drag the predecessor answers the\n * same question from the other side. With both gone the item's current slot is\n * the honest answer: nothing is known to have moved, so no reorder is reported\n * and a cancel leaves the item where it is.\n */\n #pickupSlot(session: SortSession, items: HTMLElement[]): number | null {\n const others = items.filter((candidate) => candidate !== session.item);\n if (session.anchor) {\n const at = others.indexOf(session.anchor);\n if (at !== -1) return at;\n }\n if (session.predecessor) {\n const at = others.indexOf(session.predecessor);\n if (at !== -1) return at + 1;\n }\n const here = items.indexOf(session.item);\n return here === -1 ? null : here;\n }\n\n /** Puts the item back where it was picked up from. */\n #restore(session: SortSession, items: HTMLElement[]): void {\n const slot = this.#pickupSlot(session, items);\n if (slot === null) return;\n this.#insertAt(\n session.item,\n items.filter((candidate) => candidate !== session.item),\n slot,\n );\n }\n\n /**\n * Keyboard stepping: `pointer-drag` reports *cumulative* synthetic deltas, so\n * the difference from the last consumed value is one arrow press — its sign is\n * the direction. Cross-axis arrows never change the primary delta (no move).\n *\n * The sign is physical (`ArrowRight` is always `+dx`), so a right-to-left row\n * has to invert it: there, moving the item rightward means moving it *earlier*\n * in the DOM. Skipping this would also split the two halves of one keypress —\n * `roving` already moves focus logically, so the same arrow would send the\n * focus and the grabbed item opposite ways.\n */\n #stepFromKeyboard(\n session: SortSession,\n detail: Record<string, number | string>,\n items: HTMLElement[],\n ): void {\n const primary = Number(this.#isVertical ? detail.dy : detail.dx) || 0;\n const delta = primary - session.lastPrimary;\n session.lastPrimary = primary;\n if (delta === 0) return;\n\n const index = items.indexOf(session.item);\n const step = (delta > 0 ? 1 : -1) * (this.#isReversed ? -1 : 1);\n const next = Math.max(0, Math.min(index + step, items.length - 1));\n if (next === index) return;\n this.#insertAt(\n session.item,\n items.filter((candidate) => candidate !== session.item),\n next,\n );\n this.#announce(\"moved\", session.item);\n }\n\n /**\n * Pointer following: the item moves to the slot whose siblings' midpoints the\n * pointer has passed (per `orientation`).\n *\n * Only siblings that occupy space take part. A row with no layout box — a\n * filtered-out item, a `display: none` ancestor, a collapsed `<details>` — is\n * reported with an empty rect at the document origin, and its midpoint of `0`\n * sits below every pointer position: counted, it would read as passed on the\n * very first move and send the item across a slot the pointer never crossed.\n * With no laid-out sibling at all there is nothing to compare against and the\n * move is skipped entirely.\n */\n #followPointer(\n session: SortSession,\n detail: Record<string, number | string>,\n items: HTMLElement[],\n ): void {\n const pointer = Number(this.#isVertical ? detail.y : detail.x) || 0;\n const vertical = this.#isVertical;\n // `target` counts the siblings that precede the pointer *in DOM order*, and\n // the two agree only while DOM order runs the same way as the coordinate. In\n // a right-to-left row it runs the other way, so the comparison flips;\n // leaving it physical would mix two orderings into one number and drop the\n // item at a slot the pointer never crossed. Resolved once: it cannot change\n // mid-loop, and reading it per sibling would re-run `getComputedStyle` on\n // every pointermove.\n const reversed = this.#isReversed;\n const here = items.indexOf(session.item);\n const laidOut: HTMLElement[] = [];\n let target = 0;\n // `current` is the item's own slot in that same laid-out space: the number\n // of laid-out siblings that come before it in DOM order.\n let current = 0;\n items.forEach((item, index) => {\n if (item === session.item) return;\n const rect = item.getBoundingClientRect();\n if (rect.width === 0 && rect.height === 0) return;\n if (index < here) current += 1;\n laidOut.push(item);\n const midpoint = vertical ? rect.top + rect.height / 2 : rect.left + rect.width / 2;\n if (reversed ? pointer < midpoint : pointer > midpoint) target += 1;\n });\n if (laidOut.length === 0 || target === current) return;\n\n this.#insertAt(session.item, laidOut, target);\n this.#announce(\"moved\", session.item);\n }\n\n /**\n * Reinserts `item` at `index` among `scope`, relative to the sibling already\n * standing there.\n *\n * The reorder is defined against the items, not against a container: the\n * neighbour's own parent is where the item belongs. A `list` target that is\n * not the items' parent — or none at all, which the markup contract allows —\n * therefore still lands the move in the right place instead of throwing, and\n * the last slot is *after the last item* rather than after whatever else the\n * container holds (a live region, a footer), which would put the row outside\n * the reading order the list publishes.\n */\n #insertAt(item: HTMLElement, scope: HTMLElement[], index: number): void {\n const clamped = Math.max(0, Math.min(index, scope.length));\n const ahead = scope[clamped] ?? null;\n const behind = ahead ? null : (scope[scope.length - 1] ?? null);\n const neighbour = ahead ?? behind;\n if (!neighbour) return;\n const parent = neighbour.parentNode;\n if (!parent) return;\n // Real browsers drop focus when the focused node is re-inserted (the move\n // is a remove+insert), which would strand the keyboard grab after one\n // arrow press — restore it so focus rides the moved item.\n const active = document.activeElement;\n const hadFocus = active instanceof HTMLElement && item.contains(active);\n parent.insertBefore(item, ahead ?? neighbour.nextSibling);\n if (hadFocus) active.focus();\n }\n\n /**\n * Hands one step to the page's shared announcer.\n *\n * The library carries no live region and no English copy: the wording is the\n * consumer's, written into `announceGrabbedText` / `announceMovedText` /\n * `announceDroppedText` / `announceCanceledText` with `{name}` / `{position}` /\n * `{total}` placeholders, and an unset one announces nothing.\n *\n * Only transitions reach here — the pickup, a step that actually changed the\n * landing slot, and the single end of the session — so a pointer crossing the\n * same slot twice or an arrow clamped at an end stays silent.\n *\n * @stimeoRuntimeOnly The texts word the steps of one reorder, spoken as they happen.\n */\n #announce(key: AnnounceKey, item: HTMLElement): void {\n const template = this.#announceTemplate(key);\n if (template.length === 0) return;\n const items = this.#items();\n announce(\n fillTemplate(template, {\n name: this.#nameOf(item),\n position: items.indexOf(item) + 1,\n total: items.length,\n }),\n );\n }\n\n /** The consumer's wording for one step, or `\"\"` when they authored none. */\n #announceTemplate(key: AnnounceKey): string {\n switch (key) {\n case \"grabbed\":\n return this.announceGrabbedTextValue;\n case \"moved\":\n return this.announceMovedTextValue;\n case \"dropped\":\n return this.announceDroppedTextValue;\n case \"canceled\":\n return this.announceCanceledTextValue;\n }\n }\n\n /** The announced item name: the authored override, else its collapsed text. */\n #nameOf(item: HTMLElement): string {\n const authored = item.getAttribute(`data-${this.identifier}-name`);\n if (authored) return authored;\n return (item.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n }\n\n /**\n * Resolves the sortable item owning a bubbled `pointer-drag` event.\n *\n * `pointer-drag` dispatches on its own element, and the markup contract puts\n * one on each item, so the owner is the item that **is** the target. Matching\n * an ancestor instead would make a card's own inner draggable — a knob, a\n * split pane, anything the primitive is composed into — drive the card.\n */\n #itemFor(target: EventTarget | null, items: HTMLElement[]): HTMLElement | null {\n if (!target) return null;\n return items.find((item) => item === target) ?? null;\n }\n\n /** The items in live DOM order (targets re-query the DOM on every access). */\n #items(): HTMLElement[] {\n return this.itemTargets;\n }\n\n /** The element that lays the items out: the `list` target when present, else the element. */\n get #list(): HTMLElement {\n return this.hasListTarget ? this.listTarget : this.element;\n }\n\n get #isVertical(): boolean {\n return this.orientationValue !== \"horizontal\";\n }\n\n /**\n * Whether DOM order runs opposite to the primary coordinate — true only for a\n * horizontal row under `dir=\"rtl\"`, where the first item sits at the largest\n * `x`. Both drag paths reach this controller in physical terms (`pointer-drag`\n * documents its deltas as physical and hands RTL to its consumer, which is\n * this controller), so both have to be mapped back onto DOM order here.\n *\n * Read from the list, the element that lays the items out — never from the\n * dragged item, which may carry its own `dir`. The list inherits the computed\n * direction, so authoring `dir` on the root works too. A vertical list is\n * unaffected: writing direction does not mirror the block axis.\n */\n get #isReversed(): boolean {\n return !this.#isVertical && isRtl(this.#list);\n }\n}\n"]}
|
|
@@ -34,16 +34,20 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
34
34
|
* - `stimeo--spinner:show` — the indicator became visible, after `delay`.
|
|
35
35
|
* - `stimeo--spinner:hide` — the indicator went away, after `minDuration`.
|
|
36
36
|
* - `stimeo--spinner:timeout` — `timeout` elapsed with the load still running;
|
|
37
|
-
* the controller then ends it
|
|
37
|
+
* the controller then ends it as `stop` would; `hide` follows only if it was shown.
|
|
38
38
|
*
|
|
39
39
|
* `hide`, `show`, `timeout`, and `reconcile` dispatch `{}`. The last of those
|
|
40
|
-
* reports that
|
|
40
|
+
* reports that a connection returned to idle a cycle no instance was running — a page
|
|
41
|
+
* restored from the Turbo cache in the middle of a load.
|
|
41
42
|
*
|
|
42
43
|
* @remarks
|
|
43
44
|
* Behavior only — the visual spinner is the consumer's, alongside the text and
|
|
44
45
|
* `aria-hidden="true"`. Both timers are owned by `SafeTimeout`, kept across
|
|
45
|
-
* an in-page move and dropped on a real detach via `DetachGate
|
|
46
|
-
*
|
|
46
|
+
* an in-page move and dropped on a real detach via `DetachGate`. A load in progress is
|
|
47
|
+
* never undone on `turbo:before-cache`, which Turbo also dispatches on pages that stay
|
|
48
|
+
* (a promoted frame navigation, a state-less `popstate`, a refresh of a cached URL, a
|
|
49
|
+
* `data-turbo-permanent` element carried to the next page): the load is still running
|
|
50
|
+
* there, and its `stop` still hides the spinner and announces the result.
|
|
47
51
|
*/
|
|
48
52
|
declare class SpinnerController extends Controller<HTMLElement> {
|
|
49
53
|
#private;
|
|
@@ -70,10 +74,29 @@ declare class SpinnerController extends Controller<HTMLElement> {
|
|
|
70
74
|
default: number;
|
|
71
75
|
};
|
|
72
76
|
};
|
|
77
|
+
static valueConstraints: {
|
|
78
|
+
delay: {
|
|
79
|
+
readonly finite: true;
|
|
80
|
+
readonly min: 0;
|
|
81
|
+
readonly max: 2147483647;
|
|
82
|
+
};
|
|
83
|
+
minDuration: {
|
|
84
|
+
readonly finite: true;
|
|
85
|
+
readonly min: 0;
|
|
86
|
+
readonly max: 2147483647;
|
|
87
|
+
};
|
|
88
|
+
timeout: {
|
|
89
|
+
readonly finite: true;
|
|
90
|
+
readonly min: 0;
|
|
91
|
+
readonly max: 2147483647;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
73
94
|
static actions: readonly ["start", "stop"];
|
|
74
95
|
static events: readonly ["hide", "show", "timeout", "reconcile"];
|
|
75
96
|
readonly indicatorTarget: HTMLElement;
|
|
97
|
+
readonly indicatorTargets: HTMLElement[];
|
|
76
98
|
readonly regionTarget: HTMLElement;
|
|
99
|
+
readonly regionTargets: HTMLElement[];
|
|
77
100
|
readonly messageTarget: HTMLElement;
|
|
78
101
|
readonly hasIndicatorTarget: boolean;
|
|
79
102
|
readonly hasRegionTarget: boolean;
|
|
@@ -93,6 +116,18 @@ declare class SpinnerController extends Controller<HTMLElement> {
|
|
|
93
116
|
* bring it back.
|
|
94
117
|
*/
|
|
95
118
|
indicatorTargetConnected(target: HTMLElement): void;
|
|
119
|
+
/**
|
|
120
|
+
* Gives an indicator that no longer resolves its own `hidden` back, even after
|
|
121
|
+
* `disconnect()`, and while connected re-applies the current phase to the one that stays.
|
|
122
|
+
*/
|
|
123
|
+
indicatorTargetDisconnected(indicator: HTMLElement): void;
|
|
124
|
+
/** Applies the current busy state to a region inserted or replaced after `connect()`. */
|
|
125
|
+
regionTargetConnected(region: HTMLElement): void;
|
|
126
|
+
/**
|
|
127
|
+
* Gives a region that no longer resolves its own `aria-busy` back, even after
|
|
128
|
+
* `disconnect()`, and while connected resyncs the region that stays.
|
|
129
|
+
*/
|
|
130
|
+
regionTargetDisconnected(region: HTMLElement): void;
|
|
96
131
|
disconnect(): void;
|
|
97
132
|
/** Begins loading. Honors `delay` before the spinner actually appears. */
|
|
98
133
|
start(): void;
|