stimeo-ui 0.14.0 → 0.16.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +212 -0
- data/README.md +120 -0
- data/dist/cable/index.js +123 -29
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +96 -62
- data/dist/controllers/auto_submit_controller.js +83 -7
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +38 -11
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +340 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +81 -12
- data/dist/controllers/clipboard_controller.js +63 -5
- data/dist/controllers/collapsible_controller.js +99 -14
- data/dist/controllers/color_picker_controller.js +80 -34
- data/dist/controllers/combobox_controller.js +106 -16
- data/dist/controllers/command_palette_controller.js +35 -3
- data/dist/controllers/conditional_fields_controller.js +85 -17
- data/dist/controllers/confirm_controller.js +3 -0
- data/dist/controllers/context_menu_controller.js +32 -12
- data/dist/controllers/countdown_controller.js +129 -26
- data/dist/controllers/currency_input_controller.js +221 -67
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +151 -30
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +46 -13
- data/dist/controllers/dismissible_controller.js +1 -0
- data/dist/controllers/drawer_controller.js +54 -19
- data/dist/controllers/dropdown_controller.js +36 -9
- data/dist/controllers/editable_controller.js +34 -0
- data/dist/controllers/file_dropzone_controller.js +144 -51
- data/dist/controllers/filter_controller.js +20 -6
- data/dist/controllers/flash_controller.js +432 -71
- data/dist/controllers/focus_controller.js +1 -0
- data/dist/controllers/form_field_controller.js +7 -5
- data/dist/controllers/form_validation_controller.js +19 -13
- data/dist/controllers/frame_loading_controller.js +45 -8
- data/dist/controllers/highlight_controller.js +82 -25
- data/dist/controllers/hover_card_controller.js +40 -14
- data/dist/controllers/idle_controller.js +90 -5
- data/dist/controllers/input_mask_controller.js +65 -9
- data/dist/controllers/intersection_controller.js +3 -0
- data/dist/controllers/lazy_frame_controller.js +11 -2
- data/dist/controllers/listbox_controller.js +203 -45
- data/dist/controllers/local_time_controller.js +10 -5
- data/dist/controllers/masonry_controller.js +31 -15
- data/dist/controllers/menu_controller.js +45 -16
- data/dist/controllers/menubar_controller.js +58 -24
- data/dist/controllers/meter_controller.js +9 -5
- data/dist/controllers/multi_select_controller.js +278 -104
- data/dist/controllers/navigation_menu_controller.js +48 -15
- data/dist/controllers/nested_form_controller.js +37 -8
- data/dist/controllers/network_status_controller.js +9 -1
- data/dist/controllers/number_input_controller.js +124 -21
- data/dist/controllers/optimistic_controller.js +42 -5
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +115 -21
- data/dist/controllers/overflow_menu_controller.js +141 -44
- data/dist/controllers/pagination_controller.js +74 -28
- data/dist/controllers/password_reveal_controller.js +59 -2
- data/dist/controllers/persist_controller.js +30 -8
- data/dist/controllers/pointer_drag_controller.js +131 -52
- data/dist/controllers/popover_controller.js +45 -11
- data/dist/controllers/portal_controller.js +6 -2
- data/dist/controllers/preview_guard_controller.js +16 -1
- data/dist/controllers/progress_controller.js +8 -4
- data/dist/controllers/radio_group_controller.js +42 -17
- data/dist/controllers/range_slider_controller.js +88 -42
- data/dist/controllers/rating_controller.js +39 -15
- data/dist/controllers/read_more_controller.js +100 -7
- data/dist/controllers/reading_progress_controller.js +65 -19
- data/dist/controllers/relative_time_controller.js +10 -5
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +75 -27
- data/dist/controllers/scroll_restore_controller.js +37 -16
- data/dist/controllers/scroll_visibility_controller.js +49 -30
- data/dist/controllers/scrollspy_controller.js +71 -26
- data/dist/controllers/separator_controller.js +66 -37
- data/dist/controllers/sidebar_controller.js +77 -18
- data/dist/controllers/skeleton_controller.js +6 -1
- data/dist/controllers/slider_controller.js +82 -47
- data/dist/controllers/smart_sticky_header_controller.js +60 -26
- data/dist/controllers/sortable_controller.js +17 -2
- data/dist/controllers/spinner_controller.js +10 -2
- data/dist/controllers/step_indicator_controller.js +18 -17
- data/dist/controllers/stepper_controller.js +101 -19
- data/dist/controllers/stick_to_bottom_controller.js +104 -8
- data/dist/controllers/submit_once_controller.js +45 -9
- data/dist/controllers/switch_controller.js +101 -10
- data/dist/controllers/tabs_controller.js +21 -2
- data/dist/controllers/tags_input_controller.js +209 -59
- data/dist/controllers/textarea_autosize_controller.js +29 -3
- data/dist/controllers/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +451 -105
- data/dist/controllers/toggle_group_controller.js +159 -23
- data/dist/controllers/toolbar_controller.js +32 -0
- data/dist/controllers/tooltip_controller.js +39 -13
- data/dist/controllers/transition_controller.js +4 -0
- data/dist/controllers/tree_view_controller.js +169 -16
- data/dist/index.js +5002 -1911
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -150,16 +150,51 @@ function hasTabStop(root) {
|
|
|
150
150
|
return firstTabStop(root) !== null;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// src/utils/frame_coalescer.ts
|
|
154
|
+
var FrameCoalescer = class {
|
|
155
|
+
#frame = null;
|
|
156
|
+
/**
|
|
157
|
+
* Runs `run` on the next frame, unless a frame is already pending — the first
|
|
158
|
+
* request of a burst wins and the rest are dropped. The pending frame is
|
|
159
|
+
* released before `run`, so `run` may request the next one.
|
|
160
|
+
*/
|
|
161
|
+
schedule(run) {
|
|
162
|
+
if (this.#frame !== null) return;
|
|
163
|
+
this.#frame = requestAnimationFrame(() => {
|
|
164
|
+
this.#frame = null;
|
|
165
|
+
run();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Drops the pending frame, and reaches the platform only when there is one.
|
|
170
|
+
*
|
|
171
|
+
* There is no handle value that stands for "nothing pending":
|
|
172
|
+
* `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
|
|
173
|
+
* arrives as a large positive number that the same allocator can hand out, and
|
|
174
|
+
* an idle cancel would then drop a frame belonging to someone else.
|
|
175
|
+
*/
|
|
176
|
+
cancel() {
|
|
177
|
+
if (this.#frame === null) return;
|
|
178
|
+
cancelAnimationFrame(this.#frame);
|
|
179
|
+
this.#frame = null;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
153
183
|
// src/utils/layout_observer.ts
|
|
154
184
|
var LayoutObserver = class {
|
|
155
185
|
#callback;
|
|
156
186
|
#resizeObserverFactory;
|
|
157
187
|
#resizeObserver = null;
|
|
158
188
|
#observingViewport = false;
|
|
189
|
+
#loadContainer = null;
|
|
159
190
|
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
160
191
|
#handleViewportResize = () => {
|
|
161
192
|
this.#callback();
|
|
162
193
|
};
|
|
194
|
+
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
195
|
+
#handleDescendantLoad = () => {
|
|
196
|
+
this.#callback();
|
|
197
|
+
};
|
|
163
198
|
constructor(callback, options = {}) {
|
|
164
199
|
this.#callback = callback;
|
|
165
200
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
@@ -195,14 +230,35 @@ var LayoutObserver = class {
|
|
|
195
230
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
196
231
|
}
|
|
197
232
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
233
|
+
* Starts reporting a `load` from anywhere inside `container` — an image or a
|
|
234
|
+
* frame settling changes the box it sits in, and it measures as zero high until
|
|
235
|
+
* then. `load` does not bubble, so the subscription is a capture-phase listener
|
|
236
|
+
* on the container itself and nothing the caller spells.
|
|
237
|
+
*
|
|
238
|
+
* **One container at a time.** A further call moves the observation, so a widget
|
|
239
|
+
* whose content element is swapped at runtime releases the element it let go by
|
|
240
|
+
* naming the new one — there is no second place for the release to drift from.
|
|
241
|
+
*/
|
|
242
|
+
observeDescendantLoads(container) {
|
|
243
|
+
this.unobserveDescendantLoads();
|
|
244
|
+
this.#loadContainer = container;
|
|
245
|
+
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
246
|
+
}
|
|
247
|
+
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
248
|
+
unobserveDescendantLoads() {
|
|
249
|
+
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
250
|
+
this.#loadContainer = null;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Releases every observation: disconnects the {@link ResizeObserver} and removes
|
|
254
|
+
* the viewport and descendant-load listeners. Safe to call multiple times. Call
|
|
255
|
+
* this from a controller's `disconnect()`.
|
|
201
256
|
*/
|
|
202
257
|
disconnect() {
|
|
203
258
|
this.#resizeObserver?.disconnect();
|
|
204
259
|
this.#resizeObserver = null;
|
|
205
260
|
this.unobserveViewport();
|
|
261
|
+
this.unobserveDescendantLoads();
|
|
206
262
|
}
|
|
207
263
|
};
|
|
208
264
|
|
|
@@ -359,20 +415,14 @@ var ScrollAreaController = class extends Controller {
|
|
|
359
415
|
#observedNameIds = /* @__PURE__ */ new Set();
|
|
360
416
|
#observedNameSources = [];
|
|
361
417
|
#fonts = null;
|
|
362
|
-
|
|
418
|
+
/** Coalesces scroll bursts into one position sync per frame. */
|
|
419
|
+
#frames = new FrameCoalescer();
|
|
363
420
|
#overflowing = false;
|
|
364
421
|
#lastEdge = null;
|
|
365
422
|
#ownedHostMutations = /* @__PURE__ */ new Map();
|
|
366
|
-
#onScroll = () => {
|
|
367
|
-
|
|
368
|
-
this.#
|
|
369
|
-
this.#scrollFrame = null;
|
|
370
|
-
const viewport = this.#viewport;
|
|
371
|
-
if (viewport) this.#syncPosition(viewport, this.#overflowing);
|
|
372
|
-
});
|
|
373
|
-
};
|
|
374
|
-
#onLoad = () => {
|
|
375
|
-
this.#refresh();
|
|
423
|
+
#onScroll = (event) => {
|
|
424
|
+
const viewport = event.currentTarget;
|
|
425
|
+
this.#frames.schedule(() => this.#syncPosition(viewport, this.#overflowing));
|
|
376
426
|
};
|
|
377
427
|
#onFontsSettled = () => {
|
|
378
428
|
this.#refresh();
|
|
@@ -387,7 +437,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
387
437
|
disconnect() {
|
|
388
438
|
this.#beforeCache.deactivate();
|
|
389
439
|
this.#rebind.cancel();
|
|
390
|
-
this.#
|
|
440
|
+
this.#frames.cancel();
|
|
391
441
|
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
392
442
|
this.#layout.disconnect();
|
|
393
443
|
this.#unbindFonts();
|
|
@@ -420,7 +470,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
420
470
|
return;
|
|
421
471
|
}
|
|
422
472
|
next.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
423
|
-
|
|
473
|
+
this.#layout.observeDescendantLoads(next);
|
|
424
474
|
this.#layout.observe(next);
|
|
425
475
|
this.#bindContentObserver(next);
|
|
426
476
|
this.#refresh();
|
|
@@ -455,9 +505,9 @@ var ScrollAreaController = class extends Controller {
|
|
|
455
505
|
}
|
|
456
506
|
/** Releases every resource and borrowed attribute owned by one former viewport. */
|
|
457
507
|
#unbindViewport(viewport) {
|
|
458
|
-
this.#
|
|
508
|
+
this.#frames.cancel();
|
|
459
509
|
viewport.removeEventListener("scroll", this.#onScroll);
|
|
460
|
-
|
|
510
|
+
this.#layout.unobserveDescendantLoads();
|
|
461
511
|
this.#layout.unobserve(viewport);
|
|
462
512
|
this.#content?.disconnect();
|
|
463
513
|
this.#content = null;
|
|
@@ -468,11 +518,15 @@ var ScrollAreaController = class extends Controller {
|
|
|
468
518
|
this.#overflowing = false;
|
|
469
519
|
this.#lastEdge = null;
|
|
470
520
|
}
|
|
471
|
-
/**
|
|
521
|
+
/**
|
|
522
|
+
* Runs the full structural and positional measurement pass.
|
|
523
|
+
*
|
|
524
|
+
* @stimeoRenderRoot
|
|
525
|
+
*/
|
|
472
526
|
#refresh() {
|
|
473
527
|
const viewport = this.#viewport;
|
|
474
528
|
if (!viewport) return;
|
|
475
|
-
this.#
|
|
529
|
+
this.#frames.cancel();
|
|
476
530
|
this.#syncNameSources(viewport);
|
|
477
531
|
this.#overflowing = this.#syncOverflow(viewport);
|
|
478
532
|
this.#syncKeyboardReach(viewport, this.#overflowing);
|
|
@@ -680,16 +734,10 @@ var ScrollAreaController = class extends Controller {
|
|
|
680
734
|
this.#fonts?.removeEventListener("loadingerror", this.#onFontsSettled);
|
|
681
735
|
this.#fonts = null;
|
|
682
736
|
}
|
|
683
|
-
/** Cancels a pending scroll frame. */
|
|
684
|
-
#cancelScrollFrame() {
|
|
685
|
-
if (this.#scrollFrame === null) return;
|
|
686
|
-
cancelAnimationFrame(this.#scrollFrame);
|
|
687
|
-
this.#scrollFrame = null;
|
|
688
|
-
}
|
|
689
737
|
/** Suspends live resources and returns all derived state before snapshotting. */
|
|
690
738
|
#rewindForCache() {
|
|
691
739
|
this.#rebind.cancel();
|
|
692
|
-
this.#
|
|
740
|
+
this.#frames.cancel();
|
|
693
741
|
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
694
742
|
this.#layout.disconnect();
|
|
695
743
|
this.#unbindFonts();
|
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
// src/controllers/scroll_restore_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/frame_coalescer.ts
|
|
6
|
+
var FrameCoalescer = class {
|
|
7
|
+
#frame = null;
|
|
8
|
+
/**
|
|
9
|
+
* Runs `run` on the next frame, unless a frame is already pending — the first
|
|
10
|
+
* request of a burst wins and the rest are dropped. The pending frame is
|
|
11
|
+
* released before `run`, so `run` may request the next one.
|
|
12
|
+
*/
|
|
13
|
+
schedule(run) {
|
|
14
|
+
if (this.#frame !== null) return;
|
|
15
|
+
this.#frame = requestAnimationFrame(() => {
|
|
16
|
+
this.#frame = null;
|
|
17
|
+
run();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Drops the pending frame, and reaches the platform only when there is one.
|
|
22
|
+
*
|
|
23
|
+
* There is no handle value that stands for "nothing pending":
|
|
24
|
+
* `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
|
|
25
|
+
* arrives as a large positive number that the same allocator can hand out, and
|
|
26
|
+
* an idle cancel would then drop a frame belonging to someone else.
|
|
27
|
+
*/
|
|
28
|
+
cancel() {
|
|
29
|
+
if (this.#frame === null) return;
|
|
30
|
+
cancelAnimationFrame(this.#frame);
|
|
31
|
+
this.#frame = null;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
3
35
|
// src/controllers/scroll_restore_controller.ts
|
|
4
36
|
function parseStored(raw) {
|
|
5
37
|
if (raw === null) return null;
|
|
@@ -17,8 +49,8 @@ var ScrollRestoreController = class extends Controller {
|
|
|
17
49
|
key: { type: String, default: "" },
|
|
18
50
|
axis: { type: String, default: "vertical" }
|
|
19
51
|
};
|
|
20
|
-
/**
|
|
21
|
-
#
|
|
52
|
+
/** Coalesces scroll bursts into one save per frame. */
|
|
53
|
+
#frames = new FrameCoalescer();
|
|
22
54
|
/** Resolved storage key; empty disables persistence (no key and no id). */
|
|
23
55
|
#storageKey = "";
|
|
24
56
|
/** Last offset captured while the element was live; persisted as-is on teardown. */
|
|
@@ -46,11 +78,7 @@ var ScrollRestoreController = class extends Controller {
|
|
|
46
78
|
#connected = false;
|
|
47
79
|
#onScroll = () => {
|
|
48
80
|
this.#capture();
|
|
49
|
-
|
|
50
|
-
this.#rafId = requestAnimationFrame(() => {
|
|
51
|
-
this.#rafId = null;
|
|
52
|
-
this.#persist();
|
|
53
|
-
});
|
|
81
|
+
this.#frames.schedule(() => this.#persist());
|
|
54
82
|
};
|
|
55
83
|
connect() {
|
|
56
84
|
this.#connected = true;
|
|
@@ -61,7 +89,7 @@ var ScrollRestoreController = class extends Controller {
|
|
|
61
89
|
disconnect() {
|
|
62
90
|
this.#connected = false;
|
|
63
91
|
this.element.removeEventListener("scroll", this.#onScroll);
|
|
64
|
-
this.#
|
|
92
|
+
this.#frames.cancel();
|
|
65
93
|
this.#persist();
|
|
66
94
|
}
|
|
67
95
|
/** Re-derives the namespace when application code or a Turbo morph moves `key`. */
|
|
@@ -75,7 +103,7 @@ var ScrollRestoreController = class extends Controller {
|
|
|
75
103
|
/** Rebuilds the persistence state around the Values as they now read. */
|
|
76
104
|
#resync() {
|
|
77
105
|
if (!this.#connected) return;
|
|
78
|
-
this.#
|
|
106
|
+
this.#frames.cancel();
|
|
79
107
|
this.#storageKey = this.#resolveKey();
|
|
80
108
|
this.#echoTop = null;
|
|
81
109
|
this.#echoLeft = null;
|
|
@@ -84,13 +112,6 @@ var ScrollRestoreController = class extends Controller {
|
|
|
84
112
|
this.#capturedLeft = false;
|
|
85
113
|
if (this.#storageKey) this.#restore();
|
|
86
114
|
}
|
|
87
|
-
/** Drops the pending coalesced save, if one is queued. */
|
|
88
|
-
#cancelFrame() {
|
|
89
|
-
if (this.#rafId !== null) {
|
|
90
|
-
cancelAnimationFrame(this.#rafId);
|
|
91
|
-
this.#rafId = null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
115
|
/**
|
|
95
116
|
* Records the live scroll offset for the configured axis.
|
|
96
117
|
*
|
|
@@ -77,11 +77,53 @@ function validSelector(element, raw, fallback) {
|
|
|
77
77
|
);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// src/utils/frame_coalescer.ts
|
|
81
|
+
var FrameCoalescer = class {
|
|
82
|
+
#frame = null;
|
|
83
|
+
/**
|
|
84
|
+
* Runs `run` on the next frame, unless a frame is already pending — the first
|
|
85
|
+
* request of a burst wins and the rest are dropped. The pending frame is
|
|
86
|
+
* released before `run`, so `run` may request the next one.
|
|
87
|
+
*/
|
|
88
|
+
schedule(run) {
|
|
89
|
+
if (this.#frame !== null) return;
|
|
90
|
+
this.#frame = requestAnimationFrame(() => {
|
|
91
|
+
this.#frame = null;
|
|
92
|
+
run();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Drops the pending frame, and reaches the platform only when there is one.
|
|
97
|
+
*
|
|
98
|
+
* There is no handle value that stands for "nothing pending":
|
|
99
|
+
* `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
|
|
100
|
+
* arrives as a large positive number that the same allocator can hand out, and
|
|
101
|
+
* an idle cancel would then drop a frame belonging to someone else.
|
|
102
|
+
*/
|
|
103
|
+
cancel() {
|
|
104
|
+
if (this.#frame === null) return;
|
|
105
|
+
cancelAnimationFrame(this.#frame);
|
|
106
|
+
this.#frame = null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
80
110
|
// src/utils/reduced_motion.ts
|
|
81
111
|
function prefersReducedMotion() {
|
|
82
112
|
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
83
113
|
}
|
|
84
114
|
|
|
115
|
+
// src/utils/scroll_source.ts
|
|
116
|
+
function resolveScrollContainer(selector) {
|
|
117
|
+
const match = selector ? document.querySelector(selector) : null;
|
|
118
|
+
return match instanceof HTMLElement ? match : null;
|
|
119
|
+
}
|
|
120
|
+
function resolveScrollSource(selector) {
|
|
121
|
+
return resolveScrollContainer(selector) ?? window;
|
|
122
|
+
}
|
|
123
|
+
function scrollOffset(source) {
|
|
124
|
+
return source === window ? window.scrollY ?? window.pageYOffset ?? 0 : source.scrollTop;
|
|
125
|
+
}
|
|
126
|
+
|
|
85
127
|
// src/utils/before_cache_reset.ts
|
|
86
128
|
var BeforeCacheReset = class _BeforeCacheReset {
|
|
87
129
|
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
@@ -154,8 +196,8 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
154
196
|
};
|
|
155
197
|
static actions = ["toTop"];
|
|
156
198
|
static events = ["change"];
|
|
157
|
-
/**
|
|
158
|
-
#
|
|
199
|
+
/** Coalesces scroll bursts into one measurement per frame. */
|
|
200
|
+
#frames = new FrameCoalescer();
|
|
159
201
|
/** Previous scroll position, for `direction` mode delta detection. */
|
|
160
202
|
#lastScrollY = 0;
|
|
161
203
|
/** Current visibility, tracked to dispatch `change` only on real transitions. */
|
|
@@ -191,16 +233,10 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
191
233
|
#pendingHide = new BlurDeferral(() => {
|
|
192
234
|
if (this.#connected) this.#evaluate();
|
|
193
235
|
});
|
|
194
|
-
#onScroll = () =>
|
|
195
|
-
if (this.#rafId !== null) return;
|
|
196
|
-
this.#rafId = requestAnimationFrame(() => {
|
|
197
|
-
this.#rafId = null;
|
|
198
|
-
this.#evaluate();
|
|
199
|
-
});
|
|
200
|
-
};
|
|
236
|
+
#onScroll = () => this.#frames.schedule(() => this.#evaluate());
|
|
201
237
|
connect() {
|
|
202
|
-
this.#scrollSource = this.#
|
|
203
|
-
this.#lastScrollY = this.#
|
|
238
|
+
this.#scrollSource = resolveScrollSource(this.#rootSelector);
|
|
239
|
+
this.#lastScrollY = scrollOffset(this.#scrollSource);
|
|
204
240
|
this.#scrollSource.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
205
241
|
this.#evaluate(false);
|
|
206
242
|
this.#connected = true;
|
|
@@ -208,10 +244,7 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
208
244
|
disconnect() {
|
|
209
245
|
this.#connected = false;
|
|
210
246
|
this.#scrollSource.removeEventListener("scroll", this.#onScroll);
|
|
211
|
-
|
|
212
|
-
cancelAnimationFrame(this.#rafId);
|
|
213
|
-
this.#rafId = null;
|
|
214
|
-
}
|
|
247
|
+
this.#frames.cancel();
|
|
215
248
|
this.#pendingHide.releaseAll();
|
|
216
249
|
this.#tabindex.returnAll();
|
|
217
250
|
this.#visible = null;
|
|
@@ -270,7 +303,7 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
270
303
|
* @stimeoRenderRoot
|
|
271
304
|
*/
|
|
272
305
|
#evaluate(notify = true) {
|
|
273
|
-
const y = this.#
|
|
306
|
+
const y = scrollOffset(this.#scrollSource);
|
|
274
307
|
let nextVisible;
|
|
275
308
|
if (this.modeValue === "direction") {
|
|
276
309
|
if (y <= this.#offset) {
|
|
@@ -299,14 +332,6 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
299
332
|
this.element.setAttribute("data-state", next ? "visible" : "hidden");
|
|
300
333
|
if (notify) this.dispatch("change", { detail: { visible: next } });
|
|
301
334
|
}
|
|
302
|
-
/** Resolves the scroll source from `root` (falling back to the window). */
|
|
303
|
-
#resolveScrollSource() {
|
|
304
|
-
if (this.#rootSelector) {
|
|
305
|
-
const root = document.querySelector(this.#rootSelector);
|
|
306
|
-
if (root) return root;
|
|
307
|
-
}
|
|
308
|
-
return window;
|
|
309
|
-
}
|
|
310
335
|
/**
|
|
311
336
|
* The focus owner inside the target, or `null` when focus is elsewhere.
|
|
312
337
|
*
|
|
@@ -319,12 +344,6 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
319
344
|
if (focused instanceof HTMLElement && this.elementTarget.contains(focused)) return focused;
|
|
320
345
|
return null;
|
|
321
346
|
}
|
|
322
|
-
#scrollY() {
|
|
323
|
-
if (this.#scrollSource === window) {
|
|
324
|
-
return window.scrollY ?? window.pageYOffset ?? 0;
|
|
325
|
-
}
|
|
326
|
-
return this.#scrollSource.scrollTop;
|
|
327
|
-
}
|
|
328
347
|
};
|
|
329
348
|
|
|
330
349
|
export { ScrollVisibilityController };
|
|
@@ -10,6 +10,47 @@ function parseDeclared(raw, parse, fallback) {
|
|
|
10
10
|
return fallback;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
function validSelector(element, raw, fallback) {
|
|
14
|
+
if (raw.length === 0) return fallback;
|
|
15
|
+
return parseDeclared(
|
|
16
|
+
raw,
|
|
17
|
+
(selector) => {
|
|
18
|
+
element.matches(selector);
|
|
19
|
+
return selector;
|
|
20
|
+
},
|
|
21
|
+
fallback
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/utils/frame_coalescer.ts
|
|
26
|
+
var FrameCoalescer = class {
|
|
27
|
+
#frame = null;
|
|
28
|
+
/**
|
|
29
|
+
* Runs `run` on the next frame, unless a frame is already pending — the first
|
|
30
|
+
* request of a burst wins and the rest are dropped. The pending frame is
|
|
31
|
+
* released before `run`, so `run` may request the next one.
|
|
32
|
+
*/
|
|
33
|
+
schedule(run) {
|
|
34
|
+
if (this.#frame !== null) return;
|
|
35
|
+
this.#frame = requestAnimationFrame(() => {
|
|
36
|
+
this.#frame = null;
|
|
37
|
+
run();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Drops the pending frame, and reaches the platform only when there is one.
|
|
42
|
+
*
|
|
43
|
+
* There is no handle value that stands for "nothing pending":
|
|
44
|
+
* `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
|
|
45
|
+
* arrives as a large positive number that the same allocator can hand out, and
|
|
46
|
+
* an idle cancel would then drop a frame belonging to someone else.
|
|
47
|
+
*/
|
|
48
|
+
cancel() {
|
|
49
|
+
if (this.#frame === null) return;
|
|
50
|
+
cancelAnimationFrame(this.#frame);
|
|
51
|
+
this.#frame = null;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
13
54
|
|
|
14
55
|
// src/utils/intersection_watcher.ts
|
|
15
56
|
function queryRoot(selector) {
|
|
@@ -117,6 +158,15 @@ function prefersReducedMotion() {
|
|
|
117
158
|
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
118
159
|
}
|
|
119
160
|
|
|
161
|
+
// src/utils/scroll_source.ts
|
|
162
|
+
function resolveScrollContainer(selector) {
|
|
163
|
+
const match = selector ? document.querySelector(selector) : null;
|
|
164
|
+
return match instanceof HTMLElement ? match : null;
|
|
165
|
+
}
|
|
166
|
+
function scrollOffset(source) {
|
|
167
|
+
return source === window ? window.scrollY ?? window.pageYOffset ?? 0 : source.scrollTop;
|
|
168
|
+
}
|
|
169
|
+
|
|
120
170
|
// src/controllers/scrollspy_controller.ts
|
|
121
171
|
var ANCHOR_ATTRIBUTES = ["href", "data-href"];
|
|
122
172
|
var ScrollspyController = class extends Controller {
|
|
@@ -163,8 +213,10 @@ var ScrollspyController = class extends Controller {
|
|
|
163
213
|
* rather than from whatever the selector resolves to now.
|
|
164
214
|
*/
|
|
165
215
|
#scrollSource = null;
|
|
166
|
-
/**
|
|
167
|
-
#
|
|
216
|
+
/** Coalesces a scroll burst into one measurement per frame. */
|
|
217
|
+
#frames = new FrameCoalescer();
|
|
218
|
+
/** The validated `rootSelector`, or `""` when the declaration cannot be parsed. */
|
|
219
|
+
#rootSelector = "";
|
|
168
220
|
/** Watches the link targets' anchor attributes for an in-place morph rewrite. */
|
|
169
221
|
#anchorObserver = null;
|
|
170
222
|
/** True while a coalesced observation rebuild is queued; see {@link #scheduleResync}. */
|
|
@@ -181,10 +233,7 @@ var ScrollspyController = class extends Controller {
|
|
|
181
233
|
this.#anchorObserver?.disconnect();
|
|
182
234
|
this.#anchorObserver = null;
|
|
183
235
|
this.#detachScrollListener();
|
|
184
|
-
|
|
185
|
-
cancelAnimationFrame(this.#frame);
|
|
186
|
-
this.#frame = null;
|
|
187
|
-
}
|
|
236
|
+
this.#frames.cancel();
|
|
188
237
|
this.#intersectionStates.clear();
|
|
189
238
|
this.#activeSectionId = "";
|
|
190
239
|
this.#rootElement = null;
|
|
@@ -201,6 +250,7 @@ var ScrollspyController = class extends Controller {
|
|
|
201
250
|
this.#initializeObserver();
|
|
202
251
|
}
|
|
203
252
|
rootSelectorValueChanged() {
|
|
253
|
+
this.#rootSelector = validSelector(this.element, this.rootSelectorValue, "");
|
|
204
254
|
if (!this.#isConnected) return;
|
|
205
255
|
this.#initializeObserver();
|
|
206
256
|
}
|
|
@@ -294,10 +344,10 @@ var ScrollspyController = class extends Controller {
|
|
|
294
344
|
const offset = this.#offset;
|
|
295
345
|
if (rootElement) {
|
|
296
346
|
const containerRect = rootElement.getBoundingClientRect();
|
|
297
|
-
const scrollPosition = rootElement
|
|
347
|
+
const scrollPosition = scrollOffset(rootElement) + (targetRect.top - containerRect.top) - offset;
|
|
298
348
|
rootElement.scrollTo({ top: scrollPosition, behavior });
|
|
299
349
|
} else {
|
|
300
|
-
const scrollPosition = window
|
|
350
|
+
const scrollPosition = scrollOffset(window) + targetRect.top - offset;
|
|
301
351
|
window.scrollTo({ top: scrollPosition, behavior });
|
|
302
352
|
}
|
|
303
353
|
if (this.focusSectionValue) this.#focusSection(targetElement);
|
|
@@ -332,19 +382,14 @@ var ScrollspyController = class extends Controller {
|
|
|
332
382
|
/**
|
|
333
383
|
* Resolves `rootSelector` to a scrollable element.
|
|
334
384
|
*
|
|
335
|
-
* @returns The container, or `null` meaning "spy the viewport" when the
|
|
336
|
-
* is empty, matches nothing, matches a non-HTML element
|
|
337
|
-
*
|
|
338
|
-
*
|
|
385
|
+
* @returns The container, or `null` meaning "spy the viewport" when the
|
|
386
|
+
* declaration is empty, matches nothing, or matches a non-HTML element. A typo
|
|
387
|
+
* reads as empty: the declaration is validated once when it changes, so a
|
|
388
|
+
* selector that cannot be parsed degrades to viewport spying rather than
|
|
389
|
+
* leaving the controller inert.
|
|
339
390
|
*/
|
|
340
391
|
#queryRootElement() {
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
const root = document.querySelector(this.rootSelectorValue);
|
|
344
|
-
return root instanceof HTMLElement ? root : null;
|
|
345
|
-
} catch {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
392
|
+
return resolveScrollContainer(this.#rootSelector);
|
|
348
393
|
}
|
|
349
394
|
/**
|
|
350
395
|
* Re-evaluates once per frame while the reader scrolls.
|
|
@@ -359,13 +404,7 @@ var ScrollspyController = class extends Controller {
|
|
|
359
404
|
* end in {@link #evaluateActiveSection} — which measures section rects and the
|
|
360
405
|
* root's top edge at that instant — they converge on the same answer.
|
|
361
406
|
*/
|
|
362
|
-
#onScroll = () =>
|
|
363
|
-
if (this.#frame !== null) return;
|
|
364
|
-
this.#frame = requestAnimationFrame(() => {
|
|
365
|
-
this.#frame = null;
|
|
366
|
-
this.#evaluateActiveSection();
|
|
367
|
-
});
|
|
368
|
-
};
|
|
407
|
+
#onScroll = () => this.#frames.schedule(() => this.#evaluateActiveSection());
|
|
369
408
|
/**
|
|
370
409
|
* Points the `scroll` listener at whatever the reader actually scrolls: the
|
|
371
410
|
* resolved root, or the window when the viewport is spied.
|
|
@@ -383,6 +422,10 @@ var ScrollspyController = class extends Controller {
|
|
|
383
422
|
this.#scrollSource?.removeEventListener("scroll", this.#onScroll);
|
|
384
423
|
this.#scrollSource = null;
|
|
385
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* @stimeoRuntimeOnly `rootMargin` wires the observer this call installs; the active section it
|
|
427
|
+
* republishes is the one already recorded.
|
|
428
|
+
*/
|
|
386
429
|
#initializeObserver() {
|
|
387
430
|
this.#watcher.stop();
|
|
388
431
|
this.#intersectionStates.clear();
|
|
@@ -428,6 +471,8 @@ var ScrollspyController = class extends Controller {
|
|
|
428
471
|
* threshold) against a freshly computed trigger line mixes two moments in
|
|
429
472
|
* time, which would make the result depend on how the reader arrived at a
|
|
430
473
|
* position — a smooth scroll and an instant jump to the same offset disagree.
|
|
474
|
+
*
|
|
475
|
+
* @stimeoRenderRoot
|
|
431
476
|
*/
|
|
432
477
|
#evaluateActiveSection() {
|
|
433
478
|
const rootEl = this.#scrollRoot();
|