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
|
@@ -225,15 +225,20 @@ var TabindexLoan = class {
|
|
|
225
225
|
};
|
|
226
226
|
|
|
227
227
|
// src/controllers/overflow_menu_controller.ts
|
|
228
|
-
var BANKED = "
|
|
229
|
-
var INDEX = "
|
|
230
|
-
var BOUNDARY = "
|
|
231
|
-
var SAVED_ROLE = "
|
|
232
|
-
var SAVED_TABINDEX = "
|
|
233
|
-
var SAVED_MENU_TARGET = "
|
|
228
|
+
var BANKED = "banked";
|
|
229
|
+
var INDEX = "index";
|
|
230
|
+
var BOUNDARY = "boundary";
|
|
231
|
+
var SAVED_ROLE = "role";
|
|
232
|
+
var SAVED_TABINDEX = "tabindex";
|
|
233
|
+
var SAVED_MENU_TARGET = "saved-menu";
|
|
234
234
|
var MENU_TARGET = "data-stimeo--menu-target";
|
|
235
235
|
var BOOKKEEPING = [BANKED, INDEX, SAVED_ROLE, SAVED_TABINDEX, SAVED_MENU_TARGET];
|
|
236
|
+
var OWNED_LABEL = "owns-label";
|
|
236
237
|
var OverflowMenuController = class extends Controller {
|
|
238
|
+
/** One bookkeeping attribute, in the namespace this controller is registered under. */
|
|
239
|
+
#attr(name) {
|
|
240
|
+
return `data-${this.identifier}-${name}`;
|
|
241
|
+
}
|
|
237
242
|
static targets = ["items", "more"];
|
|
238
243
|
static values = {
|
|
239
244
|
moreLabel: { type: String, default: "More" },
|
|
@@ -252,29 +257,42 @@ var OverflowMenuController = class extends Controller {
|
|
|
252
257
|
#lastHidden = null;
|
|
253
258
|
/** The `tabindex` this instance lends the root for the focus fallback. */
|
|
254
259
|
#tabindex = new TabindexLoan();
|
|
255
|
-
/** Hands Turbo a
|
|
260
|
+
/** Hands Turbo a snapshot of the bar with every item back in place. */
|
|
256
261
|
#beforeCache = new BeforeCacheReset(() => this.#restoreAll());
|
|
262
|
+
/**
|
|
263
|
+
* Whether `connect()` has run for this connection. Stimulus delivers Value
|
|
264
|
+
* callbacks ahead of it, and the first pass writes the label anyway.
|
|
265
|
+
*/
|
|
266
|
+
#connected = false;
|
|
257
267
|
connect() {
|
|
268
|
+
this.#connected = true;
|
|
258
269
|
if (!this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
259
|
-
const trigger = this.#trigger();
|
|
260
|
-
if (trigger !== null && this.#isBareTrigger(trigger)) {
|
|
261
|
-
trigger.textContent = this.moreLabelValue;
|
|
262
|
-
}
|
|
263
270
|
this.#beforeCache.activate();
|
|
264
271
|
this.#layout.observe(this.element);
|
|
265
272
|
this.#layout.observeViewport();
|
|
266
273
|
this.update();
|
|
267
274
|
}
|
|
268
275
|
disconnect() {
|
|
276
|
+
this.#connected = false;
|
|
269
277
|
this.#layout.disconnect();
|
|
270
278
|
this.#timers.clearAll();
|
|
271
279
|
this.#beforeCache.deactivate();
|
|
272
280
|
this.#restoreAll();
|
|
273
281
|
this.#lastHidden = null;
|
|
274
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Follows a More label swapped in place by a morph, on a trigger that still holds
|
|
285
|
+
* the label this controller wrote and nothing else. Render only: nothing is
|
|
286
|
+
* measured and `change` is not dispatched here; the next pass measures the bar.
|
|
287
|
+
*/
|
|
288
|
+
moreLabelValueChanged() {
|
|
289
|
+
if (!this.#connected || !this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
290
|
+
this.#syncLabel();
|
|
291
|
+
}
|
|
275
292
|
/** Re-measures and rebalances items between the bar and the More menu. */
|
|
276
293
|
update() {
|
|
277
294
|
if (!this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
295
|
+
this.#syncLabel();
|
|
278
296
|
this.#syncItems();
|
|
279
297
|
this.moreTarget.hidden = false;
|
|
280
298
|
if (this.#items.some((item) => item.parentElement !== this.itemsTarget && !this.#widths.has(item))) {
|
|
@@ -328,8 +346,8 @@ var OverflowMenuController = class extends Controller {
|
|
|
328
346
|
else if (active !== null && this.#lostFocus(active)) active.focus();
|
|
329
347
|
const count = hidden.size;
|
|
330
348
|
for (const item of this.#items) {
|
|
331
|
-
if (count > 0) item.setAttribute(INDEX, String(this.#indexOf(item)));
|
|
332
|
-
else item.removeAttribute(INDEX);
|
|
349
|
+
if (count > 0) item.setAttribute(this.#attr(INDEX), String(this.#indexOf(item)));
|
|
350
|
+
else item.removeAttribute(this.#attr(INDEX));
|
|
333
351
|
}
|
|
334
352
|
if (count > 0 && count === this.#items.length) this.#ensureBoundary();
|
|
335
353
|
else this.#removeBoundary();
|
|
@@ -344,7 +362,7 @@ var OverflowMenuController = class extends Controller {
|
|
|
344
362
|
this.element.setAttribute("data-overflow-count", String(count));
|
|
345
363
|
if (this.#lastHidden !== count) {
|
|
346
364
|
this.#lastHidden = count;
|
|
347
|
-
this.dispatch("change", { detail: {
|
|
365
|
+
this.dispatch("change", { detail: { overflowCount: count, total: this.#items.length } });
|
|
348
366
|
}
|
|
349
367
|
}
|
|
350
368
|
/** The flex `column-gap` on `el` in px (0 when none / unsupported). */
|
|
@@ -374,7 +392,7 @@ var OverflowMenuController = class extends Controller {
|
|
|
374
392
|
const bar = [];
|
|
375
393
|
let boundaryAt;
|
|
376
394
|
for (const el of this.itemsTarget.children) {
|
|
377
|
-
if (el instanceof HTMLTemplateElement && el.hasAttribute(BOUNDARY)) {
|
|
395
|
+
if (el instanceof HTMLTemplateElement && el.hasAttribute(this.#attr(BOUNDARY))) {
|
|
378
396
|
boundaryAt ??= bar.length;
|
|
379
397
|
continue;
|
|
380
398
|
}
|
|
@@ -382,14 +400,14 @@ var OverflowMenuController = class extends Controller {
|
|
|
382
400
|
}
|
|
383
401
|
const banked = [];
|
|
384
402
|
for (const el of this.#menuList().children) {
|
|
385
|
-
if (el instanceof HTMLElement && el.hasAttribute(BANKED)) banked.push(el);
|
|
403
|
+
if (el instanceof HTMLElement && el.hasAttribute(this.#attr(BANKED))) banked.push(el);
|
|
386
404
|
}
|
|
387
405
|
banked.sort((a, b) => this.#bankedIndex(a) - this.#bankedIndex(b));
|
|
388
406
|
const items = this.#merge(bar, banked, boundaryAt);
|
|
389
407
|
for (const el of previous) {
|
|
390
408
|
if (!items.includes(el)) {
|
|
391
409
|
this.#unbank(el);
|
|
392
|
-
el.removeAttribute(INDEX);
|
|
410
|
+
el.removeAttribute(this.#attr(INDEX));
|
|
393
411
|
}
|
|
394
412
|
}
|
|
395
413
|
this.#items = items;
|
|
@@ -462,7 +480,7 @@ var OverflowMenuController = class extends Controller {
|
|
|
462
480
|
}
|
|
463
481
|
/** A finite canonical index stored on either side of the overflow split. */
|
|
464
482
|
#savedIndex(item) {
|
|
465
|
-
const raw = item.getAttribute(INDEX);
|
|
483
|
+
const raw = item.getAttribute(this.#attr(INDEX));
|
|
466
484
|
if (raw === null) return void 0;
|
|
467
485
|
const value = Number(raw);
|
|
468
486
|
return Number.isFinite(value) ? value : void 0;
|
|
@@ -470,14 +488,14 @@ var OverflowMenuController = class extends Controller {
|
|
|
470
488
|
/** The inert split point written only when every managed item lives in More. */
|
|
471
489
|
#boundary() {
|
|
472
490
|
for (const el of this.itemsTarget.children) {
|
|
473
|
-
if (el instanceof HTMLTemplateElement && el.hasAttribute(BOUNDARY)) return el;
|
|
491
|
+
if (el instanceof HTMLTemplateElement && el.hasAttribute(this.#attr(BOUNDARY))) return el;
|
|
474
492
|
}
|
|
475
493
|
return null;
|
|
476
494
|
}
|
|
477
495
|
#ensureBoundary() {
|
|
478
496
|
if (this.#boundary() !== null) return;
|
|
479
497
|
const boundary = document.createElement("template");
|
|
480
|
-
boundary.setAttribute(BOUNDARY, "");
|
|
498
|
+
boundary.setAttribute(this.#attr(BOUNDARY), "");
|
|
481
499
|
this.itemsTarget.appendChild(boundary);
|
|
482
500
|
}
|
|
483
501
|
#removeBoundary() {
|
|
@@ -496,17 +514,68 @@ var OverflowMenuController = class extends Controller {
|
|
|
496
514
|
return this.moreTarget.querySelector('[data-stimeo--menu-target="trigger"]');
|
|
497
515
|
}
|
|
498
516
|
/**
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
* pass
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
|
|
517
|
+
* Keeps the More trigger's text on `moreLabel` wherever that text is this
|
|
518
|
+
* controller's to write: a bare trigger, or one still holding the label this
|
|
519
|
+
* controller wrote and nothing else. An empty `moreLabel` hands such a trigger
|
|
520
|
+
* back bare. A pass that finds the label already in place writes nothing, and a
|
|
521
|
+
* trigger that is neither is not written at all.
|
|
522
|
+
*
|
|
523
|
+
* @stimeoRenderRoot
|
|
524
|
+
*/
|
|
525
|
+
#syncLabel() {
|
|
526
|
+
const trigger = this.#trigger();
|
|
527
|
+
if (trigger === null) return;
|
|
528
|
+
const owned = this.#ownsLabel(trigger);
|
|
529
|
+
if (!owned && !this.#isBareTrigger(trigger)) return;
|
|
530
|
+
const label = this.moreLabelValue;
|
|
531
|
+
if (label === "") {
|
|
532
|
+
this.#releaseLabel(trigger);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (owned && trigger.textContent === label) return;
|
|
536
|
+
trigger.textContent = label;
|
|
537
|
+
trigger.setAttribute(this.#attr(OWNED_LABEL), label);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Whether the trigger holds the label this controller wrote there and nothing
|
|
541
|
+
* else: the text still matches the marker, and the trigger carries nothing but
|
|
542
|
+
* text. Ownership is read afresh on every call, so a trigger the consumer adds to
|
|
543
|
+
* is theirs for as long as the addition is there.
|
|
544
|
+
*/
|
|
545
|
+
#ownsLabel(trigger) {
|
|
546
|
+
return trigger.getAttribute(this.#attr(OWNED_LABEL)) === trigger.textContent && this.#carriesOnlyText(trigger);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Takes the label this controller wrote back out of the trigger, leaving it bare.
|
|
550
|
+
* A trigger that is not this controller's is left exactly as it is, marker
|
|
551
|
+
* included: taking the marker off would itself write to it.
|
|
552
|
+
*/
|
|
553
|
+
#releaseLabel(trigger) {
|
|
554
|
+
if (!this.#ownsLabel(trigger)) return;
|
|
555
|
+
trigger.textContent = "";
|
|
556
|
+
trigger.removeAttribute(this.#attr(OWNED_LABEL));
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Whether the More trigger is bare, so the label is safe to write: no text, no
|
|
560
|
+
* element child, no `aria-label` and no `aria-labelledby`. A trigger with neither
|
|
561
|
+
* text, nor children, nor a name has nothing to lose and gets the fallback; one
|
|
562
|
+
* that is empty *and* unnamed is the only case the label rescues.
|
|
507
563
|
*/
|
|
508
564
|
#isBareTrigger(trigger) {
|
|
509
|
-
return (trigger.textContent ?? "").trim() === "" &&
|
|
565
|
+
return (trigger.textContent ?? "").trim() === "" && this.#carriesOnlyText(trigger);
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Whether the trigger carries nothing but text: no element child, no
|
|
569
|
+
* `aria-label` and no `aria-labelledby`. Assigning `textContent` replaces
|
|
570
|
+
* *every* child node, so a trigger holding an icon would silently lose it — and
|
|
571
|
+
* the loss is permanent, since only the label this controller wrote is ever
|
|
572
|
+
* taken back. A trigger that carries an accessible name is left alone too:
|
|
573
|
+
* visible text written under a different `aria-label` would make the label and
|
|
574
|
+
* the name disagree. Only a trigger that carries nothing but text is ever
|
|
575
|
+
* written or emptied.
|
|
576
|
+
*/
|
|
577
|
+
#carriesOnlyText(trigger) {
|
|
578
|
+
return trigger.firstElementChild === null && !trigger.hasAttribute("aria-label") && !trigger.hasAttribute("aria-labelledby");
|
|
510
579
|
}
|
|
511
580
|
/**
|
|
512
581
|
* Whether the More menu is expanded. Menu owns that state and reflects it on the
|
|
@@ -577,9 +646,11 @@ var OverflowMenuController = class extends Controller {
|
|
|
577
646
|
this.#reorder(this.itemsTarget, this.#items);
|
|
578
647
|
for (const item of this.#items) {
|
|
579
648
|
this.#unbank(item);
|
|
580
|
-
item.removeAttribute(INDEX);
|
|
649
|
+
item.removeAttribute(this.#attr(INDEX));
|
|
581
650
|
}
|
|
582
651
|
this.#closeMenu();
|
|
652
|
+
const trigger = this.#trigger();
|
|
653
|
+
if (trigger !== null) this.#releaseLabel(trigger);
|
|
583
654
|
this.moreTarget.hidden = true;
|
|
584
655
|
this.element.removeAttribute("data-overflowing");
|
|
585
656
|
this.element.removeAttribute("data-overflow-count");
|
|
@@ -619,24 +690,24 @@ var OverflowMenuController = class extends Controller {
|
|
|
619
690
|
* saving every authored value it overwrites ("" means the attribute was absent).
|
|
620
691
|
*/
|
|
621
692
|
#bank(item) {
|
|
622
|
-
if (!item.hasAttribute(BANKED)) {
|
|
623
|
-
item.setAttribute(BANKED, "true");
|
|
624
|
-
item.setAttribute(SAVED_ROLE, item.getAttribute("role") ?? "");
|
|
625
|
-
item.setAttribute(SAVED_TABINDEX, item.getAttribute("tabindex") ?? "");
|
|
626
|
-
item.setAttribute(SAVED_MENU_TARGET, item.getAttribute(MENU_TARGET) ?? "");
|
|
693
|
+
if (!item.hasAttribute(this.#attr(BANKED))) {
|
|
694
|
+
item.setAttribute(this.#attr(BANKED), "true");
|
|
695
|
+
item.setAttribute(this.#attr(SAVED_ROLE), item.getAttribute("role") ?? "");
|
|
696
|
+
item.setAttribute(this.#attr(SAVED_TABINDEX), item.getAttribute("tabindex") ?? "");
|
|
697
|
+
item.setAttribute(this.#attr(SAVED_MENU_TARGET), item.getAttribute(MENU_TARGET) ?? "");
|
|
627
698
|
item.setAttribute("role", "menuitem");
|
|
628
699
|
item.setAttribute("tabindex", "-1");
|
|
629
700
|
item.setAttribute(MENU_TARGET, "item");
|
|
630
701
|
}
|
|
631
|
-
item.setAttribute(INDEX, String(this.#indexOf(item)));
|
|
702
|
+
item.setAttribute(this.#attr(INDEX), String(this.#indexOf(item)));
|
|
632
703
|
}
|
|
633
704
|
/** Undoes the banking: restores every authored value and drops the bookkeeping. */
|
|
634
705
|
#unbank(item) {
|
|
635
|
-
if (!item.hasAttribute(BANKED)) return;
|
|
636
|
-
this.#restoreAttr(item, "role", item.getAttribute(SAVED_ROLE));
|
|
637
|
-
this.#restoreAttr(item, "tabindex", item.getAttribute(SAVED_TABINDEX));
|
|
638
|
-
this.#restoreAttr(item, MENU_TARGET, item.getAttribute(SAVED_MENU_TARGET));
|
|
639
|
-
for (const name of BOOKKEEPING) item.removeAttribute(name);
|
|
706
|
+
if (!item.hasAttribute(this.#attr(BANKED))) return;
|
|
707
|
+
this.#restoreAttr(item, "role", item.getAttribute(this.#attr(SAVED_ROLE)));
|
|
708
|
+
this.#restoreAttr(item, "tabindex", item.getAttribute(this.#attr(SAVED_TABINDEX)));
|
|
709
|
+
this.#restoreAttr(item, MENU_TARGET, item.getAttribute(this.#attr(SAVED_MENU_TARGET)));
|
|
710
|
+
for (const name of BOOKKEEPING) item.removeAttribute(this.#attr(name));
|
|
640
711
|
}
|
|
641
712
|
/** Re-applies a saved attribute value, or removes the attribute when it was absent. */
|
|
642
713
|
#restoreAttr(item, name, original) {
|
|
@@ -20,6 +20,39 @@ function canTakeFocus(element) {
|
|
|
20
20
|
return !inheritsFieldsetDisabled(element);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// src/utils/microtask_coalescer.ts
|
|
24
|
+
var MicrotaskCoalescer = class {
|
|
25
|
+
#run;
|
|
26
|
+
#queued = false;
|
|
27
|
+
#active = false;
|
|
28
|
+
#generation = 0;
|
|
29
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
30
|
+
constructor(run) {
|
|
31
|
+
this.#run = run;
|
|
32
|
+
}
|
|
33
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
34
|
+
activate() {
|
|
35
|
+
this.#active = true;
|
|
36
|
+
}
|
|
37
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
38
|
+
cancel() {
|
|
39
|
+
this.#active = false;
|
|
40
|
+
this.#queued = false;
|
|
41
|
+
this.#generation += 1;
|
|
42
|
+
}
|
|
43
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
44
|
+
schedule() {
|
|
45
|
+
if (!this.#active || this.#queued) return;
|
|
46
|
+
this.#queued = true;
|
|
47
|
+
const generation = this.#generation;
|
|
48
|
+
queueMicrotask(() => {
|
|
49
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
50
|
+
this.#queued = false;
|
|
51
|
+
this.#run();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
23
56
|
// src/utils/before_cache_reset.ts
|
|
24
57
|
var BeforeCacheReset = class _BeforeCacheReset {
|
|
25
58
|
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
@@ -81,53 +114,57 @@ var TabindexLoan = class {
|
|
|
81
114
|
};
|
|
82
115
|
|
|
83
116
|
// src/controllers/pagination_controller.ts
|
|
84
|
-
var PaginationController = class
|
|
117
|
+
var PaginationController = class extends Controller {
|
|
85
118
|
static targets = ["page", "prev", "next"];
|
|
86
119
|
static values = {
|
|
87
120
|
page: { type: Number, default: 1 },
|
|
88
121
|
total: { type: Number, default: 1 }
|
|
89
122
|
};
|
|
90
123
|
static actions = ["next", "prev", "select"];
|
|
91
|
-
static events = ["change"];
|
|
92
|
-
/** Marks a `disabled` this controller applied at a boundary
|
|
93
|
-
|
|
94
|
-
|
|
124
|
+
static events = ["change", "reconcile"];
|
|
125
|
+
/** Marks a `disabled` this controller applied at a boundary, in its own namespace. */
|
|
126
|
+
get #boundaryAttribute() {
|
|
127
|
+
return `data-${this.identifier}-boundary-disabled`;
|
|
128
|
+
}
|
|
95
129
|
/** The `tabindex` this instance lends the root for the focus fallback. */
|
|
96
130
|
#tabindex = new TabindexLoan();
|
|
97
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Collapses the Value and target callbacks of one mutation into one pass, and
|
|
133
|
+
* refuses the ones Stimulus delivers before `connect()`, which renders itself.
|
|
134
|
+
*/
|
|
135
|
+
#repaint = new MicrotaskCoalescer(() => this.#reconcilePage());
|
|
136
|
+
/** The page shown last, which the next move of the current page is measured from. */
|
|
137
|
+
#shown = 1;
|
|
138
|
+
/** Renders the initial state from the clamped `page` and `total`. */
|
|
98
139
|
connect() {
|
|
99
|
-
this.#
|
|
100
|
-
this.#
|
|
140
|
+
this.#repaint.activate();
|
|
141
|
+
this.#shown = this.#page;
|
|
142
|
+
this.#render();
|
|
101
143
|
}
|
|
102
|
-
/**
|
|
144
|
+
/** Drops a pending pass and reverts the one attribute added outside the state hooks. */
|
|
103
145
|
disconnect() {
|
|
104
|
-
this.#
|
|
146
|
+
this.#repaint.cancel();
|
|
105
147
|
this.#tabindex.returnAll();
|
|
106
148
|
}
|
|
107
149
|
/** Re-renders when application code (or a Turbo morph) changes `page` at runtime. */
|
|
108
150
|
pageValueChanged() {
|
|
109
|
-
|
|
110
|
-
this.#normalizeAndRender();
|
|
151
|
+
this.#repaint.schedule();
|
|
111
152
|
}
|
|
112
153
|
/** Re-renders when application code (or a Turbo morph) changes `total` at runtime. */
|
|
113
154
|
totalValueChanged() {
|
|
114
|
-
|
|
115
|
-
this.#normalizeAndRender();
|
|
155
|
+
this.#repaint.schedule();
|
|
116
156
|
}
|
|
117
157
|
/** Syncs a page button appended/replaced at runtime (the consumer owns the list). */
|
|
118
158
|
pageTargetConnected() {
|
|
119
|
-
|
|
120
|
-
this.#render();
|
|
159
|
+
this.#repaint.schedule();
|
|
121
160
|
}
|
|
122
161
|
/** Syncs a `prev` button appended/replaced at runtime. */
|
|
123
162
|
prevTargetConnected() {
|
|
124
|
-
|
|
125
|
-
this.#render();
|
|
163
|
+
this.#repaint.schedule();
|
|
126
164
|
}
|
|
127
165
|
/** Syncs a `next` button appended/replaced at runtime. */
|
|
128
166
|
nextTargetConnected() {
|
|
129
|
-
|
|
130
|
-
this.#render();
|
|
167
|
+
this.#repaint.schedule();
|
|
131
168
|
}
|
|
132
169
|
/** Makes the clicked page button (its `data-page`) current. */
|
|
133
170
|
select(event) {
|
|
@@ -153,22 +190,31 @@ var PaginationController = class _PaginationController extends Controller {
|
|
|
153
190
|
const target = this.#clamp(page);
|
|
154
191
|
if (target === previous) return;
|
|
155
192
|
this.pageValue = target;
|
|
193
|
+
this.#shown = target;
|
|
156
194
|
this.#render();
|
|
157
195
|
this.dispatch("change", {
|
|
158
196
|
detail: { page: target, total: this.#total, previous }
|
|
159
197
|
});
|
|
160
198
|
}
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Renders one settled batch of Value and target changes, and reports a current
|
|
201
|
+
* page that moved from the one shown before as `reconcile`.
|
|
202
|
+
*/
|
|
203
|
+
#reconcilePage() {
|
|
204
|
+
const previous = this.#shown;
|
|
165
205
|
const page = this.#page;
|
|
166
|
-
|
|
206
|
+
this.#shown = page;
|
|
167
207
|
this.#render();
|
|
208
|
+
if (page !== previous) {
|
|
209
|
+
this.dispatch("reconcile", { detail: { page, total: this.#total, previous } });
|
|
210
|
+
}
|
|
168
211
|
}
|
|
169
212
|
/**
|
|
170
213
|
* Syncs `aria-current` on the page buttons and the prev/next `disabled` state.
|
|
171
214
|
*
|
|
215
|
+
* It reads `page` and `total` through their clamps and never writes either Value,
|
|
216
|
+
* so a declaration outside the range stays in the attributes as the page wrote it.
|
|
217
|
+
*
|
|
172
218
|
* @stimeoRenderRoot
|
|
173
219
|
*/
|
|
174
220
|
#render() {
|
|
@@ -201,7 +247,7 @@ var PaginationController = class _PaginationController extends Controller {
|
|
|
201
247
|
#release(button, atBoundary) {
|
|
202
248
|
if (!button || atBoundary || !this.#owns(button)) return;
|
|
203
249
|
button.disabled = false;
|
|
204
|
-
button.removeAttribute(
|
|
250
|
+
button.removeAttribute(this.#boundaryAttribute);
|
|
205
251
|
}
|
|
206
252
|
/**
|
|
207
253
|
* Disables a boundary button, first moving focus off it when it is the active
|
|
@@ -212,7 +258,7 @@ var PaginationController = class _PaginationController extends Controller {
|
|
|
212
258
|
if (button.disabled && !this.#owns(button)) return;
|
|
213
259
|
if (button === document.activeElement) this.#moveFocusAwayFrom(opposite);
|
|
214
260
|
button.disabled = true;
|
|
215
|
-
button.setAttribute(
|
|
261
|
+
button.setAttribute(this.#boundaryAttribute, "");
|
|
216
262
|
}
|
|
217
263
|
/**
|
|
218
264
|
* Moves focus to `opposite` (already resolved to `null` when it will stay
|
|
@@ -249,7 +295,7 @@ var PaginationController = class _PaginationController extends Controller {
|
|
|
249
295
|
}
|
|
250
296
|
/** Whether the button's current `disabled` was applied by boundary control. */
|
|
251
297
|
#owns(button) {
|
|
252
|
-
return button.hasAttribute(
|
|
298
|
+
return button.hasAttribute(this.#boundaryAttribute);
|
|
253
299
|
}
|
|
254
300
|
};
|
|
255
301
|
|
|
@@ -84,10 +84,51 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
// src/utils/state_regions.ts
|
|
88
|
+
var StateRegions = class {
|
|
89
|
+
#whenTrue;
|
|
90
|
+
#whenFalse;
|
|
91
|
+
#taken = /* @__PURE__ */ new WeakSet();
|
|
92
|
+
constructor(options) {
|
|
93
|
+
this.#whenTrue = options.whenTrue;
|
|
94
|
+
this.#whenFalse = options.whenFalse ?? null;
|
|
95
|
+
}
|
|
96
|
+
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
97
|
+
reflect(host, isTrue) {
|
|
98
|
+
const shown = this.#inside(host, this.#whenTrue());
|
|
99
|
+
if (!this.#whenFalse) {
|
|
100
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const hiddenSide = this.#inside(host, this.#whenFalse());
|
|
104
|
+
if (shown.length === 0 || hiddenSide.length === 0) {
|
|
105
|
+
for (const region of shown) this.#give(region);
|
|
106
|
+
for (const region of hiddenSide) this.#give(region);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
110
|
+
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
111
|
+
}
|
|
112
|
+
/** Writes `hidden` where it moves, noting which regions it takes out of view. */
|
|
113
|
+
#write(region, hidden) {
|
|
114
|
+
if (hidden) this.#taken.add(region);
|
|
115
|
+
else this.#taken.delete(region);
|
|
116
|
+
if (region.hidden !== hidden) region.hidden = hidden;
|
|
117
|
+
}
|
|
118
|
+
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
119
|
+
#give(region) {
|
|
120
|
+
if (this.#taken.delete(region)) region.hidden = false;
|
|
121
|
+
}
|
|
122
|
+
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
123
|
+
#inside(host, regions) {
|
|
124
|
+
return regions.filter((region) => host.contains(region));
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
87
128
|
// src/controllers/password_reveal_controller.ts
|
|
88
129
|
var MAX_DELAY = 2 ** 31 - 1;
|
|
89
130
|
var PasswordRevealController = class extends Controller {
|
|
90
|
-
static targets = ["input", "toggle"];
|
|
131
|
+
static targets = ["input", "toggle", "onLabel", "offLabel"];
|
|
91
132
|
static values = {
|
|
92
133
|
autoHide: { type: Number, default: 0 }
|
|
93
134
|
};
|
|
@@ -105,6 +146,11 @@ var PasswordRevealController = class extends Controller {
|
|
|
105
146
|
* document does not answer this — unloading the controller leaves it there.
|
|
106
147
|
*/
|
|
107
148
|
#connected = false;
|
|
149
|
+
/** Owns `hidden` on the label pair the toggle button carries. */
|
|
150
|
+
#labels = new StateRegions({
|
|
151
|
+
whenTrue: () => this.onLabelTargets,
|
|
152
|
+
whenFalse: () => this.offLabelTargets
|
|
153
|
+
});
|
|
108
154
|
/** Masks the field before Turbo copies the page into its snapshot. */
|
|
109
155
|
#beforeCache = new BeforeCacheReset(() => this.#rewindToMasked());
|
|
110
156
|
connect() {
|
|
@@ -144,6 +190,14 @@ var PasswordRevealController = class extends Controller {
|
|
|
144
190
|
toggleTargetConnected() {
|
|
145
191
|
this.#reflect(this.#isVisible);
|
|
146
192
|
}
|
|
193
|
+
/** Describes the state to a revealed-side label that arrives after connect. */
|
|
194
|
+
onLabelTargetConnected() {
|
|
195
|
+
this.#reflect(this.#isVisible);
|
|
196
|
+
}
|
|
197
|
+
/** Describes the state to a masked-side label that arrives after connect. */
|
|
198
|
+
offLabelTargetConnected() {
|
|
199
|
+
this.#reflect(this.#isVisible);
|
|
200
|
+
}
|
|
147
201
|
/** Toggles the input between masked and revealed. Bound via `data-action`. */
|
|
148
202
|
toggle() {
|
|
149
203
|
this.#setVisible(!this.#isVisible);
|
|
@@ -187,6 +241,8 @@ var PasswordRevealController = class extends Controller {
|
|
|
187
241
|
* the opposite of what the declaration asked for. A value that is no delay at
|
|
188
242
|
* all stays out of the positive range {@link PasswordRevealController.#arm}
|
|
189
243
|
* requires, so it schedules nothing.
|
|
244
|
+
*
|
|
245
|
+
* @stimeoRuntimeOnly `autoHide` is the delay of the one re-masking timer it arms.
|
|
190
246
|
*/
|
|
191
247
|
get #autoHideDelay() {
|
|
192
248
|
return Math.min(this.autoHideValue, MAX_DELAY);
|
|
@@ -202,10 +258,11 @@ var PasswordRevealController = class extends Controller {
|
|
|
202
258
|
this.inputTarget.type = "password";
|
|
203
259
|
this.#reflect(false);
|
|
204
260
|
}
|
|
205
|
-
/** Reflects the visible state onto `aria-pressed
|
|
261
|
+
/** Reflects the visible state onto `aria-pressed`, `data-state` and the labels. */
|
|
206
262
|
#reflect(visible) {
|
|
207
263
|
if (this.hasToggleTarget) {
|
|
208
264
|
this.toggleTarget.setAttribute("aria-pressed", visible ? "true" : "false");
|
|
265
|
+
this.#labels.reflect(this.toggleTarget, visible);
|
|
209
266
|
}
|
|
210
267
|
this.element.setAttribute("data-state", visible ? "visible" : "hidden");
|
|
211
268
|
}
|
|
@@ -127,7 +127,20 @@ function parseStringList(raw, fallback = []) {
|
|
|
127
127
|
return parsed.filter((entry) => typeof entry === "string");
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// src/utils/transient_hooks.ts
|
|
131
|
+
var TransientHooks = class {
|
|
132
|
+
#attributes;
|
|
133
|
+
constructor(options) {
|
|
134
|
+
this.#attributes = options.attributes;
|
|
135
|
+
}
|
|
136
|
+
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
137
|
+
reset(element) {
|
|
138
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
130
142
|
// src/controllers/persist_controller.ts
|
|
143
|
+
var TRANSIENT = new TransientHooks({ attributes: ["data-persist-restored"] });
|
|
131
144
|
var NON_VALUE_TYPES = /* @__PURE__ */ new Set(["file", "submit", "reset", "button", "image"]);
|
|
132
145
|
var SENSITIVE_TYPES = /* @__PURE__ */ new Set(["password"]);
|
|
133
146
|
var DEFAULT_EXCLUDE = ["authenticity_token", "_method", "utf8"];
|
|
@@ -197,7 +210,6 @@ var PersistController = class extends Controller {
|
|
|
197
210
|
this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);
|
|
198
211
|
this.#knownFields = /* @__PURE__ */ new WeakSet();
|
|
199
212
|
this.#forcedRestore.clear();
|
|
200
|
-
this.element.removeAttribute("data-persist-restored");
|
|
201
213
|
this.element.addEventListener("input", this.#onInput);
|
|
202
214
|
this.element.addEventListener("change", this.#onInput);
|
|
203
215
|
this.#syncClearOn();
|
|
@@ -207,7 +219,7 @@ var PersistController = class extends Controller {
|
|
|
207
219
|
attributeFilter: [
|
|
208
220
|
"checked",
|
|
209
221
|
"data-controller",
|
|
210
|
-
|
|
222
|
+
`data-${this.identifier}-target`,
|
|
211
223
|
"id",
|
|
212
224
|
"multiple",
|
|
213
225
|
"name",
|
|
@@ -230,7 +242,7 @@ var PersistController = class extends Controller {
|
|
|
230
242
|
this.#restoreDynamic.cancel();
|
|
231
243
|
this.#forcedRestore.clear();
|
|
232
244
|
this.#timeouts.clearAll();
|
|
233
|
-
this.element
|
|
245
|
+
TRANSIENT.reset(this.element);
|
|
234
246
|
this.#payload = null;
|
|
235
247
|
}
|
|
236
248
|
/** Drops the active draft after storage confirms the removal. */
|
|
@@ -329,7 +341,7 @@ var PersistController = class extends Controller {
|
|
|
329
341
|
}
|
|
330
342
|
/** Reads, validates, and restores the active namespace. */
|
|
331
343
|
#loadActiveDraft() {
|
|
332
|
-
this.element
|
|
344
|
+
TRANSIENT.reset(this.element);
|
|
333
345
|
this.#payload = null;
|
|
334
346
|
this.#knownFields = /* @__PURE__ */ new WeakSet();
|
|
335
347
|
this.#forcedRestore.clear();
|
|
@@ -481,7 +493,7 @@ var PersistController = class extends Controller {
|
|
|
481
493
|
}
|
|
482
494
|
/** Whether this instance, rather than a nested Persist host, owns a field. */
|
|
483
495
|
#ownsField(field) {
|
|
484
|
-
return field.closest(
|
|
496
|
+
return field.closest(`[data-controller~="${this.identifier}"]`) === this.element;
|
|
485
497
|
}
|
|
486
498
|
/** Marks all currently eligible fields without applying a payload. */
|
|
487
499
|
#markCurrentFieldsKnown() {
|
|
@@ -515,7 +527,12 @@ var PersistController = class extends Controller {
|
|
|
515
527
|
const name = value.trim();
|
|
516
528
|
return name.length > 0 && !/\s/.test(name) ? name : null;
|
|
517
529
|
}
|
|
518
|
-
/**
|
|
530
|
+
/**
|
|
531
|
+
* Collects dynamic controls and select-option changes into one restore pass.
|
|
532
|
+
*
|
|
533
|
+
* @stimeoRuntimeOnly `key` decides whether an id change moves the storage namespace; it decides
|
|
534
|
+
* nothing shown.
|
|
535
|
+
*/
|
|
519
536
|
#onMutations(records) {
|
|
520
537
|
let rootIdChanged = false;
|
|
521
538
|
for (const record of records) {
|
|
@@ -563,7 +580,12 @@ var PersistController = class extends Controller {
|
|
|
563
580
|
this.#forcedRestore.add(field);
|
|
564
581
|
}
|
|
565
582
|
}
|
|
566
|
-
/**
|
|
583
|
+
/**
|
|
584
|
+
* Resolves the logical key from the Value, falling back to the host id.
|
|
585
|
+
*
|
|
586
|
+
* @stimeoRuntimeOnly `key` names the storage namespace; the restored fields come from what is
|
|
587
|
+
* stored there.
|
|
588
|
+
*/
|
|
567
589
|
#resolveLogicalKey() {
|
|
568
590
|
const key = this.keyValue || this.element.id;
|
|
569
591
|
return key.length > 0 ? key : null;
|
|
@@ -572,7 +594,7 @@ var PersistController = class extends Controller {
|
|
|
572
594
|
#storageKey(logicalKey) {
|
|
573
595
|
return `${STORAGE_PREFIX}${logicalKey}`;
|
|
574
596
|
}
|
|
575
|
-
/** Normalizes invalid debounce
|
|
597
|
+
/** Normalizes an invalid `debounce` Value to the `debounce` default. */
|
|
576
598
|
get #debounceDelay() {
|
|
577
599
|
const value = this.debounceValue;
|
|
578
600
|
return Number.isFinite(value) && value >= 0 ? value : DEFAULT_DEBOUNCE;
|