stimeo-ui 0.14.0 → 0.15.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 +34 -0
- data/dist/cable/index.js +60 -14
- data/dist/controllers/announcer_controller.js +88 -61
- data/dist/controllers/auto_submit_controller.js +40 -5
- data/dist/controllers/breadcrumb_controller.js +29 -3
- data/dist/controllers/calendar_controller.js +20 -3
- data/dist/controllers/checkbox_controller.js +27 -2
- data/dist/controllers/conditional_fields_controller.js +29 -6
- data/dist/controllers/currency_input_controller.js +16 -9
- data/dist/controllers/date_range_picker_controller.js +12 -3
- data/dist/controllers/dirty_form_controller.js +32 -12
- data/dist/controllers/flash_controller.js +145 -43
- data/dist/controllers/frame_loading_controller.js +29 -6
- data/dist/controllers/highlight_controller.js +63 -23
- data/dist/controllers/local_time_controller.js +10 -5
- data/dist/controllers/masonry_controller.js +31 -13
- data/dist/controllers/optimistic_controller.js +28 -4
- data/dist/controllers/overflow_indicator_controller.js +31 -8
- data/dist/controllers/overflow_menu_controller.js +29 -3
- data/dist/controllers/read_more_controller.js +31 -5
- data/dist/controllers/reading_progress_controller.js +65 -19
- data/dist/controllers/relative_time_controller.js +10 -5
- data/dist/controllers/scroll_area_controller.js +70 -26
- data/dist/controllers/scroll_restore_controller.js +37 -16
- data/dist/controllers/scroll_visibility_controller.js +49 -29
- data/dist/controllers/scrollspy_controller.js +65 -26
- data/dist/controllers/smart_sticky_header_controller.js +49 -25
- data/dist/controllers/stick_to_bottom_controller.js +29 -3
- data/dist/controllers/submit_once_controller.js +29 -6
- data/dist/controllers/textarea_autosize_controller.js +29 -3
- data/dist/controllers/toast_controller.js +136 -73
- data/dist/index.js +473 -435
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
data/dist/index.js
CHANGED
|
@@ -547,39 +547,6 @@ var AlertDialogController = class extends Controller {
|
|
|
547
547
|
}
|
|
548
548
|
};
|
|
549
549
|
|
|
550
|
-
// src/utils/microtask_coalescer.ts
|
|
551
|
-
var MicrotaskCoalescer = class {
|
|
552
|
-
#run;
|
|
553
|
-
#queued = false;
|
|
554
|
-
#active = false;
|
|
555
|
-
#generation = 0;
|
|
556
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
557
|
-
constructor(run) {
|
|
558
|
-
this.#run = run;
|
|
559
|
-
}
|
|
560
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
561
|
-
activate() {
|
|
562
|
-
this.#active = true;
|
|
563
|
-
}
|
|
564
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
565
|
-
cancel() {
|
|
566
|
-
this.#active = false;
|
|
567
|
-
this.#queued = false;
|
|
568
|
-
this.#generation += 1;
|
|
569
|
-
}
|
|
570
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
571
|
-
schedule() {
|
|
572
|
-
if (!this.#active || this.#queued) return;
|
|
573
|
-
this.#queued = true;
|
|
574
|
-
const generation = this.#generation;
|
|
575
|
-
queueMicrotask(() => {
|
|
576
|
-
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
577
|
-
this.#queued = false;
|
|
578
|
-
this.#run();
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
};
|
|
582
|
-
|
|
583
550
|
// src/utils/safe_timeout.ts
|
|
584
551
|
var TimerRegistry = class {
|
|
585
552
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -647,6 +614,82 @@ var SafeInterval = class extends TimerRegistry {
|
|
|
647
614
|
}
|
|
648
615
|
};
|
|
649
616
|
|
|
617
|
+
// src/utils/keyed_timers.ts
|
|
618
|
+
var KeyedTimers = class {
|
|
619
|
+
#timers = new SafeTimeout();
|
|
620
|
+
/** The pending timer of each key; an entry lives exactly as long as its timer. */
|
|
621
|
+
#pending = /* @__PURE__ */ new Map();
|
|
622
|
+
/**
|
|
623
|
+
* Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had
|
|
624
|
+
* pending. The entry is dropped before the callback runs, so the callback sees
|
|
625
|
+
* the key unarmed and may arm it again for the next round.
|
|
626
|
+
*/
|
|
627
|
+
set(key, callback, delay) {
|
|
628
|
+
this.clear(key);
|
|
629
|
+
const id = this.#timers.set(() => {
|
|
630
|
+
this.#pending.delete(key);
|
|
631
|
+
callback();
|
|
632
|
+
}, delay);
|
|
633
|
+
this.#pending.set(key, id);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Cancels `key`'s pending timer, if it has one.
|
|
637
|
+
*
|
|
638
|
+
* A timer id is a positive integer, so `-1` stands for "nothing pending" and
|
|
639
|
+
* the registry ignores an id it does not own — the unarmed case needs no
|
|
640
|
+
* branch of its own, and no other key's timer can be reached from here.
|
|
641
|
+
*/
|
|
642
|
+
clear(key) {
|
|
643
|
+
this.#timers.clear(this.#pending.get(key) ?? -1);
|
|
644
|
+
this.#pending.delete(key);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Cancels every pending timer and forgets every key. Call this from a
|
|
648
|
+
* controller's `disconnect()` so no timer, and no entry, outlives the element.
|
|
649
|
+
*/
|
|
650
|
+
clearAll() {
|
|
651
|
+
this.#timers.clearAll();
|
|
652
|
+
this.#pending.clear();
|
|
653
|
+
}
|
|
654
|
+
/** Whether `key` has a timer pending. */
|
|
655
|
+
has(key) {
|
|
656
|
+
return this.#pending.has(key);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
// src/utils/microtask_coalescer.ts
|
|
661
|
+
var MicrotaskCoalescer = class {
|
|
662
|
+
#run;
|
|
663
|
+
#queued = false;
|
|
664
|
+
#active = false;
|
|
665
|
+
#generation = 0;
|
|
666
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
667
|
+
constructor(run) {
|
|
668
|
+
this.#run = run;
|
|
669
|
+
}
|
|
670
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
671
|
+
activate() {
|
|
672
|
+
this.#active = true;
|
|
673
|
+
}
|
|
674
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
675
|
+
cancel() {
|
|
676
|
+
this.#active = false;
|
|
677
|
+
this.#queued = false;
|
|
678
|
+
this.#generation += 1;
|
|
679
|
+
}
|
|
680
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
681
|
+
schedule() {
|
|
682
|
+
if (!this.#active || this.#queued) return;
|
|
683
|
+
this.#queued = true;
|
|
684
|
+
const generation = this.#generation;
|
|
685
|
+
queueMicrotask(() => {
|
|
686
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
687
|
+
this.#queued = false;
|
|
688
|
+
this.#run();
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
650
693
|
// src/controllers/announcer_controller.ts
|
|
651
694
|
var LEVELS = ["polite", "assertive"];
|
|
652
695
|
var AnnouncerController = class extends Controller {
|
|
@@ -656,8 +699,14 @@ var AnnouncerController = class extends Controller {
|
|
|
656
699
|
dedupeReannounce: { type: Boolean, default: true }
|
|
657
700
|
};
|
|
658
701
|
static actions = ["announce"];
|
|
659
|
-
/**
|
|
702
|
+
/** Drain timers, which belong to a politeness level rather than to a region. */
|
|
660
703
|
#timers = new SafeTimeout();
|
|
704
|
+
/**
|
|
705
|
+
* The one timer a region may have outstanding — its dedupe re-set, then its
|
|
706
|
+
* auto-clear. A region carries at most one, and the registry releases the
|
|
707
|
+
* previous one whenever a newer message takes the region over.
|
|
708
|
+
*/
|
|
709
|
+
#regionTimers = new KeyedTimers();
|
|
661
710
|
/** Live regions generated to stand in for absent targets, for teardown. */
|
|
662
711
|
#generated = /* @__PURE__ */ new Map();
|
|
663
712
|
/** Messages waiting to be written, oldest first, one queue per politeness. */
|
|
@@ -677,13 +726,6 @@ var AnnouncerController = class extends Controller {
|
|
|
677
726
|
});
|
|
678
727
|
/** Rewinds to an announceable initial state for the snapshot; see the remarks. */
|
|
679
728
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
680
|
-
/**
|
|
681
|
-
* The one timer a region may have outstanding — its dedupe re-set, then its
|
|
682
|
-
* auto-clear. Held weakly so a swapped-out target is not retained; a leftover
|
|
683
|
-
* id is harmless because {@link SafeTimeout.clear} no-ops on an id it does not
|
|
684
|
-
* own.
|
|
685
|
-
*/
|
|
686
|
-
#pending = /* @__PURE__ */ new WeakMap();
|
|
687
729
|
/**
|
|
688
730
|
* Guards against handling the same CustomEvent twice. An event dispatched on
|
|
689
731
|
* the controller element with `bubbles: true` reaches both the element and the
|
|
@@ -714,6 +756,7 @@ var AnnouncerController = class extends Controller {
|
|
|
714
756
|
this.element.removeEventListener("stimeo--announcer:announce", this.#onAnnounceEvent);
|
|
715
757
|
window.removeEventListener("stimeo--announcer:announce", this.#onAnnounceEvent);
|
|
716
758
|
this.#timers.clearAll();
|
|
759
|
+
this.#regionTimers.clearAll();
|
|
717
760
|
this.#queues.clear();
|
|
718
761
|
this.#draining.clear();
|
|
719
762
|
this.#removeGenerated();
|
|
@@ -844,13 +887,13 @@ var AnnouncerController = class extends Controller {
|
|
|
844
887
|
}
|
|
845
888
|
const region = this.#regionFor(level);
|
|
846
889
|
if (this.dedupeReannounceValue && region.textContent === message) {
|
|
847
|
-
this.#
|
|
890
|
+
this.#regionTimers.clear(region);
|
|
848
891
|
region.textContent = "";
|
|
849
892
|
this.#scheduleDrain(level);
|
|
850
893
|
return;
|
|
851
894
|
}
|
|
852
895
|
queue.shift();
|
|
853
|
-
this.#
|
|
896
|
+
this.#regionTimers.clear(region);
|
|
854
897
|
region.textContent = message;
|
|
855
898
|
this.#scheduleClear(region, message);
|
|
856
899
|
if (queue.length > 0) this.#scheduleDrain(level);
|
|
@@ -858,7 +901,7 @@ var AnnouncerController = class extends Controller {
|
|
|
858
901
|
/** Clears the region after `clearAfter` ms, unless a newer message replaced it. */
|
|
859
902
|
#scheduleClear(region, message) {
|
|
860
903
|
if (this.clearAfterValue <= 0) return;
|
|
861
|
-
this.#
|
|
904
|
+
this.#regionTimers.set(
|
|
862
905
|
region,
|
|
863
906
|
() => {
|
|
864
907
|
if (region.textContent === message) region.textContent = "";
|
|
@@ -866,23 +909,6 @@ var AnnouncerController = class extends Controller {
|
|
|
866
909
|
this.clearAfterValue
|
|
867
910
|
);
|
|
868
911
|
}
|
|
869
|
-
/**
|
|
870
|
-
* Arms `region`'s single pending timer. Callers reach here with the slot
|
|
871
|
-
* already free — `#drain` releases it before writing, and a fired timer clears its own
|
|
872
|
-
* entry below — so this does not cancel again.
|
|
873
|
-
*/
|
|
874
|
-
#schedule(region, callback, delay) {
|
|
875
|
-
const id = this.#timers.set(() => {
|
|
876
|
-
this.#pending.delete(region);
|
|
877
|
-
callback();
|
|
878
|
-
}, delay);
|
|
879
|
-
this.#pending.set(region, id);
|
|
880
|
-
}
|
|
881
|
-
/** Releases `region`'s pending timer, if it has one. */
|
|
882
|
-
#cancelPending(region) {
|
|
883
|
-
this.#timers.clear(this.#pending.get(region) ?? -1);
|
|
884
|
-
this.#pending.delete(region);
|
|
885
|
-
}
|
|
886
912
|
/**
|
|
887
913
|
* Resolves the live region for a politeness level.
|
|
888
914
|
*
|
|
@@ -908,6 +934,7 @@ var AnnouncerController = class extends Controller {
|
|
|
908
934
|
this.#queues.clear();
|
|
909
935
|
this.#draining.clear();
|
|
910
936
|
this.#timers.clearAll();
|
|
937
|
+
this.#regionTimers.clearAll();
|
|
911
938
|
for (const level of LEVELS) {
|
|
912
939
|
if (this.#hasTargetFor(level)) {
|
|
913
940
|
const target = level === "assertive" ? this.assertiveTarget : this.politeTarget;
|
|
@@ -981,6 +1008,23 @@ var AspectRatioController = class extends Controller {
|
|
|
981
1008
|
}
|
|
982
1009
|
};
|
|
983
1010
|
|
|
1011
|
+
// src/utils/announce.ts
|
|
1012
|
+
function announce(message, options = {}) {
|
|
1013
|
+
const text = message.trim();
|
|
1014
|
+
if (text.length === 0) return;
|
|
1015
|
+
window.dispatchEvent(
|
|
1016
|
+
new CustomEvent("stimeo--announcer:announce", {
|
|
1017
|
+
detail: { message: text, assertive: options.assertive === true }
|
|
1018
|
+
})
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
function fillTemplate(template, values) {
|
|
1022
|
+
return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
|
|
1023
|
+
const replacement = values[name];
|
|
1024
|
+
return replacement === void 0 ? match : String(replacement);
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
|
|
984
1028
|
// src/utils/composition_tracker.ts
|
|
985
1029
|
var CompositionTracker = class {
|
|
986
1030
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
@@ -1028,6 +1072,30 @@ var CompositionTracker = class {
|
|
|
1028
1072
|
};
|
|
1029
1073
|
};
|
|
1030
1074
|
|
|
1075
|
+
// src/utils/listener_set.ts
|
|
1076
|
+
var ListenerSet = class {
|
|
1077
|
+
/** The generation every `add` joins until the next `dispose()`. */
|
|
1078
|
+
#abort = new AbortController();
|
|
1079
|
+
/**
|
|
1080
|
+
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
1081
|
+
*
|
|
1082
|
+
* The set supplies the signal, so `options` carries everything else the DOM
|
|
1083
|
+
* accepts — `capture` included, which has to match at release time and no
|
|
1084
|
+
* longer has a second place to drift from.
|
|
1085
|
+
*/
|
|
1086
|
+
add(target, type, handler, options) {
|
|
1087
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Releases every listener of the open generation, synchronously, and opens the
|
|
1091
|
+
* next one. Idempotent, and safe before anything has been added.
|
|
1092
|
+
*/
|
|
1093
|
+
dispose() {
|
|
1094
|
+
this.#abort.abort();
|
|
1095
|
+
this.#abort = new AbortController();
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1031
1099
|
// src/controllers/auto_submit_controller.ts
|
|
1032
1100
|
var AutoSubmitController = class extends Controller {
|
|
1033
1101
|
static targets = ["form"];
|
|
@@ -1043,6 +1111,8 @@ var AutoSubmitController = class extends Controller {
|
|
|
1043
1111
|
#timers = new SafeTimeout();
|
|
1044
1112
|
/** Id of the pending debounce timer, so a new keystroke can reset it. */
|
|
1045
1113
|
#pendingId = 0;
|
|
1114
|
+
/** Listeners that live with the bound form rather than with the connection. */
|
|
1115
|
+
#formListeners = new ListenerSet();
|
|
1046
1116
|
/** The form the listeners are attached to; target callbacks rebind it. */
|
|
1047
1117
|
#boundForm = null;
|
|
1048
1118
|
/** Rewinds the transient state hooks just before Turbo snapshots the page. */
|
|
@@ -1052,9 +1122,7 @@ var AutoSubmitController = class extends Controller {
|
|
|
1052
1122
|
this.#boundForm?.removeAttribute("aria-busy");
|
|
1053
1123
|
const message = this.messageValue;
|
|
1054
1124
|
this.dispatch("done", { detail: { message: message || void 0 } });
|
|
1055
|
-
if (this.announceValue
|
|
1056
|
-
window.dispatchEvent(new CustomEvent("stimeo--announcer:announce", { detail: { message } }));
|
|
1057
|
-
}
|
|
1125
|
+
if (this.announceValue) announce(message);
|
|
1058
1126
|
};
|
|
1059
1127
|
/** Owns delegated IME lifecycle state and submits confirmed input text. */
|
|
1060
1128
|
#composition = new CompositionTracker({
|
|
@@ -1125,12 +1193,12 @@ var AutoSubmitController = class extends Controller {
|
|
|
1125
1193
|
if (previous) {
|
|
1126
1194
|
this.#cancelPending();
|
|
1127
1195
|
previous.removeAttribute("data-auto-submit-pending");
|
|
1128
|
-
|
|
1196
|
+
this.#formListeners.dispose();
|
|
1129
1197
|
this.#composition.unobserve(previous);
|
|
1130
1198
|
}
|
|
1131
1199
|
this.#boundForm = form;
|
|
1132
1200
|
if (!form) return;
|
|
1133
|
-
|
|
1201
|
+
this.#formListeners.add(form, "turbo:submit-end", this.#onSubmitEnd);
|
|
1134
1202
|
this.#composition.observe(form);
|
|
1135
1203
|
}
|
|
1136
1204
|
/** Resolves the form: the explicit `form` target, else a `<form>` root, else null. */
|
|
@@ -1407,10 +1475,15 @@ var LayoutObserver = class {
|
|
|
1407
1475
|
#resizeObserverFactory;
|
|
1408
1476
|
#resizeObserver = null;
|
|
1409
1477
|
#observingViewport = false;
|
|
1478
|
+
#loadContainer = null;
|
|
1410
1479
|
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
1411
1480
|
#handleViewportResize = () => {
|
|
1412
1481
|
this.#callback();
|
|
1413
1482
|
};
|
|
1483
|
+
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
1484
|
+
#handleDescendantLoad = () => {
|
|
1485
|
+
this.#callback();
|
|
1486
|
+
};
|
|
1414
1487
|
constructor(callback, options = {}) {
|
|
1415
1488
|
this.#callback = callback;
|
|
1416
1489
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
@@ -1446,14 +1519,35 @@ var LayoutObserver = class {
|
|
|
1446
1519
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
1447
1520
|
}
|
|
1448
1521
|
/**
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1522
|
+
* Starts reporting a `load` from anywhere inside `container` — an image or a
|
|
1523
|
+
* frame settling changes the box it sits in, and it measures as zero high until
|
|
1524
|
+
* then. `load` does not bubble, so the subscription is a capture-phase listener
|
|
1525
|
+
* on the container itself and nothing the caller spells.
|
|
1526
|
+
*
|
|
1527
|
+
* **One container at a time.** A further call moves the observation, so a widget
|
|
1528
|
+
* whose content element is swapped at runtime releases the element it let go by
|
|
1529
|
+
* naming the new one — there is no second place for the release to drift from.
|
|
1530
|
+
*/
|
|
1531
|
+
observeDescendantLoads(container) {
|
|
1532
|
+
this.unobserveDescendantLoads();
|
|
1533
|
+
this.#loadContainer = container;
|
|
1534
|
+
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
1535
|
+
}
|
|
1536
|
+
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
1537
|
+
unobserveDescendantLoads() {
|
|
1538
|
+
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
1539
|
+
this.#loadContainer = null;
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Releases every observation: disconnects the {@link ResizeObserver} and removes
|
|
1543
|
+
* the viewport and descendant-load listeners. Safe to call multiple times. Call
|
|
1544
|
+
* this from a controller's `disconnect()`.
|
|
1452
1545
|
*/
|
|
1453
1546
|
disconnect() {
|
|
1454
1547
|
this.#resizeObserver?.disconnect();
|
|
1455
1548
|
this.#resizeObserver = null;
|
|
1456
1549
|
this.unobserveViewport();
|
|
1550
|
+
this.unobserveDescendantLoads();
|
|
1457
1551
|
}
|
|
1458
1552
|
};
|
|
1459
1553
|
|
|
@@ -1677,25 +1771,6 @@ var BreadcrumbController = class extends Controller {
|
|
|
1677
1771
|
return active instanceof HTMLElement && element.contains(active);
|
|
1678
1772
|
}
|
|
1679
1773
|
};
|
|
1680
|
-
|
|
1681
|
-
// src/utils/announce.ts
|
|
1682
|
-
function announce(message, options = {}) {
|
|
1683
|
-
const text = message.trim();
|
|
1684
|
-
if (text.length === 0) return;
|
|
1685
|
-
window.dispatchEvent(
|
|
1686
|
-
new CustomEvent("stimeo--announcer:announce", {
|
|
1687
|
-
detail: { message: text, assertive: options.assertive === true }
|
|
1688
|
-
})
|
|
1689
|
-
);
|
|
1690
|
-
}
|
|
1691
|
-
function fillTemplate(template, values) {
|
|
1692
|
-
return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
|
|
1693
|
-
const replacement = values[name];
|
|
1694
|
-
return replacement === void 0 ? match : String(replacement);
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
// src/controllers/bulk_select_controller.ts
|
|
1699
1774
|
var BulkSelectController = class extends Controller {
|
|
1700
1775
|
static targets = ["all", "item", "bar", "count", "selectAllPages"];
|
|
1701
1776
|
static values = {
|
|
@@ -1921,6 +1996,11 @@ function monthLabelFormatter(locale) {
|
|
|
1921
1996
|
return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
|
|
1922
1997
|
}
|
|
1923
1998
|
|
|
1999
|
+
// src/utils/locale.ts
|
|
2000
|
+
function resolveLocale(element, declared = "") {
|
|
2001
|
+
return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1924
2004
|
// src/controllers/calendar_controller.ts
|
|
1925
2005
|
var OWNED_DISABLED = "data-stimeo--calendar-owns-disabled";
|
|
1926
2006
|
var CalendarController = class extends Controller {
|
|
@@ -1930,8 +2010,9 @@ var CalendarController = class extends Controller {
|
|
|
1930
2010
|
selected: { type: String, default: "" },
|
|
1931
2011
|
min: { type: String, default: "" },
|
|
1932
2012
|
max: { type: String, default: "" },
|
|
1933
|
-
weekStart: { type: Number, default: 0 }
|
|
2013
|
+
weekStart: { type: Number, default: 0 },
|
|
1934
2014
|
// 0 = Sunday, 1 = Monday, etc.
|
|
2015
|
+
locale: { type: String, default: "" }
|
|
1935
2016
|
};
|
|
1936
2017
|
static actions = ["next", "onKeydown", "prev", "selectByClick"];
|
|
1937
2018
|
static events = ["monthchange", "select"];
|
|
@@ -1973,6 +2054,17 @@ var CalendarController = class extends Controller {
|
|
|
1973
2054
|
this.#syncFocusedDateWithMonth();
|
|
1974
2055
|
this.render();
|
|
1975
2056
|
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Repaints the label when application code changes `locale` at runtime.
|
|
2059
|
+
*
|
|
2060
|
+
* Stimulus runs this for the initial Value too, before `connect()`. The paint
|
|
2061
|
+
* it triggers is idempotent — the month comes from `month` (or, while that is
|
|
2062
|
+
* still empty, from `focusedDate`), and `monthchange` only reports a move away
|
|
2063
|
+
* from a month already announced, so the first paint never reports one.
|
|
2064
|
+
*/
|
|
2065
|
+
localeValueChanged() {
|
|
2066
|
+
this.render();
|
|
2067
|
+
}
|
|
1976
2068
|
/**
|
|
1977
2069
|
* Stimulus lifecycle callback triggered automatically when the selectedValue changes.
|
|
1978
2070
|
* Re-renders grid cells to update `aria-selected` indicators.
|
|
@@ -2103,8 +2195,8 @@ var CalendarController = class extends Controller {
|
|
|
2103
2195
|
const year = monthStart.getFullYear();
|
|
2104
2196
|
const month = monthStart.getMonth() + 1;
|
|
2105
2197
|
if (this.hasLabelTarget) {
|
|
2106
|
-
const
|
|
2107
|
-
this.labelTarget.textContent = monthLabelFormatter(
|
|
2198
|
+
const locale = resolveLocale(this.element, this.localeValue);
|
|
2199
|
+
this.labelTarget.textContent = monthLabelFormatter(locale).format(monthStart);
|
|
2108
2200
|
}
|
|
2109
2201
|
const days = this.#calculateGridDays(year, month);
|
|
2110
2202
|
const dayElements = this.dayTargets;
|
|
@@ -3115,6 +3207,7 @@ var CheckboxController = class extends Controller {
|
|
|
3115
3207
|
static events = ["change", "reconcile"];
|
|
3116
3208
|
/** Collapses every lifecycle signal from one DOM update into one derived pass. */
|
|
3117
3209
|
#reconcile = new MicrotaskCoalescer(() => this.#reconcileFromChildren());
|
|
3210
|
+
#listeners = new ListenerSet();
|
|
3118
3211
|
/** Aggregate this root last settled on, so a derived repair is reported once. */
|
|
3119
3212
|
#committedState = null;
|
|
3120
3213
|
/** Watches authored checked-attribute changes on retained target elements. */
|
|
@@ -3132,14 +3225,14 @@ var CheckboxController = class extends Controller {
|
|
|
3132
3225
|
attributeFilter: ["checked"],
|
|
3133
3226
|
subtree: true
|
|
3134
3227
|
});
|
|
3135
|
-
this.element
|
|
3228
|
+
this.#listeners.add(this.element, "turbo:morph-element", this.#onMorph);
|
|
3136
3229
|
document.addEventListener("reset", this.#onReset, true);
|
|
3137
3230
|
}
|
|
3138
3231
|
/** Releases the observer, global reset listener, and every pending reconciliation. */
|
|
3139
3232
|
disconnect() {
|
|
3140
3233
|
this.#reconcile.cancel();
|
|
3141
3234
|
this.#checkedObserver.disconnect();
|
|
3142
|
-
this.
|
|
3235
|
+
this.#listeners.dispose();
|
|
3143
3236
|
document.removeEventListener("reset", this.#onReset, true);
|
|
3144
3237
|
}
|
|
3145
3238
|
/** Reconciles the aggregate for a parent added or replaced at runtime. */
|
|
@@ -4849,6 +4942,7 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
4849
4942
|
#observing = false;
|
|
4850
4943
|
/** Coalesces one target/morph/mutation batch into one full DOM reconciliation. */
|
|
4851
4944
|
#reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
|
|
4945
|
+
#listeners = new ListenerSet();
|
|
4852
4946
|
/** Provides the last-resort focus landmark without claiming an authored tabindex. */
|
|
4853
4947
|
#rootTabindex = new TabindexLoan();
|
|
4854
4948
|
/** Watches retained targets and controls whose declarative or reflected state changed. */
|
|
@@ -4887,9 +4981,9 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
4887
4981
|
subtree: true
|
|
4888
4982
|
});
|
|
4889
4983
|
this.#observing = true;
|
|
4890
|
-
this.element
|
|
4891
|
-
this.element
|
|
4892
|
-
this.element
|
|
4984
|
+
this.#listeners.add(this.element, "change", this.#onTriggerInput);
|
|
4985
|
+
this.#listeners.add(this.element, "input", this.#onTriggerInput);
|
|
4986
|
+
this.#listeners.add(this.element, "turbo:morph-element", this.#onMorph);
|
|
4893
4987
|
document.addEventListener("reset", this.#onReset, true);
|
|
4894
4988
|
}
|
|
4895
4989
|
/** Releases all external resources and returns the temporary focus landmark loan. */
|
|
@@ -4897,9 +4991,7 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
4897
4991
|
this.#reconcile.cancel();
|
|
4898
4992
|
this.#observing = false;
|
|
4899
4993
|
this.#observer.disconnect();
|
|
4900
|
-
this.
|
|
4901
|
-
this.element.removeEventListener("input", this.#onTriggerInput);
|
|
4902
|
-
this.element.removeEventListener("turbo:morph-element", this.#onMorph);
|
|
4994
|
+
this.#listeners.dispose();
|
|
4903
4995
|
document.removeEventListener("reset", this.#onReset, true);
|
|
4904
4996
|
this.#rootTabindex.returnAll();
|
|
4905
4997
|
}
|
|
@@ -5708,10 +5800,11 @@ function toHalfWidth(text) {
|
|
|
5708
5800
|
}
|
|
5709
5801
|
|
|
5710
5802
|
// src/controllers/currency_input_controller.ts
|
|
5803
|
+
var FALLBACK_LOCALE = "en-US";
|
|
5711
5804
|
var CurrencyInputController = class extends Controller {
|
|
5712
5805
|
static targets = ["display", "field", "srValue"];
|
|
5713
5806
|
static values = {
|
|
5714
|
-
locale: { type: String, default: "
|
|
5807
|
+
locale: { type: String, default: "" },
|
|
5715
5808
|
currency: { type: String, default: "" },
|
|
5716
5809
|
precision: { type: Number, default: 2 }
|
|
5717
5810
|
};
|
|
@@ -5721,7 +5814,7 @@ var CurrencyInputController = class extends Controller {
|
|
|
5721
5814
|
#lastValue = null;
|
|
5722
5815
|
#started = false;
|
|
5723
5816
|
/** Validated mirrors of the Values; the hot path never reads a raw Value. */
|
|
5724
|
-
#locale
|
|
5817
|
+
#locale;
|
|
5725
5818
|
#currency = "";
|
|
5726
5819
|
#precision = 2;
|
|
5727
5820
|
/** Formatters rebuilt only when a Value changes, never per keystroke. */
|
|
@@ -5958,8 +6051,9 @@ var CurrencyInputController = class extends Controller {
|
|
|
5958
6051
|
* formatters from the validated set.
|
|
5959
6052
|
*/
|
|
5960
6053
|
#revalidate() {
|
|
5961
|
-
const
|
|
5962
|
-
|
|
6054
|
+
const resolved = resolveLocale(this.element, this.localeValue);
|
|
6055
|
+
const usable = intlFormatter(Intl.NumberFormat, resolved, {});
|
|
6056
|
+
this.#locale = usable === null ? FALLBACK_LOCALE : resolved;
|
|
5963
6057
|
const precision = this.precisionValue;
|
|
5964
6058
|
this.#precision = Number.isInteger(precision) && precision >= 0 && precision <= 100 ? precision : 2;
|
|
5965
6059
|
this.#currency = "";
|
|
@@ -5974,7 +6068,7 @@ var CurrencyInputController = class extends Controller {
|
|
|
5974
6068
|
Intl.NumberFormat,
|
|
5975
6069
|
this.#locale,
|
|
5976
6070
|
{ useGrouping: true, maximumFractionDigits: 0 },
|
|
5977
|
-
|
|
6071
|
+
FALLBACK_LOCALE
|
|
5978
6072
|
);
|
|
5979
6073
|
this.#fixed = intlFormatter(
|
|
5980
6074
|
Intl.NumberFormat,
|
|
@@ -5984,15 +6078,15 @@ var CurrencyInputController = class extends Controller {
|
|
|
5984
6078
|
minimumFractionDigits: this.#precision,
|
|
5985
6079
|
maximumFractionDigits: this.#precision
|
|
5986
6080
|
},
|
|
5987
|
-
|
|
6081
|
+
FALLBACK_LOCALE
|
|
5988
6082
|
);
|
|
5989
6083
|
this.#accessible = this.#currency ? intlFormatter(
|
|
5990
6084
|
Intl.NumberFormat,
|
|
5991
6085
|
this.#locale,
|
|
5992
6086
|
{ style: "currency", currency: this.#currency },
|
|
5993
|
-
|
|
6087
|
+
FALLBACK_LOCALE
|
|
5994
6088
|
) : this.#fixed;
|
|
5995
|
-
const parts = intlFormatter(Intl.NumberFormat, this.#locale, {},
|
|
6089
|
+
const parts = intlFormatter(Intl.NumberFormat, this.#locale, {}, FALLBACK_LOCALE).formatToParts(
|
|
5996
6090
|
11111.1
|
|
5997
6091
|
);
|
|
5998
6092
|
this.#group = parts.find((p) => p.type === "group")?.value ?? ",";
|
|
@@ -6002,7 +6096,7 @@ var CurrencyInputController = class extends Controller {
|
|
|
6002
6096
|
Intl.NumberFormat,
|
|
6003
6097
|
this.#locale,
|
|
6004
6098
|
{ useGrouping: false },
|
|
6005
|
-
|
|
6099
|
+
FALLBACK_LOCALE
|
|
6006
6100
|
);
|
|
6007
6101
|
for (let i = 0; i <= 9; i++) {
|
|
6008
6102
|
const digit = digitFormatter.format(i);
|
|
@@ -6377,7 +6471,8 @@ var DateRangePickerController = class extends Controller {
|
|
|
6377
6471
|
// A JSON list read through `parseStringList` rather than Stimulus's `Array`
|
|
6378
6472
|
// type: that reader throws out of the value observer before any callback
|
|
6379
6473
|
// runs, so one malformed attribute would stop the picker from connecting.
|
|
6380
|
-
disabledDates: { type: String, default: "" }
|
|
6474
|
+
disabledDates: { type: String, default: "" },
|
|
6475
|
+
locale: { type: String, default: "" }
|
|
6381
6476
|
};
|
|
6382
6477
|
static actions = ["applyPreset", "next", "onKeydown", "prev", "previewTo", "selectDate"];
|
|
6383
6478
|
static events = ["change", "monthchange"];
|
|
@@ -6430,6 +6525,10 @@ var DateRangePickerController = class extends Controller {
|
|
|
6430
6525
|
this.#beforeCache.deactivate();
|
|
6431
6526
|
this.#focusTimer.clearAll();
|
|
6432
6527
|
}
|
|
6528
|
+
/** Repaints the label when application code changes `locale` at runtime. */
|
|
6529
|
+
localeValueChanged() {
|
|
6530
|
+
this.#repaint.schedule();
|
|
6531
|
+
}
|
|
6433
6532
|
/** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
|
|
6434
6533
|
minValueChanged() {
|
|
6435
6534
|
this.#repaint.schedule();
|
|
@@ -6618,8 +6717,7 @@ var DateRangePickerController = class extends Controller {
|
|
|
6618
6717
|
const { year, month } = info;
|
|
6619
6718
|
if (this.#previewDate && !this.#isSelectable(this.#previewDate)) this.#previewDate = "";
|
|
6620
6719
|
if (this.hasMonthLabelTarget) {
|
|
6621
|
-
const
|
|
6622
|
-
const formatter = monthLabelFormatter(lang);
|
|
6720
|
+
const formatter = monthLabelFormatter(resolveLocale(this.element, this.localeValue));
|
|
6623
6721
|
this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));
|
|
6624
6722
|
}
|
|
6625
6723
|
const [rangeStart, rangeEnd] = this.#visualRange();
|
|
@@ -7198,6 +7296,7 @@ var DirtyFormController = class extends Controller {
|
|
|
7198
7296
|
#dirty = false;
|
|
7199
7297
|
#beforeunloadBound = false;
|
|
7200
7298
|
#timeouts = new SafeTimeout();
|
|
7299
|
+
#listeners = new ListenerSet();
|
|
7201
7300
|
/** Whether the active submission still represents the form's current values. */
|
|
7202
7301
|
#submitting = false;
|
|
7203
7302
|
#submitAttempt = null;
|
|
@@ -7255,24 +7354,19 @@ var DirtyFormController = class extends Controller {
|
|
|
7255
7354
|
this.#baseline = this.#serialize();
|
|
7256
7355
|
this.#setDirty(false);
|
|
7257
7356
|
this.element.removeAttribute("data-dirty");
|
|
7258
|
-
this.element
|
|
7259
|
-
this.element
|
|
7260
|
-
this.element
|
|
7261
|
-
this.element
|
|
7262
|
-
this.element
|
|
7263
|
-
this.element
|
|
7357
|
+
this.#listeners.add(this.element, "input", this.#onFieldChange);
|
|
7358
|
+
this.#listeners.add(this.element, "change", this.#onFieldChange);
|
|
7359
|
+
this.#listeners.add(this.element, "submit", this.#onSubmit);
|
|
7360
|
+
this.#listeners.add(this.element, "formdata", this.#onFormData);
|
|
7361
|
+
this.#listeners.add(this.element, "turbo:submit-start", this.#onSubmitStart);
|
|
7362
|
+
this.#listeners.add(this.element, "turbo:submit-end", this.#onSubmitEnd);
|
|
7264
7363
|
this.#unregisterVisit = registerVisitParticipant(this.element.ownerDocument, {
|
|
7265
7364
|
form: this.element,
|
|
7266
7365
|
evaluate: (event) => this.#evaluateVisit(event)
|
|
7267
7366
|
});
|
|
7268
7367
|
}
|
|
7269
7368
|
disconnect() {
|
|
7270
|
-
this.
|
|
7271
|
-
this.element.removeEventListener("change", this.#onFieldChange);
|
|
7272
|
-
this.element.removeEventListener("submit", this.#onSubmit);
|
|
7273
|
-
this.element.removeEventListener("formdata", this.#onFormData);
|
|
7274
|
-
this.element.removeEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
7275
|
-
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
7369
|
+
this.#listeners.dispose();
|
|
7276
7370
|
this.#unregisterVisit?.();
|
|
7277
7371
|
this.#unregisterVisit = null;
|
|
7278
7372
|
this.#unbindBeforeUnload();
|
|
@@ -8590,6 +8684,80 @@ var FilterController = class extends Controller {
|
|
|
8590
8684
|
return this.itemTargets.some((item) => !item.hidden && group.contains(item));
|
|
8591
8685
|
}
|
|
8592
8686
|
};
|
|
8687
|
+
|
|
8688
|
+
// src/utils/pausable_timers.ts
|
|
8689
|
+
var PausableTimers = class {
|
|
8690
|
+
#timers = new KeyedTimers();
|
|
8691
|
+
/** Every key armed or held; an entry goes as its timer fires, or with the key. */
|
|
8692
|
+
#entries = /* @__PURE__ */ new Map();
|
|
8693
|
+
/**
|
|
8694
|
+
* Arms `callback` after `delay` ms for `key`, replacing whatever `key` had.
|
|
8695
|
+
* A held key stays held and banks `delay` for its resume.
|
|
8696
|
+
*/
|
|
8697
|
+
set(key, callback, delay) {
|
|
8698
|
+
const reasons = this.#entries.get(key)?.reasons ?? /* @__PURE__ */ new Set();
|
|
8699
|
+
const entry = { callback, startedAt: 0, remaining: delay, reasons };
|
|
8700
|
+
this.#entries.set(key, entry);
|
|
8701
|
+
if (reasons.size === 0) this.#arm(key, entry);
|
|
8702
|
+
}
|
|
8703
|
+
/**
|
|
8704
|
+
* Holds `key` for `reason` and banks the time left, with a floor of one
|
|
8705
|
+
* millisecond. Reports whether this call is the one that stopped a running
|
|
8706
|
+
* timer, so the caller can write its held-state hook exactly once.
|
|
8707
|
+
*/
|
|
8708
|
+
pause(key, reason) {
|
|
8709
|
+
const entry = this.#entries.get(key);
|
|
8710
|
+
if (entry === void 0) return false;
|
|
8711
|
+
entry.reasons.add(reason);
|
|
8712
|
+
if (!this.#timers.has(key)) return false;
|
|
8713
|
+
this.#timers.clear(key);
|
|
8714
|
+
entry.remaining = Math.max(1, entry.remaining - (Date.now() - entry.startedAt));
|
|
8715
|
+
return true;
|
|
8716
|
+
}
|
|
8717
|
+
/**
|
|
8718
|
+
* Releases `reason` on `key`, arming the banked time again once no reason is
|
|
8719
|
+
* left. Reports whether this call is the one that started the timer again.
|
|
8720
|
+
*/
|
|
8721
|
+
resume(key, reason) {
|
|
8722
|
+
const entry = this.#entries.get(key);
|
|
8723
|
+
if (entry === void 0) return false;
|
|
8724
|
+
entry.reasons.delete(reason);
|
|
8725
|
+
if (entry.reasons.size > 0 || this.#timers.has(key)) return false;
|
|
8726
|
+
this.#arm(key, entry);
|
|
8727
|
+
return true;
|
|
8728
|
+
}
|
|
8729
|
+
/** Whether `key` is armed or held — that is, whether this registry drives it at all. */
|
|
8730
|
+
tracks(key) {
|
|
8731
|
+
return this.#entries.has(key);
|
|
8732
|
+
}
|
|
8733
|
+
/** Cancels `key`'s timer and drops its hold. */
|
|
8734
|
+
clear(key) {
|
|
8735
|
+
this.#timers.clear(key);
|
|
8736
|
+
this.#entries.delete(key);
|
|
8737
|
+
}
|
|
8738
|
+
/**
|
|
8739
|
+
* Cancels every timer and forgets every key. Call this from a controller's
|
|
8740
|
+
* `disconnect()` so neither a timer nor a hold outlives the element.
|
|
8741
|
+
*/
|
|
8742
|
+
clearAll() {
|
|
8743
|
+
this.#timers.clearAll();
|
|
8744
|
+
this.#entries.clear();
|
|
8745
|
+
}
|
|
8746
|
+
/** Starts `entry`'s banked time running for `key`, and drops it as it fires. */
|
|
8747
|
+
#arm(key, entry) {
|
|
8748
|
+
entry.startedAt = Date.now();
|
|
8749
|
+
this.#timers.set(
|
|
8750
|
+
key,
|
|
8751
|
+
() => {
|
|
8752
|
+
this.#entries.delete(key);
|
|
8753
|
+
entry.callback();
|
|
8754
|
+
},
|
|
8755
|
+
entry.remaining
|
|
8756
|
+
);
|
|
8757
|
+
}
|
|
8758
|
+
};
|
|
8759
|
+
|
|
8760
|
+
// src/controllers/flash_controller.ts
|
|
8593
8761
|
var ASSERTIVE_TYPES = /* @__PURE__ */ new Set(["alert", "error"]);
|
|
8594
8762
|
var MESSAGE_SELECTOR = '[data-stimeo--flash-target="message"]';
|
|
8595
8763
|
var FlashController = class extends Controller {
|
|
@@ -8601,12 +8769,13 @@ var FlashController = class extends Controller {
|
|
|
8601
8769
|
};
|
|
8602
8770
|
static actions = ["dismiss"];
|
|
8603
8771
|
static events = ["show", "dismiss", "reconcile"];
|
|
8772
|
+
/** Removal timers for the leaving transition; the auto-dismiss ones live below. */
|
|
8604
8773
|
#timers = new SafeTimeout();
|
|
8774
|
+
/** Per-message auto-dismiss, held open while the message is hovered or focused. */
|
|
8775
|
+
#dismiss = new PausableTimers();
|
|
8605
8776
|
#observer = null;
|
|
8606
8777
|
/** Whether the controller is between `connect()` and `disconnect()`. */
|
|
8607
8778
|
#connected = false;
|
|
8608
|
-
/** Auto-dismiss timer state keyed by message element. */
|
|
8609
|
-
#state = /* @__PURE__ */ new Map();
|
|
8610
8779
|
/** Messages already processed, in insertion order, to enforce `max` and avoid double work. */
|
|
8611
8780
|
#order = [];
|
|
8612
8781
|
/**
|
|
@@ -8632,8 +8801,8 @@ var FlashController = class extends Controller {
|
|
|
8632
8801
|
this.#beforeCache.deactivate();
|
|
8633
8802
|
this.#stopObserving();
|
|
8634
8803
|
this.#timers.clearAll();
|
|
8804
|
+
this.#dismiss.clearAll();
|
|
8635
8805
|
for (const message of this.#order) this.#unbindPause(message);
|
|
8636
|
-
this.#state.clear();
|
|
8637
8806
|
this.#order.length = 0;
|
|
8638
8807
|
this.#leaving.clear();
|
|
8639
8808
|
}
|
|
@@ -8762,8 +8931,7 @@ var FlashController = class extends Controller {
|
|
|
8762
8931
|
* own role to do the announcing (dynamic inserts). Idempotent per message.
|
|
8763
8932
|
*/
|
|
8764
8933
|
#process(message, bridge) {
|
|
8765
|
-
if (this.#
|
|
8766
|
-
if (this.#leaving.has(message)) return;
|
|
8934
|
+
if (this.#order.includes(message) || this.#leaving.has(message)) return;
|
|
8767
8935
|
const type = message.getAttribute("data-flash-type") ?? "";
|
|
8768
8936
|
const assertive = ASSERTIVE_TYPES.has(type);
|
|
8769
8937
|
if (!message.hasAttribute("role")) {
|
|
@@ -8774,11 +8942,7 @@ var FlashController = class extends Controller {
|
|
|
8774
8942
|
this.#bindPause(message);
|
|
8775
8943
|
const text = message.textContent?.trim() ?? "";
|
|
8776
8944
|
this.dispatch("show", { target: message, detail: { type, message: text } });
|
|
8777
|
-
if (bridge
|
|
8778
|
-
window.dispatchEvent(
|
|
8779
|
-
new CustomEvent("stimeo--announcer:announce", { detail: { message: text, assertive } })
|
|
8780
|
-
);
|
|
8781
|
-
}
|
|
8945
|
+
if (bridge) announce(text, { assertive });
|
|
8782
8946
|
this.#startTimer(message);
|
|
8783
8947
|
this.#enforceMax();
|
|
8784
8948
|
}
|
|
@@ -8791,53 +8955,33 @@ var FlashController = class extends Controller {
|
|
|
8791
8955
|
this.#beginDismiss(oldest, "limit");
|
|
8792
8956
|
}
|
|
8793
8957
|
}
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
const id = this.#timers.set(() => this.#beginDismiss(message, "timeout"), duration);
|
|
8799
|
-
this.#state.set(message, {
|
|
8800
|
-
id,
|
|
8801
|
-
startedAt: Date.now(),
|
|
8802
|
-
remaining: duration,
|
|
8803
|
-
paused: existing?.paused ?? /* @__PURE__ */ new Set()
|
|
8804
|
-
});
|
|
8958
|
+
/** Arms a message's auto-dismiss; a non-positive `duration` means it never expires. */
|
|
8959
|
+
#startTimer(message) {
|
|
8960
|
+
if (this.durationValue <= 0) return;
|
|
8961
|
+
this.#dismiss.set(message, () => this.#beginDismiss(message, "timeout"), this.durationValue);
|
|
8805
8962
|
}
|
|
8806
8963
|
/**
|
|
8807
|
-
*
|
|
8808
|
-
* Hover and focus are independent reasons
|
|
8809
|
-
* first of them
|
|
8964
|
+
* Holds a message's auto-dismiss open while it is hovered or focused
|
|
8965
|
+
* (WCAG 2.2 2.2.1). Hover and focus are independent reasons, and the registry
|
|
8966
|
+
* banks the time left on the first of them and waits for the last.
|
|
8810
8967
|
*/
|
|
8811
8968
|
#pause(message, reason) {
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
if (timer.id === 0) return;
|
|
8816
|
-
this.#timers.clear(timer.id);
|
|
8817
|
-
const remaining = Math.max(1, timer.remaining - (Date.now() - timer.startedAt));
|
|
8818
|
-
this.#state.set(message, { id: 0, startedAt: 0, remaining, paused: timer.paused });
|
|
8819
|
-
}
|
|
8820
|
-
/** Resumes a paused message's auto-dismiss with the banked time. */
|
|
8969
|
+
this.#dismiss.pause(message, reason);
|
|
8970
|
+
}
|
|
8971
|
+
/** Releases one reason, resuming the banked time once no reason is left. */
|
|
8821
8972
|
#resume(message, reason) {
|
|
8822
|
-
|
|
8823
|
-
if (!timer) return;
|
|
8824
|
-
timer.paused.delete(reason);
|
|
8825
|
-
if (timer.paused.size > 0) return;
|
|
8826
|
-
if (timer.id !== 0) return;
|
|
8827
|
-
this.#startTimer(message, timer.remaining);
|
|
8973
|
+
this.#dismiss.resume(message, reason);
|
|
8828
8974
|
}
|
|
8829
8975
|
/** Releases every per-message resource: timer, stacking slot, pause listeners. */
|
|
8830
8976
|
#forget(message) {
|
|
8831
|
-
|
|
8832
|
-
if (timer?.id) this.#timers.clear(timer.id);
|
|
8833
|
-
this.#state.delete(message);
|
|
8977
|
+
this.#dismiss.clear(message);
|
|
8834
8978
|
const index = this.#order.indexOf(message);
|
|
8835
8979
|
if (index !== -1) this.#order.splice(index, 1);
|
|
8836
8980
|
this.#unbindPause(message);
|
|
8837
8981
|
}
|
|
8838
8982
|
/** Marks a message leaving, then removes it after its CSS transition and emits dismiss. */
|
|
8839
8983
|
#beginDismiss(message, reason) {
|
|
8840
|
-
if (!this.#
|
|
8984
|
+
if (!this.#order.includes(message)) return;
|
|
8841
8985
|
this.#forget(message);
|
|
8842
8986
|
this.#leaving.add(message);
|
|
8843
8987
|
message.setAttribute("data-flash-state", "leaving");
|
|
@@ -9536,6 +9680,7 @@ var FrameLoadingController = class extends Controller {
|
|
|
9536
9680
|
#floor = new MinDurationFloor(this.#timeouts);
|
|
9537
9681
|
#gate = new DetachGate();
|
|
9538
9682
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
9683
|
+
#listeners = new ListenerSet();
|
|
9539
9684
|
#loading = false;
|
|
9540
9685
|
/**
|
|
9541
9686
|
* The optional targets this controller revealed, and the content it marked inert.
|
|
@@ -9562,14 +9707,12 @@ var FrameLoadingController = class extends Controller {
|
|
|
9562
9707
|
connect() {
|
|
9563
9708
|
this.#gate.cancel();
|
|
9564
9709
|
this.#beforeCache.activate();
|
|
9565
|
-
this.element
|
|
9566
|
-
this.element
|
|
9567
|
-
this.element
|
|
9710
|
+
this.#listeners.add(this.element, "turbo:before-fetch-request", this.#onStart);
|
|
9711
|
+
this.#listeners.add(this.element, "turbo:frame-load", this.#onEnd);
|
|
9712
|
+
this.#listeners.add(this.element, "turbo:fetch-request-error", this.#onEnd);
|
|
9568
9713
|
}
|
|
9569
9714
|
disconnect() {
|
|
9570
|
-
this.
|
|
9571
|
-
this.element.removeEventListener("turbo:frame-load", this.#onEnd);
|
|
9572
|
-
this.element.removeEventListener("turbo:fetch-request-error", this.#onEnd);
|
|
9715
|
+
this.#listeners.dispose();
|
|
9573
9716
|
this.#beforeCache.deactivate();
|
|
9574
9717
|
this.#gate.disconnected(this, () => this.#teardown());
|
|
9575
9718
|
}
|
|
@@ -9731,14 +9874,11 @@ var HighlightController = class extends Controller {
|
|
|
9731
9874
|
observe: { type: Boolean, default: false }
|
|
9732
9875
|
};
|
|
9733
9876
|
static events = ["start", "end"];
|
|
9734
|
-
#timeouts = new SafeTimeout();
|
|
9735
9877
|
/**
|
|
9736
|
-
* The removal timer this connection has outstanding for
|
|
9737
|
-
*
|
|
9738
|
-
* so a cleared id can never be matched against a recycled one. Which connection owns
|
|
9739
|
-
* an element's hook is answered by the shared owner registry above.
|
|
9878
|
+
* The removal timer this connection has outstanding for each element. Which
|
|
9879
|
+
* connection owns an element's hook is answered by the shared owner registry above.
|
|
9740
9880
|
*/
|
|
9741
|
-
#
|
|
9881
|
+
#removal = new KeyedTimers();
|
|
9742
9882
|
#observer = null;
|
|
9743
9883
|
connect() {
|
|
9744
9884
|
this.#clearArrivedHook(this.element);
|
|
@@ -9755,8 +9895,7 @@ var HighlightController = class extends Controller {
|
|
|
9755
9895
|
disconnect() {
|
|
9756
9896
|
this.#observer?.disconnect();
|
|
9757
9897
|
this.#observer = null;
|
|
9758
|
-
this.#
|
|
9759
|
-
this.#pending = /* @__PURE__ */ new WeakMap();
|
|
9898
|
+
this.#removal.clearAll();
|
|
9760
9899
|
}
|
|
9761
9900
|
/** Drops a hook that arrived with the DOM, along with this connection's claim on it. */
|
|
9762
9901
|
#clearArrivedHook(el) {
|
|
@@ -9777,13 +9916,15 @@ var HighlightController = class extends Controller {
|
|
|
9777
9916
|
this.#releasePending(el);
|
|
9778
9917
|
el.setAttribute("data-highlight", "true");
|
|
9779
9918
|
this.dispatch("start", { target: el, detail: { element: el } });
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9919
|
+
this.#removal.set(
|
|
9920
|
+
el,
|
|
9921
|
+
() => {
|
|
9922
|
+
hookOwners.delete(el);
|
|
9923
|
+
el.removeAttribute("data-highlight");
|
|
9924
|
+
this.dispatch("end", { target: el, detail: { element: el } });
|
|
9925
|
+
},
|
|
9926
|
+
this.durationValue
|
|
9927
|
+
);
|
|
9787
9928
|
hookOwners.set(el, this);
|
|
9788
9929
|
}
|
|
9789
9930
|
/**
|
|
@@ -9796,10 +9937,9 @@ var HighlightController = class extends Controller {
|
|
|
9796
9937
|
if (owner !== void 0 && owner !== this) owner.#cancelPending(el);
|
|
9797
9938
|
this.#cancelPending(el);
|
|
9798
9939
|
}
|
|
9799
|
-
/** Releases `el`'s pending removal timer, if it has one. */
|
|
9940
|
+
/** Releases `el`'s pending removal timer, if it has one, and this connection's claim. */
|
|
9800
9941
|
#cancelPending(el) {
|
|
9801
|
-
this.#
|
|
9802
|
-
this.#pending.delete(el);
|
|
9942
|
+
this.#removal.clear(el);
|
|
9803
9943
|
hookOwners.delete(el);
|
|
9804
9944
|
}
|
|
9805
9945
|
};
|
|
@@ -11147,13 +11287,13 @@ var LocalTimeController = class extends Controller {
|
|
|
11147
11287
|
};
|
|
11148
11288
|
if (options.dateStyle === void 0 && options.timeStyle === void 0) return null;
|
|
11149
11289
|
if (this.timeZoneValue.length > 0) options.timeZone = this.timeZoneValue;
|
|
11150
|
-
const formatter = intlFormatter(
|
|
11290
|
+
const formatter = intlFormatter(
|
|
11291
|
+
Intl.DateTimeFormat,
|
|
11292
|
+
resolveLocale(this.element, this.localeValue),
|
|
11293
|
+
options
|
|
11294
|
+
);
|
|
11151
11295
|
return formatter === null ? null : formatter.format(date);
|
|
11152
11296
|
}
|
|
11153
|
-
/** Locale precedence: the value, then the nearest `lang` up the ancestor chain. */
|
|
11154
|
-
get #locale() {
|
|
11155
|
-
return this.localeValue || this.element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
11156
|
-
}
|
|
11157
11297
|
};
|
|
11158
11298
|
var COLUMNS_PROPERTY = "--stimeo--masonry-columns";
|
|
11159
11299
|
var DEFAULT_MIN_COLUMN_WIDTH = 240;
|
|
@@ -11190,13 +11330,6 @@ var MasonryController = class extends Controller {
|
|
|
11190
11330
|
#mutationObserver = null;
|
|
11191
11331
|
/** Last published column count, so `layout` fires only on real changes. */
|
|
11192
11332
|
#lastColumns = 0;
|
|
11193
|
-
/**
|
|
11194
|
-
* Re-pack when a descendant resource finishes loading. Images/iframes report a
|
|
11195
|
-
* height of 0 until loaded, which would skew the shortest-column packing if the
|
|
11196
|
-
* first pass ran before they settled; `load` does not bubble, so this is bound in
|
|
11197
|
-
* the capture phase to catch every descendant.
|
|
11198
|
-
*/
|
|
11199
|
-
#onLoad = () => this.#reconcile.schedule();
|
|
11200
11333
|
/** Resolves the declared column width once, falling back when it is unreadable. */
|
|
11201
11334
|
minColumnWidthValueChanged() {
|
|
11202
11335
|
this.#minColumnWidth = usableNumber(this.minColumnWidthValue, DEFAULT_MIN_COLUMN_WIDTH);
|
|
@@ -11231,18 +11364,17 @@ var MasonryController = class extends Controller {
|
|
|
11231
11364
|
this.#mutationObserver = new MutationObserver(() => this.#reconcile.schedule());
|
|
11232
11365
|
this.#mutationObserver.observe(this.element, { childList: true, subtree: true });
|
|
11233
11366
|
}
|
|
11234
|
-
this.
|
|
11367
|
+
this.#layout.observeDescendantLoads(this.element);
|
|
11235
11368
|
this.#relayout();
|
|
11236
11369
|
this.#reconcile.activate();
|
|
11237
11370
|
}
|
|
11238
|
-
/** Releases
|
|
11371
|
+
/** Releases every observation so nothing fires after detach. */
|
|
11239
11372
|
disconnect() {
|
|
11240
11373
|
this.#reconcile.cancel();
|
|
11241
11374
|
this.#released.clear();
|
|
11242
11375
|
this.#layout.disconnect();
|
|
11243
11376
|
this.#mutationObserver?.disconnect();
|
|
11244
11377
|
this.#mutationObserver = null;
|
|
11245
|
-
this.element.removeEventListener("load", this.#onLoad, true);
|
|
11246
11378
|
this.#lastColumns = 0;
|
|
11247
11379
|
}
|
|
11248
11380
|
/**
|
|
@@ -14297,6 +14429,7 @@ var OptimisticController = class extends Controller {
|
|
|
14297
14429
|
/** The submission that owns the applied optimistic state, if any. */
|
|
14298
14430
|
#pending = null;
|
|
14299
14431
|
#gate = new DetachGate();
|
|
14432
|
+
#listeners = new ListenerSet();
|
|
14300
14433
|
/** True between `connect()` and `disconnect()`, so a repeat connect rewinds nothing. */
|
|
14301
14434
|
#connected = false;
|
|
14302
14435
|
connect() {
|
|
@@ -14304,13 +14437,12 @@ var OptimisticController = class extends Controller {
|
|
|
14304
14437
|
this.#gate.cancel();
|
|
14305
14438
|
this.#connected = true;
|
|
14306
14439
|
if (restored && this.element.hasAttribute("data-optimistic")) this.#revert();
|
|
14307
|
-
this.element
|
|
14308
|
-
this.element
|
|
14440
|
+
this.#listeners.add(this.element, "turbo:submit-start", this.#onSubmitStart);
|
|
14441
|
+
this.#listeners.add(this.element, "turbo:submit-end", this.#onSubmitEnd);
|
|
14309
14442
|
}
|
|
14310
14443
|
disconnect() {
|
|
14311
14444
|
this.#connected = false;
|
|
14312
|
-
this.
|
|
14313
|
-
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
14445
|
+
this.#listeners.dispose();
|
|
14314
14446
|
this.#gate.disconnected(this, () => {
|
|
14315
14447
|
this.#pending = null;
|
|
14316
14448
|
});
|
|
@@ -15002,7 +15134,7 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
15002
15134
|
this.#observedViewport = next;
|
|
15003
15135
|
this.#layout.observe(next);
|
|
15004
15136
|
this.#layout.observeViewport();
|
|
15005
|
-
|
|
15137
|
+
this.#layout.observeDescendantLoads(next);
|
|
15006
15138
|
this.#syncContentObservation();
|
|
15007
15139
|
if (typeof MutationObserver !== "undefined") {
|
|
15008
15140
|
this.#mutationObserver = new MutationObserver(() => {
|
|
@@ -15040,16 +15172,13 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
15040
15172
|
#stopObservingViewport() {
|
|
15041
15173
|
this.#mutationObserver?.disconnect();
|
|
15042
15174
|
this.#mutationObserver = null;
|
|
15043
|
-
this.#
|
|
15175
|
+
this.#layout.unobserveDescendantLoads();
|
|
15044
15176
|
if (this.#observedViewport) this.#layout.unobserve(this.#observedViewport);
|
|
15045
15177
|
for (const content of this.#observedContent) this.#layout.unobserve(content);
|
|
15046
15178
|
this.#observedContent.clear();
|
|
15047
15179
|
this.#observedViewport = null;
|
|
15048
15180
|
this.#layout.unobserveViewport();
|
|
15049
15181
|
}
|
|
15050
|
-
#onContentLoad = () => {
|
|
15051
|
-
if (this.#connected) this.update();
|
|
15052
|
-
};
|
|
15053
15182
|
get #threshold() {
|
|
15054
15183
|
const value = this.thresholdValue;
|
|
15055
15184
|
return Number.isFinite(value) ? Math.max(0, value) : 1;
|
|
@@ -18580,7 +18709,7 @@ var ReadMoreController = class extends Controller {
|
|
|
18580
18709
|
this.#observedContent = next;
|
|
18581
18710
|
this.#layout.observe(next);
|
|
18582
18711
|
this.#layout.observeViewport();
|
|
18583
|
-
|
|
18712
|
+
this.#layout.observeDescendantLoads(next);
|
|
18584
18713
|
if (typeof MutationObserver !== "undefined") {
|
|
18585
18714
|
this.#contentMutationObserver = new MutationObserver(this.#update);
|
|
18586
18715
|
this.#contentMutationObserver.observe(next, {
|
|
@@ -18594,7 +18723,7 @@ var ReadMoreController = class extends Controller {
|
|
|
18594
18723
|
this.#deferredHide.releaseAll();
|
|
18595
18724
|
if (this.#observedContent) {
|
|
18596
18725
|
this.#layout.unobserve(this.#observedContent);
|
|
18597
|
-
this.#
|
|
18726
|
+
this.#layout.unobserveDescendantLoads();
|
|
18598
18727
|
}
|
|
18599
18728
|
this.#observedContent = null;
|
|
18600
18729
|
this.#contentMutationObserver?.disconnect();
|
|
@@ -18620,6 +18749,38 @@ var ReadMoreController = class extends Controller {
|
|
|
18620
18749
|
trigger.hidden = true;
|
|
18621
18750
|
}
|
|
18622
18751
|
};
|
|
18752
|
+
|
|
18753
|
+
// src/utils/frame_coalescer.ts
|
|
18754
|
+
var FrameCoalescer = class {
|
|
18755
|
+
#frame = null;
|
|
18756
|
+
/**
|
|
18757
|
+
* Runs `run` on the next frame, unless a frame is already pending — the first
|
|
18758
|
+
* request of a burst wins and the rest are dropped. The pending frame is
|
|
18759
|
+
* released before `run`, so `run` may request the next one.
|
|
18760
|
+
*/
|
|
18761
|
+
schedule(run) {
|
|
18762
|
+
if (this.#frame !== null) return;
|
|
18763
|
+
this.#frame = requestAnimationFrame(() => {
|
|
18764
|
+
this.#frame = null;
|
|
18765
|
+
run();
|
|
18766
|
+
});
|
|
18767
|
+
}
|
|
18768
|
+
/**
|
|
18769
|
+
* Drops the pending frame, and reaches the platform only when there is one.
|
|
18770
|
+
*
|
|
18771
|
+
* There is no handle value that stands for "nothing pending":
|
|
18772
|
+
* `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
|
|
18773
|
+
* arrives as a large positive number that the same allocator can hand out, and
|
|
18774
|
+
* an idle cancel would then drop a frame belonging to someone else.
|
|
18775
|
+
*/
|
|
18776
|
+
cancel() {
|
|
18777
|
+
if (this.#frame === null) return;
|
|
18778
|
+
cancelAnimationFrame(this.#frame);
|
|
18779
|
+
this.#frame = null;
|
|
18780
|
+
}
|
|
18781
|
+
};
|
|
18782
|
+
|
|
18783
|
+
// src/controllers/reading_progress_controller.ts
|
|
18623
18784
|
var PROGRESS_PROPERTY = "--stimeo--reading-progress";
|
|
18624
18785
|
var ReadingProgressController = class extends Controller {
|
|
18625
18786
|
static events = ["change", "complete"];
|
|
@@ -18628,18 +18789,13 @@ var ReadingProgressController = class extends Controller {
|
|
|
18628
18789
|
/** The article's own box and the viewport: either changes the span. */
|
|
18629
18790
|
#layout = new LayoutObserver(() => this.#onScroll());
|
|
18630
18791
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
18631
|
-
|
|
18792
|
+
/** Coalesces scroll bursts, and the connect baseline, into one frame. */
|
|
18793
|
+
#frames = new FrameCoalescer();
|
|
18632
18794
|
/** Last published progress, so `change`/`complete` fire only on movement. */
|
|
18633
18795
|
#progress = -1;
|
|
18634
18796
|
/** False until the connect frame has run: `complete` needs real reading. */
|
|
18635
18797
|
#baselined = false;
|
|
18636
|
-
#onScroll = () =>
|
|
18637
|
-
if (this.#frame !== null) return;
|
|
18638
|
-
this.#frame = requestAnimationFrame(() => {
|
|
18639
|
-
this.#frame = null;
|
|
18640
|
-
this.#measure();
|
|
18641
|
-
});
|
|
18642
|
-
};
|
|
18798
|
+
#onScroll = () => this.#frames.schedule(() => this.#measure());
|
|
18643
18799
|
connect() {
|
|
18644
18800
|
this.#progress = -1;
|
|
18645
18801
|
this.#baselined = false;
|
|
@@ -18648,8 +18804,7 @@ var ReadingProgressController = class extends Controller {
|
|
|
18648
18804
|
this.#layout.observeViewport();
|
|
18649
18805
|
this.#beforeCache.activate();
|
|
18650
18806
|
this.#measure();
|
|
18651
|
-
this.#
|
|
18652
|
-
this.#frame = null;
|
|
18807
|
+
this.#frames.schedule(() => {
|
|
18653
18808
|
this.#measure();
|
|
18654
18809
|
this.#baselined = true;
|
|
18655
18810
|
});
|
|
@@ -18658,7 +18813,7 @@ var ReadingProgressController = class extends Controller {
|
|
|
18658
18813
|
window.removeEventListener("scroll", this.#onScroll, { capture: true });
|
|
18659
18814
|
this.#layout.disconnect();
|
|
18660
18815
|
this.#beforeCache.deactivate();
|
|
18661
|
-
this.#
|
|
18816
|
+
this.#frames.cancel();
|
|
18662
18817
|
this.#lease.returnAll();
|
|
18663
18818
|
}
|
|
18664
18819
|
/**
|
|
@@ -18669,14 +18824,10 @@ var ReadingProgressController = class extends Controller {
|
|
|
18669
18824
|
* value that has already been handed back.
|
|
18670
18825
|
*/
|
|
18671
18826
|
#rewindForCache() {
|
|
18672
|
-
this.#
|
|
18827
|
+
this.#frames.cancel();
|
|
18673
18828
|
this.#lease.returnAll();
|
|
18674
18829
|
this.#progress = -1;
|
|
18675
18830
|
}
|
|
18676
|
-
#cancelFrame() {
|
|
18677
|
-
if (this.#frame !== null) cancelAnimationFrame(this.#frame);
|
|
18678
|
-
this.#frame = null;
|
|
18679
|
-
}
|
|
18680
18831
|
/** Computes and publishes the progress; emits on movement only. */
|
|
18681
18832
|
#measure() {
|
|
18682
18833
|
const rect = this.element.getBoundingClientRect();
|
|
@@ -18812,11 +18963,11 @@ var RelativeTimeController = class extends Controller {
|
|
|
18812
18963
|
* when the runtime rejects that locale.
|
|
18813
18964
|
*/
|
|
18814
18965
|
get #formatter() {
|
|
18815
|
-
return intlFormatter(
|
|
18816
|
-
|
|
18817
|
-
|
|
18818
|
-
|
|
18819
|
-
|
|
18966
|
+
return intlFormatter(
|
|
18967
|
+
Intl.RelativeTimeFormat,
|
|
18968
|
+
resolveLocale(this.element, this.localeValue),
|
|
18969
|
+
RELATIVE_OPTIONS
|
|
18970
|
+
);
|
|
18820
18971
|
}
|
|
18821
18972
|
};
|
|
18822
18973
|
var STATELESS_INPUT_TYPES = /* @__PURE__ */ new Set(["hidden", "submit", "reset", "button", "image"]);
|
|
@@ -19347,20 +19498,14 @@ var ScrollAreaController = class extends Controller {
|
|
|
19347
19498
|
#observedNameIds = /* @__PURE__ */ new Set();
|
|
19348
19499
|
#observedNameSources = [];
|
|
19349
19500
|
#fonts = null;
|
|
19350
|
-
|
|
19501
|
+
/** Coalesces scroll bursts into one position sync per frame. */
|
|
19502
|
+
#frames = new FrameCoalescer();
|
|
19351
19503
|
#overflowing = false;
|
|
19352
19504
|
#lastEdge = null;
|
|
19353
19505
|
#ownedHostMutations = /* @__PURE__ */ new Map();
|
|
19354
|
-
#onScroll = () => {
|
|
19355
|
-
|
|
19356
|
-
this.#
|
|
19357
|
-
this.#scrollFrame = null;
|
|
19358
|
-
const viewport = this.#viewport;
|
|
19359
|
-
if (viewport) this.#syncPosition(viewport, this.#overflowing);
|
|
19360
|
-
});
|
|
19361
|
-
};
|
|
19362
|
-
#onLoad = () => {
|
|
19363
|
-
this.#refresh();
|
|
19506
|
+
#onScroll = (event) => {
|
|
19507
|
+
const viewport = event.currentTarget;
|
|
19508
|
+
this.#frames.schedule(() => this.#syncPosition(viewport, this.#overflowing));
|
|
19364
19509
|
};
|
|
19365
19510
|
#onFontsSettled = () => {
|
|
19366
19511
|
this.#refresh();
|
|
@@ -19375,7 +19520,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
19375
19520
|
disconnect() {
|
|
19376
19521
|
this.#beforeCache.deactivate();
|
|
19377
19522
|
this.#rebind.cancel();
|
|
19378
|
-
this.#
|
|
19523
|
+
this.#frames.cancel();
|
|
19379
19524
|
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
19380
19525
|
this.#layout.disconnect();
|
|
19381
19526
|
this.#unbindFonts();
|
|
@@ -19408,7 +19553,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
19408
19553
|
return;
|
|
19409
19554
|
}
|
|
19410
19555
|
next.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
19411
|
-
|
|
19556
|
+
this.#layout.observeDescendantLoads(next);
|
|
19412
19557
|
this.#layout.observe(next);
|
|
19413
19558
|
this.#bindContentObserver(next);
|
|
19414
19559
|
this.#refresh();
|
|
@@ -19443,9 +19588,9 @@ var ScrollAreaController = class extends Controller {
|
|
|
19443
19588
|
}
|
|
19444
19589
|
/** Releases every resource and borrowed attribute owned by one former viewport. */
|
|
19445
19590
|
#unbindViewport(viewport) {
|
|
19446
|
-
this.#
|
|
19591
|
+
this.#frames.cancel();
|
|
19447
19592
|
viewport.removeEventListener("scroll", this.#onScroll);
|
|
19448
|
-
|
|
19593
|
+
this.#layout.unobserveDescendantLoads();
|
|
19449
19594
|
this.#layout.unobserve(viewport);
|
|
19450
19595
|
this.#content?.disconnect();
|
|
19451
19596
|
this.#content = null;
|
|
@@ -19460,7 +19605,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
19460
19605
|
#refresh() {
|
|
19461
19606
|
const viewport = this.#viewport;
|
|
19462
19607
|
if (!viewport) return;
|
|
19463
|
-
this.#
|
|
19608
|
+
this.#frames.cancel();
|
|
19464
19609
|
this.#syncNameSources(viewport);
|
|
19465
19610
|
this.#overflowing = this.#syncOverflow(viewport);
|
|
19466
19611
|
this.#syncKeyboardReach(viewport, this.#overflowing);
|
|
@@ -19668,16 +19813,10 @@ var ScrollAreaController = class extends Controller {
|
|
|
19668
19813
|
this.#fonts?.removeEventListener("loadingerror", this.#onFontsSettled);
|
|
19669
19814
|
this.#fonts = null;
|
|
19670
19815
|
}
|
|
19671
|
-
/** Cancels a pending scroll frame. */
|
|
19672
|
-
#cancelScrollFrame() {
|
|
19673
|
-
if (this.#scrollFrame === null) return;
|
|
19674
|
-
cancelAnimationFrame(this.#scrollFrame);
|
|
19675
|
-
this.#scrollFrame = null;
|
|
19676
|
-
}
|
|
19677
19816
|
/** Suspends live resources and returns all derived state before snapshotting. */
|
|
19678
19817
|
#rewindForCache() {
|
|
19679
19818
|
this.#rebind.cancel();
|
|
19680
|
-
this.#
|
|
19819
|
+
this.#frames.cancel();
|
|
19681
19820
|
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
19682
19821
|
this.#layout.disconnect();
|
|
19683
19822
|
this.#unbindFonts();
|
|
@@ -19700,8 +19839,8 @@ var ScrollRestoreController = class extends Controller {
|
|
|
19700
19839
|
key: { type: String, default: "" },
|
|
19701
19840
|
axis: { type: String, default: "vertical" }
|
|
19702
19841
|
};
|
|
19703
|
-
/**
|
|
19704
|
-
#
|
|
19842
|
+
/** Coalesces scroll bursts into one save per frame. */
|
|
19843
|
+
#frames = new FrameCoalescer();
|
|
19705
19844
|
/** Resolved storage key; empty disables persistence (no key and no id). */
|
|
19706
19845
|
#storageKey = "";
|
|
19707
19846
|
/** Last offset captured while the element was live; persisted as-is on teardown. */
|
|
@@ -19729,11 +19868,7 @@ var ScrollRestoreController = class extends Controller {
|
|
|
19729
19868
|
#connected = false;
|
|
19730
19869
|
#onScroll = () => {
|
|
19731
19870
|
this.#capture();
|
|
19732
|
-
|
|
19733
|
-
this.#rafId = requestAnimationFrame(() => {
|
|
19734
|
-
this.#rafId = null;
|
|
19735
|
-
this.#persist();
|
|
19736
|
-
});
|
|
19871
|
+
this.#frames.schedule(() => this.#persist());
|
|
19737
19872
|
};
|
|
19738
19873
|
connect() {
|
|
19739
19874
|
this.#connected = true;
|
|
@@ -19744,7 +19879,7 @@ var ScrollRestoreController = class extends Controller {
|
|
|
19744
19879
|
disconnect() {
|
|
19745
19880
|
this.#connected = false;
|
|
19746
19881
|
this.element.removeEventListener("scroll", this.#onScroll);
|
|
19747
|
-
this.#
|
|
19882
|
+
this.#frames.cancel();
|
|
19748
19883
|
this.#persist();
|
|
19749
19884
|
}
|
|
19750
19885
|
/** Re-derives the namespace when application code or a Turbo morph moves `key`. */
|
|
@@ -19758,7 +19893,7 @@ var ScrollRestoreController = class extends Controller {
|
|
|
19758
19893
|
/** Rebuilds the persistence state around the Values as they now read. */
|
|
19759
19894
|
#resync() {
|
|
19760
19895
|
if (!this.#connected) return;
|
|
19761
|
-
this.#
|
|
19896
|
+
this.#frames.cancel();
|
|
19762
19897
|
this.#storageKey = this.#resolveKey();
|
|
19763
19898
|
this.#echoTop = null;
|
|
19764
19899
|
this.#echoLeft = null;
|
|
@@ -19767,13 +19902,6 @@ var ScrollRestoreController = class extends Controller {
|
|
|
19767
19902
|
this.#capturedLeft = false;
|
|
19768
19903
|
if (this.#storageKey) this.#restore();
|
|
19769
19904
|
}
|
|
19770
|
-
/** Drops the pending coalesced save, if one is queued. */
|
|
19771
|
-
#cancelFrame() {
|
|
19772
|
-
if (this.#rafId !== null) {
|
|
19773
|
-
cancelAnimationFrame(this.#rafId);
|
|
19774
|
-
this.#rafId = null;
|
|
19775
|
-
}
|
|
19776
|
-
}
|
|
19777
19905
|
/**
|
|
19778
19906
|
* Records the live scroll offset for the configured axis.
|
|
19779
19907
|
*
|
|
@@ -19870,6 +19998,20 @@ var ScrollRestoreController = class extends Controller {
|
|
|
19870
19998
|
return this.axisValue === "horizontal" || this.axisValue === "both";
|
|
19871
19999
|
}
|
|
19872
20000
|
};
|
|
20001
|
+
|
|
20002
|
+
// src/utils/scroll_source.ts
|
|
20003
|
+
function resolveScrollContainer(selector) {
|
|
20004
|
+
const match = selector ? document.querySelector(selector) : null;
|
|
20005
|
+
return match instanceof HTMLElement ? match : null;
|
|
20006
|
+
}
|
|
20007
|
+
function resolveScrollSource(selector) {
|
|
20008
|
+
return resolveScrollContainer(selector) ?? window;
|
|
20009
|
+
}
|
|
20010
|
+
function scrollOffset(source) {
|
|
20011
|
+
return source === window ? window.scrollY ?? window.pageYOffset ?? 0 : source.scrollTop;
|
|
20012
|
+
}
|
|
20013
|
+
|
|
20014
|
+
// src/controllers/scroll_visibility_controller.ts
|
|
19873
20015
|
var DEFAULT_OFFSET = 400;
|
|
19874
20016
|
var ScrollVisibilityController = class extends Controller {
|
|
19875
20017
|
static targets = ["element"];
|
|
@@ -19881,8 +20023,8 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
19881
20023
|
};
|
|
19882
20024
|
static actions = ["toTop"];
|
|
19883
20025
|
static events = ["change"];
|
|
19884
|
-
/**
|
|
19885
|
-
#
|
|
20026
|
+
/** Coalesces scroll bursts into one measurement per frame. */
|
|
20027
|
+
#frames = new FrameCoalescer();
|
|
19886
20028
|
/** Previous scroll position, for `direction` mode delta detection. */
|
|
19887
20029
|
#lastScrollY = 0;
|
|
19888
20030
|
/** Current visibility, tracked to dispatch `change` only on real transitions. */
|
|
@@ -19918,16 +20060,10 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
19918
20060
|
#pendingHide = new BlurDeferral(() => {
|
|
19919
20061
|
if (this.#connected) this.#evaluate();
|
|
19920
20062
|
});
|
|
19921
|
-
#onScroll = () =>
|
|
19922
|
-
if (this.#rafId !== null) return;
|
|
19923
|
-
this.#rafId = requestAnimationFrame(() => {
|
|
19924
|
-
this.#rafId = null;
|
|
19925
|
-
this.#evaluate();
|
|
19926
|
-
});
|
|
19927
|
-
};
|
|
20063
|
+
#onScroll = () => this.#frames.schedule(() => this.#evaluate());
|
|
19928
20064
|
connect() {
|
|
19929
|
-
this.#scrollSource = this.#
|
|
19930
|
-
this.#lastScrollY = this.#
|
|
20065
|
+
this.#scrollSource = resolveScrollSource(this.#rootSelector);
|
|
20066
|
+
this.#lastScrollY = scrollOffset(this.#scrollSource);
|
|
19931
20067
|
this.#scrollSource.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
19932
20068
|
this.#evaluate(false);
|
|
19933
20069
|
this.#connected = true;
|
|
@@ -19935,10 +20071,7 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
19935
20071
|
disconnect() {
|
|
19936
20072
|
this.#connected = false;
|
|
19937
20073
|
this.#scrollSource.removeEventListener("scroll", this.#onScroll);
|
|
19938
|
-
|
|
19939
|
-
cancelAnimationFrame(this.#rafId);
|
|
19940
|
-
this.#rafId = null;
|
|
19941
|
-
}
|
|
20074
|
+
this.#frames.cancel();
|
|
19942
20075
|
this.#pendingHide.releaseAll();
|
|
19943
20076
|
this.#tabindex.returnAll();
|
|
19944
20077
|
this.#visible = null;
|
|
@@ -19997,7 +20130,7 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
19997
20130
|
* @stimeoRenderRoot
|
|
19998
20131
|
*/
|
|
19999
20132
|
#evaluate(notify = true) {
|
|
20000
|
-
const y = this.#
|
|
20133
|
+
const y = scrollOffset(this.#scrollSource);
|
|
20001
20134
|
let nextVisible;
|
|
20002
20135
|
if (this.modeValue === "direction") {
|
|
20003
20136
|
if (y <= this.#offset) {
|
|
@@ -20027,13 +20160,6 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
20027
20160
|
if (notify) this.dispatch("change", { detail: { visible: next } });
|
|
20028
20161
|
}
|
|
20029
20162
|
/** Resolves the scroll source from `root` (falling back to the window). */
|
|
20030
|
-
#resolveScrollSource() {
|
|
20031
|
-
if (this.#rootSelector) {
|
|
20032
|
-
const root = document.querySelector(this.#rootSelector);
|
|
20033
|
-
if (root) return root;
|
|
20034
|
-
}
|
|
20035
|
-
return window;
|
|
20036
|
-
}
|
|
20037
20163
|
/**
|
|
20038
20164
|
* The focus owner inside the target, or `null` when focus is elsewhere.
|
|
20039
20165
|
*
|
|
@@ -20046,12 +20172,6 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
20046
20172
|
if (focused instanceof HTMLElement && this.elementTarget.contains(focused)) return focused;
|
|
20047
20173
|
return null;
|
|
20048
20174
|
}
|
|
20049
|
-
#scrollY() {
|
|
20050
|
-
if (this.#scrollSource === window) {
|
|
20051
|
-
return window.scrollY ?? window.pageYOffset ?? 0;
|
|
20052
|
-
}
|
|
20053
|
-
return this.#scrollSource.scrollTop;
|
|
20054
|
-
}
|
|
20055
20175
|
};
|
|
20056
20176
|
var ANCHOR_ATTRIBUTES = ["href", "data-href"];
|
|
20057
20177
|
var ScrollspyController = class extends Controller {
|
|
@@ -20098,8 +20218,10 @@ var ScrollspyController = class extends Controller {
|
|
|
20098
20218
|
* rather than from whatever the selector resolves to now.
|
|
20099
20219
|
*/
|
|
20100
20220
|
#scrollSource = null;
|
|
20101
|
-
/**
|
|
20102
|
-
#
|
|
20221
|
+
/** Coalesces a scroll burst into one measurement per frame. */
|
|
20222
|
+
#frames = new FrameCoalescer();
|
|
20223
|
+
/** The validated `rootSelector`, or `""` when the declaration cannot be parsed. */
|
|
20224
|
+
#rootSelector = "";
|
|
20103
20225
|
/** Watches the link targets' anchor attributes for an in-place morph rewrite. */
|
|
20104
20226
|
#anchorObserver = null;
|
|
20105
20227
|
/** True while a coalesced observation rebuild is queued; see {@link #scheduleResync}. */
|
|
@@ -20116,10 +20238,7 @@ var ScrollspyController = class extends Controller {
|
|
|
20116
20238
|
this.#anchorObserver?.disconnect();
|
|
20117
20239
|
this.#anchorObserver = null;
|
|
20118
20240
|
this.#detachScrollListener();
|
|
20119
|
-
|
|
20120
|
-
cancelAnimationFrame(this.#frame);
|
|
20121
|
-
this.#frame = null;
|
|
20122
|
-
}
|
|
20241
|
+
this.#frames.cancel();
|
|
20123
20242
|
this.#intersectionStates.clear();
|
|
20124
20243
|
this.#activeSectionId = "";
|
|
20125
20244
|
this.#rootElement = null;
|
|
@@ -20136,6 +20255,7 @@ var ScrollspyController = class extends Controller {
|
|
|
20136
20255
|
this.#initializeObserver();
|
|
20137
20256
|
}
|
|
20138
20257
|
rootSelectorValueChanged() {
|
|
20258
|
+
this.#rootSelector = validSelector(this.element, this.rootSelectorValue, "");
|
|
20139
20259
|
if (!this.#isConnected) return;
|
|
20140
20260
|
this.#initializeObserver();
|
|
20141
20261
|
}
|
|
@@ -20229,10 +20349,10 @@ var ScrollspyController = class extends Controller {
|
|
|
20229
20349
|
const offset = this.#offset;
|
|
20230
20350
|
if (rootElement) {
|
|
20231
20351
|
const containerRect = rootElement.getBoundingClientRect();
|
|
20232
|
-
const scrollPosition = rootElement
|
|
20352
|
+
const scrollPosition = scrollOffset(rootElement) + (targetRect.top - containerRect.top) - offset;
|
|
20233
20353
|
rootElement.scrollTo({ top: scrollPosition, behavior });
|
|
20234
20354
|
} else {
|
|
20235
|
-
const scrollPosition = window
|
|
20355
|
+
const scrollPosition = scrollOffset(window) + targetRect.top - offset;
|
|
20236
20356
|
window.scrollTo({ top: scrollPosition, behavior });
|
|
20237
20357
|
}
|
|
20238
20358
|
if (this.focusSectionValue) this.#focusSection(targetElement);
|
|
@@ -20267,19 +20387,14 @@ var ScrollspyController = class extends Controller {
|
|
|
20267
20387
|
/**
|
|
20268
20388
|
* Resolves `rootSelector` to a scrollable element.
|
|
20269
20389
|
*
|
|
20270
|
-
* @returns The container, or `null` meaning "spy the viewport" when the
|
|
20271
|
-
* is empty, matches nothing, matches a non-HTML element
|
|
20272
|
-
*
|
|
20273
|
-
*
|
|
20390
|
+
* @returns The container, or `null` meaning "spy the viewport" when the
|
|
20391
|
+
* declaration is empty, matches nothing, or matches a non-HTML element. A typo
|
|
20392
|
+
* reads as empty: the declaration is validated once when it changes, so a
|
|
20393
|
+
* selector that cannot be parsed degrades to viewport spying rather than
|
|
20394
|
+
* leaving the controller inert.
|
|
20274
20395
|
*/
|
|
20275
20396
|
#queryRootElement() {
|
|
20276
|
-
|
|
20277
|
-
try {
|
|
20278
|
-
const root = document.querySelector(this.rootSelectorValue);
|
|
20279
|
-
return root instanceof HTMLElement ? root : null;
|
|
20280
|
-
} catch {
|
|
20281
|
-
return null;
|
|
20282
|
-
}
|
|
20397
|
+
return resolveScrollContainer(this.#rootSelector);
|
|
20283
20398
|
}
|
|
20284
20399
|
/**
|
|
20285
20400
|
* Re-evaluates once per frame while the reader scrolls.
|
|
@@ -20294,13 +20409,7 @@ var ScrollspyController = class extends Controller {
|
|
|
20294
20409
|
* end in {@link #evaluateActiveSection} — which measures section rects and the
|
|
20295
20410
|
* root's top edge at that instant — they converge on the same answer.
|
|
20296
20411
|
*/
|
|
20297
|
-
#onScroll = () =>
|
|
20298
|
-
if (this.#frame !== null) return;
|
|
20299
|
-
this.#frame = requestAnimationFrame(() => {
|
|
20300
|
-
this.#frame = null;
|
|
20301
|
-
this.#evaluateActiveSection();
|
|
20302
|
-
});
|
|
20303
|
-
};
|
|
20412
|
+
#onScroll = () => this.#frames.schedule(() => this.#evaluateActiveSection());
|
|
20304
20413
|
/**
|
|
20305
20414
|
* Points the `scroll` listener at whatever the reader actually scrolls: the
|
|
20306
20415
|
* resolved root, or the window when the viewport is spied.
|
|
@@ -21198,7 +21307,8 @@ var SmartStickyHeaderController = class extends Controller {
|
|
|
21198
21307
|
};
|
|
21199
21308
|
static events = ["change"];
|
|
21200
21309
|
#connected = false;
|
|
21201
|
-
|
|
21310
|
+
/** Coalesces scroll bursts into one measurement per frame. */
|
|
21311
|
+
#frames = new FrameCoalescer();
|
|
21202
21312
|
/** The scroll source resolved at connect — disconnect must unbind the SAME node. */
|
|
21203
21313
|
#scrollerEl = window;
|
|
21204
21314
|
/** The validated `containerSelector`, or `""` when the declaration cannot be parsed. */
|
|
@@ -21215,13 +21325,7 @@ var SmartStickyHeaderController = class extends Controller {
|
|
|
21215
21325
|
get #offset() {
|
|
21216
21326
|
return Number.isFinite(this.offsetValue) ? this.offsetValue : DEFAULT_OFFSET2;
|
|
21217
21327
|
}
|
|
21218
|
-
#onScroll = () =>
|
|
21219
|
-
if (this.#frame !== null) return;
|
|
21220
|
-
this.#frame = requestAnimationFrame(() => {
|
|
21221
|
-
this.#frame = null;
|
|
21222
|
-
this.#measure();
|
|
21223
|
-
});
|
|
21224
|
-
};
|
|
21328
|
+
#onScroll = () => this.#frames.schedule(() => this.#measure());
|
|
21225
21329
|
/**
|
|
21226
21330
|
* Focus inside a hidden header must reveal it (WCAG 2.4.7 / 2.4.11); the
|
|
21227
21331
|
* hold while focus *stays* inside is the `#apply` hide invariant.
|
|
@@ -21237,8 +21341,8 @@ var SmartStickyHeaderController = class extends Controller {
|
|
|
21237
21341
|
}
|
|
21238
21342
|
connect() {
|
|
21239
21343
|
this.#hidden = null;
|
|
21240
|
-
this.#scrollerEl = this.#
|
|
21241
|
-
this.#lastY = this.#
|
|
21344
|
+
this.#scrollerEl = resolveScrollSource(this.#containerSelector);
|
|
21345
|
+
this.#lastY = scrollOffset(this.#scrollerEl);
|
|
21242
21346
|
this.#scrollerEl.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
21243
21347
|
this.element.addEventListener("focusin", this.#onFocusin);
|
|
21244
21348
|
this.#apply(false, false);
|
|
@@ -21248,23 +21352,10 @@ var SmartStickyHeaderController = class extends Controller {
|
|
|
21248
21352
|
this.#connected = false;
|
|
21249
21353
|
this.#scrollerEl.removeEventListener("scroll", this.#onScroll);
|
|
21250
21354
|
this.element.removeEventListener("focusin", this.#onFocusin);
|
|
21251
|
-
|
|
21252
|
-
this.#frame = null;
|
|
21253
|
-
}
|
|
21254
|
-
/** Resolves the scroll source: the `containerSelector` match, else the window. */
|
|
21255
|
-
#resolveScroller() {
|
|
21256
|
-
if (this.#containerSelector) {
|
|
21257
|
-
const container = document.querySelector(this.#containerSelector);
|
|
21258
|
-
if (container) return container;
|
|
21259
|
-
}
|
|
21260
|
-
return window;
|
|
21261
|
-
}
|
|
21262
|
-
get #scrollY() {
|
|
21263
|
-
const scroller = this.#scrollerEl;
|
|
21264
|
-
return scroller === window ? window.scrollY : scroller.scrollTop;
|
|
21355
|
+
this.#frames.cancel();
|
|
21265
21356
|
}
|
|
21266
21357
|
#measure() {
|
|
21267
|
-
const y = this.#
|
|
21358
|
+
const y = scrollOffset(this.#scrollerEl);
|
|
21268
21359
|
if (y <= this.#offset) {
|
|
21269
21360
|
this.#lastY = y;
|
|
21270
21361
|
this.#apply(false);
|
|
@@ -22235,6 +22326,7 @@ var SubmitOnceController = class extends Controller {
|
|
|
22235
22326
|
static events = ["start", "end", "reconcile"];
|
|
22236
22327
|
#timers = new SafeTimeout();
|
|
22237
22328
|
#gate = new DetachGate();
|
|
22329
|
+
#listeners = new ListenerSet();
|
|
22238
22330
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
22239
22331
|
#sessions = /* @__PURE__ */ new Map();
|
|
22240
22332
|
#disabled = new AttributeLease("disabled");
|
|
@@ -22259,14 +22351,12 @@ var SubmitOnceController = class extends Controller {
|
|
|
22259
22351
|
connect() {
|
|
22260
22352
|
this.#gate.cancel();
|
|
22261
22353
|
this.#beforeCache.activate();
|
|
22262
|
-
this.element
|
|
22263
|
-
this.element
|
|
22264
|
-
this.element
|
|
22354
|
+
this.#listeners.add(this.element, "submit", this.#onNativeSubmit, { capture: true });
|
|
22355
|
+
this.#listeners.add(this.element, "turbo:submit-start", this.#onSubmitStart);
|
|
22356
|
+
this.#listeners.add(this.element, "turbo:submit-end", this.#onSubmitEnd);
|
|
22265
22357
|
}
|
|
22266
22358
|
disconnect() {
|
|
22267
|
-
this.
|
|
22268
|
-
this.element.removeEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
22269
|
-
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
22359
|
+
this.#listeners.dispose();
|
|
22270
22360
|
this.#beforeCache.deactivate();
|
|
22271
22361
|
this.#gate.disconnected(this, () => this.#teardown());
|
|
22272
22362
|
}
|
|
@@ -23693,30 +23783,23 @@ var ToastController = class extends Controller {
|
|
|
23693
23783
|
};
|
|
23694
23784
|
static actions = ["dismiss", "onKeydown", "pause", "resume", "show"];
|
|
23695
23785
|
static events = ["dismiss", "show"];
|
|
23696
|
-
/**
|
|
23697
|
-
* Registry for every auto-dismiss and transition-finalize timer the controller
|
|
23698
|
-
* schedules. `SafeTimeout` owns *registration and teardown only*; the
|
|
23699
|
-
* pause/resume remaining-time accounting stays in `#activeTimeouts` so the
|
|
23700
|
-
* per-widget WCAG 2.2.1 semantics are not flattened into the helper.
|
|
23701
|
-
*/
|
|
23786
|
+
/** Removal timers for the leaving transition; the auto-dismiss ones live below. */
|
|
23702
23787
|
#timers = new SafeTimeout();
|
|
23788
|
+
/** Per-toast auto-dismiss, held open while the toast is hovered or focused. */
|
|
23789
|
+
#dismiss = new PausableTimers();
|
|
23703
23790
|
/**
|
|
23704
23791
|
* Pending one-shot `requestAnimationFrame` handles (the entering→visible flip).
|
|
23705
23792
|
* Tracked so {@link disconnect} can cancel any that have not fired, preventing a
|
|
23706
23793
|
* detached element from being mutated after it leaves the DOM (Turbo).
|
|
23707
23794
|
*/
|
|
23708
23795
|
#rafHandles = /* @__PURE__ */ new Map();
|
|
23709
|
-
/** Track active timeouts mapped by each toast element for safe cancellation. */
|
|
23710
|
-
#activeTimeouts = /* @__PURE__ */ new Map();
|
|
23711
|
-
/** Track active pause reasons (hover/focus) per toast for WCAG 2.2.1 pause/resume. */
|
|
23712
|
-
#pauseReasons = /* @__PURE__ */ new Map();
|
|
23713
23796
|
/** The stable list that owns delegated listeners for dynamically added items. */
|
|
23714
23797
|
#delegatedList = null;
|
|
23715
23798
|
connect() {
|
|
23716
23799
|
this.#connectDelegatedEvents();
|
|
23717
23800
|
this.enforceMaxLimit();
|
|
23718
23801
|
for (const item of this.itemTargets) {
|
|
23719
|
-
if (!this.#
|
|
23802
|
+
if (!this.#dismiss.tracks(item) && item.dataset.state !== "leaving") {
|
|
23720
23803
|
this.#startTimer(item);
|
|
23721
23804
|
}
|
|
23722
23805
|
}
|
|
@@ -23724,12 +23807,11 @@ var ToastController = class extends Controller {
|
|
|
23724
23807
|
disconnect() {
|
|
23725
23808
|
this.#disconnectDelegatedEvents();
|
|
23726
23809
|
this.#timers.clearAll();
|
|
23810
|
+
this.#dismiss.clearAll();
|
|
23727
23811
|
for (const handle of this.#rafHandles.values()) {
|
|
23728
23812
|
window.cancelAnimationFrame(handle);
|
|
23729
23813
|
}
|
|
23730
23814
|
this.#rafHandles.clear();
|
|
23731
|
-
this.#activeTimeouts.clear();
|
|
23732
|
-
this.#pauseReasons.clear();
|
|
23733
23815
|
}
|
|
23734
23816
|
/** Rebinds delegated interaction when Turbo replaces the list target in place. */
|
|
23735
23817
|
listTargetConnected(element) {
|
|
@@ -23742,18 +23824,9 @@ var ToastController = class extends Controller {
|
|
|
23742
23824
|
durationValueChanged() {
|
|
23743
23825
|
for (const item of this.itemTargets) {
|
|
23744
23826
|
if (item.dataset.state === "leaving") continue;
|
|
23745
|
-
const pauseReasons = this.#pauseReasons.get(item);
|
|
23746
|
-
this.#clearTimer(item);
|
|
23747
23827
|
if (this.durationValue <= 0) {
|
|
23828
|
+
this.#dismiss.clear(item);
|
|
23748
23829
|
item.removeAttribute("data-paused");
|
|
23749
|
-
} else if (pauseReasons && pauseReasons.size > 0) {
|
|
23750
|
-
this.#pauseReasons.set(item, pauseReasons);
|
|
23751
|
-
this.#activeTimeouts.set(item, {
|
|
23752
|
-
id: 0,
|
|
23753
|
-
startedAt: 0,
|
|
23754
|
-
remaining: this.durationValue
|
|
23755
|
-
});
|
|
23756
|
-
item.setAttribute("data-paused", "true");
|
|
23757
23830
|
} else {
|
|
23758
23831
|
this.#startTimer(item);
|
|
23759
23832
|
}
|
|
@@ -23782,7 +23855,7 @@ var ToastController = class extends Controller {
|
|
|
23782
23855
|
}
|
|
23783
23856
|
/** Clears any active timer when a toast is removed from the DOM. */
|
|
23784
23857
|
itemTargetDisconnected(element) {
|
|
23785
|
-
this.#
|
|
23858
|
+
this.#dismiss.clear(element);
|
|
23786
23859
|
this.#cancelAnimation(element);
|
|
23787
23860
|
}
|
|
23788
23861
|
/**
|
|
@@ -23854,35 +23927,16 @@ var ToastController = class extends Controller {
|
|
|
23854
23927
|
*/
|
|
23855
23928
|
pause(event) {
|
|
23856
23929
|
const item = this.#itemFromEvent(event);
|
|
23857
|
-
if (
|
|
23858
|
-
|
|
23859
|
-
if (!timeout) return;
|
|
23860
|
-
const reasons = this.#pauseReasonsFor(item);
|
|
23861
|
-
const wasActive = reasons.size > 0;
|
|
23862
|
-
reasons.add(this.#pauseReason(event));
|
|
23863
|
-
if (wasActive || timeout.id === 0) return;
|
|
23864
|
-
this.#timers.clear(timeout.id);
|
|
23865
|
-
const elapsed = Date.now() - timeout.startedAt;
|
|
23866
|
-
const remaining = Math.max(0, timeout.remaining - elapsed);
|
|
23867
|
-
if (remaining <= 0) {
|
|
23868
|
-
this.#removeWithTransition(item, "timeout");
|
|
23869
|
-
return;
|
|
23930
|
+
if (item && this.#dismiss.pause(item, this.#pauseReason(event))) {
|
|
23931
|
+
item.setAttribute("data-paused", "true");
|
|
23870
23932
|
}
|
|
23871
|
-
this.#activeTimeouts.set(item, { id: 0, startedAt: 0, remaining });
|
|
23872
|
-
item.setAttribute("data-paused", "true");
|
|
23873
23933
|
}
|
|
23874
23934
|
/** Resumes the auto-dismiss timer once both hover and focus have been released. */
|
|
23875
23935
|
resume(event) {
|
|
23876
23936
|
const item = this.#itemFromEvent(event);
|
|
23877
|
-
if (
|
|
23878
|
-
|
|
23879
|
-
|
|
23880
|
-
if (reasons.size > 0) return;
|
|
23881
|
-
const timeout = this.#activeTimeouts.get(item);
|
|
23882
|
-
if (!timeout) return;
|
|
23883
|
-
if (timeout.id !== 0 || timeout.remaining <= 0) return;
|
|
23884
|
-
item.removeAttribute("data-paused");
|
|
23885
|
-
this.#startTimer(item, timeout.remaining);
|
|
23937
|
+
if (item && this.#dismiss.resume(item, this.#pauseReason(event))) {
|
|
23938
|
+
item.removeAttribute("data-paused");
|
|
23939
|
+
}
|
|
23886
23940
|
}
|
|
23887
23941
|
/** Resolves the toast item element a pause/resume event targets. */
|
|
23888
23942
|
#itemFromEvent(event) {
|
|
@@ -23895,36 +23949,20 @@ var ToastController = class extends Controller {
|
|
|
23895
23949
|
#pauseReason(event) {
|
|
23896
23950
|
return event.type === "focusin" || event.type === "focusout" ? "focus" : "hover";
|
|
23897
23951
|
}
|
|
23898
|
-
/**
|
|
23899
|
-
#
|
|
23900
|
-
|
|
23901
|
-
if (!reasons) {
|
|
23902
|
-
reasons = /* @__PURE__ */ new Set();
|
|
23903
|
-
this.#pauseReasons.set(item, reasons);
|
|
23904
|
-
}
|
|
23905
|
-
return reasons;
|
|
23906
|
-
}
|
|
23907
|
-
#startTimer(element, duration = this.durationValue) {
|
|
23908
|
-
if (duration <= 0 || element.dataset.state === "leaving" || element.parentNode !== this.listTarget) {
|
|
23952
|
+
/** Arms a toast's auto-dismiss; a non-positive `duration` means it never expires. */
|
|
23953
|
+
#startTimer(element) {
|
|
23954
|
+
if (this.durationValue <= 0 || element.dataset.state === "leaving" || element.parentNode !== this.listTarget) {
|
|
23909
23955
|
return;
|
|
23910
23956
|
}
|
|
23911
|
-
this.#
|
|
23912
|
-
|
|
23913
|
-
this.#removeWithTransition(element, "timeout")
|
|
23914
|
-
|
|
23915
|
-
|
|
23916
|
-
}
|
|
23917
|
-
#clearTimer(element) {
|
|
23918
|
-
const timeout = this.#activeTimeouts.get(element);
|
|
23919
|
-
if (timeout) {
|
|
23920
|
-
if (timeout.id) this.#timers.clear(timeout.id);
|
|
23921
|
-
this.#activeTimeouts.delete(element);
|
|
23922
|
-
}
|
|
23923
|
-
this.#pauseReasons.delete(element);
|
|
23957
|
+
this.#dismiss.set(
|
|
23958
|
+
element,
|
|
23959
|
+
() => this.#removeWithTransition(element, "timeout"),
|
|
23960
|
+
this.durationValue
|
|
23961
|
+
);
|
|
23924
23962
|
}
|
|
23925
23963
|
#removeWithTransition(element, reason) {
|
|
23926
23964
|
if (element.dataset.state === "leaving" || element.parentNode !== this.listTarget) return;
|
|
23927
|
-
this.#
|
|
23965
|
+
this.#dismiss.clear(element);
|
|
23928
23966
|
this.#cancelAnimation(element);
|
|
23929
23967
|
element.setAttribute("data-state", "leaving");
|
|
23930
23968
|
const finalize = () => {
|