stimeo-ui 0.15.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 +178 -0
- data/README.md +120 -0
- data/dist/cable/index.js +63 -15
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +8 -1
- data/dist/controllers/auto_submit_controller.js +43 -2
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +9 -8
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +323 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +54 -10
- 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 +56 -11
- 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 +205 -58
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +139 -27
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +14 -1
- 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 +305 -46
- 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 +16 -2
- data/dist/controllers/highlight_controller.js +19 -2
- 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/masonry_controller.js +0 -2
- 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 +14 -1
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +84 -13
- data/dist/controllers/overflow_menu_controller.js +112 -41
- 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 +69 -2
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +5 -1
- data/dist/controllers/scroll_visibility_controller.js +0 -1
- data/dist/controllers/scrollspy_controller.js +6 -0
- 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 +11 -1
- 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 +75 -5
- data/dist/controllers/submit_once_controller.js +16 -3
- 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/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +337 -54
- 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 +4588 -1535
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -18,6 +18,16 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
18
18
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// src/utils/field_mirror.ts
|
|
22
|
+
function writeField(field, value) {
|
|
23
|
+
if (field.value === value) return false;
|
|
24
|
+
field.value = value;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function commitField(target) {
|
|
28
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
// src/utils/microtask_coalescer.ts
|
|
22
32
|
var MicrotaskCoalescer = class {
|
|
23
33
|
#run;
|
|
@@ -53,54 +63,46 @@ var MicrotaskCoalescer = class {
|
|
|
53
63
|
|
|
54
64
|
// src/utils/owned_pointer_session.ts
|
|
55
65
|
var OwnedPointerSession = class {
|
|
56
|
-
pointerId;
|
|
66
|
+
#pointerId;
|
|
57
67
|
#owner;
|
|
58
68
|
#handlers;
|
|
59
69
|
#abort = new AbortController();
|
|
60
70
|
#active = true;
|
|
61
71
|
constructor(start, owner, handlers) {
|
|
62
|
-
this
|
|
72
|
+
this.#pointerId = start.pointerId;
|
|
63
73
|
this.#owner = owner;
|
|
64
74
|
this.#handlers = handlers;
|
|
65
75
|
const { signal } = this.#abort;
|
|
66
76
|
owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
|
|
67
77
|
owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
|
|
68
78
|
owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
|
|
69
|
-
owner.addEventListener("lostpointercapture", this.#onLostCapture, { signal });
|
|
70
79
|
try {
|
|
71
|
-
owner.setPointerCapture?.(this
|
|
80
|
+
owner.setPointerCapture?.(this.#pointerId);
|
|
72
81
|
} catch {
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
|
-
/**
|
|
76
|
-
get active() {
|
|
77
|
-
return this.#active;
|
|
78
|
-
}
|
|
79
|
-
/** Whether `event` belongs to the initiating pointer of the live session. */
|
|
80
|
-
owns(event) {
|
|
81
|
-
return this.#active && event.pointerId === this.pointerId;
|
|
82
|
-
}
|
|
83
|
-
/** Releases capture/listeners and invokes the end callback exactly once. */
|
|
84
|
+
/** Releases capture and listeners, then reports the end exactly once. */
|
|
84
85
|
end() {
|
|
86
|
+
this.#finish("teardown");
|
|
87
|
+
}
|
|
88
|
+
#finish(kind) {
|
|
85
89
|
if (!this.#active) return;
|
|
86
90
|
this.#active = false;
|
|
87
91
|
this.#abort.abort();
|
|
88
92
|
try {
|
|
89
|
-
this.#owner.releasePointerCapture?.(this
|
|
93
|
+
this.#owner.releasePointerCapture?.(this.#pointerId);
|
|
90
94
|
} catch {
|
|
91
95
|
}
|
|
92
|
-
this.#handlers.end?.();
|
|
96
|
+
this.#handlers.end?.(kind);
|
|
97
|
+
}
|
|
98
|
+
#owns(event) {
|
|
99
|
+
return this.#active && event.pointerId === this.#pointerId;
|
|
93
100
|
}
|
|
94
101
|
#onMove = (event) => {
|
|
95
|
-
if (this
|
|
102
|
+
if (this.#owns(event)) this.#handlers.move(event);
|
|
96
103
|
};
|
|
97
104
|
#onEndEvent = (event) => {
|
|
98
|
-
if (this
|
|
99
|
-
};
|
|
100
|
-
#onLostCapture = (event) => {
|
|
101
|
-
const pointerId = event.pointerId;
|
|
102
|
-
if (typeof pointerId === "number" && pointerId !== this.pointerId) return;
|
|
103
|
-
this.end();
|
|
105
|
+
if (this.#owns(event)) this.#finish(event.type === "pointerup" ? "up" : "cancel");
|
|
104
106
|
};
|
|
105
107
|
};
|
|
106
108
|
|
|
@@ -232,7 +234,7 @@ var END_PROPERTY = "--stimeo--range-slider-end";
|
|
|
232
234
|
var DEFAULT_MIN = 0;
|
|
233
235
|
var DEFAULT_MAX = 100;
|
|
234
236
|
var RangeSliderController = class extends Controller {
|
|
235
|
-
static targets = ["track", "startThumb", "endThumb"];
|
|
237
|
+
static targets = ["track", "startThumb", "endThumb", "startField", "endField"];
|
|
236
238
|
static values = {
|
|
237
239
|
min: { type: Number, default: DEFAULT_MIN },
|
|
238
240
|
max: { type: Number, default: DEFAULT_MAX },
|
|
@@ -242,9 +244,14 @@ var RangeSliderController = class extends Controller {
|
|
|
242
244
|
logicalTrack: { type: Boolean, default: false }
|
|
243
245
|
};
|
|
244
246
|
static actions = ["onKeydown", "onPointerDown"];
|
|
245
|
-
static events = ["change"];
|
|
247
|
+
static events = ["change", "reconcile"];
|
|
246
248
|
/** One initiating pointer owns each live drag and its stable target snapshot. */
|
|
247
249
|
#drag = null;
|
|
250
|
+
/**
|
|
251
|
+
* The pair last published: taken on connect, then moved by each user commit
|
|
252
|
+
* and by each repaint that reports a page-driven move.
|
|
253
|
+
*/
|
|
254
|
+
#settled = { start: DEFAULT_MIN, end: DEFAULT_MAX };
|
|
248
255
|
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
249
256
|
get #mirrored() {
|
|
250
257
|
return this.logicalTrackValue && isRtl(this.element);
|
|
@@ -254,11 +261,14 @@ var RangeSliderController = class extends Controller {
|
|
|
254
261
|
* pass Stimulus delivers before `connect()`.
|
|
255
262
|
*/
|
|
256
263
|
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
264
|
+
/**
|
|
265
|
+
* Renders the normalized, ordered pair without writing it back, and takes it
|
|
266
|
+
* as the baseline, so connecting reports nothing.
|
|
267
|
+
*/
|
|
257
268
|
connect() {
|
|
258
269
|
this.#repaint.activate();
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
this.#commit(pair.start, pair.end, null, false);
|
|
270
|
+
this.#settled = this.#currentPair(this.#effectiveRange);
|
|
271
|
+
this.#render();
|
|
262
272
|
}
|
|
263
273
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
264
274
|
disconnect() {
|
|
@@ -313,6 +323,14 @@ var RangeSliderController = class extends Controller {
|
|
|
313
323
|
}
|
|
314
324
|
this.#repaint.schedule();
|
|
315
325
|
}
|
|
326
|
+
/** Fills a start field inserted or replaced at runtime on the next repaint. */
|
|
327
|
+
startFieldTargetConnected() {
|
|
328
|
+
this.#repaint.schedule();
|
|
329
|
+
}
|
|
330
|
+
/** Fills an end field inserted or replaced at runtime on the next repaint. */
|
|
331
|
+
endFieldTargetConnected() {
|
|
332
|
+
this.#repaint.schedule();
|
|
333
|
+
}
|
|
316
334
|
/** Drops a stale end-thumb reference without orphaning the track gesture. */
|
|
317
335
|
endThumbTargetDisconnected(thumb) {
|
|
318
336
|
if (this.#drag?.kind === "end" && this.#drag.thumb === thumb) this.#drag.thumb = null;
|
|
@@ -439,44 +457,72 @@ var RangeSliderController = class extends Controller {
|
|
|
439
457
|
#moveThumb(kind, raw) {
|
|
440
458
|
const pair = this.#currentPair(this.#effectiveRange);
|
|
441
459
|
if (kind === "start") {
|
|
442
|
-
this.#commit(raw, pair.end, "start"
|
|
460
|
+
this.#commit(raw, pair.end, "start");
|
|
443
461
|
} else {
|
|
444
|
-
this.#commit(pair.start, raw, "end"
|
|
462
|
+
this.#commit(pair.start, raw, "end");
|
|
445
463
|
}
|
|
446
464
|
}
|
|
447
465
|
/**
|
|
448
|
-
* Clamps and snaps
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
466
|
+
* Clamps and snaps the pair a user's move of the `moving` thumb produced,
|
|
467
|
+
* stops that thumb at its partner, stores the pair, and reflects it onto the
|
|
468
|
+
* thumbs' ARIA attributes and the range custom properties. The pair is
|
|
469
|
+
* reported as `change` when it differs from the pair last published — not from
|
|
470
|
+
* the pair the move started from — so a page write no repaint has published
|
|
471
|
+
* yet is reported once, as this change, and a move that ends on the pair last
|
|
472
|
+
* published reports nothing. The baseline moves before any report goes out,
|
|
473
|
+
* so the repaint the Value writes schedule — and any move a subscriber makes
|
|
474
|
+
* while the report is dispatched — is measured from this pair.
|
|
453
475
|
*/
|
|
454
|
-
#commit(start, end, moving
|
|
476
|
+
#commit(start, end, moving) {
|
|
455
477
|
const range = this.#effectiveRange;
|
|
456
|
-
const previous = this.#currentPair(range);
|
|
457
478
|
let nextStart = snapSteppedValue(start, range);
|
|
458
479
|
let nextEnd = snapSteppedValue(end, range);
|
|
459
480
|
if (nextStart > nextEnd) {
|
|
460
481
|
if (moving === "start") nextStart = nextEnd;
|
|
461
|
-
else
|
|
462
|
-
else [nextStart, nextEnd] = [nextEnd, nextStart];
|
|
482
|
+
else nextEnd = nextStart;
|
|
463
483
|
}
|
|
484
|
+
const pair = { start: nextStart, end: nextEnd };
|
|
485
|
+
const reported = this.#settled;
|
|
486
|
+
this.#settled = pair;
|
|
464
487
|
if (!Object.is(this.startValue, nextStart)) this.startValue = nextStart;
|
|
465
488
|
if (!Object.is(this.endValue, nextEnd)) this.endValue = nextEnd;
|
|
466
|
-
const pair = { start: nextStart, end: nextEnd };
|
|
467
489
|
this.#renderPair(pair, range);
|
|
468
|
-
|
|
490
|
+
this.#mirrorFields(pair, true);
|
|
491
|
+
if (nextStart !== reported.start || nextEnd !== reported.end) {
|
|
469
492
|
this.dispatch("change", { detail: { start: nextStart, end: nextEnd } });
|
|
470
493
|
}
|
|
471
494
|
}
|
|
472
495
|
/**
|
|
473
|
-
* Reflects
|
|
496
|
+
* Reflects the Values the page supplied without writing them back. A published
|
|
497
|
+
* pair that moved from the last one is reported once as `reconcile`; the
|
|
498
|
+
* fields follow without a native `change`.
|
|
474
499
|
*
|
|
475
500
|
* @stimeoRenderRoot
|
|
476
501
|
*/
|
|
477
502
|
#render() {
|
|
478
503
|
const range = this.#effectiveRange;
|
|
479
|
-
this.#
|
|
504
|
+
const pair = this.#currentPair(range);
|
|
505
|
+
this.#renderPair(pair, range);
|
|
506
|
+
this.#mirrorFields(pair, false);
|
|
507
|
+
const settled = this.#settled;
|
|
508
|
+
if (pair.start === settled.start && pair.end === settled.end) return;
|
|
509
|
+
this.#settled = pair;
|
|
510
|
+
this.dispatch("reconcile", { detail: { start: pair.start, end: pair.end } });
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Mirrors the pair into the optional form fields, reporting only a user's
|
|
514
|
+
* move. Each end is compared on its own, so dragging one thumb never reports
|
|
515
|
+
* a commit from the field the other thumb owns.
|
|
516
|
+
*/
|
|
517
|
+
#mirrorFields(pair, notify) {
|
|
518
|
+
if (this.hasStartFieldTarget) {
|
|
519
|
+
const field = this.startFieldTarget;
|
|
520
|
+
if (writeField(field, String(pair.start)) && notify) commitField(field);
|
|
521
|
+
}
|
|
522
|
+
if (this.hasEndFieldTarget) {
|
|
523
|
+
const field = this.endFieldTarget;
|
|
524
|
+
if (writeField(field, String(pair.end)) && notify) commitField(field);
|
|
525
|
+
}
|
|
480
526
|
}
|
|
481
527
|
/** Reflects one normalized pair onto both thumbs and CSS properties. */
|
|
482
528
|
#renderPair(pair, range) {
|
|
@@ -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) => {
|
|
@@ -139,13 +139,65 @@ var LayoutObserver = class {
|
|
|
139
139
|
}
|
|
140
140
|
};
|
|
141
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));
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
142
193
|
// src/controllers/read_more_controller.ts
|
|
143
194
|
var ReadMoreController = class extends Controller {
|
|
144
|
-
static targets = ["content", "trigger"];
|
|
195
|
+
static targets = ["content", "trigger", "expandedLabel", "collapsedLabel"];
|
|
145
196
|
static values = {
|
|
146
197
|
collapsed: { type: Boolean, default: true }
|
|
147
198
|
};
|
|
148
199
|
static actions = ["toggle"];
|
|
200
|
+
static events = ["close", "open"];
|
|
149
201
|
#connected = false;
|
|
150
202
|
#collapsed = true;
|
|
151
203
|
#observedContent = null;
|
|
@@ -154,6 +206,11 @@ var ReadMoreController = class extends Controller {
|
|
|
154
206
|
if (this.#connected) this.#evaluateOverflow();
|
|
155
207
|
};
|
|
156
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
|
+
});
|
|
157
214
|
/** Holds the trigger's hide back while it has focus; re-evaluates on blur. */
|
|
158
215
|
#deferredHide = new BlurDeferral(() => {
|
|
159
216
|
this.#update();
|
|
@@ -181,12 +238,21 @@ var ReadMoreController = class extends Controller {
|
|
|
181
238
|
this.#deferredHide.release(trigger);
|
|
182
239
|
this.#syncTargets();
|
|
183
240
|
}
|
|
241
|
+
expandedLabelTargetConnected() {
|
|
242
|
+
this.#syncTargets();
|
|
243
|
+
}
|
|
244
|
+
collapsedLabelTargetConnected() {
|
|
245
|
+
this.#syncTargets();
|
|
246
|
+
}
|
|
184
247
|
/** Toggles between the collapsed (clamped) and expanded states. */
|
|
185
|
-
toggle() {
|
|
248
|
+
toggle(event) {
|
|
186
249
|
if (!this.#connected) return;
|
|
187
250
|
this.#collapsed = !this.#collapsed;
|
|
188
251
|
this.#reflect();
|
|
189
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 });
|
|
190
256
|
}
|
|
191
257
|
#initialCollapsed() {
|
|
192
258
|
if (this.hasContentTarget) {
|
|
@@ -202,6 +268,7 @@ var ReadMoreController = class extends Controller {
|
|
|
202
268
|
}
|
|
203
269
|
if (this.hasTriggerTarget) {
|
|
204
270
|
this.triggerTarget.setAttribute("aria-expanded", this.#collapsed ? "false" : "true");
|
|
271
|
+
this.#labels.reflect(this.triggerTarget, !this.#collapsed);
|
|
205
272
|
}
|
|
206
273
|
}
|
|
207
274
|
#syncTargets() {
|
|
@@ -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() {
|
|
@@ -518,7 +518,11 @@ var ScrollAreaController = class extends Controller {
|
|
|
518
518
|
this.#overflowing = false;
|
|
519
519
|
this.#lastEdge = null;
|
|
520
520
|
}
|
|
521
|
-
/**
|
|
521
|
+
/**
|
|
522
|
+
* Runs the full structural and positional measurement pass.
|
|
523
|
+
*
|
|
524
|
+
* @stimeoRenderRoot
|
|
525
|
+
*/
|
|
522
526
|
#refresh() {
|
|
523
527
|
const viewport = this.#viewport;
|
|
524
528
|
if (!viewport) return;
|
|
@@ -332,7 +332,6 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
332
332
|
this.element.setAttribute("data-state", next ? "visible" : "hidden");
|
|
333
333
|
if (notify) this.dispatch("change", { detail: { visible: next } });
|
|
334
334
|
}
|
|
335
|
-
/** Resolves the scroll source from `root` (falling back to the window). */
|
|
336
335
|
/**
|
|
337
336
|
* The focus owner inside the target, or `null` when focus is elsewhere.
|
|
338
337
|
*
|
|
@@ -422,6 +422,10 @@ var ScrollspyController = class extends Controller {
|
|
|
422
422
|
this.#scrollSource?.removeEventListener("scroll", this.#onScroll);
|
|
423
423
|
this.#scrollSource = null;
|
|
424
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* @stimeoRuntimeOnly `rootMargin` wires the observer this call installs; the active section it
|
|
427
|
+
* republishes is the one already recorded.
|
|
428
|
+
*/
|
|
425
429
|
#initializeObserver() {
|
|
426
430
|
this.#watcher.stop();
|
|
427
431
|
this.#intersectionStates.clear();
|
|
@@ -467,6 +471,8 @@ var ScrollspyController = class extends Controller {
|
|
|
467
471
|
* threshold) against a freshly computed trigger line mixes two moments in
|
|
468
472
|
* time, which would make the result depend on how the reader arrived at a
|
|
469
473
|
* position — a smooth scroll and an instant jump to the same offset disagree.
|
|
474
|
+
*
|
|
475
|
+
* @stimeoRenderRoot
|
|
470
476
|
*/
|
|
471
477
|
#evaluateActiveSection() {
|
|
472
478
|
const rootEl = this.#scrollRoot();
|