stimeo-ui 0.13.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 +73 -5
- 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 +51 -10
- data/dist/controllers/carousel_controller.js +8 -6
- data/dist/controllers/checkbox_controller.js +27 -2
- data/dist/controllers/conditional_fields_controller.js +36 -7
- data/dist/controllers/currency_input_controller.js +77 -25
- data/dist/controllers/date_range_picker_controller.js +43 -10
- data/dist/controllers/direct_upload_controller.js +21 -10
- data/dist/controllers/dirty_form_controller.js +32 -12
- data/dist/controllers/flash_controller.js +145 -43
- data/dist/controllers/form_field_controller.js +7 -1
- data/dist/controllers/frame_loading_controller.js +29 -6
- data/dist/controllers/highlight_controller.js +63 -23
- data/dist/controllers/input_mask_controller.js +33 -12
- data/dist/controllers/intersection_controller.js +10 -5
- data/dist/controllers/lazy_frame_controller.js +10 -5
- data/dist/controllers/local_time_controller.js +43 -11
- data/dist/controllers/masonry_controller.js +32 -14
- data/dist/controllers/menu_controller.js +10 -2
- data/dist/controllers/menubar_controller.js +10 -2
- data/dist/controllers/navigation_menu_controller.js +8 -2
- data/dist/controllers/optimistic_controller.js +28 -4
- data/dist/controllers/otp_controller.js +23 -10
- data/dist/controllers/overflow_indicator_controller.js +31 -8
- data/dist/controllers/overflow_menu_controller.js +39 -4
- data/dist/controllers/pointer_drag_controller.js +10 -3
- data/dist/controllers/portal_controller.js +21 -10
- data/dist/controllers/radio_group_controller.js +11 -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 +40 -9
- data/dist/controllers/reset_before_cache_controller.js +23 -10
- data/dist/controllers/resizable_controller.js +7 -2
- data/dist/controllers/roving_controller.js +7 -3
- 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 +71 -42
- data/dist/controllers/scrollspy_controller.js +76 -32
- data/dist/controllers/smart_sticky_header_controller.js +72 -37
- data/dist/controllers/stick_to_bottom_controller.js +29 -3
- data/dist/controllers/sticky_observer_controller.js +10 -5
- data/dist/controllers/submit_once_controller.js +29 -6
- data/dist/controllers/textarea_autosize_controller.js +29 -3
- data/dist/controllers/theme_controller.js +23 -13
- data/dist/controllers/toast_controller.js +136 -73
- data/dist/controllers/toggle_group_controller.js +11 -3
- data/dist/controllers/toolbar_controller.js +7 -3
- data/dist/index.js +629 -597
- 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 = {
|
|
@@ -1853,6 +1928,36 @@ var BulkSelectController = class extends Controller {
|
|
|
1853
1928
|
}
|
|
1854
1929
|
};
|
|
1855
1930
|
|
|
1931
|
+
// src/utils/intl_format.ts
|
|
1932
|
+
var formatters = /* @__PURE__ */ new WeakMap();
|
|
1933
|
+
function intlFormatter(factory, locale, options, fallbackLocale) {
|
|
1934
|
+
const cache = cacheFor(factory);
|
|
1935
|
+
const key = cacheKey(locale, options, fallbackLocale);
|
|
1936
|
+
if (cache.has(key)) return cache.get(key);
|
|
1937
|
+
const formatter = construct(factory, locale, options, fallbackLocale);
|
|
1938
|
+
cache.set(key, formatter);
|
|
1939
|
+
return formatter;
|
|
1940
|
+
}
|
|
1941
|
+
function cacheFor(factory) {
|
|
1942
|
+
const existing = formatters.get(factory);
|
|
1943
|
+
if (existing) return existing;
|
|
1944
|
+
const created = /* @__PURE__ */ new Map();
|
|
1945
|
+
formatters.set(factory, created);
|
|
1946
|
+
return created;
|
|
1947
|
+
}
|
|
1948
|
+
function cacheKey(locale, options, fallbackLocale) {
|
|
1949
|
+
return JSON.stringify([locale, options, fallbackLocale]);
|
|
1950
|
+
}
|
|
1951
|
+
function construct(factory, locale, options, fallbackLocale) {
|
|
1952
|
+
try {
|
|
1953
|
+
return new factory(locale, options);
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
if (!(error instanceof RangeError)) throw error;
|
|
1956
|
+
if (fallbackLocale === void 0) return null;
|
|
1957
|
+
return new factory(fallbackLocale, options);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1856
1961
|
// src/utils/dates.ts
|
|
1857
1962
|
function toISODateString(date) {
|
|
1858
1963
|
const year = date.getFullYear();
|
|
@@ -1886,14 +1991,14 @@ function parseISOMonthString(monthStr) {
|
|
|
1886
1991
|
function toISOMonthString(date) {
|
|
1887
1992
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
1888
1993
|
}
|
|
1994
|
+
var MONTH_LABEL_OPTIONS = { month: "long", year: "numeric" };
|
|
1889
1995
|
function monthLabelFormatter(locale) {
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
}
|
|
1996
|
+
return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/utils/locale.ts
|
|
2000
|
+
function resolveLocale(element, declared = "") {
|
|
2001
|
+
return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
1897
2002
|
}
|
|
1898
2003
|
|
|
1899
2004
|
// src/controllers/calendar_controller.ts
|
|
@@ -1905,8 +2010,9 @@ var CalendarController = class extends Controller {
|
|
|
1905
2010
|
selected: { type: String, default: "" },
|
|
1906
2011
|
min: { type: String, default: "" },
|
|
1907
2012
|
max: { type: String, default: "" },
|
|
1908
|
-
weekStart: { type: Number, default: 0 }
|
|
2013
|
+
weekStart: { type: Number, default: 0 },
|
|
1909
2014
|
// 0 = Sunday, 1 = Monday, etc.
|
|
2015
|
+
locale: { type: String, default: "" }
|
|
1910
2016
|
};
|
|
1911
2017
|
static actions = ["next", "onKeydown", "prev", "selectByClick"];
|
|
1912
2018
|
static events = ["monthchange", "select"];
|
|
@@ -1948,6 +2054,17 @@ var CalendarController = class extends Controller {
|
|
|
1948
2054
|
this.#syncFocusedDateWithMonth();
|
|
1949
2055
|
this.render();
|
|
1950
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
|
+
}
|
|
1951
2068
|
/**
|
|
1952
2069
|
* Stimulus lifecycle callback triggered automatically when the selectedValue changes.
|
|
1953
2070
|
* Re-renders grid cells to update `aria-selected` indicators.
|
|
@@ -2078,8 +2195,8 @@ var CalendarController = class extends Controller {
|
|
|
2078
2195
|
const year = monthStart.getFullYear();
|
|
2079
2196
|
const month = monthStart.getMonth() + 1;
|
|
2080
2197
|
if (this.hasLabelTarget) {
|
|
2081
|
-
const
|
|
2082
|
-
this.labelTarget.textContent = monthLabelFormatter(
|
|
2198
|
+
const locale = resolveLocale(this.element, this.localeValue);
|
|
2199
|
+
this.labelTarget.textContent = monthLabelFormatter(locale).format(monthStart);
|
|
2083
2200
|
}
|
|
2084
2201
|
const days = this.#calculateGridDays(year, month);
|
|
2085
2202
|
const dayElements = this.dayTargets;
|
|
@@ -2246,6 +2363,15 @@ var CalendarController = class extends Controller {
|
|
|
2246
2363
|
}
|
|
2247
2364
|
};
|
|
2248
2365
|
|
|
2366
|
+
// src/utils/event_owner.ts
|
|
2367
|
+
function ownerIndex(candidates, node) {
|
|
2368
|
+
if (!(node instanceof Node)) return -1;
|
|
2369
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
2370
|
+
}
|
|
2371
|
+
function ownerOf(candidates, node) {
|
|
2372
|
+
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2249
2375
|
// src/utils/reduced_motion.ts
|
|
2250
2376
|
function prefersReducedMotion() {
|
|
2251
2377
|
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
@@ -2589,9 +2715,7 @@ var CarouselController = class extends Controller {
|
|
|
2589
2715
|
}
|
|
2590
2716
|
/** Position of the picker that is or contains `node`, or `-1` when none does. */
|
|
2591
2717
|
#pickerIndexFor(node) {
|
|
2592
|
-
return this.pickerTargets
|
|
2593
|
-
(picker) => node instanceof Node && (picker === node || picker.contains(node))
|
|
2594
|
-
);
|
|
2718
|
+
return ownerIndex(this.pickerTargets, node);
|
|
2595
2719
|
}
|
|
2596
2720
|
/** Whether a focus transition lands on another control of this same carousel. */
|
|
2597
2721
|
#focusStaysInside(event) {
|
|
@@ -2810,9 +2934,7 @@ function hasModifier(event) {
|
|
|
2810
2934
|
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
2811
2935
|
}
|
|
2812
2936
|
function hits(elements, node) {
|
|
2813
|
-
return elements
|
|
2814
|
-
(element) => node instanceof Node && (element === node || element.contains(node))
|
|
2815
|
-
);
|
|
2937
|
+
return ownerIndex(elements, node) !== -1;
|
|
2816
2938
|
}
|
|
2817
2939
|
function setAttributeIfChanged(element, name, value) {
|
|
2818
2940
|
if (element.getAttribute(name) !== value) element.setAttribute(name, value);
|
|
@@ -3085,6 +3207,7 @@ var CheckboxController = class extends Controller {
|
|
|
3085
3207
|
static events = ["change", "reconcile"];
|
|
3086
3208
|
/** Collapses every lifecycle signal from one DOM update into one derived pass. */
|
|
3087
3209
|
#reconcile = new MicrotaskCoalescer(() => this.#reconcileFromChildren());
|
|
3210
|
+
#listeners = new ListenerSet();
|
|
3088
3211
|
/** Aggregate this root last settled on, so a derived repair is reported once. */
|
|
3089
3212
|
#committedState = null;
|
|
3090
3213
|
/** Watches authored checked-attribute changes on retained target elements. */
|
|
@@ -3102,14 +3225,14 @@ var CheckboxController = class extends Controller {
|
|
|
3102
3225
|
attributeFilter: ["checked"],
|
|
3103
3226
|
subtree: true
|
|
3104
3227
|
});
|
|
3105
|
-
this.element
|
|
3228
|
+
this.#listeners.add(this.element, "turbo:morph-element", this.#onMorph);
|
|
3106
3229
|
document.addEventListener("reset", this.#onReset, true);
|
|
3107
3230
|
}
|
|
3108
3231
|
/** Releases the observer, global reset listener, and every pending reconciliation. */
|
|
3109
3232
|
disconnect() {
|
|
3110
3233
|
this.#reconcile.cancel();
|
|
3111
3234
|
this.#checkedObserver.disconnect();
|
|
3112
|
-
this.
|
|
3235
|
+
this.#listeners.dispose();
|
|
3113
3236
|
document.removeEventListener("reset", this.#onReset, true);
|
|
3114
3237
|
}
|
|
3115
3238
|
/** Reconciles the aggregate for a parent added or replaced at runtime. */
|
|
@@ -4819,6 +4942,7 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
4819
4942
|
#observing = false;
|
|
4820
4943
|
/** Coalesces one target/morph/mutation batch into one full DOM reconciliation. */
|
|
4821
4944
|
#reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
|
|
4945
|
+
#listeners = new ListenerSet();
|
|
4822
4946
|
/** Provides the last-resort focus landmark without claiming an authored tabindex. */
|
|
4823
4947
|
#rootTabindex = new TabindexLoan();
|
|
4824
4948
|
/** Watches retained targets and controls whose declarative or reflected state changed. */
|
|
@@ -4857,9 +4981,9 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
4857
4981
|
subtree: true
|
|
4858
4982
|
});
|
|
4859
4983
|
this.#observing = true;
|
|
4860
|
-
this.element
|
|
4861
|
-
this.element
|
|
4862
|
-
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);
|
|
4863
4987
|
document.addEventListener("reset", this.#onReset, true);
|
|
4864
4988
|
}
|
|
4865
4989
|
/** Releases all external resources and returns the temporary focus landmark loan. */
|
|
@@ -4867,9 +4991,7 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
4867
4991
|
this.#reconcile.cancel();
|
|
4868
4992
|
this.#observing = false;
|
|
4869
4993
|
this.#observer.disconnect();
|
|
4870
|
-
this.
|
|
4871
|
-
this.element.removeEventListener("input", this.#onTriggerInput);
|
|
4872
|
-
this.element.removeEventListener("turbo:morph-element", this.#onMorph);
|
|
4994
|
+
this.#listeners.dispose();
|
|
4873
4995
|
document.removeEventListener("reset", this.#onReset, true);
|
|
4874
4996
|
this.#rootTabindex.returnAll();
|
|
4875
4997
|
}
|
|
@@ -5068,7 +5190,7 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
5068
5190
|
}
|
|
5069
5191
|
/** Whether an authored mutation target belongs to a trigger, region, or managed control. */
|
|
5070
5192
|
#elementAffectsState(target, triggers, regions) {
|
|
5071
|
-
return triggers
|
|
5193
|
+
return ownerIndex(triggers, target) !== -1 || ownerIndex(regions, target) !== -1;
|
|
5072
5194
|
}
|
|
5073
5195
|
/** Whether a form owns at least one current trigger, including `form=` associations. */
|
|
5074
5196
|
#hasTriggerOwnedBy(form) {
|
|
@@ -5678,10 +5800,11 @@ function toHalfWidth(text) {
|
|
|
5678
5800
|
}
|
|
5679
5801
|
|
|
5680
5802
|
// src/controllers/currency_input_controller.ts
|
|
5803
|
+
var FALLBACK_LOCALE = "en-US";
|
|
5681
5804
|
var CurrencyInputController = class extends Controller {
|
|
5682
5805
|
static targets = ["display", "field", "srValue"];
|
|
5683
5806
|
static values = {
|
|
5684
|
-
locale: { type: String, default: "
|
|
5807
|
+
locale: { type: String, default: "" },
|
|
5685
5808
|
currency: { type: String, default: "" },
|
|
5686
5809
|
precision: { type: Number, default: 2 }
|
|
5687
5810
|
};
|
|
@@ -5691,7 +5814,7 @@ var CurrencyInputController = class extends Controller {
|
|
|
5691
5814
|
#lastValue = null;
|
|
5692
5815
|
#started = false;
|
|
5693
5816
|
/** Validated mirrors of the Values; the hot path never reads a raw Value. */
|
|
5694
|
-
#locale
|
|
5817
|
+
#locale;
|
|
5695
5818
|
#currency = "";
|
|
5696
5819
|
#precision = 2;
|
|
5697
5820
|
/** Formatters rebuilt only when a Value changes, never per keystroke. */
|
|
@@ -5928,37 +6051,53 @@ var CurrencyInputController = class extends Controller {
|
|
|
5928
6051
|
* formatters from the validated set.
|
|
5929
6052
|
*/
|
|
5930
6053
|
#revalidate() {
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
this.#locale = this.localeValue;
|
|
5935
|
-
} catch {
|
|
5936
|
-
}
|
|
6054
|
+
const resolved = resolveLocale(this.element, this.localeValue);
|
|
6055
|
+
const usable = intlFormatter(Intl.NumberFormat, resolved, {});
|
|
6056
|
+
this.#locale = usable === null ? FALLBACK_LOCALE : resolved;
|
|
5937
6057
|
const precision = this.precisionValue;
|
|
5938
6058
|
this.#precision = Number.isInteger(precision) && precision >= 0 && precision <= 100 ? precision : 2;
|
|
5939
6059
|
this.#currency = "";
|
|
5940
6060
|
if (this.currencyValue !== "") {
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
}
|
|
5945
|
-
|
|
6061
|
+
const declaredCurrency = intlFormatter(Intl.NumberFormat, this.#locale, {
|
|
6062
|
+
style: "currency",
|
|
6063
|
+
currency: this.currencyValue
|
|
6064
|
+
});
|
|
6065
|
+
if (declaredCurrency !== null) this.#currency = this.currencyValue;
|
|
5946
6066
|
}
|
|
5947
|
-
this.#grouping =
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
6067
|
+
this.#grouping = intlFormatter(
|
|
6068
|
+
Intl.NumberFormat,
|
|
6069
|
+
this.#locale,
|
|
6070
|
+
{ useGrouping: true, maximumFractionDigits: 0 },
|
|
6071
|
+
FALLBACK_LOCALE
|
|
6072
|
+
);
|
|
6073
|
+
this.#fixed = intlFormatter(
|
|
6074
|
+
Intl.NumberFormat,
|
|
6075
|
+
this.#locale,
|
|
6076
|
+
{
|
|
6077
|
+
useGrouping: true,
|
|
6078
|
+
minimumFractionDigits: this.#precision,
|
|
6079
|
+
maximumFractionDigits: this.#precision
|
|
6080
|
+
},
|
|
6081
|
+
FALLBACK_LOCALE
|
|
6082
|
+
);
|
|
6083
|
+
this.#accessible = this.#currency ? intlFormatter(
|
|
6084
|
+
Intl.NumberFormat,
|
|
6085
|
+
this.#locale,
|
|
6086
|
+
{ style: "currency", currency: this.#currency },
|
|
6087
|
+
FALLBACK_LOCALE
|
|
6088
|
+
) : this.#fixed;
|
|
6089
|
+
const parts = intlFormatter(Intl.NumberFormat, this.#locale, {}, FALLBACK_LOCALE).formatToParts(
|
|
6090
|
+
11111.1
|
|
6091
|
+
);
|
|
5958
6092
|
this.#group = parts.find((p) => p.type === "group")?.value ?? ",";
|
|
5959
6093
|
this.#decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
|
|
5960
6094
|
this.#digits.clear();
|
|
5961
|
-
const digitFormatter =
|
|
6095
|
+
const digitFormatter = intlFormatter(
|
|
6096
|
+
Intl.NumberFormat,
|
|
6097
|
+
this.#locale,
|
|
6098
|
+
{ useGrouping: false },
|
|
6099
|
+
FALLBACK_LOCALE
|
|
6100
|
+
);
|
|
5962
6101
|
for (let i = 0; i <= 9; i++) {
|
|
5963
6102
|
const digit = digitFormatter.format(i);
|
|
5964
6103
|
if (digit !== String(i)) this.#digits.set(digit, String(i));
|
|
@@ -6332,7 +6471,8 @@ var DateRangePickerController = class extends Controller {
|
|
|
6332
6471
|
// A JSON list read through `parseStringList` rather than Stimulus's `Array`
|
|
6333
6472
|
// type: that reader throws out of the value observer before any callback
|
|
6334
6473
|
// runs, so one malformed attribute would stop the picker from connecting.
|
|
6335
|
-
disabledDates: { type: String, default: "" }
|
|
6474
|
+
disabledDates: { type: String, default: "" },
|
|
6475
|
+
locale: { type: String, default: "" }
|
|
6336
6476
|
};
|
|
6337
6477
|
static actions = ["applyPreset", "next", "onKeydown", "prev", "previewTo", "selectDate"];
|
|
6338
6478
|
static events = ["change", "monthchange"];
|
|
@@ -6385,6 +6525,10 @@ var DateRangePickerController = class extends Controller {
|
|
|
6385
6525
|
this.#beforeCache.deactivate();
|
|
6386
6526
|
this.#focusTimer.clearAll();
|
|
6387
6527
|
}
|
|
6528
|
+
/** Repaints the label when application code changes `locale` at runtime. */
|
|
6529
|
+
localeValueChanged() {
|
|
6530
|
+
this.#repaint.schedule();
|
|
6531
|
+
}
|
|
6388
6532
|
/** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
|
|
6389
6533
|
minValueChanged() {
|
|
6390
6534
|
this.#repaint.schedule();
|
|
@@ -6573,8 +6717,7 @@ var DateRangePickerController = class extends Controller {
|
|
|
6573
6717
|
const { year, month } = info;
|
|
6574
6718
|
if (this.#previewDate && !this.#isSelectable(this.#previewDate)) this.#previewDate = "";
|
|
6575
6719
|
if (this.hasMonthLabelTarget) {
|
|
6576
|
-
const
|
|
6577
|
-
const formatter = monthLabelFormatter(lang);
|
|
6720
|
+
const formatter = monthLabelFormatter(resolveLocale(this.element, this.localeValue));
|
|
6578
6721
|
this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));
|
|
6579
6722
|
}
|
|
6580
6723
|
const [rangeStart, rangeEnd] = this.#visualRange();
|
|
@@ -6790,6 +6933,43 @@ var DialogController = class extends Controller {
|
|
|
6790
6933
|
return this.hasDialogTarget && !this.dialogTarget.hidden;
|
|
6791
6934
|
}
|
|
6792
6935
|
};
|
|
6936
|
+
|
|
6937
|
+
// src/utils/declared_value.ts
|
|
6938
|
+
function parseDeclared(raw, parse, fallback) {
|
|
6939
|
+
try {
|
|
6940
|
+
return parse(raw);
|
|
6941
|
+
} catch {
|
|
6942
|
+
return fallback;
|
|
6943
|
+
}
|
|
6944
|
+
}
|
|
6945
|
+
function validSelector(element, raw, fallback) {
|
|
6946
|
+
if (raw.length === 0) return fallback;
|
|
6947
|
+
return parseDeclared(
|
|
6948
|
+
raw,
|
|
6949
|
+
(selector) => {
|
|
6950
|
+
element.matches(selector);
|
|
6951
|
+
return selector;
|
|
6952
|
+
},
|
|
6953
|
+
fallback
|
|
6954
|
+
);
|
|
6955
|
+
}
|
|
6956
|
+
function compileRegExp(source, anchor = "none") {
|
|
6957
|
+
return parseDeclared(
|
|
6958
|
+
source,
|
|
6959
|
+
(text) => {
|
|
6960
|
+
const bare = new RegExp(text);
|
|
6961
|
+
return anchor === "exact" ? new RegExp(`^(?:${text})$`) : bare;
|
|
6962
|
+
},
|
|
6963
|
+
null
|
|
6964
|
+
);
|
|
6965
|
+
}
|
|
6966
|
+
function parseJsonObject(raw) {
|
|
6967
|
+
const parsed = parseDeclared(raw, (text) => JSON.parse(text), null);
|
|
6968
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
6969
|
+
return parsed;
|
|
6970
|
+
}
|
|
6971
|
+
|
|
6972
|
+
// src/controllers/direct_upload_controller.ts
|
|
6793
6973
|
var REMOVE_DELAY = 4e3;
|
|
6794
6974
|
var DirectUploadController = class extends Controller {
|
|
6795
6975
|
static targets = ["list", "row"];
|
|
@@ -6844,16 +7024,7 @@ var DirectUploadController = class extends Controller {
|
|
|
6844
7024
|
}
|
|
6845
7025
|
/** Validates `scope` once so the per-event path never parses or throws. */
|
|
6846
7026
|
scopeValueChanged() {
|
|
6847
|
-
|
|
6848
|
-
if (selector.length > 0) {
|
|
6849
|
-
try {
|
|
6850
|
-
this.element.matches(selector);
|
|
6851
|
-
this.#scopeSelector = selector;
|
|
6852
|
-
return;
|
|
6853
|
-
} catch {
|
|
6854
|
-
}
|
|
6855
|
-
}
|
|
6856
|
-
this.#scopeSelector = "";
|
|
7027
|
+
this.#scopeSelector = validSelector(this.element, this.scopeValue, "");
|
|
6857
7028
|
}
|
|
6858
7029
|
/** Updates a row's progress and the aggregate, emitting `progress`. */
|
|
6859
7030
|
#updateProgress(id, percent, name) {
|
|
@@ -7125,6 +7296,7 @@ var DirtyFormController = class extends Controller {
|
|
|
7125
7296
|
#dirty = false;
|
|
7126
7297
|
#beforeunloadBound = false;
|
|
7127
7298
|
#timeouts = new SafeTimeout();
|
|
7299
|
+
#listeners = new ListenerSet();
|
|
7128
7300
|
/** Whether the active submission still represents the form's current values. */
|
|
7129
7301
|
#submitting = false;
|
|
7130
7302
|
#submitAttempt = null;
|
|
@@ -7182,24 +7354,19 @@ var DirtyFormController = class extends Controller {
|
|
|
7182
7354
|
this.#baseline = this.#serialize();
|
|
7183
7355
|
this.#setDirty(false);
|
|
7184
7356
|
this.element.removeAttribute("data-dirty");
|
|
7185
|
-
this.element
|
|
7186
|
-
this.element
|
|
7187
|
-
this.element
|
|
7188
|
-
this.element
|
|
7189
|
-
this.element
|
|
7190
|
-
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);
|
|
7191
7363
|
this.#unregisterVisit = registerVisitParticipant(this.element.ownerDocument, {
|
|
7192
7364
|
form: this.element,
|
|
7193
7365
|
evaluate: (event) => this.#evaluateVisit(event)
|
|
7194
7366
|
});
|
|
7195
7367
|
}
|
|
7196
7368
|
disconnect() {
|
|
7197
|
-
this.
|
|
7198
|
-
this.element.removeEventListener("change", this.#onFieldChange);
|
|
7199
|
-
this.element.removeEventListener("submit", this.#onSubmit);
|
|
7200
|
-
this.element.removeEventListener("formdata", this.#onFormData);
|
|
7201
|
-
this.element.removeEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
7202
|
-
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
7369
|
+
this.#listeners.dispose();
|
|
7203
7370
|
this.#unregisterVisit?.();
|
|
7204
7371
|
this.#unregisterVisit = null;
|
|
7205
7372
|
this.#unbindBeforeUnload();
|
|
@@ -8517,6 +8684,80 @@ var FilterController = class extends Controller {
|
|
|
8517
8684
|
return this.itemTargets.some((item) => !item.hidden && group.contains(item));
|
|
8518
8685
|
}
|
|
8519
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
|
|
8520
8761
|
var ASSERTIVE_TYPES = /* @__PURE__ */ new Set(["alert", "error"]);
|
|
8521
8762
|
var MESSAGE_SELECTOR = '[data-stimeo--flash-target="message"]';
|
|
8522
8763
|
var FlashController = class extends Controller {
|
|
@@ -8528,12 +8769,13 @@ var FlashController = class extends Controller {
|
|
|
8528
8769
|
};
|
|
8529
8770
|
static actions = ["dismiss"];
|
|
8530
8771
|
static events = ["show", "dismiss", "reconcile"];
|
|
8772
|
+
/** Removal timers for the leaving transition; the auto-dismiss ones live below. */
|
|
8531
8773
|
#timers = new SafeTimeout();
|
|
8774
|
+
/** Per-message auto-dismiss, held open while the message is hovered or focused. */
|
|
8775
|
+
#dismiss = new PausableTimers();
|
|
8532
8776
|
#observer = null;
|
|
8533
8777
|
/** Whether the controller is between `connect()` and `disconnect()`. */
|
|
8534
8778
|
#connected = false;
|
|
8535
|
-
/** Auto-dismiss timer state keyed by message element. */
|
|
8536
|
-
#state = /* @__PURE__ */ new Map();
|
|
8537
8779
|
/** Messages already processed, in insertion order, to enforce `max` and avoid double work. */
|
|
8538
8780
|
#order = [];
|
|
8539
8781
|
/**
|
|
@@ -8559,8 +8801,8 @@ var FlashController = class extends Controller {
|
|
|
8559
8801
|
this.#beforeCache.deactivate();
|
|
8560
8802
|
this.#stopObserving();
|
|
8561
8803
|
this.#timers.clearAll();
|
|
8804
|
+
this.#dismiss.clearAll();
|
|
8562
8805
|
for (const message of this.#order) this.#unbindPause(message);
|
|
8563
|
-
this.#state.clear();
|
|
8564
8806
|
this.#order.length = 0;
|
|
8565
8807
|
this.#leaving.clear();
|
|
8566
8808
|
}
|
|
@@ -8689,8 +8931,7 @@ var FlashController = class extends Controller {
|
|
|
8689
8931
|
* own role to do the announcing (dynamic inserts). Idempotent per message.
|
|
8690
8932
|
*/
|
|
8691
8933
|
#process(message, bridge) {
|
|
8692
|
-
if (this.#
|
|
8693
|
-
if (this.#leaving.has(message)) return;
|
|
8934
|
+
if (this.#order.includes(message) || this.#leaving.has(message)) return;
|
|
8694
8935
|
const type = message.getAttribute("data-flash-type") ?? "";
|
|
8695
8936
|
const assertive = ASSERTIVE_TYPES.has(type);
|
|
8696
8937
|
if (!message.hasAttribute("role")) {
|
|
@@ -8701,11 +8942,7 @@ var FlashController = class extends Controller {
|
|
|
8701
8942
|
this.#bindPause(message);
|
|
8702
8943
|
const text = message.textContent?.trim() ?? "";
|
|
8703
8944
|
this.dispatch("show", { target: message, detail: { type, message: text } });
|
|
8704
|
-
if (bridge
|
|
8705
|
-
window.dispatchEvent(
|
|
8706
|
-
new CustomEvent("stimeo--announcer:announce", { detail: { message: text, assertive } })
|
|
8707
|
-
);
|
|
8708
|
-
}
|
|
8945
|
+
if (bridge) announce(text, { assertive });
|
|
8709
8946
|
this.#startTimer(message);
|
|
8710
8947
|
this.#enforceMax();
|
|
8711
8948
|
}
|
|
@@ -8718,53 +8955,33 @@ var FlashController = class extends Controller {
|
|
|
8718
8955
|
this.#beginDismiss(oldest, "limit");
|
|
8719
8956
|
}
|
|
8720
8957
|
}
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
const id = this.#timers.set(() => this.#beginDismiss(message, "timeout"), duration);
|
|
8726
|
-
this.#state.set(message, {
|
|
8727
|
-
id,
|
|
8728
|
-
startedAt: Date.now(),
|
|
8729
|
-
remaining: duration,
|
|
8730
|
-
paused: existing?.paused ?? /* @__PURE__ */ new Set()
|
|
8731
|
-
});
|
|
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);
|
|
8732
8962
|
}
|
|
8733
8963
|
/**
|
|
8734
|
-
*
|
|
8735
|
-
* Hover and focus are independent reasons
|
|
8736
|
-
* 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.
|
|
8737
8967
|
*/
|
|
8738
8968
|
#pause(message, reason) {
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
if (timer.id === 0) return;
|
|
8743
|
-
this.#timers.clear(timer.id);
|
|
8744
|
-
const remaining = Math.max(1, timer.remaining - (Date.now() - timer.startedAt));
|
|
8745
|
-
this.#state.set(message, { id: 0, startedAt: 0, remaining, paused: timer.paused });
|
|
8746
|
-
}
|
|
8747
|
-
/** 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. */
|
|
8748
8972
|
#resume(message, reason) {
|
|
8749
|
-
|
|
8750
|
-
if (!timer) return;
|
|
8751
|
-
timer.paused.delete(reason);
|
|
8752
|
-
if (timer.paused.size > 0) return;
|
|
8753
|
-
if (timer.id !== 0) return;
|
|
8754
|
-
this.#startTimer(message, timer.remaining);
|
|
8973
|
+
this.#dismiss.resume(message, reason);
|
|
8755
8974
|
}
|
|
8756
8975
|
/** Releases every per-message resource: timer, stacking slot, pause listeners. */
|
|
8757
8976
|
#forget(message) {
|
|
8758
|
-
|
|
8759
|
-
if (timer?.id) this.#timers.clear(timer.id);
|
|
8760
|
-
this.#state.delete(message);
|
|
8977
|
+
this.#dismiss.clear(message);
|
|
8761
8978
|
const index = this.#order.indexOf(message);
|
|
8762
8979
|
if (index !== -1) this.#order.splice(index, 1);
|
|
8763
8980
|
this.#unbindPause(message);
|
|
8764
8981
|
}
|
|
8765
8982
|
/** Marks a message leaving, then removes it after its CSS transition and emits dismiss. */
|
|
8766
8983
|
#beginDismiss(message, reason) {
|
|
8767
|
-
if (!this.#
|
|
8984
|
+
if (!this.#order.includes(message)) return;
|
|
8768
8985
|
this.#forget(message);
|
|
8769
8986
|
this.#leaving.add(message);
|
|
8770
8987
|
message.setAttribute("data-flash-state", "leaving");
|
|
@@ -9088,7 +9305,7 @@ var FormFieldController = class _FormFieldController extends Controller {
|
|
|
9088
9305
|
return this.errorTargets.includes(target);
|
|
9089
9306
|
return record.attributeName === "id" && [...this.descriptionTargets, ...this.errorTargets].includes(target);
|
|
9090
9307
|
}
|
|
9091
|
-
return this.errorTargets
|
|
9308
|
+
return ownerIndex(this.errorTargets, target) !== -1;
|
|
9092
9309
|
}
|
|
9093
9310
|
/** Returns borrowed control ARIA before Turbo snapshots the page. */
|
|
9094
9311
|
#rewindForCache() {
|
|
@@ -9463,6 +9680,7 @@ var FrameLoadingController = class extends Controller {
|
|
|
9463
9680
|
#floor = new MinDurationFloor(this.#timeouts);
|
|
9464
9681
|
#gate = new DetachGate();
|
|
9465
9682
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
9683
|
+
#listeners = new ListenerSet();
|
|
9466
9684
|
#loading = false;
|
|
9467
9685
|
/**
|
|
9468
9686
|
* The optional targets this controller revealed, and the content it marked inert.
|
|
@@ -9489,14 +9707,12 @@ var FrameLoadingController = class extends Controller {
|
|
|
9489
9707
|
connect() {
|
|
9490
9708
|
this.#gate.cancel();
|
|
9491
9709
|
this.#beforeCache.activate();
|
|
9492
|
-
this.element
|
|
9493
|
-
this.element
|
|
9494
|
-
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);
|
|
9495
9713
|
}
|
|
9496
9714
|
disconnect() {
|
|
9497
|
-
this.
|
|
9498
|
-
this.element.removeEventListener("turbo:frame-load", this.#onEnd);
|
|
9499
|
-
this.element.removeEventListener("turbo:fetch-request-error", this.#onEnd);
|
|
9715
|
+
this.#listeners.dispose();
|
|
9500
9716
|
this.#beforeCache.deactivate();
|
|
9501
9717
|
this.#gate.disconnected(this, () => this.#teardown());
|
|
9502
9718
|
}
|
|
@@ -9658,14 +9874,11 @@ var HighlightController = class extends Controller {
|
|
|
9658
9874
|
observe: { type: Boolean, default: false }
|
|
9659
9875
|
};
|
|
9660
9876
|
static events = ["start", "end"];
|
|
9661
|
-
#timeouts = new SafeTimeout();
|
|
9662
9877
|
/**
|
|
9663
|
-
* The removal timer this connection has outstanding for
|
|
9664
|
-
*
|
|
9665
|
-
* so a cleared id can never be matched against a recycled one. Which connection owns
|
|
9666
|
-
* 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.
|
|
9667
9880
|
*/
|
|
9668
|
-
#
|
|
9881
|
+
#removal = new KeyedTimers();
|
|
9669
9882
|
#observer = null;
|
|
9670
9883
|
connect() {
|
|
9671
9884
|
this.#clearArrivedHook(this.element);
|
|
@@ -9682,8 +9895,7 @@ var HighlightController = class extends Controller {
|
|
|
9682
9895
|
disconnect() {
|
|
9683
9896
|
this.#observer?.disconnect();
|
|
9684
9897
|
this.#observer = null;
|
|
9685
|
-
this.#
|
|
9686
|
-
this.#pending = /* @__PURE__ */ new WeakMap();
|
|
9898
|
+
this.#removal.clearAll();
|
|
9687
9899
|
}
|
|
9688
9900
|
/** Drops a hook that arrived with the DOM, along with this connection's claim on it. */
|
|
9689
9901
|
#clearArrivedHook(el) {
|
|
@@ -9704,13 +9916,15 @@ var HighlightController = class extends Controller {
|
|
|
9704
9916
|
this.#releasePending(el);
|
|
9705
9917
|
el.setAttribute("data-highlight", "true");
|
|
9706
9918
|
this.dispatch("start", { target: el, detail: { element: el } });
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
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
|
+
);
|
|
9714
9928
|
hookOwners.set(el, this);
|
|
9715
9929
|
}
|
|
9716
9930
|
/**
|
|
@@ -9723,10 +9937,9 @@ var HighlightController = class extends Controller {
|
|
|
9723
9937
|
if (owner !== void 0 && owner !== this) owner.#cancelPending(el);
|
|
9724
9938
|
this.#cancelPending(el);
|
|
9725
9939
|
}
|
|
9726
|
-
/** 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. */
|
|
9727
9941
|
#cancelPending(el) {
|
|
9728
|
-
this.#
|
|
9729
|
-
this.#pending.delete(el);
|
|
9942
|
+
this.#removal.clear(el);
|
|
9730
9943
|
hookOwners.delete(el);
|
|
9731
9944
|
}
|
|
9732
9945
|
};
|
|
@@ -10008,22 +10221,19 @@ function applyMask(value, pattern, tokens) {
|
|
|
10008
10221
|
}
|
|
10009
10222
|
function compileTokens(declaration) {
|
|
10010
10223
|
const map = /* @__PURE__ */ new Map();
|
|
10011
|
-
for (const [key, source] of Object.entries(
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10224
|
+
for (const [key, source] of Object.entries(DEFAULT_TOKENS)) {
|
|
10225
|
+
const compiled = compileRegExp(source, "exact");
|
|
10226
|
+
if (compiled) map.set(key, compiled);
|
|
10227
|
+
}
|
|
10228
|
+
for (const [key, source] of Object.entries(parseTokens(declaration))) {
|
|
10229
|
+
const compiled = compileRegExp(source, "exact");
|
|
10230
|
+
if (compiled) map.set(key, compiled);
|
|
10016
10231
|
}
|
|
10017
10232
|
return map;
|
|
10018
10233
|
}
|
|
10019
10234
|
function parseTokens(declaration) {
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
parsed = JSON.parse(declaration);
|
|
10023
|
-
} catch {
|
|
10024
|
-
return {};
|
|
10025
|
-
}
|
|
10026
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
10235
|
+
const parsed = parseJsonObject(declaration);
|
|
10236
|
+
if (parsed === null) return {};
|
|
10027
10237
|
const tokens = {};
|
|
10028
10238
|
for (const [key, source] of Object.entries(parsed)) {
|
|
10029
10239
|
if (typeof source === "string") tokens[key] = source;
|
|
@@ -10235,11 +10445,7 @@ function isBeforeRootStart(entry) {
|
|
|
10235
10445
|
}
|
|
10236
10446
|
function queryRoot(selector) {
|
|
10237
10447
|
if (!selector) return null;
|
|
10238
|
-
|
|
10239
|
-
return document.querySelector(selector);
|
|
10240
|
-
} catch {
|
|
10241
|
-
}
|
|
10242
|
-
return null;
|
|
10448
|
+
return parseDeclared(selector, (raw) => document.querySelector(raw), null);
|
|
10243
10449
|
}
|
|
10244
10450
|
var IntersectionWatcher = class {
|
|
10245
10451
|
#onEntries;
|
|
@@ -11070,8 +11276,9 @@ var LocalTimeController = class extends Controller {
|
|
|
11070
11276
|
/**
|
|
11071
11277
|
* Formats `date` with `Intl.DateTimeFormat`, including each style only when it
|
|
11072
11278
|
* is a valid keyword (so a consumer can show date-only or time-only by clearing
|
|
11073
|
-
* the other). Returns `null` when neither style is usable or
|
|
11074
|
-
* the caller can leave the
|
|
11279
|
+
* the other). Returns `null` when neither style is usable or the declaration
|
|
11280
|
+
* names a locale or time zone `Intl` rejects, so the caller can leave the
|
|
11281
|
+
* authored text untouched.
|
|
11075
11282
|
*/
|
|
11076
11283
|
#applyFormat(date, dateStyle, timeStyle) {
|
|
11077
11284
|
const options = {
|
|
@@ -11080,15 +11287,12 @@ var LocalTimeController = class extends Controller {
|
|
|
11080
11287
|
};
|
|
11081
11288
|
if (options.dateStyle === void 0 && options.timeStyle === void 0) return null;
|
|
11082
11289
|
if (this.timeZoneValue.length > 0) options.timeZone = this.timeZoneValue;
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
/** Locale precedence: the value, then the nearest `lang` up the ancestor chain. */
|
|
11090
|
-
get #locale() {
|
|
11091
|
-
return this.localeValue || this.element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
11290
|
+
const formatter = intlFormatter(
|
|
11291
|
+
Intl.DateTimeFormat,
|
|
11292
|
+
resolveLocale(this.element, this.localeValue),
|
|
11293
|
+
options
|
|
11294
|
+
);
|
|
11295
|
+
return formatter === null ? null : formatter.format(date);
|
|
11092
11296
|
}
|
|
11093
11297
|
};
|
|
11094
11298
|
var COLUMNS_PROPERTY = "--stimeo--masonry-columns";
|
|
@@ -11126,13 +11330,6 @@ var MasonryController = class extends Controller {
|
|
|
11126
11330
|
#mutationObserver = null;
|
|
11127
11331
|
/** Last published column count, so `layout` fires only on real changes. */
|
|
11128
11332
|
#lastColumns = 0;
|
|
11129
|
-
/**
|
|
11130
|
-
* Re-pack when a descendant resource finishes loading. Images/iframes report a
|
|
11131
|
-
* height of 0 until loaded, which would skew the shortest-column packing if the
|
|
11132
|
-
* first pass ran before they settled; `load` does not bubble, so this is bound in
|
|
11133
|
-
* the capture phase to catch every descendant.
|
|
11134
|
-
*/
|
|
11135
|
-
#onLoad = () => this.#reconcile.schedule();
|
|
11136
11333
|
/** Resolves the declared column width once, falling back when it is unreadable. */
|
|
11137
11334
|
minColumnWidthValueChanged() {
|
|
11138
11335
|
this.#minColumnWidth = usableNumber(this.minColumnWidthValue, DEFAULT_MIN_COLUMN_WIDTH);
|
|
@@ -11152,7 +11349,7 @@ var MasonryController = class extends Controller {
|
|
|
11152
11349
|
*
|
|
11153
11350
|
* The removal is queued rather than immediate because teardown reports every
|
|
11154
11351
|
* target as disconnected: doing it here would strip the whole grid just before
|
|
11155
|
-
* a Turbo snapshot is taken.
|
|
11352
|
+
* a Turbo snapshot is taken. The coalescer's `cancel` drops the queue
|
|
11156
11353
|
* with the pass, so only a genuine target change reaches it.
|
|
11157
11354
|
*/
|
|
11158
11355
|
itemTargetDisconnected(item) {
|
|
@@ -11167,18 +11364,17 @@ var MasonryController = class extends Controller {
|
|
|
11167
11364
|
this.#mutationObserver = new MutationObserver(() => this.#reconcile.schedule());
|
|
11168
11365
|
this.#mutationObserver.observe(this.element, { childList: true, subtree: true });
|
|
11169
11366
|
}
|
|
11170
|
-
this.
|
|
11367
|
+
this.#layout.observeDescendantLoads(this.element);
|
|
11171
11368
|
this.#relayout();
|
|
11172
11369
|
this.#reconcile.activate();
|
|
11173
11370
|
}
|
|
11174
|
-
/** Releases
|
|
11371
|
+
/** Releases every observation so nothing fires after detach. */
|
|
11175
11372
|
disconnect() {
|
|
11176
11373
|
this.#reconcile.cancel();
|
|
11177
11374
|
this.#released.clear();
|
|
11178
11375
|
this.#layout.disconnect();
|
|
11179
11376
|
this.#mutationObserver?.disconnect();
|
|
11180
11377
|
this.#mutationObserver = null;
|
|
11181
|
-
this.element.removeEventListener("load", this.#onLoad, true);
|
|
11182
11378
|
this.#lastColumns = 0;
|
|
11183
11379
|
}
|
|
11184
11380
|
/**
|
|
@@ -11433,8 +11629,7 @@ var MenuController = class extends Controller {
|
|
|
11433
11629
|
};
|
|
11434
11630
|
/** The item target that is, or contains, `node`; `null` when it is neither. */
|
|
11435
11631
|
#itemFrom(node) {
|
|
11436
|
-
|
|
11437
|
-
return this.itemTargets.find((item) => item === node || item.contains(node)) ?? null;
|
|
11632
|
+
return ownerOf(this.itemTargets, node);
|
|
11438
11633
|
}
|
|
11439
11634
|
/**
|
|
11440
11635
|
* First look at every click inside the controller, and the only point at which
|
|
@@ -11810,8 +12005,7 @@ var MenubarController = class extends Controller {
|
|
|
11810
12005
|
* menu without moving focus.
|
|
11811
12006
|
*/
|
|
11812
12007
|
#dismissOpenMenu() {
|
|
11813
|
-
const
|
|
11814
|
-
const menu = this.menuTargets.find((candidate) => candidate.contains(active));
|
|
12008
|
+
const menu = ownerOf(this.menuTargets, document.activeElement);
|
|
11815
12009
|
if (menu) {
|
|
11816
12010
|
const top = this.#topFor(menu);
|
|
11817
12011
|
this.#closeMenu(top);
|
|
@@ -13225,7 +13419,7 @@ var NavigationMenuController = class extends Controller {
|
|
|
13225
13419
|
*/
|
|
13226
13420
|
#onPointerLeave = (event) => {
|
|
13227
13421
|
const next = event instanceof MouseEvent ? event.relatedTarget : null;
|
|
13228
|
-
if (
|
|
13422
|
+
if (ownerIndex(this.#hoverElements, next) !== -1) return;
|
|
13229
13423
|
this.#hoveredTrigger = null;
|
|
13230
13424
|
this.#hoverTimers.clearAll();
|
|
13231
13425
|
this.#hoverTimers.set(() => this.#closeFromHover(), this.hoverDelayValue);
|
|
@@ -13295,7 +13489,7 @@ var NavigationMenuController = class extends Controller {
|
|
|
13295
13489
|
*/
|
|
13296
13490
|
get #hoverElements() {
|
|
13297
13491
|
const areas = this.hoverAreaTargets;
|
|
13298
|
-
const covered = (element) => areas
|
|
13492
|
+
const covered = (element) => ownerIndex(areas, element) !== -1;
|
|
13299
13493
|
return [
|
|
13300
13494
|
...areas,
|
|
13301
13495
|
...this.triggerTargets.filter((trigger) => !covered(trigger)),
|
|
@@ -14235,6 +14429,7 @@ var OptimisticController = class extends Controller {
|
|
|
14235
14429
|
/** The submission that owns the applied optimistic state, if any. */
|
|
14236
14430
|
#pending = null;
|
|
14237
14431
|
#gate = new DetachGate();
|
|
14432
|
+
#listeners = new ListenerSet();
|
|
14238
14433
|
/** True between `connect()` and `disconnect()`, so a repeat connect rewinds nothing. */
|
|
14239
14434
|
#connected = false;
|
|
14240
14435
|
connect() {
|
|
@@ -14242,13 +14437,12 @@ var OptimisticController = class extends Controller {
|
|
|
14242
14437
|
this.#gate.cancel();
|
|
14243
14438
|
this.#connected = true;
|
|
14244
14439
|
if (restored && this.element.hasAttribute("data-optimistic")) this.#revert();
|
|
14245
|
-
this.element
|
|
14246
|
-
this.element
|
|
14440
|
+
this.#listeners.add(this.element, "turbo:submit-start", this.#onSubmitStart);
|
|
14441
|
+
this.#listeners.add(this.element, "turbo:submit-end", this.#onSubmitEnd);
|
|
14247
14442
|
}
|
|
14248
14443
|
disconnect() {
|
|
14249
14444
|
this.#connected = false;
|
|
14250
|
-
this.
|
|
14251
|
-
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
14445
|
+
this.#listeners.dispose();
|
|
14252
14446
|
this.#gate.disconnected(this, () => {
|
|
14253
14447
|
this.#pending = null;
|
|
14254
14448
|
});
|
|
@@ -14336,13 +14530,7 @@ var OptimisticController = class extends Controller {
|
|
|
14336
14530
|
}
|
|
14337
14531
|
};
|
|
14338
14532
|
var DEFAULT_PATTERN = "[0-9]";
|
|
14339
|
-
|
|
14340
|
-
try {
|
|
14341
|
-
return new RegExp(`^${source}$`);
|
|
14342
|
-
} catch {
|
|
14343
|
-
return null;
|
|
14344
|
-
}
|
|
14345
|
-
}
|
|
14533
|
+
var DEFAULT_MATCHER = new RegExp(`^(?:${DEFAULT_PATTERN})$`);
|
|
14346
14534
|
function hasModifier2(event) {
|
|
14347
14535
|
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
14348
14536
|
}
|
|
@@ -14357,7 +14545,7 @@ var OtpController = class extends Controller {
|
|
|
14357
14545
|
static actions = ["onInput", "onKeydown", "onPaste", "onPointerDown", "clear"];
|
|
14358
14546
|
static events = ["change", "complete", "invalid", "reconcile"];
|
|
14359
14547
|
/** Validated matcher; the hot path never compiles a raw declaration. */
|
|
14360
|
-
#pattern =
|
|
14548
|
+
#pattern = DEFAULT_MATCHER;
|
|
14361
14549
|
/** Source of {@link #pattern}, reported in `invalid` so consumers can word it. */
|
|
14362
14550
|
#patternSource = DEFAULT_PATTERN;
|
|
14363
14551
|
/** Public state carried by the last dispatch; keeps a no-op sync silent. */
|
|
@@ -14430,9 +14618,9 @@ var OtpController = class extends Controller {
|
|
|
14430
14618
|
* the new pattern no longer accepts, so the combined value stays interpretable.
|
|
14431
14619
|
*/
|
|
14432
14620
|
patternValueChanged() {
|
|
14433
|
-
const compiled =
|
|
14621
|
+
const compiled = compileRegExp(this.patternValue, "exact");
|
|
14434
14622
|
this.#patternSource = compiled ? this.patternValue : DEFAULT_PATTERN;
|
|
14435
|
-
this.#pattern = compiled ??
|
|
14623
|
+
this.#pattern = compiled ?? DEFAULT_MATCHER;
|
|
14436
14624
|
if (!this.#connected) return;
|
|
14437
14625
|
let dropped = false;
|
|
14438
14626
|
for (const field of this.fieldTargets) {
|
|
@@ -14946,7 +15134,7 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
14946
15134
|
this.#observedViewport = next;
|
|
14947
15135
|
this.#layout.observe(next);
|
|
14948
15136
|
this.#layout.observeViewport();
|
|
14949
|
-
|
|
15137
|
+
this.#layout.observeDescendantLoads(next);
|
|
14950
15138
|
this.#syncContentObservation();
|
|
14951
15139
|
if (typeof MutationObserver !== "undefined") {
|
|
14952
15140
|
this.#mutationObserver = new MutationObserver(() => {
|
|
@@ -14984,16 +15172,13 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
14984
15172
|
#stopObservingViewport() {
|
|
14985
15173
|
this.#mutationObserver?.disconnect();
|
|
14986
15174
|
this.#mutationObserver = null;
|
|
14987
|
-
this.#
|
|
15175
|
+
this.#layout.unobserveDescendantLoads();
|
|
14988
15176
|
if (this.#observedViewport) this.#layout.unobserve(this.#observedViewport);
|
|
14989
15177
|
for (const content of this.#observedContent) this.#layout.unobserve(content);
|
|
14990
15178
|
this.#observedContent.clear();
|
|
14991
15179
|
this.#observedViewport = null;
|
|
14992
15180
|
this.#layout.unobserveViewport();
|
|
14993
15181
|
}
|
|
14994
|
-
#onContentLoad = () => {
|
|
14995
|
-
if (this.#connected) this.update();
|
|
14996
|
-
};
|
|
14997
15182
|
get #threshold() {
|
|
14998
15183
|
const value = this.thresholdValue;
|
|
14999
15184
|
return Number.isFinite(value) ? Math.max(0, value) : 1;
|
|
@@ -15316,7 +15501,7 @@ var OverflowMenuController = class extends Controller {
|
|
|
15316
15501
|
}
|
|
15317
15502
|
/** The managed item that is, or contains, `el` — focus rescue works on either. */
|
|
15318
15503
|
#ownerOf(el) {
|
|
15319
|
-
return this.#items
|
|
15504
|
+
return ownerOf(this.#items, el);
|
|
15320
15505
|
}
|
|
15321
15506
|
/** Whether the item takes up room in the bar; an authored `hidden` one does not. */
|
|
15322
15507
|
#rendered(item) {
|
|
@@ -16833,9 +17018,7 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
16833
17018
|
}
|
|
16834
17019
|
/** Resolves the handle owning an event target (the handle or a descendant). */
|
|
16835
17020
|
#handleFor(target) {
|
|
16836
|
-
|
|
16837
|
-
if (!node) return null;
|
|
16838
|
-
return this.#handles().find((handle) => handle === node || handle.contains(node)) ?? null;
|
|
17021
|
+
return ownerOf(this.#handles(), target);
|
|
16839
17022
|
}
|
|
16840
17023
|
#pointerTypeOf(event) {
|
|
16841
17024
|
const type = event.pointerType;
|
|
@@ -17016,16 +17199,7 @@ var PortalController = class extends Controller {
|
|
|
17016
17199
|
* instead, which reads as a mistake and can be traced back to the declaration.
|
|
17017
17200
|
*/
|
|
17018
17201
|
toValueChanged() {
|
|
17019
|
-
|
|
17020
|
-
if (selector.length > 0) {
|
|
17021
|
-
try {
|
|
17022
|
-
this.element.matches(selector);
|
|
17023
|
-
this.#toSelector = selector;
|
|
17024
|
-
return;
|
|
17025
|
-
} catch {
|
|
17026
|
-
}
|
|
17027
|
-
}
|
|
17028
|
-
this.#toSelector = "body";
|
|
17202
|
+
this.#toSelector = validSelector(this.element, this.toValue, "body");
|
|
17029
17203
|
}
|
|
17030
17204
|
connect() {
|
|
17031
17205
|
this.#gate.cancel();
|
|
@@ -17751,9 +17925,8 @@ var RadioGroupController = class extends Controller {
|
|
|
17751
17925
|
}
|
|
17752
17926
|
/** Finds this group's radio containing an event target, excluding nested groups. */
|
|
17753
17927
|
#radioForEventTarget(target) {
|
|
17754
|
-
if (!this.#ownsEventTarget(target)) return
|
|
17755
|
-
|
|
17756
|
-
return this.radioTargets.find((radio) => radio === node || radio.contains(node));
|
|
17928
|
+
if (!this.#ownsEventTarget(target)) return null;
|
|
17929
|
+
return ownerOf(this.radioTargets, target);
|
|
17757
17930
|
}
|
|
17758
17931
|
/** Whether the closest Radio Group scope around a target is this instance. */
|
|
17759
17932
|
#ownsEventTarget(target) {
|
|
@@ -18536,7 +18709,7 @@ var ReadMoreController = class extends Controller {
|
|
|
18536
18709
|
this.#observedContent = next;
|
|
18537
18710
|
this.#layout.observe(next);
|
|
18538
18711
|
this.#layout.observeViewport();
|
|
18539
|
-
|
|
18712
|
+
this.#layout.observeDescendantLoads(next);
|
|
18540
18713
|
if (typeof MutationObserver !== "undefined") {
|
|
18541
18714
|
this.#contentMutationObserver = new MutationObserver(this.#update);
|
|
18542
18715
|
this.#contentMutationObserver.observe(next, {
|
|
@@ -18550,7 +18723,7 @@ var ReadMoreController = class extends Controller {
|
|
|
18550
18723
|
this.#deferredHide.releaseAll();
|
|
18551
18724
|
if (this.#observedContent) {
|
|
18552
18725
|
this.#layout.unobserve(this.#observedContent);
|
|
18553
|
-
this.#
|
|
18726
|
+
this.#layout.unobserveDescendantLoads();
|
|
18554
18727
|
}
|
|
18555
18728
|
this.#observedContent = null;
|
|
18556
18729
|
this.#contentMutationObserver?.disconnect();
|
|
@@ -18576,6 +18749,38 @@ var ReadMoreController = class extends Controller {
|
|
|
18576
18749
|
trigger.hidden = true;
|
|
18577
18750
|
}
|
|
18578
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
|
|
18579
18784
|
var PROGRESS_PROPERTY = "--stimeo--reading-progress";
|
|
18580
18785
|
var ReadingProgressController = class extends Controller {
|
|
18581
18786
|
static events = ["change", "complete"];
|
|
@@ -18584,18 +18789,13 @@ var ReadingProgressController = class extends Controller {
|
|
|
18584
18789
|
/** The article's own box and the viewport: either changes the span. */
|
|
18585
18790
|
#layout = new LayoutObserver(() => this.#onScroll());
|
|
18586
18791
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
18587
|
-
|
|
18792
|
+
/** Coalesces scroll bursts, and the connect baseline, into one frame. */
|
|
18793
|
+
#frames = new FrameCoalescer();
|
|
18588
18794
|
/** Last published progress, so `change`/`complete` fire only on movement. */
|
|
18589
18795
|
#progress = -1;
|
|
18590
18796
|
/** False until the connect frame has run: `complete` needs real reading. */
|
|
18591
18797
|
#baselined = false;
|
|
18592
|
-
#onScroll = () =>
|
|
18593
|
-
if (this.#frame !== null) return;
|
|
18594
|
-
this.#frame = requestAnimationFrame(() => {
|
|
18595
|
-
this.#frame = null;
|
|
18596
|
-
this.#measure();
|
|
18597
|
-
});
|
|
18598
|
-
};
|
|
18798
|
+
#onScroll = () => this.#frames.schedule(() => this.#measure());
|
|
18599
18799
|
connect() {
|
|
18600
18800
|
this.#progress = -1;
|
|
18601
18801
|
this.#baselined = false;
|
|
@@ -18604,8 +18804,7 @@ var ReadingProgressController = class extends Controller {
|
|
|
18604
18804
|
this.#layout.observeViewport();
|
|
18605
18805
|
this.#beforeCache.activate();
|
|
18606
18806
|
this.#measure();
|
|
18607
|
-
this.#
|
|
18608
|
-
this.#frame = null;
|
|
18807
|
+
this.#frames.schedule(() => {
|
|
18609
18808
|
this.#measure();
|
|
18610
18809
|
this.#baselined = true;
|
|
18611
18810
|
});
|
|
@@ -18614,7 +18813,7 @@ var ReadingProgressController = class extends Controller {
|
|
|
18614
18813
|
window.removeEventListener("scroll", this.#onScroll, { capture: true });
|
|
18615
18814
|
this.#layout.disconnect();
|
|
18616
18815
|
this.#beforeCache.deactivate();
|
|
18617
|
-
this.#
|
|
18816
|
+
this.#frames.cancel();
|
|
18618
18817
|
this.#lease.returnAll();
|
|
18619
18818
|
}
|
|
18620
18819
|
/**
|
|
@@ -18625,14 +18824,10 @@ var ReadingProgressController = class extends Controller {
|
|
|
18625
18824
|
* value that has already been handed back.
|
|
18626
18825
|
*/
|
|
18627
18826
|
#rewindForCache() {
|
|
18628
|
-
this.#
|
|
18827
|
+
this.#frames.cancel();
|
|
18629
18828
|
this.#lease.returnAll();
|
|
18630
18829
|
this.#progress = -1;
|
|
18631
18830
|
}
|
|
18632
|
-
#cancelFrame() {
|
|
18633
|
-
if (this.#frame !== null) cancelAnimationFrame(this.#frame);
|
|
18634
|
-
this.#frame = null;
|
|
18635
|
-
}
|
|
18636
18831
|
/** Computes and publishes the progress; emits on movement only. */
|
|
18637
18832
|
#measure() {
|
|
18638
18833
|
const rect = this.element.getBoundingClientRect();
|
|
@@ -18660,6 +18855,7 @@ var UNITS = [
|
|
|
18660
18855
|
{ limit: 31557600, unit: "month", ms: 26298e5 },
|
|
18661
18856
|
YEAR_SCALE
|
|
18662
18857
|
];
|
|
18858
|
+
var RELATIVE_OPTIONS = { numeric: "auto" };
|
|
18663
18859
|
var RelativeTimeController = class extends Controller {
|
|
18664
18860
|
static values = {
|
|
18665
18861
|
locale: { type: String, default: "" },
|
|
@@ -18767,15 +18963,11 @@ var RelativeTimeController = class extends Controller {
|
|
|
18767
18963
|
* when the runtime rejects that locale.
|
|
18768
18964
|
*/
|
|
18769
18965
|
get #formatter() {
|
|
18770
|
-
|
|
18771
|
-
|
|
18772
|
-
|
|
18773
|
-
|
|
18774
|
-
|
|
18775
|
-
}
|
|
18776
|
-
/** Locale precedence: the value, then the nearest `lang` up the ancestor chain. */
|
|
18777
|
-
get #locale() {
|
|
18778
|
-
return this.localeValue || this.element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
18966
|
+
return intlFormatter(
|
|
18967
|
+
Intl.RelativeTimeFormat,
|
|
18968
|
+
resolveLocale(this.element, this.localeValue),
|
|
18969
|
+
RELATIVE_OPTIONS
|
|
18970
|
+
);
|
|
18779
18971
|
}
|
|
18780
18972
|
};
|
|
18781
18973
|
var STATELESS_INPUT_TYPES = /* @__PURE__ */ new Set(["hidden", "submit", "reset", "button", "image"]);
|
|
@@ -18820,16 +19012,7 @@ var ResetBeforeCacheController = class extends Controller {
|
|
|
18820
19012
|
* silently taking the sweep down.
|
|
18821
19013
|
*/
|
|
18822
19014
|
scopeValueChanged() {
|
|
18823
|
-
|
|
18824
|
-
if (selector.length > 0) {
|
|
18825
|
-
try {
|
|
18826
|
-
this.element.matches(selector);
|
|
18827
|
-
this.#scopeSelector = selector;
|
|
18828
|
-
return;
|
|
18829
|
-
} catch {
|
|
18830
|
-
}
|
|
18831
|
-
}
|
|
18832
|
-
this.#scopeSelector = "";
|
|
19015
|
+
this.#scopeSelector = validSelector(this.element, this.scopeValue, "");
|
|
18833
19016
|
}
|
|
18834
19017
|
connect() {
|
|
18835
19018
|
document.addEventListener("turbo:before-cache", this.#onBeforeCache);
|
|
@@ -18931,8 +19114,7 @@ var ResizableController = class extends Controller {
|
|
|
18931
19114
|
const panes = [];
|
|
18932
19115
|
if (this.hasPrimaryTarget) panes.push(this.primaryTarget);
|
|
18933
19116
|
if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);
|
|
18934
|
-
const
|
|
18935
|
-
const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));
|
|
19117
|
+
const current = ownerIndex(panes, event.target);
|
|
18936
19118
|
const next = panes[(current + 1) % panes.length];
|
|
18937
19119
|
if (!next) return;
|
|
18938
19120
|
event.preventDefault();
|
|
@@ -19184,9 +19366,7 @@ var RovingController = class extends Controller {
|
|
|
19184
19366
|
};
|
|
19185
19367
|
/** Resolves the item index owning an event target (the item or a descendant). */
|
|
19186
19368
|
#indexOf(target) {
|
|
19187
|
-
|
|
19188
|
-
if (!node) return -1;
|
|
19189
|
-
return this.itemTargets.findIndex((item) => item === node || item.contains(node));
|
|
19369
|
+
return ownerIndex(this.itemTargets, target);
|
|
19190
19370
|
}
|
|
19191
19371
|
/** Makes `index` the tab stop (optionally focusing it), emitting `change` once. */
|
|
19192
19372
|
#activate(index, focus) {
|
|
@@ -19318,20 +19498,14 @@ var ScrollAreaController = class extends Controller {
|
|
|
19318
19498
|
#observedNameIds = /* @__PURE__ */ new Set();
|
|
19319
19499
|
#observedNameSources = [];
|
|
19320
19500
|
#fonts = null;
|
|
19321
|
-
|
|
19501
|
+
/** Coalesces scroll bursts into one position sync per frame. */
|
|
19502
|
+
#frames = new FrameCoalescer();
|
|
19322
19503
|
#overflowing = false;
|
|
19323
19504
|
#lastEdge = null;
|
|
19324
19505
|
#ownedHostMutations = /* @__PURE__ */ new Map();
|
|
19325
|
-
#onScroll = () => {
|
|
19326
|
-
|
|
19327
|
-
this.#
|
|
19328
|
-
this.#scrollFrame = null;
|
|
19329
|
-
const viewport = this.#viewport;
|
|
19330
|
-
if (viewport) this.#syncPosition(viewport, this.#overflowing);
|
|
19331
|
-
});
|
|
19332
|
-
};
|
|
19333
|
-
#onLoad = () => {
|
|
19334
|
-
this.#refresh();
|
|
19506
|
+
#onScroll = (event) => {
|
|
19507
|
+
const viewport = event.currentTarget;
|
|
19508
|
+
this.#frames.schedule(() => this.#syncPosition(viewport, this.#overflowing));
|
|
19335
19509
|
};
|
|
19336
19510
|
#onFontsSettled = () => {
|
|
19337
19511
|
this.#refresh();
|
|
@@ -19346,7 +19520,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
19346
19520
|
disconnect() {
|
|
19347
19521
|
this.#beforeCache.deactivate();
|
|
19348
19522
|
this.#rebind.cancel();
|
|
19349
|
-
this.#
|
|
19523
|
+
this.#frames.cancel();
|
|
19350
19524
|
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
19351
19525
|
this.#layout.disconnect();
|
|
19352
19526
|
this.#unbindFonts();
|
|
@@ -19379,7 +19553,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
19379
19553
|
return;
|
|
19380
19554
|
}
|
|
19381
19555
|
next.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
19382
|
-
|
|
19556
|
+
this.#layout.observeDescendantLoads(next);
|
|
19383
19557
|
this.#layout.observe(next);
|
|
19384
19558
|
this.#bindContentObserver(next);
|
|
19385
19559
|
this.#refresh();
|
|
@@ -19414,9 +19588,9 @@ var ScrollAreaController = class extends Controller {
|
|
|
19414
19588
|
}
|
|
19415
19589
|
/** Releases every resource and borrowed attribute owned by one former viewport. */
|
|
19416
19590
|
#unbindViewport(viewport) {
|
|
19417
|
-
this.#
|
|
19591
|
+
this.#frames.cancel();
|
|
19418
19592
|
viewport.removeEventListener("scroll", this.#onScroll);
|
|
19419
|
-
|
|
19593
|
+
this.#layout.unobserveDescendantLoads();
|
|
19420
19594
|
this.#layout.unobserve(viewport);
|
|
19421
19595
|
this.#content?.disconnect();
|
|
19422
19596
|
this.#content = null;
|
|
@@ -19431,7 +19605,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
19431
19605
|
#refresh() {
|
|
19432
19606
|
const viewport = this.#viewport;
|
|
19433
19607
|
if (!viewport) return;
|
|
19434
|
-
this.#
|
|
19608
|
+
this.#frames.cancel();
|
|
19435
19609
|
this.#syncNameSources(viewport);
|
|
19436
19610
|
this.#overflowing = this.#syncOverflow(viewport);
|
|
19437
19611
|
this.#syncKeyboardReach(viewport, this.#overflowing);
|
|
@@ -19639,16 +19813,10 @@ var ScrollAreaController = class extends Controller {
|
|
|
19639
19813
|
this.#fonts?.removeEventListener("loadingerror", this.#onFontsSettled);
|
|
19640
19814
|
this.#fonts = null;
|
|
19641
19815
|
}
|
|
19642
|
-
/** Cancels a pending scroll frame. */
|
|
19643
|
-
#cancelScrollFrame() {
|
|
19644
|
-
if (this.#scrollFrame === null) return;
|
|
19645
|
-
cancelAnimationFrame(this.#scrollFrame);
|
|
19646
|
-
this.#scrollFrame = null;
|
|
19647
|
-
}
|
|
19648
19816
|
/** Suspends live resources and returns all derived state before snapshotting. */
|
|
19649
19817
|
#rewindForCache() {
|
|
19650
19818
|
this.#rebind.cancel();
|
|
19651
|
-
this.#
|
|
19819
|
+
this.#frames.cancel();
|
|
19652
19820
|
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
19653
19821
|
this.#layout.disconnect();
|
|
19654
19822
|
this.#unbindFonts();
|
|
@@ -19671,8 +19839,8 @@ var ScrollRestoreController = class extends Controller {
|
|
|
19671
19839
|
key: { type: String, default: "" },
|
|
19672
19840
|
axis: { type: String, default: "vertical" }
|
|
19673
19841
|
};
|
|
19674
|
-
/**
|
|
19675
|
-
#
|
|
19842
|
+
/** Coalesces scroll bursts into one save per frame. */
|
|
19843
|
+
#frames = new FrameCoalescer();
|
|
19676
19844
|
/** Resolved storage key; empty disables persistence (no key and no id). */
|
|
19677
19845
|
#storageKey = "";
|
|
19678
19846
|
/** Last offset captured while the element was live; persisted as-is on teardown. */
|
|
@@ -19700,11 +19868,7 @@ var ScrollRestoreController = class extends Controller {
|
|
|
19700
19868
|
#connected = false;
|
|
19701
19869
|
#onScroll = () => {
|
|
19702
19870
|
this.#capture();
|
|
19703
|
-
|
|
19704
|
-
this.#rafId = requestAnimationFrame(() => {
|
|
19705
|
-
this.#rafId = null;
|
|
19706
|
-
this.#persist();
|
|
19707
|
-
});
|
|
19871
|
+
this.#frames.schedule(() => this.#persist());
|
|
19708
19872
|
};
|
|
19709
19873
|
connect() {
|
|
19710
19874
|
this.#connected = true;
|
|
@@ -19715,7 +19879,7 @@ var ScrollRestoreController = class extends Controller {
|
|
|
19715
19879
|
disconnect() {
|
|
19716
19880
|
this.#connected = false;
|
|
19717
19881
|
this.element.removeEventListener("scroll", this.#onScroll);
|
|
19718
|
-
this.#
|
|
19882
|
+
this.#frames.cancel();
|
|
19719
19883
|
this.#persist();
|
|
19720
19884
|
}
|
|
19721
19885
|
/** Re-derives the namespace when application code or a Turbo morph moves `key`. */
|
|
@@ -19729,7 +19893,7 @@ var ScrollRestoreController = class extends Controller {
|
|
|
19729
19893
|
/** Rebuilds the persistence state around the Values as they now read. */
|
|
19730
19894
|
#resync() {
|
|
19731
19895
|
if (!this.#connected) return;
|
|
19732
|
-
this.#
|
|
19896
|
+
this.#frames.cancel();
|
|
19733
19897
|
this.#storageKey = this.#resolveKey();
|
|
19734
19898
|
this.#echoTop = null;
|
|
19735
19899
|
this.#echoLeft = null;
|
|
@@ -19738,13 +19902,6 @@ var ScrollRestoreController = class extends Controller {
|
|
|
19738
19902
|
this.#capturedLeft = false;
|
|
19739
19903
|
if (this.#storageKey) this.#restore();
|
|
19740
19904
|
}
|
|
19741
|
-
/** Drops the pending coalesced save, if one is queued. */
|
|
19742
|
-
#cancelFrame() {
|
|
19743
|
-
if (this.#rafId !== null) {
|
|
19744
|
-
cancelAnimationFrame(this.#rafId);
|
|
19745
|
-
this.#rafId = null;
|
|
19746
|
-
}
|
|
19747
|
-
}
|
|
19748
19905
|
/**
|
|
19749
19906
|
* Records the live scroll offset for the configured axis.
|
|
19750
19907
|
*
|
|
@@ -19841,6 +19998,20 @@ var ScrollRestoreController = class extends Controller {
|
|
|
19841
19998
|
return this.axisValue === "horizontal" || this.axisValue === "both";
|
|
19842
19999
|
}
|
|
19843
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
|
|
19844
20015
|
var DEFAULT_OFFSET = 400;
|
|
19845
20016
|
var ScrollVisibilityController = class extends Controller {
|
|
19846
20017
|
static targets = ["element"];
|
|
@@ -19852,8 +20023,8 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
19852
20023
|
};
|
|
19853
20024
|
static actions = ["toTop"];
|
|
19854
20025
|
static events = ["change"];
|
|
19855
|
-
/**
|
|
19856
|
-
#
|
|
20026
|
+
/** Coalesces scroll bursts into one measurement per frame. */
|
|
20027
|
+
#frames = new FrameCoalescer();
|
|
19857
20028
|
/** Previous scroll position, for `direction` mode delta detection. */
|
|
19858
20029
|
#lastScrollY = 0;
|
|
19859
20030
|
/** Current visibility, tracked to dispatch `change` only on real transitions. */
|
|
@@ -19889,16 +20060,10 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
19889
20060
|
#pendingHide = new BlurDeferral(() => {
|
|
19890
20061
|
if (this.#connected) this.#evaluate();
|
|
19891
20062
|
});
|
|
19892
|
-
#onScroll = () =>
|
|
19893
|
-
if (this.#rafId !== null) return;
|
|
19894
|
-
this.#rafId = requestAnimationFrame(() => {
|
|
19895
|
-
this.#rafId = null;
|
|
19896
|
-
this.#evaluate();
|
|
19897
|
-
});
|
|
19898
|
-
};
|
|
20063
|
+
#onScroll = () => this.#frames.schedule(() => this.#evaluate());
|
|
19899
20064
|
connect() {
|
|
19900
|
-
this.#scrollSource = this.#
|
|
19901
|
-
this.#lastScrollY = this.#
|
|
20065
|
+
this.#scrollSource = resolveScrollSource(this.#rootSelector);
|
|
20066
|
+
this.#lastScrollY = scrollOffset(this.#scrollSource);
|
|
19902
20067
|
this.#scrollSource.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
19903
20068
|
this.#evaluate(false);
|
|
19904
20069
|
this.#connected = true;
|
|
@@ -19906,10 +20071,7 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
19906
20071
|
disconnect() {
|
|
19907
20072
|
this.#connected = false;
|
|
19908
20073
|
this.#scrollSource.removeEventListener("scroll", this.#onScroll);
|
|
19909
|
-
|
|
19910
|
-
cancelAnimationFrame(this.#rafId);
|
|
19911
|
-
this.#rafId = null;
|
|
19912
|
-
}
|
|
20074
|
+
this.#frames.cancel();
|
|
19913
20075
|
this.#pendingHide.releaseAll();
|
|
19914
20076
|
this.#tabindex.returnAll();
|
|
19915
20077
|
this.#visible = null;
|
|
@@ -19941,11 +20103,11 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
19941
20103
|
}
|
|
19942
20104
|
/** Validates `root` once so connect never parses a selector that throws. */
|
|
19943
20105
|
rootValueChanged() {
|
|
19944
|
-
this.#rootSelector =
|
|
20106
|
+
this.#rootSelector = validSelector(this.element, this.rootValue, "");
|
|
19945
20107
|
}
|
|
19946
20108
|
/** Validates `focusSelector` once so `toTop` never parses a selector that throws. */
|
|
19947
20109
|
focusSelectorValueChanged() {
|
|
19948
|
-
this.#focusSelector =
|
|
20110
|
+
this.#focusSelector = validSelector(this.element, this.focusSelectorValue, "");
|
|
19949
20111
|
}
|
|
19950
20112
|
/** Scrolls the source to the top and, optionally, moves focus to a safe target. */
|
|
19951
20113
|
toTop() {
|
|
@@ -19968,7 +20130,7 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
19968
20130
|
* @stimeoRenderRoot
|
|
19969
20131
|
*/
|
|
19970
20132
|
#evaluate(notify = true) {
|
|
19971
|
-
const y = this.#
|
|
20133
|
+
const y = scrollOffset(this.#scrollSource);
|
|
19972
20134
|
let nextVisible;
|
|
19973
20135
|
if (this.modeValue === "direction") {
|
|
19974
20136
|
if (y <= this.#offset) {
|
|
@@ -19998,13 +20160,6 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
19998
20160
|
if (notify) this.dispatch("change", { detail: { visible: next } });
|
|
19999
20161
|
}
|
|
20000
20162
|
/** Resolves the scroll source from `root` (falling back to the window). */
|
|
20001
|
-
#resolveScrollSource() {
|
|
20002
|
-
if (this.#rootSelector) {
|
|
20003
|
-
const root = document.querySelector(this.#rootSelector);
|
|
20004
|
-
if (root) return root;
|
|
20005
|
-
}
|
|
20006
|
-
return window;
|
|
20007
|
-
}
|
|
20008
20163
|
/**
|
|
20009
20164
|
* The focus owner inside the target, or `null` when focus is elsewhere.
|
|
20010
20165
|
*
|
|
@@ -20017,23 +20172,6 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
20017
20172
|
if (focused instanceof HTMLElement && this.elementTarget.contains(focused)) return focused;
|
|
20018
20173
|
return null;
|
|
20019
20174
|
}
|
|
20020
|
-
/** Returns `declared` when it parses as a selector, and `""` when it does not. */
|
|
20021
|
-
#validSelector(declared) {
|
|
20022
|
-
if (declared.length > 0) {
|
|
20023
|
-
try {
|
|
20024
|
-
this.element.matches(declared);
|
|
20025
|
-
return declared;
|
|
20026
|
-
} catch {
|
|
20027
|
-
}
|
|
20028
|
-
}
|
|
20029
|
-
return "";
|
|
20030
|
-
}
|
|
20031
|
-
#scrollY() {
|
|
20032
|
-
if (this.#scrollSource === window) {
|
|
20033
|
-
return window.scrollY ?? window.pageYOffset ?? 0;
|
|
20034
|
-
}
|
|
20035
|
-
return this.#scrollSource.scrollTop;
|
|
20036
|
-
}
|
|
20037
20175
|
};
|
|
20038
20176
|
var ANCHOR_ATTRIBUTES = ["href", "data-href"];
|
|
20039
20177
|
var ScrollspyController = class extends Controller {
|
|
@@ -20080,8 +20218,10 @@ var ScrollspyController = class extends Controller {
|
|
|
20080
20218
|
* rather than from whatever the selector resolves to now.
|
|
20081
20219
|
*/
|
|
20082
20220
|
#scrollSource = null;
|
|
20083
|
-
/**
|
|
20084
|
-
#
|
|
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 = "";
|
|
20085
20225
|
/** Watches the link targets' anchor attributes for an in-place morph rewrite. */
|
|
20086
20226
|
#anchorObserver = null;
|
|
20087
20227
|
/** True while a coalesced observation rebuild is queued; see {@link #scheduleResync}. */
|
|
@@ -20098,10 +20238,7 @@ var ScrollspyController = class extends Controller {
|
|
|
20098
20238
|
this.#anchorObserver?.disconnect();
|
|
20099
20239
|
this.#anchorObserver = null;
|
|
20100
20240
|
this.#detachScrollListener();
|
|
20101
|
-
|
|
20102
|
-
cancelAnimationFrame(this.#frame);
|
|
20103
|
-
this.#frame = null;
|
|
20104
|
-
}
|
|
20241
|
+
this.#frames.cancel();
|
|
20105
20242
|
this.#intersectionStates.clear();
|
|
20106
20243
|
this.#activeSectionId = "";
|
|
20107
20244
|
this.#rootElement = null;
|
|
@@ -20118,6 +20255,7 @@ var ScrollspyController = class extends Controller {
|
|
|
20118
20255
|
this.#initializeObserver();
|
|
20119
20256
|
}
|
|
20120
20257
|
rootSelectorValueChanged() {
|
|
20258
|
+
this.#rootSelector = validSelector(this.element, this.rootSelectorValue, "");
|
|
20121
20259
|
if (!this.#isConnected) return;
|
|
20122
20260
|
this.#initializeObserver();
|
|
20123
20261
|
}
|
|
@@ -20161,7 +20299,7 @@ var ScrollspyController = class extends Controller {
|
|
|
20161
20299
|
* only rewrites attributes, so Stimulus fires no target callback: without
|
|
20162
20300
|
* this, a link re-pointed from `#intro` to `#faq` would keep the controller
|
|
20163
20301
|
* observing `#intro` for the rest of the page's life. The filter is exactly
|
|
20164
|
-
*
|
|
20302
|
+
* `href` and `data-href`; guarding on `MutationObserver` keeps the
|
|
20165
20303
|
* controller usable where the API is absent, matching `IntersectionWatcher`'s
|
|
20166
20304
|
* own support guard.
|
|
20167
20305
|
*/
|
|
@@ -20211,10 +20349,10 @@ var ScrollspyController = class extends Controller {
|
|
|
20211
20349
|
const offset = this.#offset;
|
|
20212
20350
|
if (rootElement) {
|
|
20213
20351
|
const containerRect = rootElement.getBoundingClientRect();
|
|
20214
|
-
const scrollPosition = rootElement
|
|
20352
|
+
const scrollPosition = scrollOffset(rootElement) + (targetRect.top - containerRect.top) - offset;
|
|
20215
20353
|
rootElement.scrollTo({ top: scrollPosition, behavior });
|
|
20216
20354
|
} else {
|
|
20217
|
-
const scrollPosition = window
|
|
20355
|
+
const scrollPosition = scrollOffset(window) + targetRect.top - offset;
|
|
20218
20356
|
window.scrollTo({ top: scrollPosition, behavior });
|
|
20219
20357
|
}
|
|
20220
20358
|
if (this.focusSectionValue) this.#focusSection(targetElement);
|
|
@@ -20249,19 +20387,14 @@ var ScrollspyController = class extends Controller {
|
|
|
20249
20387
|
/**
|
|
20250
20388
|
* Resolves `rootSelector` to a scrollable element.
|
|
20251
20389
|
*
|
|
20252
|
-
* @returns The container, or `null` meaning "spy the viewport" when the
|
|
20253
|
-
* is empty, matches nothing, matches a non-HTML element
|
|
20254
|
-
*
|
|
20255
|
-
*
|
|
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.
|
|
20256
20395
|
*/
|
|
20257
20396
|
#queryRootElement() {
|
|
20258
|
-
|
|
20259
|
-
try {
|
|
20260
|
-
const root = document.querySelector(this.rootSelectorValue);
|
|
20261
|
-
return root instanceof HTMLElement ? root : null;
|
|
20262
|
-
} catch {
|
|
20263
|
-
return null;
|
|
20264
|
-
}
|
|
20397
|
+
return resolveScrollContainer(this.#rootSelector);
|
|
20265
20398
|
}
|
|
20266
20399
|
/**
|
|
20267
20400
|
* Re-evaluates once per frame while the reader scrolls.
|
|
@@ -20276,13 +20409,7 @@ var ScrollspyController = class extends Controller {
|
|
|
20276
20409
|
* end in {@link #evaluateActiveSection} — which measures section rects and the
|
|
20277
20410
|
* root's top edge at that instant — they converge on the same answer.
|
|
20278
20411
|
*/
|
|
20279
|
-
#onScroll = () =>
|
|
20280
|
-
if (this.#frame !== null) return;
|
|
20281
|
-
this.#frame = requestAnimationFrame(() => {
|
|
20282
|
-
this.#frame = null;
|
|
20283
|
-
this.#evaluateActiveSection();
|
|
20284
|
-
});
|
|
20285
|
-
};
|
|
20412
|
+
#onScroll = () => this.#frames.schedule(() => this.#evaluateActiveSection());
|
|
20286
20413
|
/**
|
|
20287
20414
|
* Points the `scroll` listener at whatever the reader actually scrolls: the
|
|
20288
20415
|
* resolved root, or the window when the viewport is spied.
|
|
@@ -21180,7 +21307,8 @@ var SmartStickyHeaderController = class extends Controller {
|
|
|
21180
21307
|
};
|
|
21181
21308
|
static events = ["change"];
|
|
21182
21309
|
#connected = false;
|
|
21183
|
-
|
|
21310
|
+
/** Coalesces scroll bursts into one measurement per frame. */
|
|
21311
|
+
#frames = new FrameCoalescer();
|
|
21184
21312
|
/** The scroll source resolved at connect — disconnect must unbind the SAME node. */
|
|
21185
21313
|
#scrollerEl = window;
|
|
21186
21314
|
/** The validated `containerSelector`, or `""` when the declaration cannot be parsed. */
|
|
@@ -21197,13 +21325,7 @@ var SmartStickyHeaderController = class extends Controller {
|
|
|
21197
21325
|
get #offset() {
|
|
21198
21326
|
return Number.isFinite(this.offsetValue) ? this.offsetValue : DEFAULT_OFFSET2;
|
|
21199
21327
|
}
|
|
21200
|
-
#onScroll = () =>
|
|
21201
|
-
if (this.#frame !== null) return;
|
|
21202
|
-
this.#frame = requestAnimationFrame(() => {
|
|
21203
|
-
this.#frame = null;
|
|
21204
|
-
this.#measure();
|
|
21205
|
-
});
|
|
21206
|
-
};
|
|
21328
|
+
#onScroll = () => this.#frames.schedule(() => this.#measure());
|
|
21207
21329
|
/**
|
|
21208
21330
|
* Focus inside a hidden header must reveal it (WCAG 2.4.7 / 2.4.11); the
|
|
21209
21331
|
* hold while focus *stays* inside is the `#apply` hide invariant.
|
|
@@ -21211,7 +21333,7 @@ var SmartStickyHeaderController = class extends Controller {
|
|
|
21211
21333
|
#onFocusin = () => this.#apply(false);
|
|
21212
21334
|
/** Validates `containerSelector` once so connect never parses a selector that throws. */
|
|
21213
21335
|
containerSelectorValueChanged() {
|
|
21214
|
-
this.#containerSelector =
|
|
21336
|
+
this.#containerSelector = validSelector(this.element, this.containerSelectorValue, "");
|
|
21215
21337
|
}
|
|
21216
21338
|
/** Re-decides when application code (or a Turbo morph) changes `offset` at runtime. */
|
|
21217
21339
|
offsetValueChanged() {
|
|
@@ -21219,8 +21341,8 @@ var SmartStickyHeaderController = class extends Controller {
|
|
|
21219
21341
|
}
|
|
21220
21342
|
connect() {
|
|
21221
21343
|
this.#hidden = null;
|
|
21222
|
-
this.#scrollerEl = this.#
|
|
21223
|
-
this.#lastY = this.#
|
|
21344
|
+
this.#scrollerEl = resolveScrollSource(this.#containerSelector);
|
|
21345
|
+
this.#lastY = scrollOffset(this.#scrollerEl);
|
|
21224
21346
|
this.#scrollerEl.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
21225
21347
|
this.element.addEventListener("focusin", this.#onFocusin);
|
|
21226
21348
|
this.#apply(false, false);
|
|
@@ -21230,34 +21352,10 @@ var SmartStickyHeaderController = class extends Controller {
|
|
|
21230
21352
|
this.#connected = false;
|
|
21231
21353
|
this.#scrollerEl.removeEventListener("scroll", this.#onScroll);
|
|
21232
21354
|
this.element.removeEventListener("focusin", this.#onFocusin);
|
|
21233
|
-
|
|
21234
|
-
this.#frame = null;
|
|
21235
|
-
}
|
|
21236
|
-
/** Resolves the scroll source: the `containerSelector` match, else the window. */
|
|
21237
|
-
#resolveScroller() {
|
|
21238
|
-
if (this.#containerSelector) {
|
|
21239
|
-
const container = document.querySelector(this.#containerSelector);
|
|
21240
|
-
if (container) return container;
|
|
21241
|
-
}
|
|
21242
|
-
return window;
|
|
21243
|
-
}
|
|
21244
|
-
/** Returns `declared` when it parses as a selector, and `""` when it does not. */
|
|
21245
|
-
#validSelector(declared) {
|
|
21246
|
-
if (declared.length > 0) {
|
|
21247
|
-
try {
|
|
21248
|
-
this.element.matches(declared);
|
|
21249
|
-
return declared;
|
|
21250
|
-
} catch {
|
|
21251
|
-
}
|
|
21252
|
-
}
|
|
21253
|
-
return "";
|
|
21254
|
-
}
|
|
21255
|
-
get #scrollY() {
|
|
21256
|
-
const scroller = this.#scrollerEl;
|
|
21257
|
-
return scroller === window ? window.scrollY : scroller.scrollTop;
|
|
21355
|
+
this.#frames.cancel();
|
|
21258
21356
|
}
|
|
21259
21357
|
#measure() {
|
|
21260
|
-
const y = this.#
|
|
21358
|
+
const y = scrollOffset(this.#scrollerEl);
|
|
21261
21359
|
if (y <= this.#offset) {
|
|
21262
21360
|
this.#lastY = y;
|
|
21263
21361
|
this.#apply(false);
|
|
@@ -22228,6 +22326,7 @@ var SubmitOnceController = class extends Controller {
|
|
|
22228
22326
|
static events = ["start", "end", "reconcile"];
|
|
22229
22327
|
#timers = new SafeTimeout();
|
|
22230
22328
|
#gate = new DetachGate();
|
|
22329
|
+
#listeners = new ListenerSet();
|
|
22231
22330
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
22232
22331
|
#sessions = /* @__PURE__ */ new Map();
|
|
22233
22332
|
#disabled = new AttributeLease("disabled");
|
|
@@ -22252,14 +22351,12 @@ var SubmitOnceController = class extends Controller {
|
|
|
22252
22351
|
connect() {
|
|
22253
22352
|
this.#gate.cancel();
|
|
22254
22353
|
this.#beforeCache.activate();
|
|
22255
|
-
this.element
|
|
22256
|
-
this.element
|
|
22257
|
-
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);
|
|
22258
22357
|
}
|
|
22259
22358
|
disconnect() {
|
|
22260
|
-
this.
|
|
22261
|
-
this.element.removeEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
22262
|
-
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
22359
|
+
this.#listeners.dispose();
|
|
22263
22360
|
this.#beforeCache.deactivate();
|
|
22264
22361
|
this.#gate.disconnected(this, () => this.#teardown());
|
|
22265
22362
|
}
|
|
@@ -23199,16 +23296,7 @@ var ThemeController = class extends Controller {
|
|
|
23199
23296
|
}
|
|
23200
23297
|
/** Validates the `target` declaration once, so the render path never parses. */
|
|
23201
23298
|
targetValueChanged() {
|
|
23202
|
-
|
|
23203
|
-
if (selector.length > 0) {
|
|
23204
|
-
try {
|
|
23205
|
-
this.element.matches(selector);
|
|
23206
|
-
this.#targetSelector = selector;
|
|
23207
|
-
return;
|
|
23208
|
-
} catch {
|
|
23209
|
-
}
|
|
23210
|
-
}
|
|
23211
|
-
this.#targetSelector = DEFAULT_TARGET;
|
|
23299
|
+
this.#targetSelector = validSelector(this.element, this.targetValue, DEFAULT_TARGET);
|
|
23212
23300
|
}
|
|
23213
23301
|
/** Re-derives the single Tab stop and ARIA for an option set that changed. */
|
|
23214
23302
|
optionTargetConnected() {
|
|
@@ -23221,9 +23309,8 @@ var ThemeController = class extends Controller {
|
|
|
23221
23309
|
/**
|
|
23222
23310
|
* Selects the mode the activated option declares.
|
|
23223
23311
|
*
|
|
23224
|
-
* Read through
|
|
23225
|
-
*
|
|
23226
|
-
* declares.
|
|
23312
|
+
* Read through the same lane that decides which option is checked, so the two
|
|
23313
|
+
* can never disagree about what an option declares.
|
|
23227
23314
|
*/
|
|
23228
23315
|
set(event) {
|
|
23229
23316
|
const option = event.currentTarget;
|
|
@@ -23696,30 +23783,23 @@ var ToastController = class extends Controller {
|
|
|
23696
23783
|
};
|
|
23697
23784
|
static actions = ["dismiss", "onKeydown", "pause", "resume", "show"];
|
|
23698
23785
|
static events = ["dismiss", "show"];
|
|
23699
|
-
/**
|
|
23700
|
-
* Registry for every auto-dismiss and transition-finalize timer the controller
|
|
23701
|
-
* schedules. `SafeTimeout` owns *registration and teardown only*; the
|
|
23702
|
-
* pause/resume remaining-time accounting stays in `#activeTimeouts` so the
|
|
23703
|
-
* per-widget WCAG 2.2.1 semantics are not flattened into the helper.
|
|
23704
|
-
*/
|
|
23786
|
+
/** Removal timers for the leaving transition; the auto-dismiss ones live below. */
|
|
23705
23787
|
#timers = new SafeTimeout();
|
|
23788
|
+
/** Per-toast auto-dismiss, held open while the toast is hovered or focused. */
|
|
23789
|
+
#dismiss = new PausableTimers();
|
|
23706
23790
|
/**
|
|
23707
23791
|
* Pending one-shot `requestAnimationFrame` handles (the entering→visible flip).
|
|
23708
23792
|
* Tracked so {@link disconnect} can cancel any that have not fired, preventing a
|
|
23709
23793
|
* detached element from being mutated after it leaves the DOM (Turbo).
|
|
23710
23794
|
*/
|
|
23711
23795
|
#rafHandles = /* @__PURE__ */ new Map();
|
|
23712
|
-
/** Track active timeouts mapped by each toast element for safe cancellation. */
|
|
23713
|
-
#activeTimeouts = /* @__PURE__ */ new Map();
|
|
23714
|
-
/** Track active pause reasons (hover/focus) per toast for WCAG 2.2.1 pause/resume. */
|
|
23715
|
-
#pauseReasons = /* @__PURE__ */ new Map();
|
|
23716
23796
|
/** The stable list that owns delegated listeners for dynamically added items. */
|
|
23717
23797
|
#delegatedList = null;
|
|
23718
23798
|
connect() {
|
|
23719
23799
|
this.#connectDelegatedEvents();
|
|
23720
23800
|
this.enforceMaxLimit();
|
|
23721
23801
|
for (const item of this.itemTargets) {
|
|
23722
|
-
if (!this.#
|
|
23802
|
+
if (!this.#dismiss.tracks(item) && item.dataset.state !== "leaving") {
|
|
23723
23803
|
this.#startTimer(item);
|
|
23724
23804
|
}
|
|
23725
23805
|
}
|
|
@@ -23727,12 +23807,11 @@ var ToastController = class extends Controller {
|
|
|
23727
23807
|
disconnect() {
|
|
23728
23808
|
this.#disconnectDelegatedEvents();
|
|
23729
23809
|
this.#timers.clearAll();
|
|
23810
|
+
this.#dismiss.clearAll();
|
|
23730
23811
|
for (const handle of this.#rafHandles.values()) {
|
|
23731
23812
|
window.cancelAnimationFrame(handle);
|
|
23732
23813
|
}
|
|
23733
23814
|
this.#rafHandles.clear();
|
|
23734
|
-
this.#activeTimeouts.clear();
|
|
23735
|
-
this.#pauseReasons.clear();
|
|
23736
23815
|
}
|
|
23737
23816
|
/** Rebinds delegated interaction when Turbo replaces the list target in place. */
|
|
23738
23817
|
listTargetConnected(element) {
|
|
@@ -23745,18 +23824,9 @@ var ToastController = class extends Controller {
|
|
|
23745
23824
|
durationValueChanged() {
|
|
23746
23825
|
for (const item of this.itemTargets) {
|
|
23747
23826
|
if (item.dataset.state === "leaving") continue;
|
|
23748
|
-
const pauseReasons = this.#pauseReasons.get(item);
|
|
23749
|
-
this.#clearTimer(item);
|
|
23750
23827
|
if (this.durationValue <= 0) {
|
|
23828
|
+
this.#dismiss.clear(item);
|
|
23751
23829
|
item.removeAttribute("data-paused");
|
|
23752
|
-
} else if (pauseReasons && pauseReasons.size > 0) {
|
|
23753
|
-
this.#pauseReasons.set(item, pauseReasons);
|
|
23754
|
-
this.#activeTimeouts.set(item, {
|
|
23755
|
-
id: 0,
|
|
23756
|
-
startedAt: 0,
|
|
23757
|
-
remaining: this.durationValue
|
|
23758
|
-
});
|
|
23759
|
-
item.setAttribute("data-paused", "true");
|
|
23760
23830
|
} else {
|
|
23761
23831
|
this.#startTimer(item);
|
|
23762
23832
|
}
|
|
@@ -23785,7 +23855,7 @@ var ToastController = class extends Controller {
|
|
|
23785
23855
|
}
|
|
23786
23856
|
/** Clears any active timer when a toast is removed from the DOM. */
|
|
23787
23857
|
itemTargetDisconnected(element) {
|
|
23788
|
-
this.#
|
|
23858
|
+
this.#dismiss.clear(element);
|
|
23789
23859
|
this.#cancelAnimation(element);
|
|
23790
23860
|
}
|
|
23791
23861
|
/**
|
|
@@ -23857,35 +23927,16 @@ var ToastController = class extends Controller {
|
|
|
23857
23927
|
*/
|
|
23858
23928
|
pause(event) {
|
|
23859
23929
|
const item = this.#itemFromEvent(event);
|
|
23860
|
-
if (
|
|
23861
|
-
|
|
23862
|
-
if (!timeout) return;
|
|
23863
|
-
const reasons = this.#pauseReasonsFor(item);
|
|
23864
|
-
const wasActive = reasons.size > 0;
|
|
23865
|
-
reasons.add(this.#pauseReason(event));
|
|
23866
|
-
if (wasActive || timeout.id === 0) return;
|
|
23867
|
-
this.#timers.clear(timeout.id);
|
|
23868
|
-
const elapsed = Date.now() - timeout.startedAt;
|
|
23869
|
-
const remaining = Math.max(0, timeout.remaining - elapsed);
|
|
23870
|
-
if (remaining <= 0) {
|
|
23871
|
-
this.#removeWithTransition(item, "timeout");
|
|
23872
|
-
return;
|
|
23930
|
+
if (item && this.#dismiss.pause(item, this.#pauseReason(event))) {
|
|
23931
|
+
item.setAttribute("data-paused", "true");
|
|
23873
23932
|
}
|
|
23874
|
-
this.#activeTimeouts.set(item, { id: 0, startedAt: 0, remaining });
|
|
23875
|
-
item.setAttribute("data-paused", "true");
|
|
23876
23933
|
}
|
|
23877
23934
|
/** Resumes the auto-dismiss timer once both hover and focus have been released. */
|
|
23878
23935
|
resume(event) {
|
|
23879
23936
|
const item = this.#itemFromEvent(event);
|
|
23880
|
-
if (
|
|
23881
|
-
|
|
23882
|
-
|
|
23883
|
-
if (reasons.size > 0) return;
|
|
23884
|
-
const timeout = this.#activeTimeouts.get(item);
|
|
23885
|
-
if (!timeout) return;
|
|
23886
|
-
if (timeout.id !== 0 || timeout.remaining <= 0) return;
|
|
23887
|
-
item.removeAttribute("data-paused");
|
|
23888
|
-
this.#startTimer(item, timeout.remaining);
|
|
23937
|
+
if (item && this.#dismiss.resume(item, this.#pauseReason(event))) {
|
|
23938
|
+
item.removeAttribute("data-paused");
|
|
23939
|
+
}
|
|
23889
23940
|
}
|
|
23890
23941
|
/** Resolves the toast item element a pause/resume event targets. */
|
|
23891
23942
|
#itemFromEvent(event) {
|
|
@@ -23898,36 +23949,20 @@ var ToastController = class extends Controller {
|
|
|
23898
23949
|
#pauseReason(event) {
|
|
23899
23950
|
return event.type === "focusin" || event.type === "focusout" ? "focus" : "hover";
|
|
23900
23951
|
}
|
|
23901
|
-
/**
|
|
23902
|
-
#
|
|
23903
|
-
|
|
23904
|
-
if (!reasons) {
|
|
23905
|
-
reasons = /* @__PURE__ */ new Set();
|
|
23906
|
-
this.#pauseReasons.set(item, reasons);
|
|
23907
|
-
}
|
|
23908
|
-
return reasons;
|
|
23909
|
-
}
|
|
23910
|
-
#startTimer(element, duration = this.durationValue) {
|
|
23911
|
-
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) {
|
|
23912
23955
|
return;
|
|
23913
23956
|
}
|
|
23914
|
-
this.#
|
|
23915
|
-
|
|
23916
|
-
this.#removeWithTransition(element, "timeout")
|
|
23917
|
-
|
|
23918
|
-
|
|
23919
|
-
}
|
|
23920
|
-
#clearTimer(element) {
|
|
23921
|
-
const timeout = this.#activeTimeouts.get(element);
|
|
23922
|
-
if (timeout) {
|
|
23923
|
-
if (timeout.id) this.#timers.clear(timeout.id);
|
|
23924
|
-
this.#activeTimeouts.delete(element);
|
|
23925
|
-
}
|
|
23926
|
-
this.#pauseReasons.delete(element);
|
|
23957
|
+
this.#dismiss.set(
|
|
23958
|
+
element,
|
|
23959
|
+
() => this.#removeWithTransition(element, "timeout"),
|
|
23960
|
+
this.durationValue
|
|
23961
|
+
);
|
|
23927
23962
|
}
|
|
23928
23963
|
#removeWithTransition(element, reason) {
|
|
23929
23964
|
if (element.dataset.state === "leaving" || element.parentNode !== this.listTarget) return;
|
|
23930
|
-
this.#
|
|
23965
|
+
this.#dismiss.clear(element);
|
|
23931
23966
|
this.#cancelAnimation(element);
|
|
23932
23967
|
element.setAttribute("data-state", "leaving");
|
|
23933
23968
|
const finalize = () => {
|
|
@@ -24280,9 +24315,8 @@ var ToggleGroupController = class extends Controller {
|
|
|
24280
24315
|
}
|
|
24281
24316
|
/** Finds this group's item containing an event target, excluding nested groups. */
|
|
24282
24317
|
#itemForEventTarget(target) {
|
|
24283
|
-
if (!this.#ownsEventTarget(target)) return
|
|
24284
|
-
|
|
24285
|
-
return this.itemTargets.find((item) => item === node || item.contains(node));
|
|
24318
|
+
if (!this.#ownsEventTarget(target)) return null;
|
|
24319
|
+
return ownerOf(this.itemTargets, target);
|
|
24286
24320
|
}
|
|
24287
24321
|
/** Whether the closest Toggle Group scope around a target is this instance. */
|
|
24288
24322
|
#ownsEventTarget(target) {
|
|
@@ -24540,9 +24574,7 @@ var ToolbarController = class extends Controller {
|
|
|
24540
24574
|
}
|
|
24541
24575
|
/** Index in `controlTargets` of the control owning `target` (it or a descendant). */
|
|
24542
24576
|
#indexOf(target) {
|
|
24543
|
-
|
|
24544
|
-
if (!node) return -1;
|
|
24545
|
-
return this.controlTargets.findIndex((control) => control === node || control.contains(node));
|
|
24577
|
+
return ownerIndex(this.controlTargets, target);
|
|
24546
24578
|
}
|
|
24547
24579
|
/** Controls eligible for the roving tab stop (excludes native disabled / hidden). */
|
|
24548
24580
|
get #navigableControls() {
|