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
|
|
|
@@ -229,7 +231,7 @@ function finiteFallback(min, max) {
|
|
|
229
231
|
// src/controllers/slider_controller.ts
|
|
230
232
|
var FRACTION_PROPERTY = "--stimeo--slider-fraction";
|
|
231
233
|
var SliderController = class extends Controller {
|
|
232
|
-
static targets = ["track", "thumb"];
|
|
234
|
+
static targets = ["track", "thumb", "field"];
|
|
233
235
|
static values = {
|
|
234
236
|
min: { type: Number, default: 0 },
|
|
235
237
|
max: { type: Number, default: 100 },
|
|
@@ -238,38 +240,47 @@ var SliderController = class extends Controller {
|
|
|
238
240
|
logicalTrack: { type: Boolean, default: false }
|
|
239
241
|
};
|
|
240
242
|
static actions = ["onKeydown", "onPointerDown"];
|
|
241
|
-
static events = ["change"];
|
|
243
|
+
static events = ["change", "reconcile"];
|
|
242
244
|
/** One initiating pointer owns each live drag and its stable target snapshot. */
|
|
243
245
|
#drag = null;
|
|
244
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* The value last published: taken on connect, then moved by each user commit
|
|
248
|
+
* and by each repaint that reports a page-driven move.
|
|
249
|
+
*/
|
|
250
|
+
#settled = 0;
|
|
251
|
+
/** Collapses a morph that swaps several render Values into one repaint. */
|
|
245
252
|
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
246
253
|
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
247
254
|
get #mirrored() {
|
|
248
255
|
return this.logicalTrackValue && isRtl(this.element);
|
|
249
256
|
}
|
|
250
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* Renders the normalized value as the starting position without writing it
|
|
259
|
+
* back, and takes it as the baseline, so connecting reports nothing.
|
|
260
|
+
*/
|
|
251
261
|
connect() {
|
|
252
262
|
this.#repaint.activate();
|
|
253
|
-
this.#
|
|
263
|
+
this.#settled = this.#currentValue();
|
|
264
|
+
this.#render();
|
|
254
265
|
}
|
|
255
266
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
256
267
|
disconnect() {
|
|
257
268
|
this.#repaint.cancel();
|
|
258
269
|
this.#endDrag();
|
|
259
270
|
}
|
|
260
|
-
/**
|
|
271
|
+
/** Repaints a minimum changed by application code or a Turbo morph. */
|
|
261
272
|
minValueChanged() {
|
|
262
273
|
this.#repaint.schedule();
|
|
263
274
|
}
|
|
264
|
-
/**
|
|
275
|
+
/** Repaints a maximum changed by application code or a Turbo morph. */
|
|
265
276
|
maxValueChanged() {
|
|
266
277
|
this.#repaint.schedule();
|
|
267
278
|
}
|
|
268
|
-
/**
|
|
279
|
+
/** Repaints a step changed by application code or a Turbo morph. */
|
|
269
280
|
stepValueChanged() {
|
|
270
281
|
this.#repaint.schedule();
|
|
271
282
|
}
|
|
272
|
-
/**
|
|
283
|
+
/** Repaints a value changed by application code or a Turbo morph. */
|
|
273
284
|
valueValueChanged() {
|
|
274
285
|
this.#repaint.schedule();
|
|
275
286
|
}
|
|
@@ -284,6 +295,10 @@ var SliderController = class extends Controller {
|
|
|
284
295
|
}
|
|
285
296
|
this.#repaint.schedule();
|
|
286
297
|
}
|
|
298
|
+
/** Fills a form field inserted or replaced at runtime on the next repaint. */
|
|
299
|
+
fieldTargetConnected() {
|
|
300
|
+
this.#repaint.schedule();
|
|
301
|
+
}
|
|
287
302
|
/** Drops the stale focus owner while allowing a live track gesture to continue. */
|
|
288
303
|
thumbTargetDisconnected(thumb) {
|
|
289
304
|
if (this.#drag?.thumb === thumb) this.#drag.thumb = null;
|
|
@@ -296,22 +311,22 @@ var SliderController = class extends Controller {
|
|
|
296
311
|
onKeydown(event) {
|
|
297
312
|
if (isReservedArrowChord(event)) return;
|
|
298
313
|
let next = null;
|
|
299
|
-
const
|
|
314
|
+
const declared = this.valueValue;
|
|
300
315
|
const range = this.#steppedRange;
|
|
301
316
|
switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {
|
|
302
317
|
case "ArrowRight":
|
|
303
318
|
case "ArrowUp":
|
|
304
|
-
next = stepSteppedValue(
|
|
319
|
+
next = stepSteppedValue(declared, 1, range);
|
|
305
320
|
break;
|
|
306
321
|
case "ArrowLeft":
|
|
307
322
|
case "ArrowDown":
|
|
308
|
-
next = stepSteppedValue(
|
|
323
|
+
next = stepSteppedValue(declared, -1, range);
|
|
309
324
|
break;
|
|
310
325
|
case "PageUp":
|
|
311
|
-
next = stepSteppedValue(
|
|
326
|
+
next = stepSteppedValue(declared, 10, range);
|
|
312
327
|
break;
|
|
313
328
|
case "PageDown":
|
|
314
|
-
next = stepSteppedValue(
|
|
329
|
+
next = stepSteppedValue(declared, -10, range);
|
|
315
330
|
break;
|
|
316
331
|
case "Home":
|
|
317
332
|
next = this.minValue;
|
|
@@ -323,7 +338,7 @@ var SliderController = class extends Controller {
|
|
|
323
338
|
return;
|
|
324
339
|
}
|
|
325
340
|
event.preventDefault();
|
|
326
|
-
this.#commit(next
|
|
341
|
+
this.#commit(next);
|
|
327
342
|
}
|
|
328
343
|
/** Begins a pointer drag: sets the value and tracks subsequent movement. */
|
|
329
344
|
onPointerDown(event) {
|
|
@@ -334,7 +349,7 @@ var SliderController = class extends Controller {
|
|
|
334
349
|
const value = this.#valueFromClientX(event.clientX, mirrored, track);
|
|
335
350
|
if (value === null) return;
|
|
336
351
|
event.preventDefault();
|
|
337
|
-
this.#commit(value
|
|
352
|
+
this.#commit(value);
|
|
338
353
|
thumb.focus();
|
|
339
354
|
const drag = { pointer: null, track, thumb };
|
|
340
355
|
drag.pointer = new OwnedPointerSession(event, track, {
|
|
@@ -344,7 +359,7 @@ var SliderController = class extends Controller {
|
|
|
344
359
|
return;
|
|
345
360
|
}
|
|
346
361
|
const moved = this.#valueFromClientX(move.clientX, mirrored, track);
|
|
347
|
-
if (moved !== null) this.#commit(moved
|
|
362
|
+
if (moved !== null) this.#commit(moved);
|
|
348
363
|
},
|
|
349
364
|
end: () => {
|
|
350
365
|
if (this.#drag === drag) this.#drag = null;
|
|
@@ -361,24 +376,44 @@ var SliderController = class extends Controller {
|
|
|
361
376
|
return this.minValue + fraction * (this.maxValue - this.minValue);
|
|
362
377
|
}
|
|
363
378
|
/**
|
|
364
|
-
* Stores
|
|
365
|
-
*
|
|
366
|
-
*
|
|
379
|
+
* Stores the normalized value the user moved to, renders it synchronously for
|
|
380
|
+
* responsive input, and reports it as `change` when it differs from the value
|
|
381
|
+
* last published. The comparison is with the value last published, not with
|
|
382
|
+
* the value the move started from, so a page write no repaint has published
|
|
383
|
+
* yet is reported once, as this change, and a move that ends where the value
|
|
384
|
+
* was last published reports nothing. The baseline moves before either report
|
|
385
|
+
* goes out, so the repaint the Value write schedules — and any move a
|
|
386
|
+
* subscriber makes while the report is dispatched — is measured from this
|
|
387
|
+
* value.
|
|
367
388
|
*/
|
|
368
|
-
#commit(raw
|
|
369
|
-
const previous = this.#currentValue();
|
|
389
|
+
#commit(raw) {
|
|
370
390
|
const value = snapSteppedValue(raw, this.#steppedRange);
|
|
391
|
+
const reported = this.#settled;
|
|
392
|
+
this.#settled = value;
|
|
371
393
|
if (!Object.is(this.valueValue, value)) this.valueValue = value;
|
|
372
394
|
this.#renderValue(value);
|
|
373
|
-
|
|
395
|
+
this.#mirrorField(value, true);
|
|
396
|
+
if (value !== reported) this.dispatch("change", { detail: { value } });
|
|
397
|
+
}
|
|
398
|
+
/** Mirrors `value` into the optional form field, reporting only a user's move. */
|
|
399
|
+
#mirrorField(value, notify) {
|
|
400
|
+
if (!this.hasFieldTarget) return;
|
|
401
|
+
if (writeField(this.fieldTarget, String(value)) && notify) commitField(this.fieldTarget);
|
|
374
402
|
}
|
|
375
403
|
/**
|
|
376
|
-
* Reflects the normalized current Value without
|
|
404
|
+
* Reflects the normalized current Value without writing it back. A published
|
|
405
|
+
* value that moved from the last one is reported once as `reconcile`; the
|
|
406
|
+
* field follows without a native `change`.
|
|
377
407
|
*
|
|
378
408
|
* @stimeoRenderRoot
|
|
379
409
|
*/
|
|
380
410
|
#render() {
|
|
381
|
-
this.#
|
|
411
|
+
const value = this.#currentValue();
|
|
412
|
+
this.#renderValue(value);
|
|
413
|
+
this.#mirrorField(value, false);
|
|
414
|
+
if (value === this.#settled) return;
|
|
415
|
+
this.#settled = value;
|
|
416
|
+
this.dispatch("reconcile", { detail: { value } });
|
|
382
417
|
}
|
|
383
418
|
/** Reflects one normalized value on the current target and CSS output. */
|
|
384
419
|
#renderValue(value) {
|
|
@@ -121,6 +121,7 @@ var SmartStickyHeaderController = class extends Controller {
|
|
|
121
121
|
this.element.removeEventListener("focusin", this.#onFocusin);
|
|
122
122
|
this.#frames.cancel();
|
|
123
123
|
}
|
|
124
|
+
/** @stimeoRenderRoot */
|
|
124
125
|
#measure() {
|
|
125
126
|
const y = scrollOffset(this.#scrollerEl);
|
|
126
127
|
if (y <= this.#offset) {
|
|
@@ -129,10 +130,19 @@ var SmartStickyHeaderController = class extends Controller {
|
|
|
129
130
|
return;
|
|
130
131
|
}
|
|
131
132
|
const delta = y - this.#lastY;
|
|
132
|
-
if (
|
|
133
|
+
if (this.#isJitter(delta)) return;
|
|
133
134
|
this.#lastY = y;
|
|
134
135
|
this.#apply(delta > 0);
|
|
135
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Whether one scroll step is too small to act on.
|
|
139
|
+
*
|
|
140
|
+
* @stimeoRuntimeOnly `tolerance` is the allowance one scroll step is compared against; the
|
|
141
|
+
* header's state at rest does not depend on it.
|
|
142
|
+
*/
|
|
143
|
+
#isJitter(delta) {
|
|
144
|
+
return Math.abs(delta) < this.toleranceValue;
|
|
145
|
+
}
|
|
136
146
|
/**
|
|
137
147
|
* Reflects the state onto the hook and emits `change` on transitions.
|
|
138
148
|
*
|
|
@@ -57,7 +57,20 @@ var MicrotaskCoalescer = class {
|
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
// src/utils/transient_hooks.ts
|
|
61
|
+
var TransientHooks = class {
|
|
62
|
+
#attributes;
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this.#attributes = options.attributes;
|
|
65
|
+
}
|
|
66
|
+
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
67
|
+
reset(element) {
|
|
68
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
60
72
|
// src/controllers/sortable_controller.ts
|
|
73
|
+
var TRANSIENT = new TransientHooks({ attributes: ["data-sortable-dragging"] });
|
|
61
74
|
var SortableController = class extends Controller {
|
|
62
75
|
static targets = ["list", "item"];
|
|
63
76
|
static values = {
|
|
@@ -72,7 +85,7 @@ var SortableController = class extends Controller {
|
|
|
72
85
|
/** Defers the lost-item check to after the mutation batch (see below). */
|
|
73
86
|
#settle = new MicrotaskCoalescer(() => this.#dropLostSession());
|
|
74
87
|
connect() {
|
|
75
|
-
this.element
|
|
88
|
+
TRANSIENT.reset(this.element);
|
|
76
89
|
this.#settle.activate();
|
|
77
90
|
this.element.addEventListener("stimeo--pointer-drag:start", this.#onDragStart);
|
|
78
91
|
this.element.addEventListener("stimeo--pointer-drag:move", this.#onDragMove);
|
|
@@ -295,6 +308,8 @@ var SortableController = class extends Controller {
|
|
|
295
308
|
* Only transitions reach here — the pickup, a step that actually changed the
|
|
296
309
|
* landing slot, and the single end of the session — so a pointer crossing the
|
|
297
310
|
* same slot twice or an arrow clamped at an end stays silent.
|
|
311
|
+
*
|
|
312
|
+
* @stimeoRuntimeOnly The texts word the steps of one reorder, spoken as they happen.
|
|
298
313
|
*/
|
|
299
314
|
#announce(key, item) {
|
|
300
315
|
const template = this.#announceTemplate(key);
|
|
@@ -323,7 +338,7 @@ var SortableController = class extends Controller {
|
|
|
323
338
|
}
|
|
324
339
|
/** The announced item name: the authored override, else its collapsed text. */
|
|
325
340
|
#nameOf(item) {
|
|
326
|
-
const authored = item.getAttribute(
|
|
341
|
+
const authored = item.getAttribute(`data-${this.identifier}-name`);
|
|
327
342
|
if (authored) return authored;
|
|
328
343
|
return (item.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
329
344
|
}
|
|
@@ -292,7 +292,11 @@ var SpinnerController = class extends Controller {
|
|
|
292
292
|
this.#setBusy(false);
|
|
293
293
|
this.#floor.schedule(this.minDurationValue, () => this.#hide());
|
|
294
294
|
}
|
|
295
|
-
/**
|
|
295
|
+
/**
|
|
296
|
+
* Reveals the indicator, marks the moment shown, and announces via the live region.
|
|
297
|
+
*
|
|
298
|
+
* @stimeoRuntimeOnly `announceText` is the wording of the one announcement this show makes.
|
|
299
|
+
*/
|
|
296
300
|
#show() {
|
|
297
301
|
this.#floor.begin();
|
|
298
302
|
this.#setBusy(true);
|
|
@@ -301,7 +305,11 @@ var SpinnerController = class extends Controller {
|
|
|
301
305
|
this.dispatch("show", { detail: {} });
|
|
302
306
|
announce(fillTemplate(this.announceTextValue, {}));
|
|
303
307
|
}
|
|
304
|
-
/**
|
|
308
|
+
/**
|
|
309
|
+
* Hides the indicator and returns to the idle state.
|
|
310
|
+
*
|
|
311
|
+
* @stimeoRuntimeOnly `announceReadyText` is the wording of the one announcement this hide makes.
|
|
312
|
+
*/
|
|
305
313
|
#hide() {
|
|
306
314
|
if (this.hasIndicatorTarget) this.indicatorTarget.hidden = true;
|
|
307
315
|
this.element.setAttribute("data-state", "idle");
|
|
@@ -39,17 +39,17 @@ var MicrotaskCoalescer = class {
|
|
|
39
39
|
var StepIndicatorController = class extends Controller {
|
|
40
40
|
static targets = ["step"];
|
|
41
41
|
static values = {
|
|
42
|
-
|
|
42
|
+
index: { type: Number, default: 0 }
|
|
43
43
|
};
|
|
44
|
-
static actions = ["
|
|
44
|
+
static actions = ["setIndex"];
|
|
45
45
|
static events = ["change"];
|
|
46
46
|
/**
|
|
47
|
-
* Collapses a batch of step callbacks — and a morph that swaps `
|
|
47
|
+
* Collapses a batch of step callbacks — and a morph that swaps `index` with
|
|
48
48
|
* them — into one repaint. Replacing a list of N steps delivers N callbacks, and
|
|
49
49
|
* each one would otherwise rewrite every step's state.
|
|
50
50
|
*/
|
|
51
51
|
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
52
|
-
/** Renders the initial state from the `
|
|
52
|
+
/** Renders the initial state from the `index` value. */
|
|
53
53
|
connect() {
|
|
54
54
|
this.#repaint.activate();
|
|
55
55
|
this.#render();
|
|
@@ -66,32 +66,33 @@ var StepIndicatorController = class extends Controller {
|
|
|
66
66
|
stepTargetDisconnected() {
|
|
67
67
|
this.#repaint.schedule();
|
|
68
68
|
}
|
|
69
|
-
/** Repaints when application code (or a Turbo morph) changes `
|
|
70
|
-
|
|
69
|
+
/** Repaints when application code (or a Turbo morph) changes `index` at runtime. */
|
|
70
|
+
indexValueChanged() {
|
|
71
71
|
this.#repaint.schedule();
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
74
|
+
* Moves the indicator from an external event (`detail.index`, 0-based) and
|
|
75
|
+
* dispatches `change`. Out-of-range positions are clamped to the step set,
|
|
76
76
|
* and both sides of the no-op test are clamped, so moving onto the step an
|
|
77
|
-
* out-of-range `
|
|
77
|
+
* out-of-range `index` already renders is not reported as a change.
|
|
78
78
|
*/
|
|
79
|
-
|
|
80
|
-
const next = event.detail?.
|
|
79
|
+
setIndex(event) {
|
|
80
|
+
const next = event.detail?.index;
|
|
81
81
|
if (typeof next !== "number" || !Number.isFinite(next)) return;
|
|
82
82
|
const clamped = this.#clamp(next);
|
|
83
|
-
const
|
|
84
|
-
|
|
83
|
+
const previous = this.#clamp(this.indexValue);
|
|
84
|
+
const moved = clamped !== previous;
|
|
85
|
+
this.indexValue = clamped;
|
|
85
86
|
if (!moved) return;
|
|
86
87
|
this.#render();
|
|
87
88
|
this.dispatch("change", {
|
|
88
|
-
detail: {
|
|
89
|
+
detail: { index: clamped, previous, total: this.stepTargets.length }
|
|
89
90
|
});
|
|
90
91
|
}
|
|
91
92
|
/**
|
|
92
93
|
* Applies `data-state`, `aria-current`, and the progress ratio custom property.
|
|
93
94
|
*
|
|
94
|
-
* A pure function of the step set and `
|
|
95
|
+
* A pure function of the step set and `index`, so running it again writes the
|
|
95
96
|
* same values — which is what lets the action path paint synchronously (the event
|
|
96
97
|
* goes out after the DOM is updated) while a coalesced pass may still follow.
|
|
97
98
|
*
|
|
@@ -99,7 +100,7 @@ var StepIndicatorController = class extends Controller {
|
|
|
99
100
|
*/
|
|
100
101
|
#render() {
|
|
101
102
|
const total = this.stepTargets.length;
|
|
102
|
-
const current = this.#clamp(this.
|
|
103
|
+
const current = this.#clamp(this.indexValue);
|
|
103
104
|
this.stepTargets.forEach((step, index) => {
|
|
104
105
|
step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
|
|
105
106
|
if (index === current) {
|
|
@@ -113,7 +114,7 @@ var StepIndicatorController = class extends Controller {
|
|
|
113
114
|
}
|
|
114
115
|
/**
|
|
115
116
|
* Constrains an index to `[0, total-1]` (or `0` when there are no steps). A
|
|
116
|
-
* non-finite index falls back to the first step: `
|
|
117
|
+
* non-finite index falls back to the first step: `index` is read from markup,
|
|
117
118
|
* so an unparsable attribute arrives as `NaN` and would otherwise propagate
|
|
118
119
|
* into every state hook.
|
|
119
120
|
*/
|
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
// src/controllers/stepper_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/microtask_coalescer.ts
|
|
6
|
+
var MicrotaskCoalescer = class {
|
|
7
|
+
#run;
|
|
8
|
+
#queued = false;
|
|
9
|
+
#active = false;
|
|
10
|
+
#generation = 0;
|
|
11
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
12
|
+
constructor(run) {
|
|
13
|
+
this.#run = run;
|
|
14
|
+
}
|
|
15
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
16
|
+
activate() {
|
|
17
|
+
this.#active = true;
|
|
18
|
+
}
|
|
19
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
20
|
+
cancel() {
|
|
21
|
+
this.#active = false;
|
|
22
|
+
this.#queued = false;
|
|
23
|
+
this.#generation += 1;
|
|
24
|
+
}
|
|
25
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
26
|
+
schedule() {
|
|
27
|
+
if (!this.#active || this.#queued) return;
|
|
28
|
+
this.#queued = true;
|
|
29
|
+
const generation = this.#generation;
|
|
30
|
+
queueMicrotask(() => {
|
|
31
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
32
|
+
this.#queued = false;
|
|
33
|
+
this.#run();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
3
38
|
// src/controllers/stepper_controller.ts
|
|
4
39
|
var StepperController = class extends Controller {
|
|
5
40
|
static targets = ["step"];
|
|
@@ -8,20 +43,39 @@ var StepperController = class extends Controller {
|
|
|
8
43
|
linear: { type: Boolean, default: false }
|
|
9
44
|
};
|
|
10
45
|
static actions = ["goto", "next", "prev"];
|
|
11
|
-
static events = ["change"];
|
|
12
|
-
|
|
13
|
-
|
|
46
|
+
static events = ["change", "reconcile"];
|
|
47
|
+
/**
|
|
48
|
+
* Collapses the Value and target callbacks of one mutation into one pass, and
|
|
49
|
+
* refuses the ones Stimulus delivers before `connect()`, which renders itself.
|
|
50
|
+
*/
|
|
51
|
+
#repaint = new MicrotaskCoalescer(() => this.#reconcileStep());
|
|
52
|
+
/**
|
|
53
|
+
* The position shown current last, which the next move is measured from, or
|
|
54
|
+
* `null` before any step has been shown. While no step is present it keeps the
|
|
55
|
+
* last one.
|
|
56
|
+
*/
|
|
57
|
+
#shown = null;
|
|
58
|
+
/** Renders the initial state from `index`, clamped into the step range. */
|
|
14
59
|
connect() {
|
|
15
|
-
this.#
|
|
16
|
-
this.#
|
|
60
|
+
this.#repaint.activate();
|
|
61
|
+
this.#shown = this.#currentStep;
|
|
62
|
+
this.#render();
|
|
17
63
|
}
|
|
64
|
+
/** Drops a pending pass, so nothing renders or reports for a stepper that left. */
|
|
18
65
|
disconnect() {
|
|
19
|
-
this.#
|
|
66
|
+
this.#repaint.cancel();
|
|
20
67
|
}
|
|
21
68
|
/** Re-renders when Turbo Morph or application code changes `index` at runtime. */
|
|
22
69
|
indexValueChanged() {
|
|
23
|
-
|
|
24
|
-
|
|
70
|
+
this.#repaint.schedule();
|
|
71
|
+
}
|
|
72
|
+
/** Re-derives every step's state for a step added at runtime. */
|
|
73
|
+
stepTargetConnected() {
|
|
74
|
+
this.#repaint.schedule();
|
|
75
|
+
}
|
|
76
|
+
/** Re-derives every step's state for a step removed at runtime. */
|
|
77
|
+
stepTargetDisconnected() {
|
|
78
|
+
this.#repaint.schedule();
|
|
25
79
|
}
|
|
26
80
|
/** Advances to the next step (ignored at the last step). */
|
|
27
81
|
next() {
|
|
@@ -31,9 +85,16 @@ var StepperController = class extends Controller {
|
|
|
31
85
|
prev() {
|
|
32
86
|
this.#moveTo(this.#clampIndex(this.indexValue) - 1);
|
|
33
87
|
}
|
|
34
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Jumps to the step carried in the action's `index` param. A param that is empty,
|
|
90
|
+
* or that Stimulus parses into anything but a number or a string, is no index,
|
|
91
|
+
* so it jumps nowhere.
|
|
92
|
+
*/
|
|
35
93
|
goto(event) {
|
|
36
|
-
const
|
|
94
|
+
const { index } = event.params;
|
|
95
|
+
if (index === "") return;
|
|
96
|
+
if (typeof index !== "number" && typeof index !== "string") return;
|
|
97
|
+
const target = Number(index);
|
|
37
98
|
if (!Number.isFinite(target) || !Number.isInteger(target)) return;
|
|
38
99
|
this.#moveTo(target);
|
|
39
100
|
}
|
|
@@ -51,19 +112,39 @@ var StepperController = class extends Controller {
|
|
|
51
112
|
if (this.linearValue && target > current + 1) return;
|
|
52
113
|
const previous = current;
|
|
53
114
|
this.indexValue = target;
|
|
54
|
-
this.#
|
|
115
|
+
this.#shown = target;
|
|
116
|
+
this.#render();
|
|
55
117
|
this.dispatch("change", {
|
|
56
|
-
detail: { index: target, previous, step: this.stepTargets[target] }
|
|
118
|
+
detail: { index: target, previous, total, step: this.stepTargets[target] }
|
|
57
119
|
});
|
|
58
120
|
}
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Re-derives one settled batch of Value and target changes, and reports a
|
|
123
|
+
* current position that moved from the one shown before as `reconcile`. With no
|
|
124
|
+
* step present there is nothing to report, and the position shown last stays the
|
|
125
|
+
* one the next move is measured from.
|
|
126
|
+
*/
|
|
127
|
+
#reconcileStep() {
|
|
128
|
+
const previous = this.#shown;
|
|
129
|
+
const index = this.#currentStep;
|
|
130
|
+
if (index !== null) this.#shown = index;
|
|
131
|
+
this.#render();
|
|
132
|
+
if (index === null || previous === null || index === previous) return;
|
|
133
|
+
const steps = this.stepTargets;
|
|
134
|
+
this.dispatch("reconcile", {
|
|
135
|
+
detail: { index, previous, total: steps.length, step: steps[index] }
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** The current position, clamped into the step range, or `null` when there is no step. */
|
|
139
|
+
get #currentStep() {
|
|
140
|
+
return this.stepTargets.length > 0 ? this.#clampIndex(this.indexValue) : null;
|
|
64
141
|
}
|
|
65
142
|
/**
|
|
66
|
-
* Derives each step's `data-state` and the current button's `aria-current
|
|
143
|
+
* Derives each step's `data-state` and the current button's `aria-current` from
|
|
144
|
+
* `index`, clamped into the step range, without dispatching an action event.
|
|
145
|
+
*
|
|
146
|
+
* The clamp decides only what is shown. This body never writes `index`, so an
|
|
147
|
+
* out-of-range declaration stays in the attribute as the page wrote it.
|
|
67
148
|
*
|
|
68
149
|
* `aria-current="step"` is placed on the step's **first** `<button>`; the markup
|
|
69
150
|
* contract assumes one operable button per step. If a step needs multiple
|
|
@@ -71,7 +152,8 @@ var StepperController = class extends Controller {
|
|
|
71
152
|
*
|
|
72
153
|
* @stimeoRenderRoot
|
|
73
154
|
*/
|
|
74
|
-
#render(
|
|
155
|
+
#render() {
|
|
156
|
+
const current = this.#clampIndex(this.indexValue);
|
|
75
157
|
this.stepTargets.forEach((step, index) => {
|
|
76
158
|
step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
|
|
77
159
|
const button = step.querySelector("button");
|