stimeo-ui 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +8 -1
- package/dist/cable/index.d.ts +79 -23
- package/dist/cable/index.js +363 -436
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +4 -2
- package/dist/controllers/accordion_controller.js +166 -55
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +12 -2
- package/dist/controllers/alert_dialog_controller.js +862 -221
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +35 -5
- package/dist/controllers/announcer_controller.js +135 -198
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.js +0 -10
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +30 -6
- package/dist/controllers/auto_submit_controller.js +271 -127
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +171 -105
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +0 -130
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +18 -0
- package/dist/controllers/bulk_select_controller.js +262 -52
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +24 -5
- package/dist/controllers/calendar_controller.js +175 -209
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +34 -7
- package/dist/controllers/carousel_controller.js +461 -399
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +17 -1
- package/dist/controllers/character_counter_controller.js +246 -156
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +7 -2
- package/dist/controllers/checkbox_controller.js +32 -74
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +16 -5
- package/dist/controllers/clipboard_controller.js +168 -147
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +16 -4
- package/dist/controllers/collapsible_controller.js +301 -120
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +18 -6
- package/dist/controllers/color_picker_controller.js +149 -139
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +23 -5
- package/dist/controllers/combobox_controller.js +360 -133
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +45 -14
- package/dist/controllers/command_palette_controller.js +970 -358
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +15 -2
- package/dist/controllers/conditional_fields_controller.js +166 -124
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +49 -12
- package/dist/controllers/confirm_controller.js +939 -248
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +31 -4
- package/dist/controllers/context_menu_controller.js +256 -128
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +13 -1
- package/dist/controllers/count_up_controller.js +73 -21
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +45 -1
- package/dist/controllers/countdown_controller.js +183 -148
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +21 -2
- package/dist/controllers/currency_input_controller.js +134 -158
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +15 -4
- package/dist/controllers/data_grid_controller.js +114 -186
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +30 -5
- package/dist/controllers/date_range_picker_controller.js +288 -206
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +13 -1
- package/dist/controllers/dialog_controller.js +858 -223
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +24 -7
- package/dist/controllers/direct_upload_controller.js +73 -153
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.js +14 -48
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +19 -0
- package/dist/controllers/dismissible_controller.js +175 -21
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +39 -11
- package/dist/controllers/drawer_controller.js +933 -341
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +31 -1
- package/dist/controllers/dropdown_controller.js +312 -82
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +158 -68
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +67 -53
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +60 -18
- package/dist/controllers/file_dropzone_controller.js +311 -207
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +67 -39
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +31 -8
- package/dist/controllers/flash_controller.js +172 -314
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +12 -3
- package/dist/controllers/focus_controller.js +654 -240
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +147 -132
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +13 -86
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +41 -9
- package/dist/controllers/frame_loading_controller.js +264 -213
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +22 -2
- package/dist/controllers/highlight_controller.js +107 -75
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +56 -3
- package/dist/controllers/hover_card_controller.js +309 -118
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +42 -1
- package/dist/controllers/idle_controller.js +267 -101
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js +71 -72
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +32 -13
- package/dist/controllers/intersection_controller.js +147 -106
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +16 -0
- package/dist/controllers/lazy_frame_controller.js +9 -65
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +34 -2
- package/dist/controllers/listbox_controller.js +306 -204
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +1 -1
- package/dist/controllers/local_time_controller.js +54 -64
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +11 -3
- package/dist/controllers/masonry_controller.js +70 -93
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +32 -7
- package/dist/controllers/menu_controller.js +214 -156
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +4 -4
- package/dist/controllers/menubar_controller.js +170 -329
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +20 -0
- package/dist/controllers/meter_controller.js +151 -67
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +32 -4
- package/dist/controllers/multi_select_controller.js +593 -358
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +8 -1
- package/dist/controllers/navigation_menu_controller.js +124 -232
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +10 -0
- package/dist/controllers/nested_form_controller.js +352 -182
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +23 -0
- package/dist/controllers/network_status_controller.js +242 -54
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +26 -2
- package/dist/controllers/number_input_controller.js +320 -263
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +16 -76
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +20 -4
- package/dist/controllers/otp_controller.js +198 -193
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
- package/dist/controllers/overflow_indicator_controller.js +116 -113
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +48 -11
- package/dist/controllers/overflow_menu_controller.js +343 -283
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +35 -6
- package/dist/controllers/pagination_controller.js +295 -119
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +45 -8
- package/dist/controllers/password_reveal_controller.js +389 -121
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +14 -5
- package/dist/controllers/password_strength_controller.js +99 -153
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +11 -4
- package/dist/controllers/persist_controller.js +237 -103
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +19 -7
- package/dist/controllers/pointer_drag_controller.js +185 -165
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +38 -2
- package/dist/controllers/popover_controller.js +236 -97
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +44 -4
- package/dist/controllers/portal_controller.js +209 -76
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +10 -4
- package/dist/controllers/preview_guard_controller.js +232 -116
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +11 -0
- package/dist/controllers/progress_controller.js +125 -56
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +14 -5
- package/dist/controllers/radio_group_controller.js +117 -137
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +43 -3
- package/dist/controllers/range_slider_controller.js +192 -105
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +20 -4
- package/dist/controllers/rating_controller.js +418 -207
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +2 -0
- package/dist/controllers/read_more_controller.js +134 -63
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +1 -1
- package/dist/controllers/reading_progress_controller.js +147 -135
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +18 -4
- package/dist/controllers/relative_time_controller.js +145 -110
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
- package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- package/dist/controllers/reset_on_restore_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +28 -2
- package/dist/controllers/resizable_controller.js +255 -139
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +39 -84
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -3
- package/dist/controllers/scroll_area_controller.js +428 -225
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +0 -53
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
- package/dist/controllers/scroll_visibility_controller.js +395 -145
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +6 -1
- package/dist/controllers/scrollspy_controller.js +181 -242
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +19 -4
- package/dist/controllers/separator_controller.js +267 -140
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +64 -7
- package/dist/controllers/sidebar_controller.js +1023 -386
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +23 -0
- package/dist/controllers/skeleton_controller.js +248 -97
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +27 -1
- package/dist/controllers/slider_controller.js +159 -79
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
- package/dist/controllers/smart_sticky_header_controller.js +162 -63
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +0 -112
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +39 -4
- package/dist/controllers/spinner_controller.js +277 -169
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +5 -0
- package/dist/controllers/step_indicator_controller.js +99 -46
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +12 -3
- package/dist/controllers/stepper_controller.js +114 -73
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +6 -0
- package/dist/controllers/stick_to_bottom_controller.js +262 -138
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +11 -0
- package/dist/controllers/sticky_observer_controller.js +214 -49
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +23 -8
- package/dist/controllers/submit_once_controller.js +294 -205
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -1
- package/dist/controllers/switch_controller.js +33 -60
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +2 -2
- package/dist/controllers/tabs_controller.js +35 -29
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +12 -0
- package/dist/controllers/tags_input_controller.js +136 -145
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
- package/dist/controllers/textarea_autosize_controller.js +71 -58
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +21 -9
- package/dist/controllers/theme_controller.js +130 -112
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +18 -1
- package/dist/controllers/time_picker_controller.js +146 -80
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +36 -11
- package/dist/controllers/toast_controller.js +186 -297
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +12 -3
- package/dist/controllers/toggle_group_controller.js +95 -136
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +68 -98
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +45 -3
- package/dist/controllers/tooltip_controller.js +158 -124
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +22 -4
- package/dist/controllers/transition_controller.js +123 -119
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +21 -11
- package/dist/controllers/tree_view_controller.js +150 -345
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6370 -9346
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +23 -3
- package/dist/inspector/cli.js +92 -37
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +92 -56
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +8 -8
- package/dist/inspector/manifest.json +787 -100
- package/dist/positioning/index.d.ts +8 -0
- package/dist/positioning/index.js +148 -47
- package/dist/positioning/index.js.map +1 -1
- package/package.json +6 -6
- package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
- package/dist/controllers/reset_before_cache_controller.js.map +0 -1
|
@@ -25,6 +25,11 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
25
25
|
* is the position shown before — when a change the page made moves the position of
|
|
26
26
|
* the current step (the clamped `index`).
|
|
27
27
|
*
|
|
28
|
+
* User `change` reports compare the resulting state with the last published
|
|
29
|
+
* state. A pending page write handled in the same script joins that confirmation;
|
|
30
|
+
* a browser-delivered listener may settle it first as `reconcile`. Confirming
|
|
31
|
+
* the last published state reports nothing.
|
|
32
|
+
*
|
|
28
33
|
* @remarks
|
|
29
34
|
* Behavior only. The controller never traps or restores focus — each step button
|
|
30
35
|
* is in the natural Tab order. `data-state` is purely derived from `index` and the
|
|
@@ -63,6 +68,11 @@ declare class StepperController extends Controller<HTMLElement> {
|
|
|
63
68
|
default: boolean;
|
|
64
69
|
};
|
|
65
70
|
};
|
|
71
|
+
static valueConstraints: {
|
|
72
|
+
index: {
|
|
73
|
+
readonly finite: true;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
66
76
|
static actions: readonly ["goto", "next", "prev"];
|
|
67
77
|
static events: readonly ["change", "reconcile"];
|
|
68
78
|
readonly stepTargets: HTMLElement[];
|
|
@@ -83,9 +93,8 @@ declare class StepperController extends Controller<HTMLElement> {
|
|
|
83
93
|
/** Returns to the previous step (ignored at the first step). */
|
|
84
94
|
prev(): void;
|
|
85
95
|
/**
|
|
86
|
-
* Jumps to the
|
|
87
|
-
*
|
|
88
|
-
* so it jumps nowhere.
|
|
96
|
+
* Jumps to the finite integer carried in the action's `index` param.
|
|
97
|
+
* Blank strings and nonnumeric JSON values do not select a step.
|
|
89
98
|
*/
|
|
90
99
|
goto(event: {
|
|
91
100
|
params: {
|
|
@@ -1,28 +1,58 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var NUMBER_BOUNDS = {
|
|
4
|
+
finite: { finite: true }};
|
|
5
|
+
function matchesNumberBounds(value, bounds) {
|
|
6
|
+
if (!Number.isFinite(value)) {
|
|
7
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
8
|
+
if (direction === null) return false;
|
|
9
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
10
|
+
}
|
|
11
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
12
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
13
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
14
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
15
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toFiniteNumber(raw) {
|
|
20
|
+
if (typeof raw !== "number" && typeof raw !== "string") return null;
|
|
21
|
+
if (typeof raw === "string" && raw.trim().length === 0) return null;
|
|
22
|
+
const value = Number(raw);
|
|
23
|
+
return Number.isFinite(value) ? value : null;
|
|
24
|
+
}
|
|
25
|
+
function readNumber(raw, fallback, bounds) {
|
|
26
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var ListenerSet = class {
|
|
30
|
+
#abort = new AbortController();
|
|
31
|
+
add(target, type, handler, options) {
|
|
32
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
33
|
+
}
|
|
34
|
+
dispose() {
|
|
35
|
+
this.#abort.abort();
|
|
36
|
+
this.#abort = new AbortController();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
4
39
|
|
|
5
|
-
// src/utils/microtask_coalescer.ts
|
|
6
40
|
var MicrotaskCoalescer = class {
|
|
7
41
|
#run;
|
|
8
42
|
#queued = false;
|
|
9
43
|
#active = false;
|
|
10
44
|
#generation = 0;
|
|
11
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
12
45
|
constructor(run) {
|
|
13
46
|
this.#run = run;
|
|
14
47
|
}
|
|
15
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
16
48
|
activate() {
|
|
17
49
|
this.#active = true;
|
|
18
50
|
}
|
|
19
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
20
51
|
cancel() {
|
|
21
52
|
this.#active = false;
|
|
22
53
|
this.#queued = false;
|
|
23
54
|
this.#generation += 1;
|
|
24
55
|
}
|
|
25
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
26
56
|
schedule() {
|
|
27
57
|
if (!this.#active || this.#queued) return;
|
|
28
58
|
this.#queued = true;
|
|
@@ -35,95 +65,113 @@ var MicrotaskCoalescer = class {
|
|
|
35
65
|
}
|
|
36
66
|
};
|
|
37
67
|
|
|
38
|
-
|
|
39
|
-
|
|
68
|
+
var MorphRenderWatcher = class {
|
|
69
|
+
#listeners = new ListenerSet();
|
|
70
|
+
#pass;
|
|
71
|
+
#element = null;
|
|
72
|
+
#onMorph = () => this.#pass.schedule();
|
|
73
|
+
constructor(run) {
|
|
74
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
75
|
+
}
|
|
76
|
+
observe(element) {
|
|
77
|
+
if (this.#element === element) return;
|
|
78
|
+
this.disconnect();
|
|
79
|
+
this.#element = element;
|
|
80
|
+
this.#pass.activate();
|
|
81
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
82
|
+
}
|
|
83
|
+
schedule() {
|
|
84
|
+
this.#pass.schedule();
|
|
85
|
+
}
|
|
86
|
+
disconnect() {
|
|
87
|
+
this.#listeners.dispose();
|
|
88
|
+
this.#pass.cancel();
|
|
89
|
+
this.#element = null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
var NumberValueReader = class {
|
|
94
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
95
|
+
read(owner, name, raw, fallback, bounds) {
|
|
96
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
97
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
98
|
+
this.#lastRejected.delete(name);
|
|
99
|
+
return resolved;
|
|
100
|
+
}
|
|
101
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
102
|
+
const literal = owner.element.getAttribute(attribute);
|
|
103
|
+
if (literal === null) {
|
|
104
|
+
this.#lastRejected.delete(name);
|
|
105
|
+
return resolved;
|
|
106
|
+
}
|
|
107
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
108
|
+
this.#lastRejected.set(name, literal);
|
|
109
|
+
console.warn(
|
|
110
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return resolved;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
var StepperController = class _StepperController extends Controller {
|
|
118
|
+
#numbers = new NumberValueReader();
|
|
40
119
|
static targets = ["step"];
|
|
41
120
|
static values = {
|
|
42
121
|
index: { type: Number, default: 0 },
|
|
43
122
|
linear: { type: Boolean, default: false }
|
|
44
123
|
};
|
|
124
|
+
static valueConstraints = {
|
|
125
|
+
index: NUMBER_BOUNDS.finite
|
|
126
|
+
};
|
|
45
127
|
static actions = ["goto", "next", "prev"];
|
|
46
128
|
static events = ["change", "reconcile"];
|
|
47
|
-
|
|
48
|
-
* Collapses the Value and target callbacks of one mutation into one pass, and
|
|
49
|
-
* refuses the ones Stimulus delivers before `connect()`, which renders itself.
|
|
50
|
-
*/
|
|
51
|
-
#repaint = new MicrotaskCoalescer(() => this.#reconcileStep());
|
|
52
|
-
/**
|
|
53
|
-
* The position shown current last, which the next move is measured from, or
|
|
54
|
-
* `null` before any step has been shown. While no step is present it keeps the
|
|
55
|
-
* last one.
|
|
56
|
-
*/
|
|
129
|
+
#repaint = new MorphRenderWatcher(() => this.#reconcileStep());
|
|
57
130
|
#shown = null;
|
|
58
|
-
/** Renders the initial state from `index`, clamped into the step range. */
|
|
59
131
|
connect() {
|
|
60
|
-
this.#repaint.
|
|
132
|
+
this.#repaint.observe(this.element);
|
|
61
133
|
this.#shown = this.#currentStep;
|
|
62
134
|
this.#render();
|
|
63
135
|
}
|
|
64
|
-
/** Drops a pending pass, so nothing renders or reports for a stepper that left. */
|
|
65
136
|
disconnect() {
|
|
66
|
-
this.#repaint.
|
|
137
|
+
this.#repaint.disconnect();
|
|
67
138
|
}
|
|
68
|
-
/** Re-renders when Turbo Morph or application code changes `index` at runtime. */
|
|
69
139
|
indexValueChanged() {
|
|
70
140
|
this.#repaint.schedule();
|
|
71
141
|
}
|
|
72
|
-
/** Re-derives every step's state for a step added at runtime. */
|
|
73
142
|
stepTargetConnected() {
|
|
74
143
|
this.#repaint.schedule();
|
|
75
144
|
}
|
|
76
|
-
/** Re-derives every step's state for a step removed at runtime. */
|
|
77
145
|
stepTargetDisconnected() {
|
|
78
146
|
this.#repaint.schedule();
|
|
79
147
|
}
|
|
80
|
-
/** Advances to the next step (ignored at the last step). */
|
|
81
148
|
next() {
|
|
82
|
-
this.#moveTo(this.#clampIndex(this
|
|
149
|
+
this.#moveTo(this.#clampIndex(this.#safeIndex) + 1);
|
|
83
150
|
}
|
|
84
|
-
/** Returns to the previous step (ignored at the first step). */
|
|
85
151
|
prev() {
|
|
86
|
-
this.#moveTo(this.#clampIndex(this
|
|
152
|
+
this.#moveTo(this.#clampIndex(this.#safeIndex) - 1);
|
|
87
153
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Jumps to the step carried in the action's `index` param. A param that is empty,
|
|
90
|
-
* or that Stimulus parses into anything but a number or a string, is no index,
|
|
91
|
-
* so it jumps nowhere.
|
|
92
|
-
*/
|
|
93
154
|
goto(event) {
|
|
94
155
|
const { index } = event.params;
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
const target = Number(index);
|
|
98
|
-
if (!Number.isFinite(target) || !Number.isInteger(target)) return;
|
|
156
|
+
const target = toFiniteNumber(index);
|
|
157
|
+
if (target === null || !Number.isInteger(target)) return;
|
|
99
158
|
this.#moveTo(target);
|
|
100
159
|
}
|
|
101
|
-
/**
|
|
102
|
-
* Moves the current step to `target` when allowed: in range, not a no-op, and
|
|
103
|
-
* — under `linear` — not skipping more than one step ahead. Re-renders and
|
|
104
|
-
* dispatches `change`.
|
|
105
|
-
*/
|
|
106
160
|
#moveTo(target) {
|
|
107
161
|
const total = this.stepTargets.length;
|
|
108
|
-
if (!Number.isFinite(target) || !Number.isInteger(target)) return;
|
|
109
162
|
if (target < 0 || target >= total) return;
|
|
110
|
-
const current = this.#clampIndex(this
|
|
111
|
-
if (target === current) return;
|
|
163
|
+
const current = this.#clampIndex(this.#safeIndex);
|
|
164
|
+
if (target === current && target === this.#shown) return;
|
|
112
165
|
if (this.linearValue && target > current + 1) return;
|
|
113
|
-
const previous = current;
|
|
166
|
+
const previous = this.#shown ?? current;
|
|
114
167
|
this.indexValue = target;
|
|
115
168
|
this.#shown = target;
|
|
116
169
|
this.#render();
|
|
170
|
+
if (target === previous) return;
|
|
117
171
|
this.dispatch("change", {
|
|
118
172
|
detail: { index: target, previous, total, step: this.stepTargets[target] }
|
|
119
173
|
});
|
|
120
174
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Re-derives one settled batch of Value and target changes, and reports a
|
|
123
|
-
* current position that moved from the one shown before as `reconcile`. With no
|
|
124
|
-
* step present there is nothing to report, and the position shown last stays the
|
|
125
|
-
* one the next move is measured from.
|
|
126
|
-
*/
|
|
127
175
|
#reconcileStep() {
|
|
128
176
|
const previous = this.#shown;
|
|
129
177
|
const index = this.#currentStep;
|
|
@@ -135,25 +183,11 @@ var StepperController = class extends Controller {
|
|
|
135
183
|
detail: { index, previous, total: steps.length, step: steps[index] }
|
|
136
184
|
});
|
|
137
185
|
}
|
|
138
|
-
/** The current position, clamped into the step range, or `null` when there is no step. */
|
|
139
186
|
get #currentStep() {
|
|
140
|
-
return this.stepTargets.length > 0 ? this.#clampIndex(this
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Derives each step's `data-state` and the current button's `aria-current` from
|
|
144
|
-
* `index`, clamped into the step range, without dispatching an action event.
|
|
145
|
-
*
|
|
146
|
-
* The clamp decides only what is shown. This body never writes `index`, so an
|
|
147
|
-
* out-of-range declaration stays in the attribute as the page wrote it.
|
|
148
|
-
*
|
|
149
|
-
* `aria-current="step"` is placed on the step's **first** `<button>`; the markup
|
|
150
|
-
* contract assumes one operable button per step. If a step needs multiple
|
|
151
|
-
* buttons, mark the navigational one first (or this would target the wrong one).
|
|
152
|
-
*
|
|
153
|
-
* @stimeoRenderRoot
|
|
154
|
-
*/
|
|
187
|
+
return this.stepTargets.length > 0 ? this.#clampIndex(this.#safeIndex) : null;
|
|
188
|
+
}
|
|
155
189
|
#render() {
|
|
156
|
-
const current = this.#clampIndex(this
|
|
190
|
+
const current = this.#clampIndex(this.#safeIndex);
|
|
157
191
|
this.stepTargets.forEach((step, index) => {
|
|
158
192
|
step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
|
|
159
193
|
const button = step.querySelector("button");
|
|
@@ -165,14 +199,21 @@ var StepperController = class extends Controller {
|
|
|
165
199
|
}
|
|
166
200
|
});
|
|
167
201
|
}
|
|
168
|
-
/** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */
|
|
169
202
|
#clampIndex(index) {
|
|
170
203
|
const last = this.stepTargets.length - 1;
|
|
171
|
-
if (last < 0
|
|
204
|
+
if (last < 0) return 0;
|
|
172
205
|
return Math.min(last, Math.max(0, Math.trunc(index)));
|
|
173
206
|
}
|
|
207
|
+
get #safeIndex() {
|
|
208
|
+
return this.#numbers.read(
|
|
209
|
+
this,
|
|
210
|
+
"index",
|
|
211
|
+
this.indexValue,
|
|
212
|
+
_StepperController.values.index.default,
|
|
213
|
+
_StepperController.valueConstraints.index
|
|
214
|
+
);
|
|
215
|
+
}
|
|
174
216
|
};
|
|
175
217
|
|
|
176
218
|
export { StepperController };
|
|
177
|
-
//# sourceMappingURL=stepper_controller.js.map
|
|
178
219
|
//# sourceMappingURL=stepper_controller.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/microtask_coalescer.ts","../../src/controllers/stepper_controller.ts"],"names":[],"mappings":"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","file":"stepper_controller.js","sourcesContent":["/**\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","import { Controller } from \"@hotwired/stimulus\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/**\n * Headless, accessible stepper / wizard navigation behavior.\n *\n * Markup contract (identifier: `stimeo--stepper`):\n * <ol data-controller=\"stimeo--stepper\" data-stimeo--stepper-index-value=\"0\">\n * <li data-stimeo--stepper-target=\"step\">\n * <button aria-current=\"step\" data-stimeo--stepper-index-param=\"0\"\n * data-action=\"click->stimeo--stepper#goto\">Account</button>\n * </li>\n * <!-- more steps -->\n * </ol>\n *\n * There is no dedicated APG widget; the current step is expressed with\n * `aria-current=\"step\"` on the operable `<button>`. Each step `<li>` also gets a\n * `data-state` (`complete`/`current`/`upcoming`) derived from the current index.\n * For a read-only progress display use\n * {@link StepIndicatorController | Step Indicator}; for panel switching use\n * {@link TabsController | Tabs}.\n *\n * `change` dispatches `{ index: number, previous: number, total: number, step: HTMLElement }`,\n * and `reconcile` dispatches the same\n * `{ index: number, previous: number, total: number, step: HTMLElement }` — `previous`\n * is the position shown before — when a change the page made moves the position of\n * the current step (the clamped `index`).\n *\n * @remarks\n * Behavior only. The controller never traps or restores focus — each step button\n * is in the natural Tab order. `data-state` is purely derived from `index` and the\n * steps present (completion is not persisted).\n *\n * Behavior provided:\n * - `next`/`prev` move one step, ignoring moves past either end.\n * - `goto` jumps to the step in its `index` action param.\n * - With `linear=true`, `goto` may not skip more than one step ahead of the\n * current one (moving backward is always allowed).\n * - Runtime changes to the `index` Value, and steps added or removed at runtime,\n * re-derive every step's state once per batch. Once connected, a batch that\n * moves the current position (the clamped `index`) away from the one shown\n * reports `stimeo--stepper:reconcile` once. A batch that leaves the position\n * where it was reports nothing, even when another step now stands there — a\n * step added before the current one, or the current step removed while\n * another follows it — and only redraws `data-state`/`aria-current`;\n * connecting reports nothing.\n * - An `index` outside the step range, fractional, or not a number at all is\n * clamped for display only. The Value keeps what the page declared, so the\n * declared step comes back once enough steps are present; a move the user makes\n * is the one path that writes it.\n * - Each move re-derives `data-state`/`aria-current` and dispatches\n * `stimeo--stepper:change`.\n */\nexport class StepperController extends Controller<HTMLElement> {\n static override targets = [\"step\"];\n static override values = {\n index: { type: Number, default: 0 },\n linear: { type: Boolean, default: false },\n };\n static actions = [\"goto\", \"next\", \"prev\"] as const;\n static events = [\"change\", \"reconcile\"] as const;\n\n declare readonly stepTargets: HTMLElement[];\n declare indexValue: number;\n declare linearValue: boolean;\n\n /**\n * Collapses the Value and target callbacks of one mutation into one pass, and\n * refuses the ones Stimulus delivers before `connect()`, which renders itself.\n */\n readonly #repaint = new MicrotaskCoalescer(() => this.#reconcileStep());\n\n /**\n * The position shown current last, which the next move is measured from, or\n * `null` before any step has been shown. While no step is present it keeps the\n * last one.\n */\n #shown: number | null = null;\n\n /** Renders the initial state from `index`, clamped into the step range. */\n override connect(): void {\n this.#repaint.activate();\n this.#shown = this.#currentStep;\n this.#render();\n }\n\n /** Drops a pending pass, so nothing renders or reports for a stepper that left. */\n override disconnect(): void {\n this.#repaint.cancel();\n }\n\n /** Re-renders when Turbo Morph or application code changes `index` at runtime. */\n indexValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-derives every step's state for a step added at runtime. */\n stepTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Re-derives every step's state for a step removed at runtime. */\n stepTargetDisconnected(): void {\n this.#repaint.schedule();\n }\n\n /** Advances to the next step (ignored at the last step). */\n next(): void {\n this.#moveTo(this.#clampIndex(this.indexValue) + 1);\n }\n\n /** Returns to the previous step (ignored at the first step). */\n prev(): void {\n this.#moveTo(this.#clampIndex(this.indexValue) - 1);\n }\n\n /**\n * Jumps to the step carried in the action's `index` param. A param that is empty,\n * or that Stimulus parses into anything but a number or a string, is no index,\n * so it jumps nowhere.\n */\n goto(event: { params: { index?: unknown } }): void {\n const { index } = event.params;\n if (index === \"\") return;\n if (typeof index !== \"number\" && typeof index !== \"string\") return;\n const target = Number(index);\n if (!Number.isFinite(target) || !Number.isInteger(target)) return;\n this.#moveTo(target);\n }\n\n /**\n * Moves the current step to `target` when allowed: in range, not a no-op, and\n * — under `linear` — not skipping more than one step ahead. Re-renders and\n * dispatches `change`.\n */\n #moveTo(target: number): void {\n const total = this.stepTargets.length;\n if (!Number.isFinite(target) || !Number.isInteger(target)) return;\n if (target < 0 || target >= total) return;\n const current = this.#clampIndex(this.indexValue);\n if (target === current) return;\n if (this.linearValue && target > current + 1) return;\n\n const previous = current;\n this.indexValue = target;\n // Settled before the report, so the pass the Value write starts finds the step\n // already shown, and a listener that moves on is measured from this step.\n this.#shown = target;\n this.#render();\n this.dispatch(\"change\", {\n detail: { index: target, previous, total, step: this.stepTargets[target] },\n });\n }\n\n /**\n * Re-derives one settled batch of Value and target changes, and reports a\n * current position that moved from the one shown before as `reconcile`. With no\n * step present there is nothing to report, and the position shown last stays the\n * one the next move is measured from.\n */\n #reconcileStep(): void {\n const previous = this.#shown;\n const index = this.#currentStep;\n if (index !== null) this.#shown = index;\n this.#render();\n if (index === null || previous === null || index === previous) return;\n const steps = this.stepTargets;\n this.dispatch(\"reconcile\", {\n detail: { index, previous, total: steps.length, step: steps[index] },\n });\n }\n\n /** The current position, clamped into the step range, or `null` when there is no step. */\n get #currentStep(): number | null {\n return this.stepTargets.length > 0 ? this.#clampIndex(this.indexValue) : null;\n }\n\n /**\n * Derives each step's `data-state` and the current button's `aria-current` from\n * `index`, clamped into the step range, without dispatching an action event.\n *\n * The clamp decides only what is shown. This body never writes `index`, so an\n * out-of-range declaration stays in the attribute as the page wrote it.\n *\n * `aria-current=\"step\"` is placed on the step's **first** `<button>`; the markup\n * contract assumes one operable button per step. If a step needs multiple\n * buttons, mark the navigational one first (or this would target the wrong one).\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const current = this.#clampIndex(this.indexValue);\n this.stepTargets.forEach((step, index) => {\n step.dataset.state =\n index < current ? \"complete\" : index === current ? \"current\" : \"upcoming\";\n const button = step.querySelector<HTMLElement>(\"button\");\n if (!button) return;\n if (index === current) {\n button.setAttribute(\"aria-current\", \"step\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n });\n }\n\n /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */\n #clampIndex(index: number): number {\n const last = this.stepTargets.length - 1;\n if (last < 0 || !Number.isFinite(index)) return 0;\n return Math.min(last, Math.max(0, Math.trunc(index)));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/number_bounds.ts","../../src/utils/coerce.ts","../../src/utils/listener_set.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/morph_render_watcher.ts","../../src/utils/number_value.ts","../../src/controllers/stepper_controller.ts"],"names":[],"mappings":"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","file":"stepper_controller.js","sourcesContent":["import { MAX_TIMER_DELAY_MS } from \"./timer_bounds\";\n\n/** Semantic constraints on an already decoded number, independent of the DOM. */\nexport interface NumberBounds {\n /** Finite numbers are required, including when this field is omitted. */\n readonly finite?: true;\n /** Inclusive lower endpoint. */\n readonly min?: number;\n /** Inclusive upper endpoint. */\n readonly max?: number;\n /** Exclusive lower endpoint. */\n readonly exclusiveMin?: number;\n /** Require a number without a fractional part. */\n readonly integer?: boolean;\n /** Exact decoded numbers accepted by a discrete contract. */\n readonly allowedValues?: readonly number[];\n /** JSON-safe exceptions to finite-number checking for unbounded endpoints. */\n readonly allowInfinity?: \"negative\" | \"positive\" | \"both\";\n}\n\n/** One constraint for each Number Value, including shorthand declarations. */\nexport type NumberValueConstraints<Values> = {\n readonly [Key in keyof Values as Values[Key] extends\n | NumberConstructor\n | { type: NumberConstructor }\n ? Key\n : never]: NumberBounds;\n};\n\n/** Reusable semantic domains; fractional inputs remain fractional unless excluded. */\nexport const NUMBER_BOUNDS = {\n finite: { finite: true },\n nonNegative: { finite: true, min: 0 },\n positive: { finite: true, exclusiveMin: 0 },\n nonNegativeInteger: { finite: true, min: 0, integer: true },\n positiveInteger: { finite: true, exclusiveMin: 0, integer: true },\n lowerBound: { finite: true, allowInfinity: \"negative\" },\n upperBound: { finite: true, allowInfinity: \"positive\" },\n timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS },\n positiveTimer: { finite: true, exclusiveMin: 0, max: MAX_TIMER_DELAY_MS },\n} as const satisfies Record<string, NumberBounds>;\n\n/**\n * Checks all declared bounds without rounding, clamping, or coercing the input.\n * Infinity exceptions relax only finite checking; every other bound still applies.\n */\nexport function matchesNumberBounds(value: number, bounds: NumberBounds): boolean {\n if (!Number.isFinite(value)) {\n const direction = value === Infinity ? \"positive\" : value === -Infinity ? \"negative\" : null;\n if (direction === null) return false;\n if (bounds.allowInfinity !== \"both\" && bounds.allowInfinity !== direction) return false;\n }\n if (bounds.min !== undefined && value < bounds.min) return false;\n if (bounds.max !== undefined && value > bounds.max) return false;\n if (bounds.exclusiveMin !== undefined && value <= bounds.exclusiveMin) return false;\n if (bounds.integer && !Number.isInteger(value)) return false;\n if (bounds.allowedValues !== undefined && !bounds.allowedValues.includes(value)) return false;\n return true;\n}\n\n/** Decodes a Number Value literal; action params use their own JSON decoder. */\nexport function decodeNumberValue(raw: string): number {\n return Number(raw.replace(/_/g, \"\"));\n}\n","/**\n * Numeric coercion for controllers that accept a number through a Value, an action\n * param, or an event detail.\n *\n * Stimulus decodes action params as JSON when possible and leaves other text\n * untouched. CustomEvent detail can also supply a number or numeric string.\n * The reader accepts those two types while rejecting blank and non-finite input.\n */\n\nimport { matchesNumberBounds, type NumberBounds } from \"./number_bounds\";\n\n/**\n * Reads a finite number or nonblank numeric string; other input returns `null`.\n * Blank strings cannot silently reset a value to zero.\n */\nexport function toFiniteNumber(raw: unknown): number | null {\n if (typeof raw !== \"number\" && typeof raw !== \"string\") return null;\n if (typeof raw === \"string\" && raw.trim().length === 0) return null;\n const value = Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n\n/**\n * Returns an accepted decoded Number Value or the caller's fallback unchanged.\n * The caller owns the fallback contract; this reader does not parse or write a Value.\n */\nexport function readNumber(raw: number, fallback: number, bounds: NumberBounds): number {\n return matchesNumberBounds(raw, bounds) ? raw : fallback;\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. {@link\n * 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 * 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, or one that\n * lives for less than the connection, keeps its own pair of calls.\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, which has to match at release time and no\n * longer has a second place to drift from.\n */\n add(\n target: EventTarget,\n type: string,\n handler: EventListener,\n options?: Omit<AddEventListenerOptions, \"signal\">,\n ): 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","/**\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 /** 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","import { ListenerSet } from \"./listener_set\";\nimport { MicrotaskCoalescer } from \"./microtask_coalescer\";\n\n/**\n * Coalesces retained-element morphs and explicit lifecycle requests into one pass.\n *\n * Observe the controller's element after its initial state is settled. Morphs on\n * that element and bubbling morphs on its descendants share the same microtask\n * as {@link schedule}. The callback repairs derived output; the controller owns\n * any logical transition reporting. {@link disconnect} releases the listener and\n * cancels pending work, including work from an earlier connected lifetime.\n */\nexport class MorphRenderWatcher {\n readonly #listeners = new ListenerSet();\n readonly #pass: MicrotaskCoalescer;\n #element: Element | null = null;\n readonly #onMorph = (): void => this.#pass.schedule();\n\n /** @param run - the single pass that repairs the settled controller's output. */\n constructor(run: () => void) {\n this.#pass = new MicrotaskCoalescer(run);\n }\n\n /** Opens observation; observing the same element again preserves queued work. */\n observe(element: Element): void {\n if (this.#element === element) return;\n this.disconnect();\n this.#element = element;\n this.#pass.activate();\n this.#listeners.add(element, \"turbo:morph-element\", this.#onMorph);\n }\n\n /** Requests one pass; requests outside an observed lifetime are ignored. */\n schedule(): void {\n this.#pass.schedule();\n }\n\n /** Releases observation and pending work synchronously; safe to repeat. */\n disconnect(): void {\n this.#listeners.dispose();\n this.#pass.cancel();\n this.#element = null;\n }\n}\n","import { readNumber } from \"./coerce\";\nimport { matchesNumberBounds, type NumberBounds } from \"./number_bounds\";\n\n/** The instance identity carried by a Number Value read. */\nexport interface NumberValueOwner {\n readonly identifier: string;\n readonly element: Element;\n}\n\n/** Reads current declarations at the point of use without rewriting their attributes. */\nexport class NumberValueReader {\n /** The last rejected literal per Value; no history grows for an unchanged instance. */\n readonly #lastRejected = new Map<string, string>();\n\n /** Resolves the declared number using its own fallback and shared class contract. */\n read(\n owner: NumberValueOwner,\n name: string,\n raw: number,\n fallback: number,\n bounds: NumberBounds,\n ): number {\n const resolved = readNumber(raw, fallback, bounds);\n if (matchesNumberBounds(raw, bounds)) {\n this.#lastRejected.delete(name);\n return resolved;\n }\n const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;\n const literal = owner.element.getAttribute(attribute);\n if (literal === null) {\n this.#lastRejected.delete(name);\n return resolved;\n }\n if (this.#lastRejected.get(name) !== literal) {\n this.#lastRejected.set(name, literal);\n console.warn(\n `Stimeo UI: \"${owner.identifier}\" has an invalid number Value \"${name}\" declaration ${JSON.stringify(literal)}; using ${fallback}.`,\n );\n }\n return resolved;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { toFiniteNumber } from \"../utils/coerce\";\nimport { MorphRenderWatcher } from \"../utils/morph_render_watcher\";\nimport { NUMBER_BOUNDS, type NumberValueConstraints } from \"../utils/number_bounds\";\nimport { NumberValueReader } from \"../utils/number_value\";\n\n/**\n * Headless, accessible stepper / wizard navigation behavior.\n *\n * Markup contract (identifier: `stimeo--stepper`):\n * <ol data-controller=\"stimeo--stepper\" data-stimeo--stepper-index-value=\"0\">\n * <li data-stimeo--stepper-target=\"step\">\n * <button aria-current=\"step\" data-stimeo--stepper-index-param=\"0\"\n * data-action=\"click->stimeo--stepper#goto\">Account</button>\n * </li>\n * <!-- more steps -->\n * </ol>\n *\n * There is no dedicated APG widget; the current step is expressed with\n * `aria-current=\"step\"` on the operable `<button>`. Each step `<li>` also gets a\n * `data-state` (`complete`/`current`/`upcoming`) derived from the current index.\n * For a read-only progress display use\n * {@link StepIndicatorController | Step Indicator}; for panel switching use\n * {@link TabsController | Tabs}.\n *\n * `change` dispatches `{ index: number, previous: number, total: number, step: HTMLElement }`,\n * and `reconcile` dispatches the same\n * `{ index: number, previous: number, total: number, step: HTMLElement }` — `previous`\n * is the position shown before — when a change the page made moves the position of\n * the current step (the clamped `index`).\n *\n * User `change` reports compare the resulting state with the last published\n * state. A pending page write handled in the same script joins that confirmation;\n * a browser-delivered listener may settle it first as `reconcile`. Confirming\n * the last published state reports nothing.\n *\n * @remarks\n * Behavior only. The controller never traps or restores focus — each step button\n * is in the natural Tab order. `data-state` is purely derived from `index` and the\n * steps present (completion is not persisted).\n *\n * Behavior provided:\n * - `next`/`prev` move one step, ignoring moves past either end.\n * - `goto` jumps to the step in its `index` action param.\n * - With `linear=true`, `goto` may not skip more than one step ahead of the\n * current one (moving backward is always allowed).\n * - Runtime changes to the `index` Value, and steps added or removed at runtime,\n * re-derive every step's state once per batch. Once connected, a batch that\n * moves the current position (the clamped `index`) away from the one shown\n * reports `stimeo--stepper:reconcile` once. A batch that leaves the position\n * where it was reports nothing, even when another step now stands there — a\n * step added before the current one, or the current step removed while\n * another follows it — and only redraws `data-state`/`aria-current`;\n * connecting reports nothing.\n * - An `index` outside the step range, fractional, or not a number at all is\n * clamped for display only. The Value keeps what the page declared, so the\n * declared step comes back once enough steps are present; a move the user makes\n * is the one path that writes it.\n * - Each move re-derives `data-state`/`aria-current` and dispatches\n * `stimeo--stepper:change`.\n */\nexport class StepperController extends Controller<HTMLElement> {\n /** Numeric read boundaries share one reader for this controller instance. */\n readonly #numbers = new NumberValueReader();\n\n static override targets = [\"step\"];\n static override values = {\n index: { type: Number, default: 0 },\n linear: { type: Boolean, default: false },\n };\n\n static valueConstraints = {\n index: NUMBER_BOUNDS.finite,\n } satisfies NumberValueConstraints<typeof StepperController.values>;\n static actions = [\"goto\", \"next\", \"prev\"] as const;\n static events = [\"change\", \"reconcile\"] as const;\n\n declare readonly stepTargets: HTMLElement[];\n declare indexValue: number;\n declare linearValue: boolean;\n\n /**\n * Collapses the Value and target callbacks of one mutation into one pass, and\n * refuses the ones Stimulus delivers before `connect()`, which renders itself.\n */\n readonly #repaint = new MorphRenderWatcher(() => this.#reconcileStep());\n\n /**\n * The position shown current last, which the next move is measured from, or\n * `null` before any step has been shown. While no step is present it keeps the\n * last one.\n */\n #shown: number | null = null;\n\n /** Renders the initial state from `index`, clamped into the step range. */\n override connect(): void {\n this.#repaint.observe(this.element);\n this.#shown = this.#currentStep;\n this.#render();\n }\n\n /** Drops a pending pass, so nothing renders or reports for a stepper that left. */\n override disconnect(): void {\n this.#repaint.disconnect();\n }\n\n /** Re-renders when Turbo Morph or application code changes `index` at runtime. */\n indexValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-derives every step's state for a step added at runtime. */\n stepTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Re-derives every step's state for a step removed at runtime. */\n stepTargetDisconnected(): void {\n this.#repaint.schedule();\n }\n\n /** Advances to the next step (ignored at the last step). */\n next(): void {\n this.#moveTo(this.#clampIndex(this.#safeIndex) + 1);\n }\n\n /** Returns to the previous step (ignored at the first step). */\n prev(): void {\n this.#moveTo(this.#clampIndex(this.#safeIndex) - 1);\n }\n\n /**\n * Jumps to the finite integer carried in the action's `index` param.\n * Blank strings and nonnumeric JSON values do not select a step.\n */\n goto(event: { params: { index?: unknown } }): void {\n const { index } = event.params;\n const target = toFiniteNumber(index);\n if (target === null || !Number.isInteger(target)) return;\n this.#moveTo(target);\n }\n\n /**\n * Moves the current step to `target` when allowed: in range, not a no-op, and\n * — under `linear` — not skipping more than one step ahead. Re-renders and\n * dispatches `change`.\n */\n #moveTo(target: number): void {\n const total = this.stepTargets.length;\n if (target < 0 || target >= total) return;\n const current = this.#clampIndex(this.#safeIndex);\n if (target === current && target === this.#shown) return;\n if (this.linearValue && target > current + 1) return;\n\n const previous = this.#shown ?? current;\n this.indexValue = target;\n // Settled before the report, so the pass the Value write starts finds the step\n // already shown, and a listener that moves on is measured from this step.\n this.#shown = target;\n this.#render();\n if (target === previous) return;\n this.dispatch(\"change\", {\n detail: { index: target, previous, total, step: this.stepTargets[target] },\n });\n }\n\n /**\n * Re-derives one settled batch of Value and target changes, and reports a\n * current position that moved from the one shown before as `reconcile`. With no\n * step present there is nothing to report, and the position shown last stays the\n * one the next move is measured from.\n */\n #reconcileStep(): void {\n const previous = this.#shown;\n const index = this.#currentStep;\n if (index !== null) this.#shown = index;\n this.#render();\n if (index === null || previous === null || index === previous) return;\n const steps = this.stepTargets;\n this.dispatch(\"reconcile\", {\n detail: { index, previous, total: steps.length, step: steps[index] },\n });\n }\n\n /** The current position, clamped into the step range, or `null` when there is no step. */\n get #currentStep(): number | null {\n return this.stepTargets.length > 0 ? this.#clampIndex(this.#safeIndex) : null;\n }\n\n /**\n * Derives each step's `data-state` and the current button's `aria-current` from\n * `index`, clamped into the step range, without dispatching an action event.\n *\n * The clamp decides only what is shown. This body never writes `index`, so an\n * out-of-range declaration stays in the attribute as the page wrote it.\n *\n * `aria-current=\"step\"` is placed on the step's **first** `<button>`; the markup\n * contract assumes one operable button per step. If a step needs multiple\n * buttons, mark the navigational one first (or this would target the wrong one).\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const current = this.#clampIndex(this.#safeIndex);\n this.stepTargets.forEach((step, index) => {\n step.dataset.state =\n index < current ? \"complete\" : index === current ? \"current\" : \"upcoming\";\n const button = step.querySelector<HTMLElement>(\"button\");\n if (!button) return;\n if (index === current) {\n button.setAttribute(\"aria-current\", \"step\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n });\n }\n\n /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */\n #clampIndex(index: number): number {\n const last = this.stepTargets.length - 1;\n if (last < 0) return 0;\n return Math.min(last, Math.max(0, Math.trunc(index)));\n }\n /** Current `index` declaration resolved against its numeric contract. */\n get #safeIndex(): number {\n return this.#numbers.read(\n this,\n \"index\",\n this.indexValue,\n StepperController.values.index.default,\n StepperController.valueConstraints.index,\n );\n }\n}\n"]}
|
|
@@ -76,6 +76,12 @@ declare class StickToBottomController extends Controller<HTMLElement> {
|
|
|
76
76
|
default: boolean;
|
|
77
77
|
};
|
|
78
78
|
};
|
|
79
|
+
static valueConstraints: {
|
|
80
|
+
threshold: {
|
|
81
|
+
readonly finite: true;
|
|
82
|
+
readonly min: 0;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
79
85
|
static actions: readonly ["scrollToBottom"];
|
|
80
86
|
static events: readonly ["pin", "new"];
|
|
81
87
|
readonly contentTarget: HTMLElement;
|