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,15 +1,7 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/scroll_restore_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/frame_coalescer.ts
|
|
6
3
|
var FrameCoalescer = class {
|
|
7
4
|
#frame = null;
|
|
8
|
-
/**
|
|
9
|
-
* Runs `run` on the next frame, unless a frame is already pending — the first
|
|
10
|
-
* request of a burst wins and the rest are dropped. The pending frame is
|
|
11
|
-
* released before `run`, so `run` may request the next one.
|
|
12
|
-
*/
|
|
13
5
|
schedule(run) {
|
|
14
6
|
if (this.#frame !== null) return;
|
|
15
7
|
this.#frame = requestAnimationFrame(() => {
|
|
@@ -17,14 +9,6 @@ var FrameCoalescer = class {
|
|
|
17
9
|
run();
|
|
18
10
|
});
|
|
19
11
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Drops the pending frame, and reaches the platform only when there is one.
|
|
22
|
-
*
|
|
23
|
-
* There is no handle value that stands for "nothing pending":
|
|
24
|
-
* `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
|
|
25
|
-
* arrives as a large positive number that the same allocator can hand out, and
|
|
26
|
-
* an idle cancel would then drop a frame belonging to someone else.
|
|
27
|
-
*/
|
|
28
12
|
cancel() {
|
|
29
13
|
if (this.#frame === null) return;
|
|
30
14
|
cancelAnimationFrame(this.#frame);
|
|
@@ -32,7 +16,17 @@ var FrameCoalescer = class {
|
|
|
32
16
|
}
|
|
33
17
|
};
|
|
34
18
|
|
|
35
|
-
|
|
19
|
+
var ListenerSet = class {
|
|
20
|
+
#abort = new AbortController();
|
|
21
|
+
add(target, type, handler, options) {
|
|
22
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
23
|
+
}
|
|
24
|
+
dispose() {
|
|
25
|
+
this.#abort.abort();
|
|
26
|
+
this.#abort = new AbortController();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
36
30
|
function parseStored(raw) {
|
|
37
31
|
if (raw === null) return null;
|
|
38
32
|
try {
|
|
@@ -49,58 +43,39 @@ var ScrollRestoreController = class extends Controller {
|
|
|
49
43
|
key: { type: String, default: "" },
|
|
50
44
|
axis: { type: String, default: "vertical" }
|
|
51
45
|
};
|
|
52
|
-
/** Coalesces scroll bursts into one save per frame. */
|
|
53
46
|
#frames = new FrameCoalescer();
|
|
54
|
-
/** Resolved storage key; empty disables persistence (no key and no id). */
|
|
55
47
|
#storageKey = "";
|
|
56
|
-
/** Last offset captured while the element was live; persisted as-is on teardown. */
|
|
57
48
|
#lastTop = 0;
|
|
58
49
|
#lastLeft = 0;
|
|
59
|
-
/**
|
|
60
|
-
* The offsets this controller's own restore produced, awaiting the `scroll`
|
|
61
|
-
* the engine fires for them. A restore the layout cannot reach yet lands short,
|
|
62
|
-
* so treating that echo as the reader's position would cut the saved offset
|
|
63
|
-
* down to whatever the unfinished layout allowed.
|
|
64
|
-
*/
|
|
65
50
|
#echoTop = null;
|
|
66
51
|
#echoLeft = null;
|
|
67
|
-
/** Offsets stored for an axis that is not tracked, carried through saves. */
|
|
68
52
|
#carried = {};
|
|
69
|
-
/**
|
|
70
|
-
* Whether each axis holds an offset worth saving — one this namespace restored
|
|
71
|
-
* or the reader moved. A save writes only the axes that are marked, so an
|
|
72
|
-
* offset taken under a different key or axis is never re-published as if the
|
|
73
|
-
* reader had left it there.
|
|
74
|
-
*/
|
|
75
53
|
#capturedTop = false;
|
|
76
54
|
#capturedLeft = false;
|
|
77
|
-
/** Whether the controller is connected; Value callbacks outside that window do not resync. */
|
|
78
55
|
#connected = false;
|
|
79
56
|
#onScroll = () => {
|
|
80
57
|
this.#capture();
|
|
81
58
|
this.#frames.schedule(() => this.#persist());
|
|
82
59
|
};
|
|
60
|
+
#listeners = new ListenerSet();
|
|
83
61
|
connect() {
|
|
84
62
|
this.#connected = true;
|
|
85
63
|
this.#storageKey = this.#resolveKey();
|
|
86
|
-
this.element
|
|
64
|
+
this.#listeners.add(this.element, "scroll", this.#onScroll, { passive: true });
|
|
87
65
|
if (this.#storageKey) this.#restore();
|
|
88
66
|
}
|
|
89
67
|
disconnect() {
|
|
90
68
|
this.#connected = false;
|
|
91
|
-
this.
|
|
69
|
+
this.#listeners.dispose();
|
|
92
70
|
this.#frames.cancel();
|
|
93
71
|
this.#persist();
|
|
94
72
|
}
|
|
95
|
-
/** Re-derives the namespace when application code or a Turbo morph moves `key`. */
|
|
96
73
|
keyValueChanged() {
|
|
97
74
|
this.#resync();
|
|
98
75
|
}
|
|
99
|
-
/** Re-derives the tracked axes when application code or a Turbo morph moves `axis`. */
|
|
100
76
|
axisValueChanged() {
|
|
101
77
|
this.#resync();
|
|
102
78
|
}
|
|
103
|
-
/** Rebuilds the persistence state around the Values as they now read. */
|
|
104
79
|
#resync() {
|
|
105
80
|
if (!this.#connected) return;
|
|
106
81
|
this.#frames.cancel();
|
|
@@ -112,15 +87,6 @@ var ScrollRestoreController = class extends Controller {
|
|
|
112
87
|
this.#capturedLeft = false;
|
|
113
88
|
if (this.#storageKey) this.#restore();
|
|
114
89
|
}
|
|
115
|
-
/**
|
|
116
|
-
* Records the live scroll offset for the configured axis.
|
|
117
|
-
*
|
|
118
|
-
* An axis holds its echo until that axis actually moves. A scroll event names
|
|
119
|
-
* no axis, so consuming both on the first one to arrive would leave the other
|
|
120
|
-
* unguarded: on `both`, moving one axis would take the clamped reading of the
|
|
121
|
-
* untouched axis as the reader's own position and save it, cutting the stored
|
|
122
|
-
* offset down to whatever the layout could reach at restore time.
|
|
123
|
-
*/
|
|
124
90
|
#capture() {
|
|
125
91
|
if (this.#tracksVertical) {
|
|
126
92
|
const top = this.element.scrollTop;
|
|
@@ -139,10 +105,6 @@ var ScrollRestoreController = class extends Controller {
|
|
|
139
105
|
}
|
|
140
106
|
}
|
|
141
107
|
}
|
|
142
|
-
/**
|
|
143
|
-
* Writes the marked axes, carrying through any offset held for an untracked one.
|
|
144
|
-
* Without a key, or with neither axis marked, it writes nothing.
|
|
145
|
-
*/
|
|
146
108
|
#persist() {
|
|
147
109
|
if (!this.#storageKey || !(this.#capturedTop || this.#capturedLeft)) return;
|
|
148
110
|
const data = { ...this.#carried };
|
|
@@ -153,7 +115,6 @@ var ScrollRestoreController = class extends Controller {
|
|
|
153
115
|
} catch {
|
|
154
116
|
}
|
|
155
117
|
}
|
|
156
|
-
/** Applies the persisted scroll offset, if any, without moving focus. */
|
|
157
118
|
#restore() {
|
|
158
119
|
let raw;
|
|
159
120
|
try {
|
|
@@ -196,7 +157,6 @@ var ScrollRestoreController = class extends Controller {
|
|
|
196
157
|
if (this.element.scrollTop !== beforeTop) this.#echoTop = this.element.scrollTop;
|
|
197
158
|
if (this.element.scrollLeft !== beforeLeft) this.#echoLeft = this.element.scrollLeft;
|
|
198
159
|
}
|
|
199
|
-
/** The `sessionStorage` key: explicit `key`, else the element `id`, else none. */
|
|
200
160
|
#resolveKey() {
|
|
201
161
|
const base = this.keyValue || this.element.id;
|
|
202
162
|
return base ? `stimeo--scroll-restore:${base}` : "";
|
|
@@ -210,5 +170,4 @@ var ScrollRestoreController = class extends Controller {
|
|
|
210
170
|
};
|
|
211
171
|
|
|
212
172
|
export { ScrollRestoreController };
|
|
213
|
-
//# sourceMappingURL=scroll_restore_controller.js.map
|
|
214
173
|
//# sourceMappingURL=scroll_restore_controller.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/frame_coalescer.ts","../../src/controllers/scroll_restore_controller.ts"],"names":[],"mappings":"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","file":"scroll_restore_controller.js","sourcesContent":["/**\n * Runs one piece of scroll-driven work per animation frame.\n *\n * A `scroll` listener fires many times between two paints, and each firing would\n * measure the same layout. Requesting a frame on the first firing and ignoring\n * the rest until it runs keeps the reads to one per frame; running on the frame\n * — rather than on a timer — keeps the measurement in step with what the reader\n * sees.\n *\n * **The first request of a burst wins.** Work handed in while a frame is already\n * pending is dropped, so a caller that queues a special first pass and then\n * receives scrolls gets that first pass, not the last scroll's.\n *\n * Cancelling drops the pending frame outright, and does nothing at all when none\n * is pending. A frame really is cancelled, so nothing needs a generation to tell\n * a stale callback from a fresh one, and nothing has to be refused before the\n * caller starts listening.\n *\n * Scope is the scheduling only. What to measure stays with the caller.\n *\n * @example\n * ```ts\n * readonly #frames = new FrameCoalescer();\n *\n * readonly #onScroll = (): void => this.#frames.schedule(() => this.#measure());\n *\n * disconnect(): void {\n * this.#frames.cancel();\n * }\n * ```\n */\nexport class FrameCoalescer {\n #frame: number | null = null;\n\n /**\n * Runs `run` on the next frame, unless a frame is already pending — the first\n * request of a burst wins and the rest are dropped. The pending frame is\n * released before `run`, so `run` may request the next one.\n */\n schedule(run: () => void): void {\n if (this.#frame !== null) return;\n this.#frame = requestAnimationFrame(() => {\n this.#frame = null;\n run();\n });\n }\n\n /**\n * Drops the pending frame, and reaches the platform only when there is one.\n *\n * There is no handle value that stands for \"nothing pending\":\n * `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder\n * arrives as a large positive number that the same allocator can hand out, and\n * an idle cancel would then drop a frame belonging to someone else.\n */\n cancel(): void {\n if (this.#frame === null) return;\n cancelAnimationFrame(this.#frame);\n this.#frame = null;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FrameCoalescer } from \"../utils/frame_coalescer\";\n\n/** Persisted scroll position; fields are present only for the saved axis. */\ninterface StoredScroll {\n top?: number;\n left?: number;\n}\n\n/**\n * Reads one persisted entry, or `null` when there is nothing usable to read.\n *\n * `JSON.parse` returns `null` for the literal `null` without throwing, so the\n * parse guard alone would hand back a value with no fields to read. Every field the\n * caller applies is validated there, which is what makes a bare literal harmless here.\n */\nfunction parseStored(raw: string | null): StoredScroll | null {\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as StoredScroll | null;\n } catch {\n return null;\n }\n}\n\n/** One axis offset from a persisted entry, or `null` when it cannot be applied. */\nfunction storedOffset(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\n/**\n * Headless **Scroll Restore** behavior: persists and restores the scroll\n * position of an inner scrolling region across Turbo Drive navigations (and full\n * reloads within the same tab session). No APG widget — a pure state-preservation\n * utility, like {@link import(\"./scroll_visibility_controller\").ScrollVisibilityController}\n * and the Sticky State Observer.\n *\n * Markup contract (identifier: `stimeo--scroll-restore`):\n * <div data-controller=\"stimeo--scroll-restore\"\n * data-stimeo--scroll-restore-key-value=\"sidebar\"\n * style=\"overflow: auto\">\n * …long content…\n * </div>\n *\n * Turbo swaps the whole `<body>` on navigation, so an inner scroll container is\n * rebuilt with `scrollTop` reset to 0. Rather than have every app hand-write a\n * controller for this (which contradicts a \"ship the behavior\" library), this\n * persists the offset under a stable key in `sessionStorage` and restores it on\n * `connect()`.\n *\n * Values:\n * - `key` — the `sessionStorage` namespace. Empty falls back to the element `id`;\n * with neither, nothing is saved or restored, so positions from unrelated pages\n * cannot mix.\n * - `axis` — which offset is tracked: `vertical` (default, `scrollTop`),\n * `horizontal` (`scrollLeft`) or `both`. An offset stored for an axis that is\n * not tracked is neither applied nor discarded.\n *\n * Both Values are re-derived when they change at runtime, so a morph that\n * rewrites the attribute in place switches namespace or axis instead of going on\n * writing under the one that held at connect. A `key` left empty resolves through\n * the element `id`, which no Value callback observes; runtime `id` changes are\n * out of scope.\n *\n * @remarks\n * Behavior only — it sets no ARIA/`data-*`/CSS and never moves focus. Restoring\n * goes through `scrollTo({ behavior: \"instant\" })` so that a consumer's\n * `scroll-behavior: smooth` cannot turn it into an animation whose intermediate\n * offsets would be recorded as the reader's own scrolling. The `scroll` listener\n * is internal and `passive` (no consumer `data-action` needed): each event\n * records the live offset synchronously, and a `requestAnimationFrame` coalesces\n * the writes to `sessionStorage`. On `disconnect()` (Turbo navigation included)\n * it flushes the **last captured** offset rather than re-reading the element — by\n * the time Turbo fires `disconnect` it has already detached the node, whose\n * `scrollTop` then reads `0`, so a fresh read would clobber the saved position\n * with `0`. Keying by `key` (falling back to the element `id`) in\n * `sessionStorage` makes it multi-instance safe and survives full reloads —\n * unlike a module-scope variable, which only survives Turbo Drive and assumes a\n * single instance. Storage access is best-effort: a browser that refuses it\n * (private mode, quota) leaves the element working, just without persistence.\n */\nexport class ScrollRestoreController extends Controller<HTMLElement> {\n static override values = {\n key: { type: String, default: \"\" },\n axis: { type: String, default: \"vertical\" },\n };\n\n declare keyValue: string;\n declare axisValue: string;\n\n /** Coalesces scroll bursts into one save per frame. */\n readonly #frames = new FrameCoalescer();\n /** Resolved storage key; empty disables persistence (no key and no id). */\n #storageKey = \"\";\n /** Last offset captured while the element was live; persisted as-is on teardown. */\n #lastTop = 0;\n #lastLeft = 0;\n /**\n * The offsets this controller's own restore produced, awaiting the `scroll`\n * the engine fires for them. A restore the layout cannot reach yet lands short,\n * so treating that echo as the reader's position would cut the saved offset\n * down to whatever the unfinished layout allowed.\n */\n #echoTop: number | null = null;\n #echoLeft: number | null = null;\n /** Offsets stored for an axis that is not tracked, carried through saves. */\n #carried: StoredScroll = {};\n /**\n * Whether each axis holds an offset worth saving — one this namespace restored\n * or the reader moved. A save writes only the axes that are marked, so an\n * offset taken under a different key or axis is never re-published as if the\n * reader had left it there.\n */\n #capturedTop = false;\n #capturedLeft = false;\n /** Whether the controller is connected; Value callbacks outside that window do not resync. */\n #connected = false;\n\n readonly #onScroll = (): void => {\n // Capture synchronously while the element is still connected and measurable;\n // the rAF only debounces the sessionStorage write, never the read.\n this.#capture();\n this.#frames.schedule(() => this.#persist());\n };\n\n override connect(): void {\n this.#connected = true;\n this.#storageKey = this.#resolveKey();\n this.element.addEventListener(\"scroll\", this.#onScroll, { passive: true });\n if (this.#storageKey) this.#restore();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.element.removeEventListener(\"scroll\", this.#onScroll);\n this.#frames.cancel();\n // Flush the last captured offset (a throttled burst may have left the final\n // frame unsaved). We do NOT re-read the element here: Turbo detaches the node\n // before disconnect, so its scrollTop is 0 and would overwrite the real value.\n this.#persist();\n }\n\n /** Re-derives the namespace when application code or a Turbo morph moves `key`. */\n keyValueChanged(): void {\n this.#resync();\n }\n\n /** Re-derives the tracked axes when application code or a Turbo morph moves `axis`. */\n axisValueChanged(): void {\n this.#resync();\n }\n\n /** Rebuilds the persistence state around the Values as they now read. */\n #resync(): void {\n if (!this.#connected) return;\n // The pending frame holds an offset belonging to the previous namespace and\n // axis, so it is dropped rather than written under the new ones.\n this.#frames.cancel();\n this.#storageKey = this.#resolveKey();\n this.#echoTop = null;\n this.#echoLeft = null;\n this.#carried = {};\n // An offset is only ever written for an axis this namespace seeded or the\n // reader moved. Clearing both marks drops the ones taken under the previous\n // key and axis, which the restore below re-establishes only where the new\n // key actually holds a value.\n this.#capturedTop = false;\n this.#capturedLeft = false;\n if (this.#storageKey) this.#restore();\n }\n\n /**\n * Records the live scroll offset for the configured axis.\n *\n * An axis holds its echo until that axis actually moves. A scroll event names\n * no axis, so consuming both on the first one to arrive would leave the other\n * unguarded: on `both`, moving one axis would take the clamped reading of the\n * untouched axis as the reader's own position and save it, cutting the stored\n * offset down to whatever the layout could reach at restore time.\n */\n #capture(): void {\n if (this.#tracksVertical) {\n const top = this.element.scrollTop;\n if (top !== this.#echoTop) {\n this.#echoTop = null;\n this.#lastTop = top;\n this.#capturedTop = true;\n }\n }\n if (this.#tracksHorizontal) {\n const left = this.element.scrollLeft;\n if (left !== this.#echoLeft) {\n this.#echoLeft = null;\n this.#lastLeft = left;\n this.#capturedLeft = true;\n }\n }\n }\n\n /**\n * Writes the marked axes, carrying through any offset held for an untracked one.\n * Without a key, or with neither axis marked, it writes nothing.\n */\n #persist(): void {\n if (!this.#storageKey || !(this.#capturedTop || this.#capturedLeft)) return;\n const data: StoredScroll = { ...this.#carried };\n if (this.#tracksVertical && this.#capturedTop) data.top = this.#lastTop;\n if (this.#tracksHorizontal && this.#capturedLeft) data.left = this.#lastLeft;\n try {\n window.sessionStorage.setItem(this.#storageKey, JSON.stringify(data));\n } catch {\n // sessionStorage can throw (private mode / quota); persistence is best-effort.\n }\n }\n\n /** Applies the persisted scroll offset, if any, without moving focus. */\n #restore(): void {\n let raw: string | null;\n try {\n raw = window.sessionStorage.getItem(this.#storageKey);\n } catch {\n return;\n }\n const data = parseStored(raw);\n if (data === null) return;\n\n // A restore writes its own echoes below; one left over from an earlier pass\n // would otherwise swallow the first matching move the reader makes.\n this.#echoTop = null;\n this.#echoLeft = null;\n\n const top = storedOffset(data.top);\n const left = storedOffset(data.left);\n const options: ScrollToOptions = { behavior: \"instant\" };\n let requested = false;\n // Seed the capture cache from the restored value so an immediate teardown\n // (before any scroll) re-persists the restored position, not 0. Guard by the\n // configured axis so a stale field from a different axis is never applied —\n // and keep it, since the next save would otherwise drop it.\n if (this.#tracksVertical) {\n if (top !== null) {\n options.top = top;\n this.#lastTop = top;\n this.#capturedTop = true;\n requested = true;\n }\n } else if (data.top !== undefined) {\n this.#carried.top = data.top;\n }\n if (this.#tracksHorizontal) {\n if (left !== null) {\n options.left = left;\n this.#lastLeft = left;\n this.#capturedLeft = true;\n requested = true;\n }\n } else if (data.left !== undefined) {\n this.#carried.left = data.left;\n }\n if (!requested) return;\n\n const beforeTop = this.element.scrollTop;\n const beforeLeft = this.element.scrollLeft;\n this.element.scrollTo(options);\n if (this.element.scrollTop !== beforeTop) this.#echoTop = this.element.scrollTop;\n if (this.element.scrollLeft !== beforeLeft) this.#echoLeft = this.element.scrollLeft;\n }\n\n /** The `sessionStorage` key: explicit `key`, else the element `id`, else none. */\n #resolveKey(): string {\n const base = this.keyValue || this.element.id;\n return base ? `stimeo--scroll-restore:${base}` : \"\";\n }\n\n get #tracksVertical(): boolean {\n return this.axisValue !== \"horizontal\";\n }\n\n get #tracksHorizontal(): boolean {\n return this.axisValue === \"horizontal\" || this.axisValue === \"both\";\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/frame_coalescer.ts","../../src/utils/listener_set.ts","../../src/controllers/scroll_restore_controller.ts"],"names":[],"mappings":"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","file":"scroll_restore_controller.js","sourcesContent":["/**\n * Runs one piece of scroll-driven work per animation frame.\n *\n * A `scroll` listener fires many times between two paints, and each firing would\n * measure the same layout. Requesting a frame on the first firing and ignoring\n * the rest until it runs keeps the reads to one per frame; running on the frame\n * — rather than on a timer — keeps the measurement in step with what the reader\n * sees.\n *\n * **The first request of a burst wins.** Work handed in while a frame is already\n * pending is dropped, so a caller that queues a special first pass and then\n * receives scrolls gets that first pass, not the last scroll's.\n *\n * Cancelling drops the pending frame outright, and does nothing at all when none\n * is pending. A frame really is cancelled, so nothing needs a generation to tell\n * a stale callback from a fresh one, and nothing has to be refused before the\n * caller starts listening.\n *\n * Scope is the scheduling only. What to measure stays with the caller.\n *\n * @example\n * ```ts\n * readonly #frames = new FrameCoalescer();\n *\n * readonly #onScroll = (): void => this.#frames.schedule(() => this.#measure());\n *\n * disconnect(): void {\n * this.#frames.cancel();\n * }\n * ```\n */\nexport class FrameCoalescer {\n #frame: number | null = null;\n\n /**\n * Runs `run` on the next frame, unless a frame is already pending — the first\n * request of a burst wins and the rest are dropped. The pending frame is\n * released before `run`, so `run` may request the next one.\n */\n schedule(run: () => void): void {\n if (this.#frame !== null) return;\n this.#frame = requestAnimationFrame(() => {\n this.#frame = null;\n run();\n });\n }\n\n /**\n * Drops the pending frame, and reaches the platform only when there is one.\n *\n * There is no handle value that stands for \"nothing pending\":\n * `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder\n * arrives as a large positive number that the same allocator can hand out, and\n * an idle cancel would then drop a frame belonging to someone else.\n */\n cancel(): void {\n if (this.#frame === null) return;\n cancelAnimationFrame(this.#frame);\n this.#frame = null;\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","import { Controller } from \"@hotwired/stimulus\";\nimport { FrameCoalescer } from \"../utils/frame_coalescer\";\nimport { ListenerSet } from \"../utils/listener_set\";\n\n/** Persisted scroll position; fields are present only for the saved axis. */\ninterface StoredScroll {\n top?: number;\n left?: number;\n}\n\n/**\n * Reads one persisted entry, or `null` when there is nothing usable to read.\n *\n * `JSON.parse` returns `null` for the literal `null` without throwing, so the\n * parse guard alone would hand back a value with no fields to read. Every field the\n * caller applies is validated there, which is what makes a bare literal harmless here.\n */\nfunction parseStored(raw: string | null): StoredScroll | null {\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as StoredScroll | null;\n } catch {\n return null;\n }\n}\n\n/** One axis offset from a persisted entry, or `null` when it cannot be applied. */\nfunction storedOffset(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\n/**\n * Headless **Scroll Restore** behavior: persists and restores the scroll\n * position of an inner scrolling region across Turbo Drive navigations (and full\n * reloads within the same tab session). Like `stimeo--scroll-visibility` and\n * `stimeo--sticky-observer`, it implements no APG widget; it is a pure\n * state-preservation utility.\n *\n * Markup contract (identifier: `stimeo--scroll-restore`):\n * <div data-controller=\"stimeo--scroll-restore\"\n * data-stimeo--scroll-restore-key-value=\"sidebar\"\n * style=\"overflow: auto\">\n * …long content…\n * </div>\n *\n * Turbo swaps the whole `<body>` on navigation, so an inner scroll container is\n * rebuilt with `scrollTop` reset to 0. Rather than have every app hand-write a\n * controller for this (which contradicts a \"ship the behavior\" library), this\n * persists the offset under a stable key in `sessionStorage` and restores it on\n * `connect()`.\n *\n * Values:\n * - `key` — the `sessionStorage` namespace. Empty falls back to the element `id`;\n * with neither, nothing is saved or restored, so positions from unrelated pages\n * cannot mix.\n * - `axis` — which offset is tracked: `vertical` (default, `scrollTop`),\n * `horizontal` (`scrollLeft`) or `both`. An offset stored for an axis that is\n * not tracked is neither applied nor discarded.\n *\n * Both Values are re-derived when they change at runtime, so a morph that\n * rewrites the attribute in place switches namespace or axis instead of going on\n * writing under the one that held at connect. A `key` left empty resolves through\n * the element `id`, which no Value callback observes; runtime `id` changes are\n * out of scope.\n *\n * @remarks\n * Behavior only — it sets no ARIA/`data-*`/CSS and never moves focus. Restoring\n * goes through `scrollTo({ behavior: \"instant\" })` so that a consumer's\n * `scroll-behavior: smooth` cannot turn it into an animation whose intermediate\n * offsets would be recorded as the reader's own scrolling. The `scroll` listener\n * is internal and `passive` (no consumer `data-action` needed): each event\n * records the live offset synchronously, and a `requestAnimationFrame` coalesces\n * the writes to `sessionStorage`. On `disconnect()` (Turbo navigation included)\n * it flushes the **last captured** offset rather than re-reading the element — by\n * the time Turbo fires `disconnect` it has already detached the node, whose\n * `scrollTop` then reads `0`, so a fresh read would clobber the saved position\n * with `0`. Keying by `key` (falling back to the element `id`) in\n * `sessionStorage` makes it multi-instance safe and survives full reloads —\n * unlike a module-scope variable, which only survives Turbo Drive and assumes a\n * single instance. Storage access is best-effort: a browser that refuses it\n * (private mode, quota) leaves the element working, just without persistence.\n */\nexport class ScrollRestoreController extends Controller<HTMLElement> {\n static override values = {\n key: { type: String, default: \"\" },\n axis: { type: String, default: \"vertical\" },\n };\n\n declare keyValue: string;\n declare axisValue: string;\n\n /** Coalesces scroll bursts into one save per frame. */\n readonly #frames = new FrameCoalescer();\n /** Resolved storage key; empty disables persistence (no key and no id). */\n #storageKey = \"\";\n /** Last offset captured while the element was live; persisted as-is on teardown. */\n #lastTop = 0;\n #lastLeft = 0;\n /**\n * The offsets this controller's own restore produced, awaiting the `scroll`\n * the engine fires for them. A restore the layout cannot reach yet lands short,\n * so treating that echo as the reader's position would cut the saved offset\n * down to whatever the unfinished layout allowed.\n */\n #echoTop: number | null = null;\n #echoLeft: number | null = null;\n /** Offsets stored for an axis that is not tracked, carried through saves. */\n #carried: StoredScroll = {};\n /**\n * Whether each axis holds an offset worth saving — one this namespace restored\n * or the reader moved. A save writes only the axes that are marked, so an\n * offset taken under a different key or axis is never re-published as if the\n * reader had left it there.\n */\n #capturedTop = false;\n #capturedLeft = false;\n /** Whether the controller is connected; Value callbacks outside that window do not resync. */\n #connected = false;\n\n readonly #onScroll = (): void => {\n // Capture synchronously while the element is still connected and measurable;\n // the rAF only debounces the sessionStorage write, never the read.\n this.#capture();\n this.#frames.schedule(() => this.#persist());\n };\n\n /** The listeners of one connection, released together on `disconnect()`. */\n readonly #listeners = new ListenerSet();\n\n override connect(): void {\n this.#connected = true;\n this.#storageKey = this.#resolveKey();\n this.#listeners.add(this.element, \"scroll\", this.#onScroll, { passive: true });\n if (this.#storageKey) this.#restore();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#listeners.dispose();\n this.#frames.cancel();\n // Flush the last captured offset (a throttled burst may have left the final\n // frame unsaved). We do NOT re-read the element here: Turbo detaches the node\n // before disconnect, so its scrollTop is 0 and would overwrite the real value.\n this.#persist();\n }\n\n /** Re-derives the namespace when application code or a Turbo morph moves `key`. */\n keyValueChanged(): void {\n this.#resync();\n }\n\n /** Re-derives the tracked axes when application code or a Turbo morph moves `axis`. */\n axisValueChanged(): void {\n this.#resync();\n }\n\n /** Rebuilds the persistence state around the Values as they now read. */\n #resync(): void {\n if (!this.#connected) return;\n // A frame a scroll queued before the swap belongs to the previous namespace and\n // axis, so it is dropped rather than run against the new ones.\n this.#frames.cancel();\n this.#storageKey = this.#resolveKey();\n this.#echoTop = null;\n this.#echoLeft = null;\n this.#carried = {};\n // An offset is only ever written for an axis this namespace seeded or the\n // reader moved. Clearing both marks drops the ones taken under the previous\n // key and axis, which the restore below re-establishes only where the new\n // key actually holds a value.\n this.#capturedTop = false;\n this.#capturedLeft = false;\n if (this.#storageKey) this.#restore();\n }\n\n /**\n * Records the live scroll offset for the configured axis.\n *\n * An axis holds its echo until that axis actually moves. A scroll event names\n * no axis, so consuming both on the first one to arrive would leave the other\n * unguarded: on `both`, moving one axis would take the clamped reading of the\n * untouched axis as the reader's own position and save it, cutting the stored\n * offset down to whatever the layout could reach at restore time.\n */\n #capture(): void {\n if (this.#tracksVertical) {\n const top = this.element.scrollTop;\n if (top !== this.#echoTop) {\n this.#echoTop = null;\n this.#lastTop = top;\n this.#capturedTop = true;\n }\n }\n if (this.#tracksHorizontal) {\n const left = this.element.scrollLeft;\n if (left !== this.#echoLeft) {\n this.#echoLeft = null;\n this.#lastLeft = left;\n this.#capturedLeft = true;\n }\n }\n }\n\n /**\n * Writes the marked axes, carrying through any offset held for an untracked one.\n * Without a key, or with neither axis marked, it writes nothing.\n */\n #persist(): void {\n if (!this.#storageKey || !(this.#capturedTop || this.#capturedLeft)) return;\n const data: StoredScroll = { ...this.#carried };\n if (this.#tracksVertical && this.#capturedTop) data.top = this.#lastTop;\n if (this.#tracksHorizontal && this.#capturedLeft) data.left = this.#lastLeft;\n try {\n window.sessionStorage.setItem(this.#storageKey, JSON.stringify(data));\n } catch {\n // sessionStorage can throw (private mode / quota); persistence is best-effort.\n }\n }\n\n /** Applies the persisted scroll offset, if any, without moving focus. */\n #restore(): void {\n let raw: string | null;\n try {\n raw = window.sessionStorage.getItem(this.#storageKey);\n } catch {\n return;\n }\n const data = parseStored(raw);\n if (data === null) return;\n\n // A restore writes its own echoes below; one left over from an earlier pass\n // would otherwise swallow the first matching move the reader makes.\n this.#echoTop = null;\n this.#echoLeft = null;\n\n const top = storedOffset(data.top);\n const left = storedOffset(data.left);\n const options: ScrollToOptions = { behavior: \"instant\" };\n let requested = false;\n // Seed the capture cache from the restored value so an immediate teardown\n // (before any scroll) re-persists the restored position, not 0. Guard by the\n // configured axis so a stale field from a different axis is never applied —\n // and keep it, since the next save would otherwise drop it.\n if (this.#tracksVertical) {\n if (top !== null) {\n options.top = top;\n this.#lastTop = top;\n this.#capturedTop = true;\n requested = true;\n }\n } else if (data.top !== undefined) {\n this.#carried.top = data.top;\n }\n if (this.#tracksHorizontal) {\n if (left !== null) {\n options.left = left;\n this.#lastLeft = left;\n this.#capturedLeft = true;\n requested = true;\n }\n } else if (data.left !== undefined) {\n this.#carried.left = data.left;\n }\n if (!requested) return;\n\n const beforeTop = this.element.scrollTop;\n const beforeLeft = this.element.scrollLeft;\n this.element.scrollTo(options);\n if (this.element.scrollTop !== beforeTop) this.#echoTop = this.element.scrollTop;\n if (this.element.scrollLeft !== beforeLeft) this.#echoLeft = this.element.scrollLeft;\n }\n\n /** The `sessionStorage` key: explicit `key`, else the element `id`, else none. */\n #resolveKey(): string {\n const base = this.keyValue || this.element.id;\n return base ? `stimeo--scroll-restore:${base}` : \"\";\n }\n\n get #tracksVertical(): boolean {\n return this.axisValue !== \"horizontal\";\n }\n\n get #tracksHorizontal(): boolean {\n return this.axisValue === \"horizontal\" || this.axisValue === \"both\";\n }\n}\n"]}
|
|
@@ -36,10 +36,14 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
36
36
|
* @remarks
|
|
37
37
|
* Behavior only — the look and any transition are the consumer's CSS. The scroll
|
|
38
38
|
* listener is `passive`, coalesced through `requestAnimationFrame`, and removed on
|
|
39
|
-
* `disconnect()` (Turbo navigation included). `offset`
|
|
40
|
-
* runtime
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* `disconnect()` (Turbo navigation included). `offset`, `mode` and `root` follow a
|
|
40
|
+
* runtime change — a Turbo morph, a Stream, an author script — without waiting for
|
|
41
|
+
* the next scroll: the Values a batch changes are applied in one pass after it. A
|
|
42
|
+
* `root` that names another container, or a morph that reaches this element after
|
|
43
|
+
* replacing the container its selector names, moves the listener to the container
|
|
44
|
+
* resolved now; the position found there is a baseline rather than a movement, and
|
|
45
|
+
* the visibility is decided again from it. A selector (`root`, `focusSelector`) or
|
|
46
|
+
* a threshold that cannot be parsed reads as its default, keeping the rest of the
|
|
43
47
|
* element alive. **A scroll never hides the control while it owns focus**: that
|
|
44
48
|
* hide waits for it to blur and is decided again from the scroll position at that
|
|
45
49
|
* moment. `toTop` honors `prefers-reduced-motion` by forcing an
|
|
@@ -72,9 +76,15 @@ declare class ScrollVisibilityController extends Controller<HTMLElement> {
|
|
|
72
76
|
default: string;
|
|
73
77
|
};
|
|
74
78
|
};
|
|
79
|
+
static valueConstraints: {
|
|
80
|
+
offset: {
|
|
81
|
+
readonly finite: true;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
75
84
|
static actions: readonly ["toTop"];
|
|
76
85
|
static events: readonly ["change"];
|
|
77
86
|
readonly elementTarget: HTMLElement;
|
|
87
|
+
readonly elementTargets: HTMLElement[];
|
|
78
88
|
readonly hasElementTarget: boolean;
|
|
79
89
|
offsetValue: number;
|
|
80
90
|
modeValue: string;
|
|
@@ -84,21 +94,27 @@ declare class ScrollVisibilityController extends Controller<HTMLElement> {
|
|
|
84
94
|
disconnect(): void;
|
|
85
95
|
/** Writes the current visibility onto a control that arrives after connect. */
|
|
86
96
|
elementTargetConnected(element: HTMLElement): void;
|
|
87
|
-
/** Drops a held-back hide together with the control it was waiting on. */
|
|
88
|
-
elementTargetDisconnected(): void;
|
|
89
97
|
/**
|
|
90
|
-
*
|
|
98
|
+
* Drops a held-back hide with its control, gives a control that no longer resolves its
|
|
99
|
+
* own `hidden` back, even after `disconnect()`, and writes the visibility onto the one
|
|
100
|
+
* that stays.
|
|
101
|
+
*/
|
|
102
|
+
elementTargetDisconnected(element: HTMLElement): void;
|
|
103
|
+
/**
|
|
104
|
+
* Validates `offset` once, then asks for a pass.
|
|
91
105
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* the comparison path never sees `NaN` — which would answer `false` to every
|
|
106
|
+
* A declaration that is not a finite number reads as the default, so the
|
|
107
|
+
* comparison path never sees `NaN` — which would answer `false` to every
|
|
95
108
|
* comparison and strand the element (in `direction` mode, even the guarantee
|
|
96
109
|
* that the very top always reveals).
|
|
97
110
|
*/
|
|
98
111
|
offsetValueChanged(): void;
|
|
99
|
-
/**
|
|
112
|
+
/** Asks for a pass when application code (or a Turbo morph) changes `mode`. */
|
|
100
113
|
modeValueChanged(): void;
|
|
101
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Validates `root` once, so no resolve parses a selector that throws, then asks
|
|
116
|
+
* for a pass that follows the source it names.
|
|
117
|
+
*/
|
|
102
118
|
rootValueChanged(): void;
|
|
103
119
|
/** Validates `focusSelector` once so `toTop` never parses a selector that throws. */
|
|
104
120
|
focusSelectorValueChanged(): void;
|