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
|
@@ -83,6 +83,16 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
+
// src/utils/field_mirror.ts
|
|
87
|
+
function writeField(field, value) {
|
|
88
|
+
if (field.value === value) return false;
|
|
89
|
+
field.value = value;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function commitField(target) {
|
|
93
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
94
|
+
}
|
|
95
|
+
|
|
86
96
|
// src/utils/microtask_coalescer.ts
|
|
87
97
|
var MicrotaskCoalescer = class {
|
|
88
98
|
#run;
|
|
@@ -201,11 +211,14 @@ var RatingController = class extends Controller {
|
|
|
201
211
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
202
212
|
#connected = false;
|
|
203
213
|
#rescuedFocus = false;
|
|
214
|
+
/** The rating shown last, which the next move is measured from. */
|
|
215
|
+
#shown = 0;
|
|
204
216
|
/** Reflects declarative state without announcing an initial user change. */
|
|
205
217
|
connect() {
|
|
206
218
|
this.#repaint.activate();
|
|
207
219
|
this.#beforeCache.activate();
|
|
208
|
-
this.#
|
|
220
|
+
this.#shown = this.#normalize(this.valueValue);
|
|
221
|
+
this.#apply(this.#shown, { focus: false });
|
|
209
222
|
this.#connected = true;
|
|
210
223
|
}
|
|
211
224
|
/** Drops a queued reconciliation and hands every borrowed attribute back. */
|
|
@@ -306,29 +319,35 @@ var RatingController = class extends Controller {
|
|
|
306
319
|
this.#commit(next, { focus: true });
|
|
307
320
|
}
|
|
308
321
|
/**
|
|
309
|
-
* Repaints one settled target/Value mutation batch and reports
|
|
310
|
-
*
|
|
322
|
+
* Repaints one settled target/Value mutation batch and reports a rating shown
|
|
323
|
+
* that moved from the one shown before it.
|
|
311
324
|
*
|
|
312
325
|
* @stimeoRenderRoot
|
|
313
326
|
*/
|
|
314
327
|
#reconcileScale() {
|
|
315
|
-
const
|
|
316
|
-
const
|
|
328
|
+
const value = this.#normalize(this.valueValue);
|
|
329
|
+
const previous = this.#shown;
|
|
330
|
+
this.#shown = value;
|
|
317
331
|
this.#apply(value, { focus: false });
|
|
318
|
-
if (
|
|
319
|
-
this.dispatch("reconcile", { detail: { value } });
|
|
320
|
-
}
|
|
332
|
+
if (value !== previous) this.dispatch("reconcile", { detail: { value } });
|
|
321
333
|
}
|
|
322
|
-
/**
|
|
334
|
+
/**
|
|
335
|
+
* Applies one user operation and emits only when the rating shown changes.
|
|
336
|
+
*
|
|
337
|
+
* The user's value is written into `value` before anything reports it, so a
|
|
338
|
+
* listener of the field's native `change` already reads it there, and the pass
|
|
339
|
+
* that the write starts finds nothing left to report.
|
|
340
|
+
*/
|
|
323
341
|
#commit(raw, { focus }) {
|
|
324
|
-
const previous = this.#
|
|
342
|
+
const previous = this.#shown;
|
|
325
343
|
const value = this.#normalize(raw);
|
|
326
|
-
this
|
|
344
|
+
if (!Object.is(this.valueValue, value)) this.valueValue = value;
|
|
345
|
+
this.#shown = value;
|
|
346
|
+
this.#apply(value, { focus, notify: true });
|
|
327
347
|
if (value !== previous) this.dispatch("change", { detail: { value } });
|
|
328
348
|
}
|
|
329
|
-
/** Synchronizes
|
|
330
|
-
#apply(value, { focus }) {
|
|
331
|
-
if (!Object.is(this.valueValue, value)) this.valueValue = value;
|
|
349
|
+
/** Synchronizes ARIA, roving focus, form state, and the visual fill hook. */
|
|
350
|
+
#apply(value, { focus, notify = false }) {
|
|
332
351
|
this.symbolTargets.forEach((symbol, index) => {
|
|
333
352
|
symbol.setAttribute("aria-checked", value > 0 && index + 1 === value ? "true" : "false");
|
|
334
353
|
});
|
|
@@ -338,9 +357,14 @@ var RatingController = class extends Controller {
|
|
|
338
357
|
const returning = this.#releaseReadonly();
|
|
339
358
|
this.#roving.setActive(value > 0 ? value - 1 : 0, { focus: focus || returning });
|
|
340
359
|
}
|
|
341
|
-
|
|
360
|
+
this.#mirrorField(value, notify);
|
|
342
361
|
this.#setFillRange(value);
|
|
343
362
|
}
|
|
363
|
+
/** Mirrors `value` into the optional form field, reporting only a user's move. */
|
|
364
|
+
#mirrorField(value, notify) {
|
|
365
|
+
if (!this.hasFieldTarget) return;
|
|
366
|
+
if (writeField(this.fieldTarget, String(value)) && notify) commitField(this.fieldTarget);
|
|
367
|
+
}
|
|
344
368
|
/** Marks the first `range` symbols with the consumer-owned fill hook. */
|
|
345
369
|
#setFillRange(range) {
|
|
346
370
|
this.symbolTargets.forEach((symbol, index) => {
|
|
@@ -63,10 +63,15 @@ var LayoutObserver = class {
|
|
|
63
63
|
#resizeObserverFactory;
|
|
64
64
|
#resizeObserver = null;
|
|
65
65
|
#observingViewport = false;
|
|
66
|
+
#loadContainer = null;
|
|
66
67
|
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
67
68
|
#handleViewportResize = () => {
|
|
68
69
|
this.#callback();
|
|
69
70
|
};
|
|
71
|
+
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
72
|
+
#handleDescendantLoad = () => {
|
|
73
|
+
this.#callback();
|
|
74
|
+
};
|
|
70
75
|
constructor(callback, options = {}) {
|
|
71
76
|
this.#callback = callback;
|
|
72
77
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
@@ -102,24 +107,97 @@ var LayoutObserver = class {
|
|
|
102
107
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
103
108
|
}
|
|
104
109
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
110
|
+
* Starts reporting a `load` from anywhere inside `container` — an image or a
|
|
111
|
+
* frame settling changes the box it sits in, and it measures as zero high until
|
|
112
|
+
* then. `load` does not bubble, so the subscription is a capture-phase listener
|
|
113
|
+
* on the container itself and nothing the caller spells.
|
|
114
|
+
*
|
|
115
|
+
* **One container at a time.** A further call moves the observation, so a widget
|
|
116
|
+
* whose content element is swapped at runtime releases the element it let go by
|
|
117
|
+
* naming the new one — there is no second place for the release to drift from.
|
|
118
|
+
*/
|
|
119
|
+
observeDescendantLoads(container) {
|
|
120
|
+
this.unobserveDescendantLoads();
|
|
121
|
+
this.#loadContainer = container;
|
|
122
|
+
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
123
|
+
}
|
|
124
|
+
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
125
|
+
unobserveDescendantLoads() {
|
|
126
|
+
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
127
|
+
this.#loadContainer = null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Releases every observation: disconnects the {@link ResizeObserver} and removes
|
|
131
|
+
* the viewport and descendant-load listeners. Safe to call multiple times. Call
|
|
132
|
+
* this from a controller's `disconnect()`.
|
|
108
133
|
*/
|
|
109
134
|
disconnect() {
|
|
110
135
|
this.#resizeObserver?.disconnect();
|
|
111
136
|
this.#resizeObserver = null;
|
|
112
137
|
this.unobserveViewport();
|
|
138
|
+
this.unobserveDescendantLoads();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/utils/state_reason.ts
|
|
143
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
144
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
145
|
+
function stateReasonFor(event) {
|
|
146
|
+
if (!event) return "api";
|
|
147
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
148
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
149
|
+
return "user";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/utils/state_regions.ts
|
|
153
|
+
var StateRegions = class {
|
|
154
|
+
#whenTrue;
|
|
155
|
+
#whenFalse;
|
|
156
|
+
#taken = /* @__PURE__ */ new WeakSet();
|
|
157
|
+
constructor(options) {
|
|
158
|
+
this.#whenTrue = options.whenTrue;
|
|
159
|
+
this.#whenFalse = options.whenFalse ?? null;
|
|
160
|
+
}
|
|
161
|
+
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
162
|
+
reflect(host, isTrue) {
|
|
163
|
+
const shown = this.#inside(host, this.#whenTrue());
|
|
164
|
+
if (!this.#whenFalse) {
|
|
165
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const hiddenSide = this.#inside(host, this.#whenFalse());
|
|
169
|
+
if (shown.length === 0 || hiddenSide.length === 0) {
|
|
170
|
+
for (const region of shown) this.#give(region);
|
|
171
|
+
for (const region of hiddenSide) this.#give(region);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
175
|
+
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
176
|
+
}
|
|
177
|
+
/** Writes `hidden` where it moves, noting which regions it takes out of view. */
|
|
178
|
+
#write(region, hidden) {
|
|
179
|
+
if (hidden) this.#taken.add(region);
|
|
180
|
+
else this.#taken.delete(region);
|
|
181
|
+
if (region.hidden !== hidden) region.hidden = hidden;
|
|
182
|
+
}
|
|
183
|
+
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
184
|
+
#give(region) {
|
|
185
|
+
if (this.#taken.delete(region)) region.hidden = false;
|
|
186
|
+
}
|
|
187
|
+
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
188
|
+
#inside(host, regions) {
|
|
189
|
+
return regions.filter((region) => host.contains(region));
|
|
113
190
|
}
|
|
114
191
|
};
|
|
115
192
|
|
|
116
193
|
// src/controllers/read_more_controller.ts
|
|
117
194
|
var ReadMoreController = class extends Controller {
|
|
118
|
-
static targets = ["content", "trigger"];
|
|
195
|
+
static targets = ["content", "trigger", "expandedLabel", "collapsedLabel"];
|
|
119
196
|
static values = {
|
|
120
197
|
collapsed: { type: Boolean, default: true }
|
|
121
198
|
};
|
|
122
199
|
static actions = ["toggle"];
|
|
200
|
+
static events = ["close", "open"];
|
|
123
201
|
#connected = false;
|
|
124
202
|
#collapsed = true;
|
|
125
203
|
#observedContent = null;
|
|
@@ -128,6 +206,11 @@ var ReadMoreController = class extends Controller {
|
|
|
128
206
|
if (this.#connected) this.#evaluateOverflow();
|
|
129
207
|
};
|
|
130
208
|
#layout = new LayoutObserver(this.#update);
|
|
209
|
+
/** Owns `hidden` on the trigger's two labels. */
|
|
210
|
+
#labels = new StateRegions({
|
|
211
|
+
whenTrue: () => this.expandedLabelTargets,
|
|
212
|
+
whenFalse: () => this.collapsedLabelTargets
|
|
213
|
+
});
|
|
131
214
|
/** Holds the trigger's hide back while it has focus; re-evaluates on blur. */
|
|
132
215
|
#deferredHide = new BlurDeferral(() => {
|
|
133
216
|
this.#update();
|
|
@@ -155,12 +238,21 @@ var ReadMoreController = class extends Controller {
|
|
|
155
238
|
this.#deferredHide.release(trigger);
|
|
156
239
|
this.#syncTargets();
|
|
157
240
|
}
|
|
241
|
+
expandedLabelTargetConnected() {
|
|
242
|
+
this.#syncTargets();
|
|
243
|
+
}
|
|
244
|
+
collapsedLabelTargetConnected() {
|
|
245
|
+
this.#syncTargets();
|
|
246
|
+
}
|
|
158
247
|
/** Toggles between the collapsed (clamped) and expanded states. */
|
|
159
|
-
toggle() {
|
|
248
|
+
toggle(event) {
|
|
160
249
|
if (!this.#connected) return;
|
|
161
250
|
this.#collapsed = !this.#collapsed;
|
|
162
251
|
this.#reflect();
|
|
163
252
|
this.#evaluateOverflow();
|
|
253
|
+
const detail = { reason: stateReasonFor(event) };
|
|
254
|
+
if (this.#collapsed) this.dispatch("close", { detail, cancelable: false });
|
|
255
|
+
else this.dispatch("open", { detail, cancelable: false });
|
|
164
256
|
}
|
|
165
257
|
#initialCollapsed() {
|
|
166
258
|
if (this.hasContentTarget) {
|
|
@@ -176,6 +268,7 @@ var ReadMoreController = class extends Controller {
|
|
|
176
268
|
}
|
|
177
269
|
if (this.hasTriggerTarget) {
|
|
178
270
|
this.triggerTarget.setAttribute("aria-expanded", this.#collapsed ? "false" : "true");
|
|
271
|
+
this.#labels.reflect(this.triggerTarget, !this.#collapsed);
|
|
179
272
|
}
|
|
180
273
|
}
|
|
181
274
|
#syncTargets() {
|
|
@@ -192,7 +285,7 @@ var ReadMoreController = class extends Controller {
|
|
|
192
285
|
this.#observedContent = next;
|
|
193
286
|
this.#layout.observe(next);
|
|
194
287
|
this.#layout.observeViewport();
|
|
195
|
-
|
|
288
|
+
this.#layout.observeDescendantLoads(next);
|
|
196
289
|
if (typeof MutationObserver !== "undefined") {
|
|
197
290
|
this.#contentMutationObserver = new MutationObserver(this.#update);
|
|
198
291
|
this.#contentMutationObserver.observe(next, {
|
|
@@ -206,7 +299,7 @@ var ReadMoreController = class extends Controller {
|
|
|
206
299
|
this.#deferredHide.releaseAll();
|
|
207
300
|
if (this.#observedContent) {
|
|
208
301
|
this.#layout.unobserve(this.#observedContent);
|
|
209
|
-
this.#
|
|
302
|
+
this.#layout.unobserveDescendantLoads();
|
|
210
303
|
}
|
|
211
304
|
this.#observedContent = null;
|
|
212
305
|
this.#contentMutationObserver?.disconnect();
|
|
@@ -31,16 +31,51 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// src/utils/frame_coalescer.ts
|
|
35
|
+
var FrameCoalescer = class {
|
|
36
|
+
#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
|
+
schedule(run) {
|
|
43
|
+
if (this.#frame !== null) return;
|
|
44
|
+
this.#frame = requestAnimationFrame(() => {
|
|
45
|
+
this.#frame = null;
|
|
46
|
+
run();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
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
|
+
cancel() {
|
|
58
|
+
if (this.#frame === null) return;
|
|
59
|
+
cancelAnimationFrame(this.#frame);
|
|
60
|
+
this.#frame = null;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
34
64
|
// src/utils/layout_observer.ts
|
|
35
65
|
var LayoutObserver = class {
|
|
36
66
|
#callback;
|
|
37
67
|
#resizeObserverFactory;
|
|
38
68
|
#resizeObserver = null;
|
|
39
69
|
#observingViewport = false;
|
|
70
|
+
#loadContainer = null;
|
|
40
71
|
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
41
72
|
#handleViewportResize = () => {
|
|
42
73
|
this.#callback();
|
|
43
74
|
};
|
|
75
|
+
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
76
|
+
#handleDescendantLoad = () => {
|
|
77
|
+
this.#callback();
|
|
78
|
+
};
|
|
44
79
|
constructor(callback, options = {}) {
|
|
45
80
|
this.#callback = callback;
|
|
46
81
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
@@ -76,14 +111,35 @@ var LayoutObserver = class {
|
|
|
76
111
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
77
112
|
}
|
|
78
113
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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
|
+
observeDescendantLoads(container) {
|
|
124
|
+
this.unobserveDescendantLoads();
|
|
125
|
+
this.#loadContainer = container;
|
|
126
|
+
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
127
|
+
}
|
|
128
|
+
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
129
|
+
unobserveDescendantLoads() {
|
|
130
|
+
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
131
|
+
this.#loadContainer = null;
|
|
132
|
+
}
|
|
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()`.
|
|
82
137
|
*/
|
|
83
138
|
disconnect() {
|
|
84
139
|
this.#resizeObserver?.disconnect();
|
|
85
140
|
this.#resizeObserver = null;
|
|
86
141
|
this.unobserveViewport();
|
|
142
|
+
this.unobserveDescendantLoads();
|
|
87
143
|
}
|
|
88
144
|
};
|
|
89
145
|
|
|
@@ -148,18 +204,13 @@ var ReadingProgressController = class extends Controller {
|
|
|
148
204
|
/** The article's own box and the viewport: either changes the span. */
|
|
149
205
|
#layout = new LayoutObserver(() => this.#onScroll());
|
|
150
206
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
151
|
-
|
|
207
|
+
/** Coalesces scroll bursts, and the connect baseline, into one frame. */
|
|
208
|
+
#frames = new FrameCoalescer();
|
|
152
209
|
/** Last published progress, so `change`/`complete` fire only on movement. */
|
|
153
210
|
#progress = -1;
|
|
154
211
|
/** False until the connect frame has run: `complete` needs real reading. */
|
|
155
212
|
#baselined = false;
|
|
156
|
-
#onScroll = () =>
|
|
157
|
-
if (this.#frame !== null) return;
|
|
158
|
-
this.#frame = requestAnimationFrame(() => {
|
|
159
|
-
this.#frame = null;
|
|
160
|
-
this.#measure();
|
|
161
|
-
});
|
|
162
|
-
};
|
|
213
|
+
#onScroll = () => this.#frames.schedule(() => this.#measure());
|
|
163
214
|
connect() {
|
|
164
215
|
this.#progress = -1;
|
|
165
216
|
this.#baselined = false;
|
|
@@ -168,8 +219,7 @@ var ReadingProgressController = class extends Controller {
|
|
|
168
219
|
this.#layout.observeViewport();
|
|
169
220
|
this.#beforeCache.activate();
|
|
170
221
|
this.#measure();
|
|
171
|
-
this.#
|
|
172
|
-
this.#frame = null;
|
|
222
|
+
this.#frames.schedule(() => {
|
|
173
223
|
this.#measure();
|
|
174
224
|
this.#baselined = true;
|
|
175
225
|
});
|
|
@@ -178,7 +228,7 @@ var ReadingProgressController = class extends Controller {
|
|
|
178
228
|
window.removeEventListener("scroll", this.#onScroll, { capture: true });
|
|
179
229
|
this.#layout.disconnect();
|
|
180
230
|
this.#beforeCache.deactivate();
|
|
181
|
-
this.#
|
|
231
|
+
this.#frames.cancel();
|
|
182
232
|
this.#lease.returnAll();
|
|
183
233
|
}
|
|
184
234
|
/**
|
|
@@ -189,14 +239,10 @@ var ReadingProgressController = class extends Controller {
|
|
|
189
239
|
* value that has already been handed back.
|
|
190
240
|
*/
|
|
191
241
|
#rewindForCache() {
|
|
192
|
-
this.#
|
|
242
|
+
this.#frames.cancel();
|
|
193
243
|
this.#lease.returnAll();
|
|
194
244
|
this.#progress = -1;
|
|
195
245
|
}
|
|
196
|
-
#cancelFrame() {
|
|
197
|
-
if (this.#frame !== null) cancelAnimationFrame(this.#frame);
|
|
198
|
-
this.#frame = null;
|
|
199
|
-
}
|
|
200
246
|
/** Computes and publishes the progress; emits on movement only. */
|
|
201
247
|
#measure() {
|
|
202
248
|
const rect = this.element.getBoundingClientRect();
|
|
@@ -60,6 +60,11 @@ function construct(factory, locale, options, fallbackLocale) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// src/utils/locale.ts
|
|
64
|
+
function resolveLocale(element, declared = "") {
|
|
65
|
+
return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
63
68
|
// src/utils/microtask_coalescer.ts
|
|
64
69
|
var MicrotaskCoalescer = class {
|
|
65
70
|
#run;
|
|
@@ -265,11 +270,11 @@ var RelativeTimeController = class extends Controller {
|
|
|
265
270
|
* when the runtime rejects that locale.
|
|
266
271
|
*/
|
|
267
272
|
get #formatter() {
|
|
268
|
-
return intlFormatter(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
return intlFormatter(
|
|
274
|
+
Intl.RelativeTimeFormat,
|
|
275
|
+
resolveLocale(this.element, this.localeValue),
|
|
276
|
+
RELATIVE_OPTIONS
|
|
277
|
+
);
|
|
273
278
|
}
|
|
274
279
|
};
|
|
275
280
|
|
|
@@ -107,7 +107,20 @@ var TabindexLoan = class {
|
|
|
107
107
|
}
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
+
// src/utils/transient_hooks.ts
|
|
111
|
+
var TransientHooks = class {
|
|
112
|
+
#attributes;
|
|
113
|
+
constructor(options) {
|
|
114
|
+
this.#attributes = options.attributes;
|
|
115
|
+
}
|
|
116
|
+
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
117
|
+
reset(element) {
|
|
118
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
110
122
|
// src/controllers/resizable_controller.ts
|
|
123
|
+
var TRANSIENT = new TransientHooks({ attributes: ["data-dragging"] });
|
|
111
124
|
var DEFAULT_MIN = 0;
|
|
112
125
|
var DEFAULT_MAX = 100;
|
|
113
126
|
var DEFAULT_VALUE = 50;
|
|
@@ -121,9 +134,20 @@ var ResizableController = class extends Controller {
|
|
|
121
134
|
value: { type: Number, default: DEFAULT_VALUE }
|
|
122
135
|
};
|
|
123
136
|
static actions = ["onKeydown", "onPointerDown", "toggle"];
|
|
124
|
-
static events = ["change"];
|
|
137
|
+
static events = ["change", "reconcile"];
|
|
125
138
|
/** Where the divider sat before the current collapse; `null` when unknown. */
|
|
126
139
|
#valueBeforeCollapse = null;
|
|
140
|
+
/**
|
|
141
|
+
* The position last painted: moved by each move the user makes — every step of
|
|
142
|
+
* a drag included — and by each repaint that finds a page-driven move.
|
|
143
|
+
*/
|
|
144
|
+
#painted = DEFAULT_VALUE;
|
|
145
|
+
/**
|
|
146
|
+
* The position last confirmed: the one connecting published, then moved by
|
|
147
|
+
* each `change` and `reconcile`. A drag moves the painted position step by
|
|
148
|
+
* step and this one only when it ends.
|
|
149
|
+
*/
|
|
150
|
+
#confirmed = DEFAULT_VALUE;
|
|
127
151
|
/** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
|
|
128
152
|
#dragAbort = null;
|
|
129
153
|
/** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */
|
|
@@ -133,8 +157,9 @@ var ResizableController = class extends Controller {
|
|
|
133
157
|
/** Folds the range and position inputs of one morph batch into a single paint. */
|
|
134
158
|
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
135
159
|
connect() {
|
|
136
|
-
this.element
|
|
160
|
+
TRANSIENT.reset(this.element);
|
|
137
161
|
this.#repaint.activate();
|
|
162
|
+
this.#confirmed = this.#position;
|
|
138
163
|
this.#render();
|
|
139
164
|
this.#cycleAbort = new AbortController();
|
|
140
165
|
this.element.addEventListener("keydown", this.#onCycleKeydown, {
|
|
@@ -251,9 +276,7 @@ var ResizableController = class extends Controller {
|
|
|
251
276
|
}
|
|
252
277
|
if (handled) {
|
|
253
278
|
event.preventDefault();
|
|
254
|
-
this
|
|
255
|
-
this.#render();
|
|
256
|
-
this.#dispatchChange();
|
|
279
|
+
this.#commit(nextValue);
|
|
257
280
|
}
|
|
258
281
|
}
|
|
259
282
|
/** Collapses the primary pane to its minimum, or returns it to the last position. */
|
|
@@ -261,22 +284,19 @@ var ResizableController = class extends Controller {
|
|
|
261
284
|
const { min, max } = this.#range;
|
|
262
285
|
if (this.#position > min) {
|
|
263
286
|
this.#valueBeforeCollapse = this.#position;
|
|
264
|
-
this
|
|
287
|
+
this.#commit(min);
|
|
265
288
|
} else {
|
|
266
|
-
|
|
289
|
+
const restored = this.#valueBeforeCollapse ?? max;
|
|
267
290
|
this.#valueBeforeCollapse = null;
|
|
291
|
+
this.#commit(restored);
|
|
268
292
|
}
|
|
269
|
-
this.#render();
|
|
270
|
-
this.#dispatchChange();
|
|
271
293
|
}
|
|
272
294
|
#onPointerMove = (event) => {
|
|
273
295
|
if (!this.hasSeparatorTarget) return;
|
|
274
296
|
const rect = this.element.getBoundingClientRect();
|
|
275
297
|
const raw = this.#isVertical ? (event.clientX - rect.left) / rect.width : (event.clientY - rect.top) / rect.height;
|
|
276
298
|
const fraction = Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0;
|
|
277
|
-
|
|
278
|
-
this.valueValue = Math.max(min, Math.min(Math.round(fraction * 100), max));
|
|
279
|
-
this.#render();
|
|
299
|
+
this.#move(Math.round(fraction * 100));
|
|
280
300
|
};
|
|
281
301
|
#onPointerUp = (event) => {
|
|
282
302
|
this.element.removeAttribute("data-dragging");
|
|
@@ -285,21 +305,50 @@ var ResizableController = class extends Controller {
|
|
|
285
305
|
if (this.hasSeparatorTarget) {
|
|
286
306
|
this.separatorTarget.releasePointerCapture(event.pointerId);
|
|
287
307
|
}
|
|
288
|
-
this.#
|
|
308
|
+
this.#confirm();
|
|
289
309
|
};
|
|
310
|
+
/** Moves to a position the user chose and reports it if it moved. */
|
|
311
|
+
#commit(raw) {
|
|
312
|
+
this.#move(raw);
|
|
313
|
+
this.#confirm();
|
|
314
|
+
}
|
|
290
315
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
316
|
+
* Stores the clamped position the user moved to and paints it. The painted
|
|
317
|
+
* position moves with it before anything is reported, so the repaint the Value
|
|
318
|
+
* write schedules — and any move a subscriber makes while `change` is
|
|
319
|
+
* dispatched — is measured from this position.
|
|
320
|
+
*/
|
|
321
|
+
#move(raw) {
|
|
322
|
+
const { min, max } = this.#range;
|
|
323
|
+
const position = Math.max(min, Math.min(raw, max));
|
|
324
|
+
this.valueValue = position;
|
|
325
|
+
this.#painted = position;
|
|
326
|
+
this.#paint();
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Publishes the position the Values declare without writing it back. A
|
|
330
|
+
* position the user did not paint is the page's move, reported once as
|
|
331
|
+
* `reconcile` unless it lands on the position last confirmed — a drag the page
|
|
332
|
+
* puts back where it started has nothing new to report.
|
|
294
333
|
*
|
|
295
334
|
* @stimeoRenderRoot
|
|
296
335
|
*/
|
|
297
336
|
#render() {
|
|
337
|
+
this.#paint();
|
|
338
|
+
const position = this.#position;
|
|
339
|
+
if (position === this.#painted) return;
|
|
340
|
+
this.#painted = position;
|
|
341
|
+
if (position === this.#confirmed) return;
|
|
342
|
+
this.#confirmed = position;
|
|
343
|
+
this.dispatch("reconcile", { detail: this.#detail(position) });
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Publishes the position: the fraction consumer CSS multiplies a pane by, and
|
|
347
|
+
* the range assistive tech reads off the separator.
|
|
348
|
+
*/
|
|
349
|
+
#paint() {
|
|
298
350
|
const { min, max } = this.#range;
|
|
299
351
|
const position = this.#position;
|
|
300
|
-
if (this.valueValue !== position) {
|
|
301
|
-
this.valueValue = position;
|
|
302
|
-
}
|
|
303
352
|
this.element.style.setProperty("--stimeo--resizable-fraction", String(position / 100));
|
|
304
353
|
if (this.hasSeparatorTarget) {
|
|
305
354
|
this.separatorTarget.setAttribute("aria-valuenow", String(position));
|
|
@@ -307,9 +356,20 @@ var ResizableController = class extends Controller {
|
|
|
307
356
|
this.separatorTarget.setAttribute("aria-valuemax", String(max));
|
|
308
357
|
}
|
|
309
358
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Reports the published position as `change` when the user's operation moved
|
|
361
|
+
* it from the position last confirmed. A key at an edge, a press released
|
|
362
|
+
* without moving and a drag released where it started report nothing.
|
|
363
|
+
*/
|
|
364
|
+
#confirm() {
|
|
365
|
+
const position = this.#position;
|
|
366
|
+
if (position === this.#confirmed) return;
|
|
367
|
+
this.#confirmed = position;
|
|
368
|
+
this.dispatch("change", { detail: this.#detail(position) });
|
|
369
|
+
}
|
|
370
|
+
/** The detail both events carry for `position`. */
|
|
371
|
+
#detail(position) {
|
|
372
|
+
return { value: position, fraction: position / 100 };
|
|
313
373
|
}
|
|
314
374
|
/** The declared range after validation; an unreadable bound uses its default. */
|
|
315
375
|
get #range() {
|