stimeo-ui 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +212 -0
- data/README.md +120 -0
- data/dist/cable/index.js +123 -29
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +96 -62
- data/dist/controllers/auto_submit_controller.js +83 -7
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +38 -11
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +340 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +81 -12
- data/dist/controllers/clipboard_controller.js +63 -5
- data/dist/controllers/collapsible_controller.js +99 -14
- data/dist/controllers/color_picker_controller.js +80 -34
- data/dist/controllers/combobox_controller.js +106 -16
- data/dist/controllers/command_palette_controller.js +35 -3
- data/dist/controllers/conditional_fields_controller.js +85 -17
- data/dist/controllers/confirm_controller.js +3 -0
- data/dist/controllers/context_menu_controller.js +32 -12
- data/dist/controllers/countdown_controller.js +129 -26
- data/dist/controllers/currency_input_controller.js +221 -67
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +151 -30
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +46 -13
- data/dist/controllers/dismissible_controller.js +1 -0
- data/dist/controllers/drawer_controller.js +54 -19
- data/dist/controllers/dropdown_controller.js +36 -9
- data/dist/controllers/editable_controller.js +34 -0
- data/dist/controllers/file_dropzone_controller.js +144 -51
- data/dist/controllers/filter_controller.js +20 -6
- data/dist/controllers/flash_controller.js +432 -71
- data/dist/controllers/focus_controller.js +1 -0
- data/dist/controllers/form_field_controller.js +7 -5
- data/dist/controllers/form_validation_controller.js +19 -13
- data/dist/controllers/frame_loading_controller.js +45 -8
- data/dist/controllers/highlight_controller.js +82 -25
- data/dist/controllers/hover_card_controller.js +40 -14
- data/dist/controllers/idle_controller.js +90 -5
- data/dist/controllers/input_mask_controller.js +65 -9
- data/dist/controllers/intersection_controller.js +3 -0
- data/dist/controllers/lazy_frame_controller.js +11 -2
- data/dist/controllers/listbox_controller.js +203 -45
- data/dist/controllers/local_time_controller.js +10 -5
- data/dist/controllers/masonry_controller.js +31 -15
- data/dist/controllers/menu_controller.js +45 -16
- data/dist/controllers/menubar_controller.js +58 -24
- data/dist/controllers/meter_controller.js +9 -5
- data/dist/controllers/multi_select_controller.js +278 -104
- data/dist/controllers/navigation_menu_controller.js +48 -15
- data/dist/controllers/nested_form_controller.js +37 -8
- data/dist/controllers/network_status_controller.js +9 -1
- data/dist/controllers/number_input_controller.js +124 -21
- data/dist/controllers/optimistic_controller.js +42 -5
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +115 -21
- data/dist/controllers/overflow_menu_controller.js +141 -44
- data/dist/controllers/pagination_controller.js +74 -28
- data/dist/controllers/password_reveal_controller.js +59 -2
- data/dist/controllers/persist_controller.js +30 -8
- data/dist/controllers/pointer_drag_controller.js +131 -52
- data/dist/controllers/popover_controller.js +45 -11
- data/dist/controllers/portal_controller.js +6 -2
- data/dist/controllers/preview_guard_controller.js +16 -1
- data/dist/controllers/progress_controller.js +8 -4
- data/dist/controllers/radio_group_controller.js +42 -17
- data/dist/controllers/range_slider_controller.js +88 -42
- data/dist/controllers/rating_controller.js +39 -15
- data/dist/controllers/read_more_controller.js +100 -7
- data/dist/controllers/reading_progress_controller.js +65 -19
- data/dist/controllers/relative_time_controller.js +10 -5
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +75 -27
- data/dist/controllers/scroll_restore_controller.js +37 -16
- data/dist/controllers/scroll_visibility_controller.js +49 -30
- data/dist/controllers/scrollspy_controller.js +71 -26
- data/dist/controllers/separator_controller.js +66 -37
- data/dist/controllers/sidebar_controller.js +77 -18
- data/dist/controllers/skeleton_controller.js +6 -1
- data/dist/controllers/slider_controller.js +82 -47
- data/dist/controllers/smart_sticky_header_controller.js +60 -26
- data/dist/controllers/sortable_controller.js +17 -2
- data/dist/controllers/spinner_controller.js +10 -2
- data/dist/controllers/step_indicator_controller.js +18 -17
- data/dist/controllers/stepper_controller.js +101 -19
- data/dist/controllers/stick_to_bottom_controller.js +104 -8
- data/dist/controllers/submit_once_controller.js +45 -9
- data/dist/controllers/switch_controller.js +101 -10
- data/dist/controllers/tabs_controller.js +21 -2
- data/dist/controllers/tags_input_controller.js +209 -59
- data/dist/controllers/textarea_autosize_controller.js +29 -3
- data/dist/controllers/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +451 -105
- data/dist/controllers/toggle_group_controller.js +159 -23
- data/dist/controllers/toolbar_controller.js +32 -0
- data/dist/controllers/tooltip_controller.js +39 -13
- data/dist/controllers/transition_controller.js +4 -0
- data/dist/controllers/tree_view_controller.js +169 -16
- data/dist/index.js +5002 -1911
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
data/dist/cable/index.js
CHANGED
|
@@ -164,6 +164,18 @@ var SafeInterval = class extends TimerRegistry {
|
|
|
164
164
|
window.clearInterval(id);
|
|
165
165
|
}
|
|
166
166
|
};
|
|
167
|
+
|
|
168
|
+
// src/utils/transient_hooks.ts
|
|
169
|
+
var TransientHooks = class {
|
|
170
|
+
#attributes;
|
|
171
|
+
constructor(options) {
|
|
172
|
+
this.#attributes = options.attributes;
|
|
173
|
+
}
|
|
174
|
+
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
175
|
+
reset(element) {
|
|
176
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
167
179
|
function parseSubscriptionParams(raw) {
|
|
168
180
|
try {
|
|
169
181
|
const parsed = JSON.parse(raw);
|
|
@@ -270,6 +282,7 @@ function openSharedSubscription(consumer, channel) {
|
|
|
270
282
|
}
|
|
271
283
|
|
|
272
284
|
// src/cable/live_counter_controller.ts
|
|
285
|
+
var TRANSIENT = new TransientHooks({ attributes: ["data-live-counter-rejected"] });
|
|
273
286
|
var DISABLED_MARKER = "data-live-counter-disabled";
|
|
274
287
|
var LiveCounterController = class _LiveCounterController extends Controller {
|
|
275
288
|
static targets = ["value", "trigger"];
|
|
@@ -323,9 +336,12 @@ var LiveCounterController = class _LiveCounterController extends Controller {
|
|
|
323
336
|
* The rejected hook is transient server state: a Turbo cache snapshot must not
|
|
324
337
|
* resurrect it — the fresh subscription re-decides it. An outstanding guess
|
|
325
338
|
* belongs to the identifier that is going away, so it is dropped with it.
|
|
339
|
+
*
|
|
340
|
+
* @stimeoRuntimeOnly `channel` decides whether this call opens a subscription at all; the
|
|
341
|
+
* rejected hook it clears does not depend on it.
|
|
326
342
|
*/
|
|
327
343
|
#subscribe() {
|
|
328
|
-
this.element
|
|
344
|
+
TRANSIENT.reset(this.element);
|
|
329
345
|
this.#outstanding.length = 0;
|
|
330
346
|
if (this.channelValue) {
|
|
331
347
|
this.#subscription = this.#open();
|
|
@@ -340,6 +356,9 @@ var LiveCounterController = class _LiveCounterController extends Controller {
|
|
|
340
356
|
* half-wired: no gate reflected, and a trigger that looks live. The gate reads
|
|
341
357
|
* the declaration, so a null subscription keeps it shut and the display never
|
|
342
358
|
* moves past what the server can receive.
|
|
359
|
+
*
|
|
360
|
+
* @stimeoRuntimeOnly `channel` and its params choose the one subscription this call opens; `id`
|
|
361
|
+
* is read by the handlers it wires.
|
|
343
362
|
*/
|
|
344
363
|
#open() {
|
|
345
364
|
try {
|
|
@@ -388,7 +407,7 @@ var LiveCounterController = class _LiveCounterController extends Controller {
|
|
|
388
407
|
this.#focusedTriggers.releaseAll();
|
|
389
408
|
this.#subscription?.unsubscribe();
|
|
390
409
|
this.#subscription = null;
|
|
391
|
-
this.element
|
|
410
|
+
TRANSIENT.reset(this.element);
|
|
392
411
|
for (const trigger of this.triggerTargets) this.#release(trigger);
|
|
393
412
|
}
|
|
394
413
|
/** Late-added triggers (e.g. via a Turbo Stream) pick up the current gate. */
|
|
@@ -467,6 +486,8 @@ var LiveCounterController = class _LiveCounterController extends Controller {
|
|
|
467
486
|
* authored-disabled trigger ("disabled until valid", say) is never
|
|
468
487
|
* re-enabled by the gate. A marked disabled restored from a Turbo cache
|
|
469
488
|
* snapshot is recognized as ours and lifted once the gate opens.
|
|
489
|
+
*
|
|
490
|
+
* @stimeoRenderRoot
|
|
470
491
|
*/
|
|
471
492
|
#syncTrigger(trigger) {
|
|
472
493
|
if (this.#ready) {
|
|
@@ -510,6 +531,9 @@ var LiveCounterController = class _LiveCounterController extends Controller {
|
|
|
510
531
|
* Settles the display on a count the server stated, announcing what it settled
|
|
511
532
|
* on. Announcing the optimistic bump instead would read a guess out and then
|
|
512
533
|
* correct it, so the announcement rides this path only.
|
|
534
|
+
*
|
|
535
|
+
* @stimeoRuntimeOnly The count shown is the value received; `announceText` words the one
|
|
536
|
+
* announcement of this change.
|
|
513
537
|
*/
|
|
514
538
|
#reconcile(count) {
|
|
515
539
|
if (this.#write(count)) this.#announce(count);
|
|
@@ -530,6 +554,60 @@ var LiveCounterController = class _LiveCounterController extends Controller {
|
|
|
530
554
|
}, _LiveCounterController.#announceDelay);
|
|
531
555
|
}
|
|
532
556
|
};
|
|
557
|
+
|
|
558
|
+
// src/utils/keyed_timers.ts
|
|
559
|
+
var KeyedTimers = class {
|
|
560
|
+
#timers = new SafeTimeout();
|
|
561
|
+
/** The pending timer of each key; an entry lives exactly as long as its timer. */
|
|
562
|
+
#pending = /* @__PURE__ */ new Map();
|
|
563
|
+
/**
|
|
564
|
+
* Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had
|
|
565
|
+
* pending. The entry is dropped before the callback runs, so the callback sees
|
|
566
|
+
* the key unarmed and may arm it again for the next round.
|
|
567
|
+
*/
|
|
568
|
+
set(key, callback, delay) {
|
|
569
|
+
this.clear(key);
|
|
570
|
+
const id = this.#timers.set(() => {
|
|
571
|
+
this.#pending.delete(key);
|
|
572
|
+
callback();
|
|
573
|
+
}, delay);
|
|
574
|
+
this.#pending.set(key, id);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Cancels `key`'s pending timer, if it has one.
|
|
578
|
+
*
|
|
579
|
+
* A timer id is a positive integer, so `-1` stands for "nothing pending" and
|
|
580
|
+
* the registry ignores an id it does not own — the unarmed case needs no
|
|
581
|
+
* branch of its own, and no other key's timer can be reached from here.
|
|
582
|
+
*/
|
|
583
|
+
clear(key) {
|
|
584
|
+
this.#timers.clear(this.#pending.get(key) ?? -1);
|
|
585
|
+
this.#pending.delete(key);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Cancels every pending timer and forgets every key. Call this from a
|
|
589
|
+
* controller's `disconnect()` so no timer, and no entry, outlives the element.
|
|
590
|
+
*/
|
|
591
|
+
clearAll() {
|
|
592
|
+
this.#timers.clearAll();
|
|
593
|
+
this.#pending.clear();
|
|
594
|
+
}
|
|
595
|
+
/** Whether `key` has a timer pending. */
|
|
596
|
+
has(key) {
|
|
597
|
+
return this.#pending.has(key);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/utils/template_row.ts
|
|
602
|
+
function cloneTemplateRoot(template) {
|
|
603
|
+
const root = template.content.firstElementChild;
|
|
604
|
+
return root instanceof HTMLElement ? root.cloneNode(true) : null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/cable/presence_controller.ts
|
|
608
|
+
var TRANSIENT2 = new TransientHooks({
|
|
609
|
+
attributes: ["data-present", "data-present-count", "data-presence-rejected"]
|
|
610
|
+
});
|
|
533
611
|
var BEACON_THROTTLE_MS = 2e3;
|
|
534
612
|
var DEFAULT_HEARTBEAT_MS = 15e3;
|
|
535
613
|
var DEFAULT_TIMEOUT_MS = 4e4;
|
|
@@ -554,6 +632,8 @@ var PresenceController = class _PresenceController extends Controller {
|
|
|
554
632
|
#subscription = null;
|
|
555
633
|
/** Present peers keyed by id (insertion order = join order). */
|
|
556
634
|
#peers = /* @__PURE__ */ new Map();
|
|
635
|
+
/** Each peer's auto-expiry timer, restarted by every beacon from that peer. */
|
|
636
|
+
#expiry = new KeyedTimers();
|
|
557
637
|
#timers = new SafeTimeout();
|
|
558
638
|
#intervals = new SafeInterval();
|
|
559
639
|
/** Epoch ms of the last outgoing beacon, for the convergence throttle. */
|
|
@@ -622,7 +702,6 @@ var PresenceController = class _PresenceController extends Controller {
|
|
|
622
702
|
}
|
|
623
703
|
connect() {
|
|
624
704
|
this.#reset();
|
|
625
|
-
this.element.removeAttribute("data-presence-rejected");
|
|
626
705
|
if (this.hasCountTarget) this.countTarget.textContent = this.#countMessage(0);
|
|
627
706
|
if (!this.channelValue) return;
|
|
628
707
|
this.#claimSpeaker();
|
|
@@ -648,7 +727,6 @@ var PresenceController = class _PresenceController extends Controller {
|
|
|
648
727
|
this.#subscription = null;
|
|
649
728
|
this.#intervals.clearAll();
|
|
650
729
|
this.#reset();
|
|
651
|
-
this.element.removeAttribute("data-presence-rejected");
|
|
652
730
|
}
|
|
653
731
|
/**
|
|
654
732
|
* Registers this element as one of the voices for its peer, so a sibling on the
|
|
@@ -728,6 +806,9 @@ ${this.idValue}`;
|
|
|
728
806
|
* Tracks a broadcast beacon: upserts the peer (restarting its expiry timer),
|
|
729
807
|
* removes it on a `leaving` notice, and re-announces this client when the
|
|
730
808
|
* peer was unknown (roster convergence — see the class doc).
|
|
809
|
+
*
|
|
810
|
+
* @stimeoRuntimeOnly `id` filters this client's own echo, `timeout` arms one peer's expiry,
|
|
811
|
+
* `name` answers one beacon and the texts word one announcement.
|
|
731
812
|
*/
|
|
732
813
|
#onReceived(data) {
|
|
733
814
|
const beacon = data;
|
|
@@ -739,9 +820,8 @@ ${this.idValue}`;
|
|
|
739
820
|
}
|
|
740
821
|
const name = typeof beacon?.name === "string" ? beacon.name : "";
|
|
741
822
|
const existing = this.#peers.get(id);
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
this.#peers.set(id, { name, timer });
|
|
823
|
+
this.#expiry.set(id, () => this.#drop(id), this.#timeout);
|
|
824
|
+
this.#peers.set(id, { name });
|
|
745
825
|
if (existing === void 0) {
|
|
746
826
|
this.#appendClone(id, name);
|
|
747
827
|
this.#render();
|
|
@@ -753,18 +833,24 @@ ${this.idValue}`;
|
|
|
753
833
|
this.#render();
|
|
754
834
|
}
|
|
755
835
|
}
|
|
756
|
-
/**
|
|
836
|
+
/**
|
|
837
|
+
* Removes a peer (expiry or graceful leave) and reflects the change.
|
|
838
|
+
*
|
|
839
|
+
* @stimeoRuntimeOnly `announceLeaveText` words the one announcement this departure makes.
|
|
840
|
+
*/
|
|
757
841
|
#drop(id) {
|
|
758
842
|
const peer = this.#peers.get(id);
|
|
759
843
|
if (peer === void 0) return;
|
|
760
|
-
this.#
|
|
844
|
+
this.#expiry.clear(id);
|
|
761
845
|
this.#peers.delete(id);
|
|
762
846
|
this.#removeClone(id);
|
|
763
847
|
this.#render();
|
|
764
848
|
this.#announce(this.announceLeaveTextValue, peer.name);
|
|
765
849
|
this.dispatch("leave", { detail: { id } });
|
|
766
850
|
}
|
|
767
|
-
/**
|
|
851
|
+
/**
|
|
852
|
+
* Reflects the roster onto the hooks + count target and emits `change`.
|
|
853
|
+
*/
|
|
768
854
|
#render() {
|
|
769
855
|
const users = [...this.#peers.entries()].map(([id, peer]) => ({ id, name: peer.name }));
|
|
770
856
|
this.element.setAttribute("data-present", users.length > 0 ? "true" : "false");
|
|
@@ -803,8 +889,8 @@ ${this.idValue}`;
|
|
|
803
889
|
/** Appends one template clone for a newly present peer (list + template only). */
|
|
804
890
|
#appendClone(id, name) {
|
|
805
891
|
if (!this.hasListTarget || !this.hasTemplateTarget) return;
|
|
806
|
-
const root = this.templateTarget
|
|
807
|
-
if (!
|
|
892
|
+
const root = cloneTemplateRoot(this.templateTarget);
|
|
893
|
+
if (!root) return;
|
|
808
894
|
root.setAttribute("data-presence-id", id);
|
|
809
895
|
this.#fillName(root, name);
|
|
810
896
|
this.listTarget.appendChild(root);
|
|
@@ -834,12 +920,12 @@ ${this.idValue}`;
|
|
|
834
920
|
/** Clears the transient roster state (connect reset + disconnect teardown). */
|
|
835
921
|
#reset() {
|
|
836
922
|
this.#timers.clearAll();
|
|
923
|
+
this.#expiry.clearAll();
|
|
837
924
|
this.#pendingBeacon = null;
|
|
838
925
|
this.#announceId = null;
|
|
839
926
|
this.#peers.clear();
|
|
840
927
|
this.#lastBeaconAt = 0;
|
|
841
|
-
this.element
|
|
842
|
-
this.element.removeAttribute("data-present-count");
|
|
928
|
+
TRANSIENT2.reset(this.element);
|
|
843
929
|
if (this.hasCountTarget) this.countTarget.textContent = "";
|
|
844
930
|
if (this.hasListTarget) {
|
|
845
931
|
for (const child of this.listTarget.querySelectorAll("[data-presence-id]")) {
|
|
@@ -848,6 +934,9 @@ ${this.idValue}`;
|
|
|
848
934
|
}
|
|
849
935
|
}
|
|
850
936
|
};
|
|
937
|
+
var TRANSIENT3 = new TransientHooks({
|
|
938
|
+
attributes: ["data-typing", "data-typing-indicator-rejected"]
|
|
939
|
+
});
|
|
851
940
|
var DEFAULT_TIMEOUT = 3e3;
|
|
852
941
|
var DEFAULT_THROTTLE = 2e3;
|
|
853
942
|
var fillTokens = (template, values) => template.replace(/%\{(name|names|count)\}/g, (match, token) => values[token] ?? match);
|
|
@@ -871,8 +960,10 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
|
|
|
871
960
|
/** Identifier parameters parsed once from their declaration, never in the hot path. */
|
|
872
961
|
#params = {};
|
|
873
962
|
#subscription = null;
|
|
874
|
-
/** Names currently typing (other clients),
|
|
875
|
-
#typers = /* @__PURE__ */ new
|
|
963
|
+
/** Names currently typing (other clients), in the order their first signal arrived. */
|
|
964
|
+
#typers = /* @__PURE__ */ new Set();
|
|
965
|
+
/** Each typer's auto-clear timer, restarted by every further signal from that name. */
|
|
966
|
+
#expiry = new KeyedTimers();
|
|
876
967
|
#timers = new SafeTimeout();
|
|
877
968
|
/** Epoch ms of the last broadcast, for leading-edge throttling. */
|
|
878
969
|
#lastSentAt = 0;
|
|
@@ -899,8 +990,7 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
|
|
|
899
990
|
if (this.#typers.size > 0) this.#paint();
|
|
900
991
|
}
|
|
901
992
|
connect() {
|
|
902
|
-
this.element
|
|
903
|
-
this.element.removeAttribute("data-typing-indicator-rejected");
|
|
993
|
+
TRANSIENT3.reset(this.element);
|
|
904
994
|
if (this.hasStatusTarget) this.statusTarget.textContent = "";
|
|
905
995
|
this.element.addEventListener("input", this.#onInput);
|
|
906
996
|
if (this.channelValue) {
|
|
@@ -922,10 +1012,10 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
|
|
|
922
1012
|
this.#subscription?.unsubscribe();
|
|
923
1013
|
this.#subscription = null;
|
|
924
1014
|
this.#timers.clearAll();
|
|
1015
|
+
this.#expiry.clearAll();
|
|
925
1016
|
this.#announceId = null;
|
|
926
1017
|
this.#typers.clear();
|
|
927
|
-
this.element
|
|
928
|
-
this.element.removeAttribute("data-typing-indicator-rejected");
|
|
1018
|
+
TRANSIENT3.reset(this.element);
|
|
929
1019
|
if (this.hasStatusTarget) this.statusTarget.textContent = "";
|
|
930
1020
|
this.#lastSentAt = 0;
|
|
931
1021
|
}
|
|
@@ -941,17 +1031,16 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
|
|
|
941
1031
|
* Tracks a broadcast typer. The own echo is dropped (same `name`); every
|
|
942
1032
|
* further signal from a name restarts its auto-clear timer, so the indicator
|
|
943
1033
|
* survives continuous typing and clears `timeout` ms after the last signal.
|
|
1034
|
+
*
|
|
1035
|
+
* @stimeoRuntimeOnly `name` filters this client's own echo and `timeout` arms one typer's expiry;
|
|
1036
|
+
* the typers shown come from what is received.
|
|
944
1037
|
*/
|
|
945
1038
|
#onReceived(data) {
|
|
946
1039
|
const name = data?.name;
|
|
947
1040
|
if (typeof name !== "string" || name === "" || name === this.nameValue) return;
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
this.#typers.set(
|
|
952
|
-
name,
|
|
953
|
-
this.#timers.set(() => this.#untrack(name), this.#timeout)
|
|
954
|
-
);
|
|
1041
|
+
const added = !this.#typers.has(name);
|
|
1042
|
+
this.#typers.add(name);
|
|
1043
|
+
this.#expiry.set(name, () => this.#untrack(name), this.#timeout);
|
|
955
1044
|
if (added) this.#render();
|
|
956
1045
|
}
|
|
957
1046
|
/**
|
|
@@ -981,7 +1070,9 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
|
|
|
981
1070
|
this.#typers.delete(name);
|
|
982
1071
|
this.#render();
|
|
983
1072
|
}
|
|
984
|
-
/**
|
|
1073
|
+
/**
|
|
1074
|
+
* Reflects the typer set onto the display, announces it, and emits `change`.
|
|
1075
|
+
*/
|
|
985
1076
|
#render() {
|
|
986
1077
|
const names = this.#paint();
|
|
987
1078
|
this.#announce(names);
|
|
@@ -995,6 +1086,9 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
|
|
|
995
1086
|
* a reader for, and the visible copy already clears. Wording comes from the
|
|
996
1087
|
* consumer, and {@link announce} drops an empty message, so an undeclared template
|
|
997
1088
|
* announces nothing at all.
|
|
1089
|
+
*
|
|
1090
|
+
* @stimeoRuntimeOnly The wording Values are read to compose one announcement; the
|
|
1091
|
+
* visible typer list is rendered from the set, not from them.
|
|
998
1092
|
*/
|
|
999
1093
|
#announce(names) {
|
|
1000
1094
|
if (this.#announceId !== null) this.#timers.clear(this.#announceId);
|
|
@@ -1011,7 +1105,7 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
|
|
|
1011
1105
|
}
|
|
1012
1106
|
/** Writes the current typer set onto the hook and the status slot. */
|
|
1013
1107
|
#paint() {
|
|
1014
|
-
const names = [...this.#typers
|
|
1108
|
+
const names = [...this.#typers];
|
|
1015
1109
|
this.element.setAttribute("data-typing", names.length > 0 ? "true" : "false");
|
|
1016
1110
|
if (this.hasStatusTarget) {
|
|
1017
1111
|
this.statusTarget.textContent = this.#message(names);
|
|
@@ -8,36 +8,125 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
8
8
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// src/utils/state_reason.ts
|
|
12
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
13
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
14
|
+
function stateReasonFor(event) {
|
|
15
|
+
if (!event) return "api";
|
|
16
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
17
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
18
|
+
return "user";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/utils/state_regions.ts
|
|
22
|
+
var StateRegions = class {
|
|
23
|
+
#whenTrue;
|
|
24
|
+
#whenFalse;
|
|
25
|
+
#taken = /* @__PURE__ */ new WeakSet();
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.#whenTrue = options.whenTrue;
|
|
28
|
+
this.#whenFalse = options.whenFalse ?? null;
|
|
29
|
+
}
|
|
30
|
+
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
31
|
+
reflect(host, isTrue) {
|
|
32
|
+
const shown = this.#inside(host, this.#whenTrue());
|
|
33
|
+
if (!this.#whenFalse) {
|
|
34
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const hiddenSide = this.#inside(host, this.#whenFalse());
|
|
38
|
+
if (shown.length === 0 || hiddenSide.length === 0) {
|
|
39
|
+
for (const region of shown) this.#give(region);
|
|
40
|
+
for (const region of hiddenSide) this.#give(region);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
44
|
+
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
45
|
+
}
|
|
46
|
+
/** Writes `hidden` where it moves, noting which regions it takes out of view. */
|
|
47
|
+
#write(region, hidden) {
|
|
48
|
+
if (hidden) this.#taken.add(region);
|
|
49
|
+
else this.#taken.delete(region);
|
|
50
|
+
if (region.hidden !== hidden) region.hidden = hidden;
|
|
51
|
+
}
|
|
52
|
+
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
53
|
+
#give(region) {
|
|
54
|
+
if (this.#taken.delete(region)) region.hidden = false;
|
|
55
|
+
}
|
|
56
|
+
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
57
|
+
#inside(host, regions) {
|
|
58
|
+
return regions.filter((region) => host.contains(region));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
11
62
|
// src/controllers/accordion_controller.ts
|
|
12
63
|
var AccordionController = class extends Controller {
|
|
13
|
-
static targets = ["trigger", "panel"];
|
|
64
|
+
static targets = ["trigger", "panel", "expandedLabel", "collapsedLabel"];
|
|
14
65
|
static actions = ["collapseAll", "expandAll", "onKeydown", "toggle"];
|
|
66
|
+
static events = ["close", "open"];
|
|
67
|
+
/** Owns `hidden` on the label pair a header declares, narrowed to that header. */
|
|
68
|
+
#labels = new StateRegions({
|
|
69
|
+
whenTrue: () => this.expandedLabelTargets,
|
|
70
|
+
whenFalse: () => this.collapsedLabelTargets
|
|
71
|
+
});
|
|
72
|
+
/** Settles the label pair of a header as it joins the group. */
|
|
73
|
+
triggerTargetConnected(trigger) {
|
|
74
|
+
this.#reflectLabels(trigger);
|
|
75
|
+
}
|
|
76
|
+
/** Settles the pair of the header an arriving expanded half belongs to. */
|
|
77
|
+
expandedLabelTargetConnected(label) {
|
|
78
|
+
this.#settleLabelHost(label);
|
|
79
|
+
}
|
|
80
|
+
/** Settles the pair of the header an arriving collapsed half belongs to. */
|
|
81
|
+
collapsedLabelTargetConnected(label) {
|
|
82
|
+
this.#settleLabelHost(label);
|
|
83
|
+
}
|
|
84
|
+
/** Reflects the pair of the header a label half sits in, where a header holds it. */
|
|
85
|
+
#settleLabelHost(label) {
|
|
86
|
+
const trigger = this.triggerTargets.find((candidate) => candidate.contains(label));
|
|
87
|
+
if (trigger) this.#reflectLabels(trigger);
|
|
88
|
+
}
|
|
15
89
|
/** Toggles the panel controlled by the activated header. */
|
|
16
90
|
toggle(event) {
|
|
17
91
|
const trigger = event.currentTarget;
|
|
18
92
|
const panel = this.#panelFor(trigger);
|
|
19
93
|
if (!panel) return;
|
|
20
|
-
this.#setExpanded(
|
|
94
|
+
this.#setExpanded(
|
|
95
|
+
trigger,
|
|
96
|
+
panel,
|
|
97
|
+
trigger.getAttribute("aria-expanded") !== "true",
|
|
98
|
+
stateReasonFor(event)
|
|
99
|
+
);
|
|
21
100
|
}
|
|
22
101
|
/** Opens every panel. Bound via `data-action` on an "expand all" control. */
|
|
23
|
-
expandAll() {
|
|
24
|
-
this.#setAll(true);
|
|
102
|
+
expandAll(event) {
|
|
103
|
+
this.#setAll(true, stateReasonFor(event));
|
|
25
104
|
}
|
|
26
105
|
/** Closes every panel. Bound via `data-action` on a "collapse all" control. */
|
|
27
|
-
collapseAll() {
|
|
28
|
-
this.#setAll(false);
|
|
106
|
+
collapseAll(event) {
|
|
107
|
+
this.#setAll(false, stateReasonFor(event));
|
|
29
108
|
}
|
|
30
109
|
/** Drives every header/panel pair to the same expanded state. */
|
|
31
|
-
#setAll(open) {
|
|
110
|
+
#setAll(open, reason) {
|
|
32
111
|
for (const trigger of this.triggerTargets) {
|
|
33
112
|
const panel = this.#panelFor(trigger);
|
|
34
|
-
if (panel) this.#setExpanded(trigger, panel, open);
|
|
113
|
+
if (panel) this.#setExpanded(trigger, panel, open, reason);
|
|
35
114
|
}
|
|
36
115
|
}
|
|
37
116
|
/** Reflects one header/panel pair's state through `aria-expanded` + `hidden`. */
|
|
38
|
-
#setExpanded(trigger, panel, open) {
|
|
117
|
+
#setExpanded(trigger, panel, open, reason) {
|
|
118
|
+
const was = trigger.getAttribute("aria-expanded") === "true";
|
|
39
119
|
trigger.setAttribute("aria-expanded", open ? "true" : "false");
|
|
40
120
|
panel.hidden = !open;
|
|
121
|
+
this.#labels.reflect(trigger, open);
|
|
122
|
+
if (was === open) return;
|
|
123
|
+
const detail = { reason, index: this.triggerTargets.indexOf(trigger), trigger, panel };
|
|
124
|
+
if (open) this.dispatch("open", { detail, cancelable: false });
|
|
125
|
+
else this.dispatch("close", { detail, cancelable: false });
|
|
126
|
+
}
|
|
127
|
+
/** Shows the half of a header's label pair that belongs to its `aria-expanded`. */
|
|
128
|
+
#reflectLabels(trigger) {
|
|
129
|
+
this.#labels.reflect(trigger, trigger.getAttribute("aria-expanded") === "true");
|
|
41
130
|
}
|
|
42
131
|
/**
|
|
43
132
|
* Moves focus between headers per the APG keyboard model, skipping any header
|