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,44 +1,7 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/reading_progress_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/before_cache_reset.ts
|
|
6
|
-
var BeforeCacheReset = class _BeforeCacheReset {
|
|
7
|
-
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
8
|
-
static #subscribers = /* @__PURE__ */ new Set();
|
|
9
|
-
/** The shared listener; installed while at least one instance is subscribed. */
|
|
10
|
-
static #onBeforeCache = () => {
|
|
11
|
-
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
12
|
-
};
|
|
13
|
-
#rewind;
|
|
14
|
-
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
15
|
-
constructor(rewind) {
|
|
16
|
-
this.#rewind = rewind;
|
|
17
|
-
}
|
|
18
|
-
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
19
|
-
activate() {
|
|
20
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
21
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
22
|
-
if (first) {
|
|
23
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
27
|
-
deactivate() {
|
|
28
|
-
_BeforeCacheReset.#subscribers.delete(this);
|
|
29
|
-
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
30
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// src/utils/frame_coalescer.ts
|
|
35
3
|
var FrameCoalescer = class {
|
|
36
4
|
#frame = null;
|
|
37
|
-
/**
|
|
38
|
-
* Runs `run` on the next frame, unless a frame is already pending — the first
|
|
39
|
-
* request of a burst wins and the rest are dropped. The pending frame is
|
|
40
|
-
* released before `run`, so `run` may request the next one.
|
|
41
|
-
*/
|
|
42
5
|
schedule(run) {
|
|
43
6
|
if (this.#frame !== null) return;
|
|
44
7
|
this.#frame = requestAnimationFrame(() => {
|
|
@@ -46,14 +9,6 @@ var FrameCoalescer = class {
|
|
|
46
9
|
run();
|
|
47
10
|
});
|
|
48
11
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Drops the pending frame, and reaches the platform only when there is one.
|
|
51
|
-
*
|
|
52
|
-
* There is no handle value that stands for "nothing pending":
|
|
53
|
-
* `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
|
|
54
|
-
* arrives as a large positive number that the same allocator can hand out, and
|
|
55
|
-
* an idle cancel would then drop a frame belonging to someone else.
|
|
56
|
-
*/
|
|
57
12
|
cancel() {
|
|
58
13
|
if (this.#frame === null) return;
|
|
59
14
|
cancelAnimationFrame(this.#frame);
|
|
@@ -61,18 +16,15 @@ var FrameCoalescer = class {
|
|
|
61
16
|
}
|
|
62
17
|
};
|
|
63
18
|
|
|
64
|
-
// src/utils/layout_observer.ts
|
|
65
19
|
var LayoutObserver = class {
|
|
66
20
|
#callback;
|
|
67
21
|
#resizeObserverFactory;
|
|
68
22
|
#resizeObserver = null;
|
|
69
23
|
#observingViewport = false;
|
|
70
24
|
#loadContainer = null;
|
|
71
|
-
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
72
25
|
#handleViewportResize = () => {
|
|
73
26
|
this.#callback();
|
|
74
27
|
};
|
|
75
|
-
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
76
28
|
#handleDescendantLoad = () => {
|
|
77
29
|
this.#callback();
|
|
78
30
|
};
|
|
@@ -80,11 +32,6 @@ var LayoutObserver = class {
|
|
|
80
32
|
this.#callback = callback;
|
|
81
33
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
82
34
|
}
|
|
83
|
-
/**
|
|
84
|
-
* Starts observing an element's size. Repeated calls observe additional
|
|
85
|
-
* elements through the same shared observer. No-ops when no
|
|
86
|
-
* `ResizeObserver` implementation is available.
|
|
87
|
-
*/
|
|
88
35
|
observe(element) {
|
|
89
36
|
if (!this.#resizeObserverFactory) return;
|
|
90
37
|
if (!this.#resizeObserver) {
|
|
@@ -94,47 +41,28 @@ var LayoutObserver = class {
|
|
|
94
41
|
}
|
|
95
42
|
this.#resizeObserver.observe(element);
|
|
96
43
|
}
|
|
97
|
-
/** Stops observing a single element while leaving any others in place. */
|
|
98
44
|
unobserve(element) {
|
|
99
45
|
this.#resizeObserver?.unobserve(element);
|
|
100
46
|
}
|
|
101
|
-
/** Starts observing viewport resizes. Idempotent: the listener is added once. */
|
|
102
47
|
observeViewport() {
|
|
103
48
|
if (this.#observingViewport) return;
|
|
104
49
|
this.#observingViewport = true;
|
|
105
50
|
window.addEventListener("resize", this.#handleViewportResize);
|
|
106
51
|
}
|
|
107
|
-
/** Stops observing viewport resizes without affecting element observation. */
|
|
108
52
|
unobserveViewport() {
|
|
109
53
|
if (!this.#observingViewport) return;
|
|
110
54
|
this.#observingViewport = false;
|
|
111
55
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
112
56
|
}
|
|
113
|
-
/**
|
|
114
|
-
* Starts reporting a `load` from anywhere inside `container` — an image or a
|
|
115
|
-
* frame settling changes the box it sits in, and it measures as zero high until
|
|
116
|
-
* then. `load` does not bubble, so the subscription is a capture-phase listener
|
|
117
|
-
* on the container itself and nothing the caller spells.
|
|
118
|
-
*
|
|
119
|
-
* **One container at a time.** A further call moves the observation, so a widget
|
|
120
|
-
* whose content element is swapped at runtime releases the element it let go by
|
|
121
|
-
* naming the new one — there is no second place for the release to drift from.
|
|
122
|
-
*/
|
|
123
57
|
observeDescendantLoads(container) {
|
|
124
58
|
this.unobserveDescendantLoads();
|
|
125
59
|
this.#loadContainer = container;
|
|
126
60
|
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
127
61
|
}
|
|
128
|
-
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
129
62
|
unobserveDescendantLoads() {
|
|
130
63
|
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
131
64
|
this.#loadContainer = null;
|
|
132
65
|
}
|
|
133
|
-
/**
|
|
134
|
-
* Releases every observation: disconnects the {@link ResizeObserver} and removes
|
|
135
|
-
* the viewport and descendant-load listeners. Safe to call multiple times. Call
|
|
136
|
-
* this from a controller's `disconnect()`.
|
|
137
|
-
*/
|
|
138
66
|
disconnect() {
|
|
139
67
|
this.#resizeObserver?.disconnect();
|
|
140
68
|
this.#resizeObserver = null;
|
|
@@ -143,81 +71,196 @@ var LayoutObserver = class {
|
|
|
143
71
|
}
|
|
144
72
|
};
|
|
145
73
|
|
|
146
|
-
|
|
147
|
-
|
|
74
|
+
var ListenerSet = class {
|
|
75
|
+
#abort = new AbortController();
|
|
76
|
+
add(target, type, handler, options) {
|
|
77
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
78
|
+
}
|
|
79
|
+
dispose() {
|
|
80
|
+
this.#abort.abort();
|
|
81
|
+
this.#abort = new AbortController();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function setAttributeIfChanged(element, name, value, beforeWrite) {
|
|
86
|
+
if (element.getAttribute(name) === value) return false;
|
|
87
|
+
if (value === null) element.removeAttribute(name);
|
|
88
|
+
else element.setAttribute(name, value);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sharedRegistry(key, create) {
|
|
93
|
+
const symbol = Symbol.for(key);
|
|
94
|
+
const scope = globalThis;
|
|
95
|
+
const existing = scope[symbol];
|
|
96
|
+
if (existing !== void 0) return existing;
|
|
97
|
+
const registry = create();
|
|
98
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
99
|
+
return registry;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
var StylePropertyLease = class _StylePropertyLease {
|
|
103
|
+
static #registry = sharedRegistry(
|
|
104
|
+
"stimeo-ui.style-property-lease.registry.v1",
|
|
105
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
106
|
+
);
|
|
148
107
|
#property;
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
constructor(property) {
|
|
108
|
+
#record;
|
|
109
|
+
#held = /* @__PURE__ */ new Set();
|
|
110
|
+
constructor(property, owner) {
|
|
152
111
|
this.#property = property;
|
|
112
|
+
this.#record = `data-${owner}-style-${property}-lease`;
|
|
113
|
+
const names = _StylePropertyLease.#registry.names;
|
|
114
|
+
names.set(property, (names.get(property) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
153
115
|
}
|
|
154
|
-
/** Writes or removes the leased declaration while preserving its authored value. */
|
|
155
116
|
write(element, value, priority = "") {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
writtenValue: value,
|
|
165
|
-
writtenPriority: value === null ? "" : priority
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
this.#reflect(element, value, priority);
|
|
117
|
+
const stack = this.#stack(element);
|
|
118
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
119
|
+
this.#held.add(element);
|
|
120
|
+
const shows = !this.#covered(element, stack, hold);
|
|
121
|
+
hold.writtenValue = value;
|
|
122
|
+
hold.writtenPriority = value === null ? "" : priority;
|
|
123
|
+
this.#mark(element, stack);
|
|
124
|
+
if (shows) this.#show(element, value, hold.writtenPriority);
|
|
169
125
|
}
|
|
170
|
-
/** Returns one lease without overwriting a later consumer declaration. */
|
|
171
126
|
return(element) {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
this.#
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
127
|
+
const held = this.#held.delete(element);
|
|
128
|
+
const known = _StylePropertyLease.#registry.stacks.get(element)?.has(this.#property);
|
|
129
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
130
|
+
const hold = this.#take(stack);
|
|
131
|
+
if (!hold) return;
|
|
132
|
+
const owns = this.#carries(element, hold) && !this.#covered(element, stack, hold);
|
|
133
|
+
const index = stack.indexOf(hold);
|
|
134
|
+
stack.splice(index, 1);
|
|
135
|
+
const above = stack[index];
|
|
136
|
+
if (above) above.linked &&= hold.linked;
|
|
137
|
+
this.#mark(element, stack);
|
|
138
|
+
if (!owns) return;
|
|
139
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
140
|
+
if (below) this.#show(element, below.writtenValue, below.writtenPriority);
|
|
141
|
+
else this.#show(element, hold.originalValue, hold.originalPriority);
|
|
180
142
|
}
|
|
181
|
-
/** Returns every outstanding declaration lease. */
|
|
182
143
|
returnAll() {
|
|
183
|
-
for (const element of Array.from(this.#
|
|
144
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
145
|
+
}
|
|
146
|
+
#recorded(element, name) {
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(element.getAttribute(name) ?? "null");
|
|
149
|
+
if (!Array.isArray(parsed)) return null;
|
|
150
|
+
const [originalValue, originalPriority, writtenValue, writtenPriority, ...rest] = parsed;
|
|
151
|
+
if (typeof originalValue !== "string" || typeof originalPriority !== "string") return null;
|
|
152
|
+
if (typeof writtenValue !== "string" && writtenValue !== null) return null;
|
|
153
|
+
if (typeof writtenPriority !== "string") return null;
|
|
154
|
+
const linked = rest[0] !== false;
|
|
155
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
156
|
+
if (!beneath.every((under) => typeof under === "string")) return null;
|
|
157
|
+
return { originalValue, originalPriority, writtenValue, writtenPriority, linked, beneath };
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
184
161
|
}
|
|
185
|
-
|
|
186
|
-
|
|
162
|
+
#stack(element) {
|
|
163
|
+
const stacks = _StylePropertyLease.#registry.stacks;
|
|
164
|
+
const byProperty = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
165
|
+
stacks.set(element, byProperty);
|
|
166
|
+
const stack = byProperty.get(this.#property) ?? this.#seed(element);
|
|
167
|
+
byProperty.set(this.#property, stack);
|
|
168
|
+
return stack;
|
|
169
|
+
}
|
|
170
|
+
#seed(element) {
|
|
171
|
+
const found = [];
|
|
172
|
+
for (const record of _StylePropertyLease.#registry.names.get(this.#property) ?? []) {
|
|
173
|
+
const read = this.#recorded(element, record);
|
|
174
|
+
if (read) found.push({ ...read, record });
|
|
175
|
+
}
|
|
176
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
177
|
+
return found.sort((one, other) => depth(one) - depth(other)).map((entry, index) => ({
|
|
178
|
+
lease: null,
|
|
179
|
+
record: entry.record,
|
|
180
|
+
writtenValue: entry.writtenValue,
|
|
181
|
+
writtenPriority: entry.writtenPriority,
|
|
182
|
+
originalValue: entry.originalValue,
|
|
183
|
+
originalPriority: entry.originalPriority,
|
|
184
|
+
linked: entry.linked && index > 0
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
#take(stack) {
|
|
188
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
189
|
+
if (hold) hold.lease = this;
|
|
190
|
+
return hold;
|
|
191
|
+
}
|
|
192
|
+
#acquire(element, stack) {
|
|
193
|
+
const top = stack.at(-1);
|
|
194
|
+
const over = top && this.#carries(element, top) ? top : void 0;
|
|
187
195
|
const style = element.style;
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
const hold = {
|
|
197
|
+
lease: this,
|
|
198
|
+
record: this.#record,
|
|
199
|
+
writtenValue: null,
|
|
200
|
+
writtenPriority: "",
|
|
201
|
+
originalValue: over ? over.originalValue : style.getPropertyValue(this.#property),
|
|
202
|
+
originalPriority: over ? over.originalPriority : style.getPropertyPriority(this.#property),
|
|
203
|
+
linked: over !== void 0
|
|
204
|
+
};
|
|
205
|
+
stack.push(hold);
|
|
206
|
+
return hold;
|
|
207
|
+
}
|
|
208
|
+
#covered(element, stack, hold) {
|
|
209
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => this.#carries(element, above));
|
|
210
|
+
}
|
|
211
|
+
#mark(element, stack) {
|
|
212
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
213
|
+
stack.forEach((hold, index) => {
|
|
214
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
215
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
216
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
217
|
+
const { originalValue, originalPriority, writtenValue, writtenPriority } = hold;
|
|
218
|
+
records.set(
|
|
219
|
+
hold.record,
|
|
220
|
+
(writtenValue ?? "") === originalValue && writtenPriority === originalPriority && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([
|
|
221
|
+
originalValue,
|
|
222
|
+
originalPriority,
|
|
223
|
+
writtenValue,
|
|
224
|
+
writtenPriority,
|
|
225
|
+
...marker,
|
|
226
|
+
...beneath
|
|
227
|
+
])
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
for (const [name, recorded] of records) {
|
|
231
|
+
setAttributeIfChanged(element, name, recorded);
|
|
192
232
|
}
|
|
193
|
-
|
|
194
|
-
|
|
233
|
+
}
|
|
234
|
+
#carries(element, hold) {
|
|
235
|
+
return this.#holds(element, hold.writtenValue ?? "", hold.writtenPriority);
|
|
236
|
+
}
|
|
237
|
+
#holds(element, value, priority) {
|
|
238
|
+
const style = element.style;
|
|
239
|
+
return style.getPropertyValue(this.#property) === value && style.getPropertyPriority(this.#property) === priority;
|
|
240
|
+
}
|
|
241
|
+
#show(element, value, priority) {
|
|
242
|
+
if (this.#holds(element, value ?? "", priority)) return;
|
|
243
|
+
if (value === null) element.style.removeProperty(this.#property);
|
|
244
|
+
else element.style.setProperty(this.#property, value, priority);
|
|
195
245
|
}
|
|
196
246
|
};
|
|
197
247
|
|
|
198
|
-
// src/controllers/reading_progress_controller.ts
|
|
199
248
|
var PROGRESS_PROPERTY = "--stimeo--reading-progress";
|
|
200
249
|
var ReadingProgressController = class extends Controller {
|
|
201
250
|
static events = ["change", "complete"];
|
|
202
|
-
|
|
203
|
-
#lease = new StylePropertyLease(PROGRESS_PROPERTY);
|
|
204
|
-
/** The article's own box and the viewport: either changes the span. */
|
|
251
|
+
#lease = new StylePropertyLease(PROGRESS_PROPERTY, this.identifier);
|
|
205
252
|
#layout = new LayoutObserver(() => this.#onScroll());
|
|
206
|
-
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
207
|
-
/** Coalesces scroll bursts, and the connect baseline, into one frame. */
|
|
208
253
|
#frames = new FrameCoalescer();
|
|
209
|
-
/** Last published progress, so `change`/`complete` fire only on movement. */
|
|
210
254
|
#progress = -1;
|
|
211
|
-
/** False until the connect frame has run: `complete` needs real reading. */
|
|
212
255
|
#baselined = false;
|
|
213
256
|
#onScroll = () => this.#frames.schedule(() => this.#measure());
|
|
257
|
+
#listeners = new ListenerSet();
|
|
214
258
|
connect() {
|
|
215
259
|
this.#progress = -1;
|
|
216
260
|
this.#baselined = false;
|
|
217
|
-
|
|
261
|
+
this.#listeners.add(window, "scroll", this.#onScroll, { passive: true, capture: true });
|
|
218
262
|
this.#layout.observe(this.element);
|
|
219
263
|
this.#layout.observeViewport();
|
|
220
|
-
this.#beforeCache.activate();
|
|
221
264
|
this.#measure();
|
|
222
265
|
this.#frames.schedule(() => {
|
|
223
266
|
this.#measure();
|
|
@@ -225,25 +268,12 @@ var ReadingProgressController = class extends Controller {
|
|
|
225
268
|
});
|
|
226
269
|
}
|
|
227
270
|
disconnect() {
|
|
228
|
-
|
|
271
|
+
this.#listeners.dispose();
|
|
229
272
|
this.#layout.disconnect();
|
|
230
|
-
this.#beforeCache.deactivate();
|
|
231
|
-
this.#frames.cancel();
|
|
232
|
-
this.#lease.returnAll();
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Hands both declarations back before the page is snapshotted, so a restored
|
|
236
|
-
* page starts from the authored DOM rather than from someone else's progress.
|
|
237
|
-
* The baseline goes back with them: a cancelled visit leaves this page on
|
|
238
|
-
* screen, and the next measurement has to publish afresh rather than match a
|
|
239
|
-
* value that has already been handed back.
|
|
240
|
-
*/
|
|
241
|
-
#rewindForCache() {
|
|
242
273
|
this.#frames.cancel();
|
|
243
|
-
this.#lease.
|
|
244
|
-
this.#
|
|
274
|
+
this.#lease.return(this.element);
|
|
275
|
+
this.#lease.return(document.documentElement);
|
|
245
276
|
}
|
|
246
|
-
/** Computes and publishes the progress; emits on movement only. */
|
|
247
277
|
#measure() {
|
|
248
278
|
const rect = this.element.getBoundingClientRect();
|
|
249
279
|
if (rect.width === 0 && rect.height === 0) return;
|
|
@@ -262,5 +292,4 @@ var ReadingProgressController = class extends Controller {
|
|
|
262
292
|
};
|
|
263
293
|
|
|
264
294
|
export { ReadingProgressController };
|
|
265
|
-
//# sourceMappingURL=reading_progress_controller.js.map
|
|
266
295
|
//# sourceMappingURL=reading_progress_controller.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/frame_coalescer.ts","../../src/utils/layout_observer.ts","../../src/utils/style_property_lease.ts","../../src/controllers/reading_progress_controller.ts"],"names":[],"mappings":"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","file":"reading_progress_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\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 #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\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","/**\n * Unified layout observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — have three sources that move the layout under them: their\n * *own* box changing — via {@link ResizeObserver} — the *viewport* changing — via the\n * `window` `resize` event — and a descendant *resource settling*, because an image or\n * a frame reports a height of zero until it has loaded. Wiring those three by hand in\n * every controller risks leaked listeners on `disconnect()`. {@link LayoutObserver}\n * owns all three behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * this.#layout.observeDescendantLoads(this.panelTarget);\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n #loadContainer: Element | null = null;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */\n readonly #handleDescendantLoad = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Starts reporting a `load` from anywhere inside `container` — an image or a\n * frame settling changes the box it sits in, and it measures as zero high until\n * then. `load` does not bubble, so the subscription is a capture-phase listener\n * on the container itself and nothing the caller spells.\n *\n * **One container at a time.** A further call moves the observation, so a widget\n * whose content element is swapped at runtime releases the element it let go by\n * naming the new one — there is no second place for the release to drift from.\n */\n observeDescendantLoads(container: Element): void {\n this.unobserveDescendantLoads();\n this.#loadContainer = container;\n container.addEventListener(\"load\", this.#handleDescendantLoad, true);\n }\n\n /** Stops reporting descendant loads without affecting element or viewport observation. */\n unobserveDescendantLoads(): void {\n this.#loadContainer?.removeEventListener(\"load\", this.#handleDescendantLoad, true);\n this.#loadContainer = null;\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and removes\n * the viewport and descendant-load listeners. Safe to call multiple times. Call\n * this from a controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n this.unobserveDescendantLoads();\n }\n}\n","/** One temporarily controlled style property and the authored declaration it displaced. */\ninterface StylePropertyLeaseRecord {\n readonly originalValue: string;\n readonly originalPriority: string;\n writtenValue: string | null;\n writtenPriority: string;\n}\n\n/**\n * Temporarily controls one inline CSS property across a changing set of elements.\n *\n * Authored value and priority are restored only while the declaration still matches\n * the last leased write. A later consumer write therefore wins.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events:\n * a consumer returns its leases from its own `turbo:before-cache` rewind.\n */\nexport class StylePropertyLease<T extends HTMLElement = HTMLElement> {\n readonly #property: string;\n readonly #records = new Map<T, StylePropertyLeaseRecord>();\n\n /** @param property - The CSS property whose temporary values this lease owns. */\n constructor(property: string) {\n this.#property = property;\n }\n\n /** Writes or removes the leased declaration while preserving its authored value. */\n write(element: T, value: string | null, priority = \"\"): void {\n const existing = this.#records.get(element);\n if (existing) {\n existing.writtenValue = value;\n existing.writtenPriority = value === null ? \"\" : priority;\n } else {\n this.#records.set(element, {\n originalValue: element.style.getPropertyValue(this.#property),\n originalPriority: element.style.getPropertyPriority(this.#property),\n writtenValue: value,\n writtenPriority: value === null ? \"\" : priority,\n });\n }\n\n this.#reflect(element, value, priority);\n }\n\n /** Returns one lease without overwriting a later consumer declaration. */\n return(element: T): void {\n const record = this.#records.get(element);\n if (!record) return;\n this.#records.delete(element);\n\n const style = element.style;\n const stillOwned =\n style.getPropertyValue(this.#property) === (record.writtenValue ?? \"\") &&\n style.getPropertyPriority(this.#property) === record.writtenPriority;\n if (stillOwned) {\n this.#reflect(element, record.originalValue, record.originalPriority);\n }\n }\n\n /** Returns every outstanding declaration lease. */\n returnAll(): void {\n for (const element of Array.from(this.#records.keys())) this.return(element);\n }\n\n /** Reflects only a real declaration transition. */\n #reflect(element: T, value: string | null, priority: string): void {\n const style = element.style;\n const nextValue = value ?? \"\";\n const nextPriority = value === null ? \"\" : priority;\n if (\n style.getPropertyValue(this.#property) === nextValue &&\n style.getPropertyPriority(this.#property) === nextPriority\n ) {\n return;\n }\n if (value === null) style.removeProperty(this.#property);\n else style.setProperty(this.#property, value, priority);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { FrameCoalescer } from \"../utils/frame_coalescer\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\nimport { StylePropertyLease } from \"../utils/style_property_lease\";\n\n/** Name of the CSS custom property exposing the reading progress (0..1). */\nconst PROGRESS_PROPERTY = \"--stimeo--reading-progress\";\n\n/**\n * Headless **reading progress**: how far the reader has scrolled *through* this\n * element (an article), published as a CSS custom property and a `change`\n * event stream — the classic top-of-page progress bar.\n * `IntersectionObserver` alone cannot express this (the ratio is constant\n * while a tall article scrolls through the viewport), so this controller owns\n * the scroll math; compose with `stimeo--intersection` when you also need\n * enter/exit triggers. Core (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--reading-progress`):\n * <article data-controller=\"stimeo--reading-progress\">…</article>\n * <div class=\"progress-bar\" aria-hidden=\"true\"></div>\n * <!-- .progress-bar { width: calc(var(--stimeo--reading-progress, 0) * 100%); } -->\n *\n * Progress is `0` before the article's top reaches the viewport top and `1`\n * once its bottom fits the viewport: `-top / (height - viewportHeight)`,\n * clamped. An article no taller than the viewport has no such span, so reading\n * it is binary: `1` from the moment its top reaches the viewport top. An\n * article with no layout box at all — a `display: none` ancestor, a collapsed\n * `<details>`, an inactive tab panel — is not measured: an empty rect sits at\n * the document origin and carries no reading position, so publishing anything\n * for it would report a place the reader never reached.\n *\n * The property is written on the controller element **and** on\n * `document.documentElement`, so a fixed bar anywhere in the page can consume\n * it without being a descendant. Both are written through a lease, so an\n * authored declaration comes back on teardown and a later writer is left alone.\n * `complete` fires on *reaching* 1, and never during the baseline: the frame in\n * which the controller connects belongs to establishing where the reader\n * already is (a restored scroll position lands there), not to reading.\n *\n * `change` dispatches `{ progress }`.\n *\n * @remarks\n * Behavior only — the bar itself (and hiding it, e.g. before any scroll) is\n * the consumer's CSS; the progress value carries no ARIA (a decorative\n * indicator — mark the bar `aria-hidden`; a *semantic* progress belongs to\n * `stimeo--progress`). The article's own box is watched as well as the\n * viewport, so content that settles late (images, fonts) re-measures instead of\n * leaving a stale span. Scroll and layout work is rAF-throttled; the listeners,\n * the observers, any pending frame and both leased declarations are released on\n * `disconnect()` and returned again for a `turbo:before-cache` snapshot.\n */\nexport class ReadingProgressController extends Controller<HTMLElement> {\n static events = [\"change\", \"complete\"] as const;\n\n /** Owns both faces of the published property so teardown can hand them back. */\n readonly #lease = new StylePropertyLease(PROGRESS_PROPERTY);\n /** The article's own box and the viewport: either changes the span. */\n readonly #layout = new LayoutObserver(() => this.#onScroll());\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n /** Coalesces scroll bursts, and the connect baseline, into one frame. */\n readonly #frames = new FrameCoalescer();\n /** Last published progress, so `change`/`complete` fire only on movement. */\n #progress = -1;\n /** False until the connect frame has run: `complete` needs real reading. */\n #baselined = false;\n\n readonly #onScroll = (): void => this.#frames.schedule(() => this.#measure());\n\n override connect(): void {\n this.#progress = -1;\n this.#baselined = false;\n // Capture phase: element scrolls do not bubble, but they ARE observable at\n // the window in capture — so an article inside an overflow container still\n // drives the progress.\n window.addEventListener(\"scroll\", this.#onScroll, { passive: true, capture: true });\n this.#layout.observe(this.element);\n this.#layout.observeViewport();\n this.#beforeCache.activate();\n this.#measure();\n // The page's scroll position is restored *after* the controller connects,\n // so that jump arrives as a move the reader never made. Everything up to\n // the end of this frame is still the baseline — a restored scroll coalesces\n // into the frame below, and no reader can cross an article inside one.\n this.#frames.schedule(() => {\n this.#measure();\n this.#baselined = true;\n });\n }\n\n override disconnect(): void {\n window.removeEventListener(\"scroll\", this.#onScroll, { capture: true });\n this.#layout.disconnect();\n this.#beforeCache.deactivate();\n this.#frames.cancel();\n this.#lease.returnAll();\n }\n\n /**\n * Hands both declarations back before the page is snapshotted, so a restored\n * page starts from the authored DOM rather than from someone else's progress.\n * The baseline goes back with them: a cancelled visit leaves this page on\n * screen, and the next measurement has to publish afresh rather than match a\n * value that has already been handed back.\n */\n #rewindForCache(): void {\n this.#frames.cancel();\n this.#lease.returnAll();\n this.#progress = -1;\n }\n\n /** Computes and publishes the progress; emits on movement only. */\n #measure(): void {\n const rect = this.element.getBoundingClientRect();\n // No layout box (a `display: none` ancestor, a collapsed `<details>`): the\n // empty rect sits at the document origin, where the binary branch below\n // would read its `top` of 0 as \"the reader reached it\". There is no reading\n // position to publish, so the last one stands until the article is laid out\n // again — which the box observer reports.\n if (rect.width === 0 && rect.height === 0) return;\n\n const span = rect.height - window.innerHeight;\n // Shorter than the viewport: reading it is binary (reached or not).\n const raw = span > 0 ? -rect.top / span : rect.top <= 0 ? 1 : 0;\n const progress = Math.min(1, Math.max(0, raw));\n if (progress === this.#progress) return;\n\n const previous = this.#progress;\n this.#progress = progress;\n const value = String(progress);\n this.#lease.write(this.element, value);\n this.#lease.write(document.documentElement, value);\n this.dispatch(\"change\", { detail: { progress } });\n if (progress === 1 && previous !== -1 && this.#baselined) this.dispatch(\"complete\");\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/frame_coalescer.ts","../../src/utils/layout_observer.ts","../../src/utils/listener_set.ts","../../src/utils/attribute_write.ts","../../src/utils/shared_registry.ts","../../src/utils/style_property_lease.ts","../../src/controllers/reading_progress_controller.ts"],"names":[],"mappings":"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","file":"reading_progress_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","/**\n * Unified layout observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — have three sources that move the layout under them: their\n * *own* box changing — via {@link ResizeObserver} — the *viewport* changing — via the\n * `window` `resize` event — and a descendant *resource settling*, because an image or\n * a frame reports a height of zero until it has loaded. Wiring those three by hand in\n * every controller risks leaked listeners on `disconnect()`. {@link LayoutObserver}\n * owns all three behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * this.#layout.observeDescendantLoads(this.panelTarget);\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n #loadContainer: Element | null = null;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */\n readonly #handleDescendantLoad = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Starts reporting a `load` from anywhere inside `container` — an image or a\n * frame settling changes the box it sits in, and it measures as zero high until\n * then. `load` does not bubble, so the subscription is a capture-phase listener\n * on the container itself and nothing the caller spells.\n *\n * **One container at a time.** A further call moves the observation, so a widget\n * whose content element is swapped at runtime releases the element it let go by\n * naming the new one — there is no second place for the release to drift from.\n */\n observeDescendantLoads(container: Element): void {\n this.unobserveDescendantLoads();\n this.#loadContainer = container;\n container.addEventListener(\"load\", this.#handleDescendantLoad, true);\n }\n\n /** Stops reporting descendant loads without affecting element or viewport observation. */\n unobserveDescendantLoads(): void {\n this.#loadContainer?.removeEventListener(\"load\", this.#handleDescendantLoad, true);\n this.#loadContainer = null;\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and removes\n * the viewport and descendant-load listeners. Safe to call multiple times. Call\n * this from a controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n this.unobserveDescendantLoads();\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","/**\n * Reflects a serialized attribute value only when the current value differs.\n *\n * A same-value `setAttribute` still queues a mutation record for every observer, so an\n * unchanged value writes nothing.\n *\n * @param element - The element whose attribute is reflected.\n * @param name - The attribute name, using the non-namespaced DOM API.\n * @param value - The serialized value, or null to remove the attribute.\n * @param beforeWrite - Called only for a change, before the DOM mutation, so the\n * caller can record ownership or identify its own observer records.\n * @returns Whether an attribute was written or removed.\n */\nexport function setAttributeIfChanged(\n element: Element,\n name: string,\n value: string | null,\n beforeWrite?: () => void,\n): boolean {\n if (element.getAttribute(name) === value) return false;\n beforeWrite?.();\n if (value === null) element.removeAttribute(name);\n else element.setAttribute(name, value);\n return true;\n}\n","/**\n * One registry per page for every copy of the library.\n *\n * Each `stimeo-ui/controllers/*` file, and each opt-in entry, is built on its own and carries\n * its own copy of every utility it uses, so a page that imports controllers one by one, or one\n * of them next to the barrel, evaluates a utility several times. A registry that coordinates\n * instances of different controllers — the stacks of leased values, the loans of a\n * `tabindex`, the Escape layers, the focus traps — must not be split by that: a dialog inside\n * a drawer, or a character counter and a form field on one input, coordinate whichever files\n * they came from. So such a registry lives once on `globalThis`, under a `Symbol.for` key the\n * copies agree on, and the first copy to ask for it creates it.\n *\n * The key names the registry and the version of its shape\n * (`stimeo-ui.<utility>.registry.v<n>`). Copies whose registries have the same shape share\n * one; a release that changes the shape changes the version, so a copy of another release\n * keeps a registry of its own instead of reading entries it does not understand — the two\n * then coordinate within each release only. An entry another copy put in a registry is read\n * through its fields alone, never through `#private` members, which belong to the class of\n * the copy that created it.\n */\n\n/**\n * The registry stored under `key`, created by `create` on first use.\n *\n * @param key - `stimeo-ui.<utility>.registry.v<n>`: the version is that of the shape `create`\n * returns.\n * @param create - Builds the empty registry.\n */\nexport function sharedRegistry<T extends object>(key: string, create: () => T): T {\n const symbol = Symbol.for(key);\n const scope = globalThis as unknown as Record<symbol, T | undefined>;\n const existing = scope[symbol];\n if (existing !== undefined) return existing;\n const registry = create();\n Object.defineProperty(globalThis, symbol, { value: registry });\n return registry;\n}\n","import { setAttributeIfChanged } from \"./attribute_write\";\nimport { sharedRegistry } from \"./shared_registry\";\n\n/** What a lease's record says: the authored declaration, the last write and the leases beneath. */\ninterface StylePropertyLeaseRecord {\n readonly originalValue: string;\n readonly originalPriority: string;\n readonly writtenValue: string | null;\n readonly writtenPriority: string;\n /** Whether it hands back what the lease right beneath it wrote last. */\n readonly linked: boolean;\n /** The records of the other owners' leases beneath it, nearest first. */\n readonly beneath: readonly string[];\n}\n\n/** One lease's hold on one element's property, in the stack every lease of it shares. */\ninterface StyleHold {\n /** The lease holding it; `null` for a hold read from a copy's record until its lease takes it. */\n lease: object | null;\n readonly record: string;\n writtenValue: string | null;\n writtenPriority: string;\n /** What it hands back with nothing beneath it: the author's declaration. */\n readonly originalValue: string;\n readonly originalPriority: string;\n /** Whether it hands back what the hold beneath it wrote last instead. */\n linked: boolean;\n}\n\n/** The holds on each element by property, and every lease's record name by property. */\ninterface StylePropertyLeaseRegistry {\n readonly stacks: WeakMap<HTMLElement, Map<string, StyleHold[]>>;\n readonly names: Map<string, Set<string>>;\n}\n\n/**\n * Temporarily controls one inline CSS property across a changing set of elements.\n *\n * Authored value and priority are restored only while the declaration still matches\n * the last leased write. A later consumer write therefore wins.\n *\n * **Every lease of a property shares one stack of holds per element**, whatever its owner\n * and whichever file of the package constructed it (the stacks are one per page): the holds\n * sit in the order their leases first wrote the element, each with the declaration it wrote\n * last. The element's declaration is the highest hold's that carries it, and the page's when\n * none does. A write reaches the element unless a hold above the writer carries the element's\n * declaration, so the top hold always shows its writes and the page's declaration gives way to\n * the next write of any hold. A return gives a declaration back only from the hold the\n * element's declaration is: the one the hold beneath it wrote last, or the author's once\n * nothing is beneath it; any other return only leaves the stack. Holds that wrote the same\n * declaration are told apart by the stack: the higher one keeps it. A first write over a top\n * hold whose declaration the page has replaced takes the element's declaration as the\n * author's. Two instances of one controller that write the same property on\n * `document.documentElement` stack the same way.\n *\n * **The stack travels with the element.** While a hold carries a declaration other than the\n * author's, lies on another hold or is covered by one, the element carries its owner's record\n * `data-<owner>-style-<property>-lease`: JSON of the author's value and priority, the value\n * and priority the hold wrote last (`null` for a removal), `false` where the hold hands back\n * the author's declaration rather than the latest one of the hold beneath it, and the records\n * of every hold beneath it, nearest first. Returning the last lease of an owner removes its\n * record. A copy of the element — a page Turbo restores from its cache — therefore carries the\n * stack, read back under the record names of the leases constructed so far and ordered by the\n * records each hold lists beneath it, and each lease takes the hold its owner's record\n * describes over in place. Complete records for distinct owners preserve their order across\n * a copy. Records describe owners, not instances: one owner holding the element more than\n * once leaves only its topmost hold's record, which the first of its leases to write or return\n * on the copy takes. Its lower holds and positions are absent; mutually listed owners cannot\n * express their full live order. A hold that showed the author's declaration alone carries\n * no record and is taken by its first write on the copy. Missing or unreadable records also\n * leave their holds unknown, so copying the DOM does not promise identical decisions for\n * every live stack. A new stack with no readable record takes the current declaration as the\n * author's; a morph can remove records while existing holds remain in memory until returned.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events.\n */\nexport class StylePropertyLease<T extends HTMLElement = HTMLElement> {\n /** The holds on each element, bottom first, and the record names; one per page. */\n static readonly #registry = sharedRegistry(\n \"stimeo-ui.style-property-lease.registry.v1\",\n (): StylePropertyLeaseRegistry => ({ stacks: new WeakMap(), names: new Map() }),\n );\n readonly #property: string;\n readonly #record: string;\n readonly #held = new Set<T>();\n\n /**\n * @param property - The CSS property whose temporary values this lease owns.\n * @param owner - Names the record on the element; the controller's identifier.\n */\n constructor(property: string, owner: string) {\n this.#property = property;\n this.#record = `data-${owner}-style-${property}-lease`;\n const names = StylePropertyLease.#registry.names;\n names.set(property, (names.get(property) ?? new Set<string>()).add(this.#record));\n }\n\n /** Writes or removes the leased declaration while preserving its authored value. */\n write(element: T, value: string | null, priority = \"\"): void {\n const stack = this.#stack(element);\n const hold = this.#take(stack) ?? this.#acquire(element, stack);\n this.#held.add(element);\n const shows = !this.#covered(element, stack, hold);\n hold.writtenValue = value;\n hold.writtenPriority = value === null ? \"\" : priority;\n this.#mark(element, stack);\n if (shows) this.#show(element, value, hold.writtenPriority);\n }\n\n /**\n * Returns one lease, also on a restored copy, to the declaration the hold beneath it wrote\n * last or the author's, without overwriting a declaration a consumer wrote since.\n */\n return(element: T): void {\n const held = this.#held.delete(element);\n const known = StylePropertyLease.#registry.stacks.get(element)?.has(this.#property);\n const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];\n const hold = this.#take(stack);\n if (!hold) return;\n const owns = this.#carries(element, hold) && !this.#covered(element, stack, hold);\n const index = stack.indexOf(hold);\n stack.splice(index, 1);\n const above = stack[index];\n if (above) above.linked &&= hold.linked;\n this.#mark(element, stack);\n if (!owns) return;\n const below = hold.linked ? stack[index - 1] : undefined;\n if (below) this.#show(element, below.writtenValue, below.writtenPriority);\n else this.#show(element, hold.originalValue, hold.originalPriority);\n }\n\n /** Returns every outstanding declaration lease. */\n returnAll(): void {\n for (const element of Array.from(this.#held)) this.return(element);\n }\n\n /** The record `name` on `element`, or `null` when it is none a lease could write. */\n #recorded(element: T, name: string): StylePropertyLeaseRecord | null {\n try {\n const parsed: unknown = JSON.parse(element.getAttribute(name) ?? \"null\");\n if (!Array.isArray(parsed)) return null;\n const [originalValue, originalPriority, writtenValue, writtenPriority, ...rest] =\n parsed as unknown[];\n if (typeof originalValue !== \"string\" || typeof originalPriority !== \"string\") return null;\n if (typeof writtenValue !== \"string\" && writtenValue !== null) return null;\n if (typeof writtenPriority !== \"string\") return null;\n const linked = rest[0] !== false;\n const beneath = linked ? rest : rest.slice(1);\n if (!beneath.every((under): under is string => typeof under === \"string\")) return null;\n return { originalValue, originalPriority, writtenValue, writtenPriority, linked, beneath };\n } catch {\n return null;\n }\n }\n\n /** The element's stack, read from its records the first time. */\n #stack(element: T): StyleHold[] {\n const stacks = StylePropertyLease.#registry.stacks;\n const byProperty = stacks.get(element) ?? new Map<string, StyleHold[]>();\n stacks.set(element, byProperty);\n const stack = byProperty.get(this.#property) ?? this.#seed(element);\n byProperty.set(this.#property, stack);\n return stack;\n }\n\n /** The holds a copy's records describe, each above every hold its record lists beneath. */\n #seed(element: T): StyleHold[] {\n const found: (StylePropertyLeaseRecord & { readonly record: string })[] = [];\n for (const record of StylePropertyLease.#registry.names.get(this.#property) ?? []) {\n const read = this.#recorded(element, record);\n if (read) found.push({ ...read, record });\n }\n const depth = ({ beneath }: (typeof found)[number]) =>\n beneath.filter((name) => found.some((other) => other.record === name)).length;\n return found\n .sort((one, other) => depth(one) - depth(other))\n .map((entry, index) => ({\n lease: null,\n record: entry.record,\n writtenValue: entry.writtenValue,\n writtenPriority: entry.writtenPriority,\n originalValue: entry.originalValue,\n originalPriority: entry.originalPriority,\n linked: entry.linked && index > 0,\n }));\n }\n\n /** Its hold in `stack`, taking over one read from its record. */\n #take(stack: StyleHold[]): StyleHold | undefined {\n const hold =\n stack.find((held) => held.lease === this) ??\n stack.find((held) => held.lease === null && held.record === this.#record);\n if (hold) hold.lease = this;\n return hold;\n }\n\n /** A new hold on top, over the top hold while the element carries its declaration. */\n #acquire(element: T, stack: StyleHold[]): StyleHold {\n const top = stack.at(-1);\n const over = top && this.#carries(element, top) ? top : undefined;\n const style = element.style;\n const hold: StyleHold = {\n lease: this,\n record: this.#record,\n writtenValue: null,\n writtenPriority: \"\",\n originalValue: over ? over.originalValue : style.getPropertyValue(this.#property),\n originalPriority: over ? over.originalPriority : style.getPropertyPriority(this.#property),\n linked: over !== undefined,\n };\n stack.push(hold);\n return hold;\n }\n\n /** Whether a hold above `hold` carries the element's declaration, then that hold's. */\n #covered(element: T, stack: readonly StyleHold[], hold: StyleHold): boolean {\n return stack.slice(stack.indexOf(hold) + 1).some((above) => this.#carries(element, above));\n }\n\n /** Leaves each owner's record of its topmost hold, or none where nothing needs one. */\n #mark(element: T, stack: readonly StyleHold[]): void {\n const records = new Map<string, string | null>([[this.#record, null]]);\n stack.forEach((hold, index) => {\n const names = stack.slice(0, index).map((under) => under.record);\n const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);\n const marker = hold.linked || beneath.length === 0 ? [] : [false];\n const { originalValue, originalPriority, writtenValue, writtenPriority } = hold;\n records.set(\n hold.record,\n (writtenValue ?? \"\") === originalValue &&\n writtenPriority === originalPriority &&\n beneath.length === 0 &&\n index === stack.length - 1\n ? null\n : JSON.stringify([\n originalValue,\n originalPriority,\n writtenValue,\n writtenPriority,\n ...marker,\n ...beneath,\n ]),\n );\n });\n for (const [name, recorded] of records) {\n setAttributeIfChanged(element, name, recorded);\n }\n }\n\n /** Whether the element carries the declaration `hold` wrote last. */\n #carries(element: T, hold: StyleHold): boolean {\n return this.#holds(element, hold.writtenValue ?? \"\", hold.writtenPriority);\n }\n\n /** Whether the inline declaration is `value` with `priority`. */\n #holds(element: T, value: string, priority: string): boolean {\n const style = element.style;\n return (\n style.getPropertyValue(this.#property) === value &&\n style.getPropertyPriority(this.#property) === priority\n );\n }\n\n /** Writes the declaration, or removes it for `null`, unless the element already carries it. */\n #show(element: T, value: string | null, priority: string): void {\n if (this.#holds(element, value ?? \"\", priority)) return;\n if (value === null) element.style.removeProperty(this.#property);\n else element.style.setProperty(this.#property, value, priority);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FrameCoalescer } from \"../utils/frame_coalescer\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\nimport { ListenerSet } from \"../utils/listener_set\";\nimport { StylePropertyLease } from \"../utils/style_property_lease\";\n\n/** Name of the CSS custom property exposing the reading progress (0..1). */\nconst PROGRESS_PROPERTY = \"--stimeo--reading-progress\";\n\n/**\n * Headless **reading progress**: how far the reader has scrolled *through* this\n * element (an article), published as a CSS custom property and a `change`\n * event stream — the classic top-of-page progress bar.\n * `IntersectionObserver` alone cannot express this (the ratio is constant\n * while a tall article scrolls through the viewport), so this controller owns\n * the scroll math; compose with `stimeo--intersection` when you also need\n * enter/exit triggers. Core (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--reading-progress`):\n * <article data-controller=\"stimeo--reading-progress\">…</article>\n * <div class=\"progress-bar\" aria-hidden=\"true\"></div>\n * <!-- .progress-bar { width: calc(var(--stimeo--reading-progress, 0) * 100%); } -->\n *\n * Progress is `0` before the article's top reaches the viewport top and `1`\n * once its bottom fits the viewport: `-top / (height - viewportHeight)`,\n * clamped. An article no taller than the viewport has no such span, so reading\n * it is binary: `1` from the moment its top reaches the viewport top. An\n * article with no layout box at all — a `display: none` ancestor, a collapsed\n * `<details>`, an inactive tab panel — is not measured: an empty rect sits at\n * the document origin and carries no reading position, so publishing anything\n * for it would report a place the reader never reached.\n *\n * The property is written on the controller element **and** on\n * `document.documentElement`, so a fixed bar anywhere in the page can consume\n * it without being a descendant. Both are written through a lease, so an\n * authored declaration comes back on teardown and a later writer is left alone.\n * `complete` fires on *reaching* 1, and never during the baseline: the frame in\n * which the controller connects belongs to establishing where the reader\n * already is (a restored scroll position lands there), not to reading.\n *\n * `change` dispatches `{ progress }`.\n *\n * @remarks\n * Behavior only — the bar itself (and hiding it, e.g. before any scroll) is\n * the consumer's CSS; the progress value carries no ARIA (a decorative\n * indicator — mark the bar `aria-hidden`; a *semantic* progress belongs to\n * `stimeo--progress`). The article's own box is watched as well as the\n * viewport, so content that settles late (images, fonts) re-measures instead of\n * leaving a stale span. Scroll and layout work is rAF-throttled; the listeners,\n * the observers, any pending frame and both leased declarations are released on\n * `disconnect()`.\n */\nexport class ReadingProgressController extends Controller<HTMLElement> {\n static events = [\"change\", \"complete\"] as const;\n\n /** Owns both faces of the published property so teardown can hand them back. */\n readonly #lease = new StylePropertyLease(PROGRESS_PROPERTY, this.identifier);\n /** The article's own box and the viewport: either changes the span. */\n readonly #layout = new LayoutObserver(() => this.#onScroll());\n /** Coalesces scroll bursts, and the connect baseline, into one frame. */\n readonly #frames = new FrameCoalescer();\n /** Last published progress, so `change`/`complete` fire only on movement. */\n #progress = -1;\n /** False until the connect frame has run: `complete` needs real reading. */\n #baselined = false;\n\n readonly #onScroll = (): void => this.#frames.schedule(() => this.#measure());\n\n /** The listeners of one connection, released together on `disconnect()`. */\n readonly #listeners = new ListenerSet();\n\n override connect(): void {\n this.#progress = -1;\n this.#baselined = false;\n // Capture phase: element scrolls do not bubble, but they ARE observable at\n // the window in capture — so an article inside an overflow container still\n // drives the progress.\n this.#listeners.add(window, \"scroll\", this.#onScroll, { passive: true, capture: true });\n this.#layout.observe(this.element);\n this.#layout.observeViewport();\n this.#measure();\n // The page's scroll position is restored *after* the controller connects,\n // so that jump arrives as a move the reader never made. Everything up to\n // the end of this frame is still the baseline — a restored scroll coalesces\n // into the frame below, and no reader can cross an article inside one.\n this.#frames.schedule(() => {\n this.#measure();\n this.#baselined = true;\n });\n }\n\n override disconnect(): void {\n this.#listeners.dispose();\n this.#layout.disconnect();\n this.#frames.cancel();\n this.#lease.return(this.element);\n this.#lease.return(document.documentElement);\n }\n\n /** Computes and publishes the progress; emits on movement only. */\n #measure(): void {\n const rect = this.element.getBoundingClientRect();\n // No layout box (a `display: none` ancestor, a collapsed `<details>`): the\n // empty rect sits at the document origin, where the binary branch below\n // would read its `top` of 0 as \"the reader reached it\". There is no reading\n // position to publish, so the last one stands until the article is laid out\n // again — which the box observer reports.\n if (rect.width === 0 && rect.height === 0) return;\n\n const span = rect.height - window.innerHeight;\n // Shorter than the viewport: reading it is binary (reached or not).\n const raw = span > 0 ? -rect.top / span : rect.top <= 0 ? 1 : 0;\n const progress = Math.min(1, Math.max(0, raw));\n if (progress === this.#progress) return;\n\n const previous = this.#progress;\n this.#progress = progress;\n const value = String(progress);\n this.#lease.write(this.element, value);\n this.#lease.write(document.documentElement, value);\n this.dispatch(\"change\", { detail: { progress } });\n if (progress === 1 && previous !== -1 && this.#baselined) this.dispatch(\"complete\");\n }\n}\n"]}
|
|
@@ -22,12 +22,15 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
22
22
|
* timer is owned by `SafeTimeout` and torn down on `disconnect()` (Turbo
|
|
23
23
|
* navigation included).
|
|
24
24
|
*
|
|
25
|
-
* Render inputs are followed at runtime: a morph that swaps
|
|
26
|
-
* or `tickInterval` on the live element repaints through one
|
|
27
|
-
* (`
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
25
|
+
* Render inputs are followed at runtime: changing `datetime` or a morph that swaps
|
|
26
|
+
* `locale`, `threshold`, or `tickInterval` on the live element repaints through one
|
|
27
|
+
* coalesced pass (`MorphRenderWatcher`) rather than leaving the reading frozen. While
|
|
28
|
+
* the element shows the relative form it records the authored absolute text on itself as
|
|
29
|
+
* `data-<identifier>-text` (a JSON string), so a copy of it — a page Turbo restores from
|
|
30
|
+
* its cache — keeps the threshold fallback: the connection that adopts the copy takes
|
|
31
|
+
* the authored text from the record, never the relative form on display. Nothing is
|
|
32
|
+
* written back on `turbo:before-cache`, which Turbo also dispatches on pages that stay,
|
|
33
|
+
* where the reading must stay current.
|
|
31
34
|
*/
|
|
32
35
|
declare class RelativeTimeController extends Controller<HTMLElement> {
|
|
33
36
|
#private;
|
|
@@ -45,6 +48,17 @@ declare class RelativeTimeController extends Controller<HTMLElement> {
|
|
|
45
48
|
default: number;
|
|
46
49
|
};
|
|
47
50
|
};
|
|
51
|
+
static valueConstraints: {
|
|
52
|
+
threshold: {
|
|
53
|
+
readonly finite: true;
|
|
54
|
+
readonly min: 0;
|
|
55
|
+
};
|
|
56
|
+
tickInterval: {
|
|
57
|
+
readonly finite: true;
|
|
58
|
+
readonly min: 0;
|
|
59
|
+
readonly max: 2147483647;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
48
62
|
localeValue: string;
|
|
49
63
|
thresholdValue: number;
|
|
50
64
|
tickIntervalValue: number;
|