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
|
@@ -31,39 +31,6 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
// src/utils/microtask_coalescer.ts
|
|
35
|
-
var MicrotaskCoalescer = class {
|
|
36
|
-
#run;
|
|
37
|
-
#queued = false;
|
|
38
|
-
#active = false;
|
|
39
|
-
#generation = 0;
|
|
40
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
41
|
-
constructor(run) {
|
|
42
|
-
this.#run = run;
|
|
43
|
-
}
|
|
44
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
45
|
-
activate() {
|
|
46
|
-
this.#active = true;
|
|
47
|
-
}
|
|
48
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
49
|
-
cancel() {
|
|
50
|
-
this.#active = false;
|
|
51
|
-
this.#queued = false;
|
|
52
|
-
this.#generation += 1;
|
|
53
|
-
}
|
|
54
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
55
|
-
schedule() {
|
|
56
|
-
if (!this.#active || this.#queued) return;
|
|
57
|
-
this.#queued = true;
|
|
58
|
-
const generation = this.#generation;
|
|
59
|
-
queueMicrotask(() => {
|
|
60
|
-
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
61
|
-
this.#queued = false;
|
|
62
|
-
this.#run();
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
34
|
// src/utils/safe_timeout.ts
|
|
68
35
|
var TimerRegistry = class {
|
|
69
36
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -117,6 +84,82 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
117
84
|
}
|
|
118
85
|
};
|
|
119
86
|
|
|
87
|
+
// src/utils/keyed_timers.ts
|
|
88
|
+
var KeyedTimers = class {
|
|
89
|
+
#timers = new SafeTimeout();
|
|
90
|
+
/** The pending timer of each key; an entry lives exactly as long as its timer. */
|
|
91
|
+
#pending = /* @__PURE__ */ new Map();
|
|
92
|
+
/**
|
|
93
|
+
* Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had
|
|
94
|
+
* pending. The entry is dropped before the callback runs, so the callback sees
|
|
95
|
+
* the key unarmed and may arm it again for the next round.
|
|
96
|
+
*/
|
|
97
|
+
set(key, callback, delay) {
|
|
98
|
+
this.clear(key);
|
|
99
|
+
const id = this.#timers.set(() => {
|
|
100
|
+
this.#pending.delete(key);
|
|
101
|
+
callback();
|
|
102
|
+
}, delay);
|
|
103
|
+
this.#pending.set(key, id);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Cancels `key`'s pending timer, if it has one.
|
|
107
|
+
*
|
|
108
|
+
* A timer id is a positive integer, so `-1` stands for "nothing pending" and
|
|
109
|
+
* the registry ignores an id it does not own — the unarmed case needs no
|
|
110
|
+
* branch of its own, and no other key's timer can be reached from here.
|
|
111
|
+
*/
|
|
112
|
+
clear(key) {
|
|
113
|
+
this.#timers.clear(this.#pending.get(key) ?? -1);
|
|
114
|
+
this.#pending.delete(key);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Cancels every pending timer and forgets every key. Call this from a
|
|
118
|
+
* controller's `disconnect()` so no timer, and no entry, outlives the element.
|
|
119
|
+
*/
|
|
120
|
+
clearAll() {
|
|
121
|
+
this.#timers.clearAll();
|
|
122
|
+
this.#pending.clear();
|
|
123
|
+
}
|
|
124
|
+
/** Whether `key` has a timer pending. */
|
|
125
|
+
has(key) {
|
|
126
|
+
return this.#pending.has(key);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/utils/microtask_coalescer.ts
|
|
131
|
+
var MicrotaskCoalescer = class {
|
|
132
|
+
#run;
|
|
133
|
+
#queued = false;
|
|
134
|
+
#active = false;
|
|
135
|
+
#generation = 0;
|
|
136
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
137
|
+
constructor(run) {
|
|
138
|
+
this.#run = run;
|
|
139
|
+
}
|
|
140
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
141
|
+
activate() {
|
|
142
|
+
this.#active = true;
|
|
143
|
+
}
|
|
144
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
145
|
+
cancel() {
|
|
146
|
+
this.#active = false;
|
|
147
|
+
this.#queued = false;
|
|
148
|
+
this.#generation += 1;
|
|
149
|
+
}
|
|
150
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
151
|
+
schedule() {
|
|
152
|
+
if (!this.#active || this.#queued) return;
|
|
153
|
+
this.#queued = true;
|
|
154
|
+
const generation = this.#generation;
|
|
155
|
+
queueMicrotask(() => {
|
|
156
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
157
|
+
this.#queued = false;
|
|
158
|
+
this.#run();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
120
163
|
// src/controllers/announcer_controller.ts
|
|
121
164
|
var LEVELS = ["polite", "assertive"];
|
|
122
165
|
var AnnouncerController = class extends Controller {
|
|
@@ -126,8 +169,14 @@ var AnnouncerController = class extends Controller {
|
|
|
126
169
|
dedupeReannounce: { type: Boolean, default: true }
|
|
127
170
|
};
|
|
128
171
|
static actions = ["announce"];
|
|
129
|
-
/**
|
|
172
|
+
/** Drain timers, which belong to a politeness level rather than to a region. */
|
|
130
173
|
#timers = new SafeTimeout();
|
|
174
|
+
/**
|
|
175
|
+
* The one timer a region may have outstanding — its dedupe re-set, then its
|
|
176
|
+
* auto-clear. A region carries at most one, and the registry releases the
|
|
177
|
+
* previous one whenever a newer message takes the region over.
|
|
178
|
+
*/
|
|
179
|
+
#regionTimers = new KeyedTimers();
|
|
131
180
|
/** Live regions generated to stand in for absent targets, for teardown. */
|
|
132
181
|
#generated = /* @__PURE__ */ new Map();
|
|
133
182
|
/** Messages waiting to be written, oldest first, one queue per politeness. */
|
|
@@ -147,13 +196,6 @@ var AnnouncerController = class extends Controller {
|
|
|
147
196
|
});
|
|
148
197
|
/** Rewinds to an announceable initial state for the snapshot; see the remarks. */
|
|
149
198
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
150
|
-
/**
|
|
151
|
-
* The one timer a region may have outstanding — its dedupe re-set, then its
|
|
152
|
-
* auto-clear. Held weakly so a swapped-out target is not retained; a leftover
|
|
153
|
-
* id is harmless because {@link SafeTimeout.clear} no-ops on an id it does not
|
|
154
|
-
* own.
|
|
155
|
-
*/
|
|
156
|
-
#pending = /* @__PURE__ */ new WeakMap();
|
|
157
199
|
/**
|
|
158
200
|
* Guards against handling the same CustomEvent twice. An event dispatched on
|
|
159
201
|
* the controller element with `bubbles: true` reaches both the element and the
|
|
@@ -184,6 +226,7 @@ var AnnouncerController = class extends Controller {
|
|
|
184
226
|
this.element.removeEventListener("stimeo--announcer:announce", this.#onAnnounceEvent);
|
|
185
227
|
window.removeEventListener("stimeo--announcer:announce", this.#onAnnounceEvent);
|
|
186
228
|
this.#timers.clearAll();
|
|
229
|
+
this.#regionTimers.clearAll();
|
|
187
230
|
this.#queues.clear();
|
|
188
231
|
this.#draining.clear();
|
|
189
232
|
this.#removeGenerated();
|
|
@@ -303,6 +346,9 @@ var AnnouncerController = class extends Controller {
|
|
|
303
346
|
* region (it has to be in the accessibility tree before the text arrives) and
|
|
304
347
|
* emptying a region that already holds this exact text (an unchanged node is not
|
|
305
348
|
* re-read, so `dedupeReannounce` clears first and writes on the following pass).
|
|
349
|
+
*
|
|
350
|
+
* @stimeoRuntimeOnly `dedupeReannounce` decides whether one queued message is spoken again; the
|
|
351
|
+
* region's text is the message itself.
|
|
306
352
|
*/
|
|
307
353
|
#drain(level) {
|
|
308
354
|
const queue = this.#queues.get(level);
|
|
@@ -314,21 +360,25 @@ var AnnouncerController = class extends Controller {
|
|
|
314
360
|
}
|
|
315
361
|
const region = this.#regionFor(level);
|
|
316
362
|
if (this.dedupeReannounceValue && region.textContent === message) {
|
|
317
|
-
this.#
|
|
363
|
+
this.#regionTimers.clear(region);
|
|
318
364
|
region.textContent = "";
|
|
319
365
|
this.#scheduleDrain(level);
|
|
320
366
|
return;
|
|
321
367
|
}
|
|
322
368
|
queue.shift();
|
|
323
|
-
this.#
|
|
369
|
+
this.#regionTimers.clear(region);
|
|
324
370
|
region.textContent = message;
|
|
325
371
|
this.#scheduleClear(region, message);
|
|
326
372
|
if (queue.length > 0) this.#scheduleDrain(level);
|
|
327
373
|
}
|
|
328
|
-
/**
|
|
374
|
+
/**
|
|
375
|
+
* Clears the region after `clearAfter` ms, unless a newer message replaced it.
|
|
376
|
+
*
|
|
377
|
+
* @stimeoRuntimeOnly `clearAfter` is the delay of the one clearing timer this call arms.
|
|
378
|
+
*/
|
|
329
379
|
#scheduleClear(region, message) {
|
|
330
380
|
if (this.clearAfterValue <= 0) return;
|
|
331
|
-
this.#
|
|
381
|
+
this.#regionTimers.set(
|
|
332
382
|
region,
|
|
333
383
|
() => {
|
|
334
384
|
if (region.textContent === message) region.textContent = "";
|
|
@@ -336,23 +386,6 @@ var AnnouncerController = class extends Controller {
|
|
|
336
386
|
this.clearAfterValue
|
|
337
387
|
);
|
|
338
388
|
}
|
|
339
|
-
/**
|
|
340
|
-
* Arms `region`'s single pending timer. Callers reach here with the slot
|
|
341
|
-
* already free — `#drain` releases it before writing, and a fired timer clears its own
|
|
342
|
-
* entry below — so this does not cancel again.
|
|
343
|
-
*/
|
|
344
|
-
#schedule(region, callback, delay) {
|
|
345
|
-
const id = this.#timers.set(() => {
|
|
346
|
-
this.#pending.delete(region);
|
|
347
|
-
callback();
|
|
348
|
-
}, delay);
|
|
349
|
-
this.#pending.set(region, id);
|
|
350
|
-
}
|
|
351
|
-
/** Releases `region`'s pending timer, if it has one. */
|
|
352
|
-
#cancelPending(region) {
|
|
353
|
-
this.#timers.clear(this.#pending.get(region) ?? -1);
|
|
354
|
-
this.#pending.delete(region);
|
|
355
|
-
}
|
|
356
389
|
/**
|
|
357
390
|
* Resolves the live region for a politeness level.
|
|
358
391
|
*
|
|
@@ -378,6 +411,7 @@ var AnnouncerController = class extends Controller {
|
|
|
378
411
|
this.#queues.clear();
|
|
379
412
|
this.#draining.clear();
|
|
380
413
|
this.#timers.clearAll();
|
|
414
|
+
this.#regionTimers.clearAll();
|
|
381
415
|
for (const level of LEVELS) {
|
|
382
416
|
if (this.#hasTargetFor(level)) {
|
|
383
417
|
const target = level === "assertive" ? this.assertiveTarget : this.politeTarget;
|
|
@@ -2,6 +2,17 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/auto_submit_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/announce.ts
|
|
6
|
+
function announce(message, options = {}) {
|
|
7
|
+
const text = message.trim();
|
|
8
|
+
if (text.length === 0) return;
|
|
9
|
+
window.dispatchEvent(
|
|
10
|
+
new CustomEvent("stimeo--announcer:announce", {
|
|
11
|
+
detail: { message: text, assertive: options.assertive === true }
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
// src/utils/before_cache_reset.ts
|
|
6
17
|
var BeforeCacheReset = class _BeforeCacheReset {
|
|
7
18
|
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
@@ -32,11 +43,19 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
32
43
|
};
|
|
33
44
|
|
|
34
45
|
// src/utils/composition_tracker.ts
|
|
46
|
+
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
47
|
+
"insertCompositionText",
|
|
48
|
+
"insertFromComposition",
|
|
49
|
+
"deleteCompositionText",
|
|
50
|
+
"deleteByComposition"
|
|
51
|
+
]);
|
|
35
52
|
var CompositionTracker = class {
|
|
36
53
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
37
54
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
38
55
|
#onStart;
|
|
39
56
|
#onEnd;
|
|
57
|
+
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
58
|
+
#confirmedTarget = null;
|
|
40
59
|
constructor(options = {}) {
|
|
41
60
|
this.#onStart = options.onStart;
|
|
42
61
|
this.#onEnd = options.onEnd;
|
|
@@ -46,6 +65,7 @@ var CompositionTracker = class {
|
|
|
46
65
|
if (this.#observedTargets.has(target)) return;
|
|
47
66
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
48
67
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
68
|
+
target.addEventListener("keydown", this.#handleKeydown);
|
|
49
69
|
this.#observedTargets.add(target);
|
|
50
70
|
}
|
|
51
71
|
/** Stops tracking one target and clears any active composition it owned. */
|
|
@@ -53,29 +73,76 @@ var CompositionTracker = class {
|
|
|
53
73
|
if (!this.#observedTargets.delete(target)) return;
|
|
54
74
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
55
75
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
76
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
56
77
|
this.#activeTargets.delete(target);
|
|
78
|
+
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
57
79
|
}
|
|
58
80
|
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
59
81
|
disconnect() {
|
|
60
82
|
for (const target of this.#observedTargets) {
|
|
61
83
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
62
84
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
85
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
63
86
|
}
|
|
64
87
|
this.#observedTargets.clear();
|
|
65
88
|
this.#activeTargets.clear();
|
|
89
|
+
this.#confirmedTarget = null;
|
|
66
90
|
}
|
|
67
91
|
/** True when lifecycle tracking or the current event reports composition. */
|
|
68
92
|
isComposing(event) {
|
|
69
93
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
70
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
97
|
+
*
|
|
98
|
+
* Asking closes the window either way, so one confirmation is folded at most
|
|
99
|
+
* once and a consumer asks once per `input`.
|
|
100
|
+
*/
|
|
101
|
+
consumesConfirmedInput(event) {
|
|
102
|
+
const confirmed = this.#confirmedTarget;
|
|
103
|
+
this.#confirmedTarget = null;
|
|
104
|
+
if (confirmed === null || confirmed !== event.target) return false;
|
|
105
|
+
const inputType = event.inputType;
|
|
106
|
+
return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
|
|
107
|
+
}
|
|
71
108
|
#handleStart = (event) => {
|
|
109
|
+
this.#confirmedTarget = null;
|
|
72
110
|
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
73
111
|
this.#onStart?.(event);
|
|
74
112
|
};
|
|
75
113
|
#handleEnd = (event) => {
|
|
76
114
|
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
115
|
+
this.#confirmedTarget = event.target;
|
|
77
116
|
this.#onEnd?.(event);
|
|
78
117
|
};
|
|
118
|
+
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
119
|
+
#handleKeydown = () => {
|
|
120
|
+
this.#confirmedTarget = null;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/utils/listener_set.ts
|
|
125
|
+
var ListenerSet = class {
|
|
126
|
+
/** The generation every `add` joins until the next `dispose()`. */
|
|
127
|
+
#abort = new AbortController();
|
|
128
|
+
/**
|
|
129
|
+
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
130
|
+
*
|
|
131
|
+
* The set supplies the signal, so `options` carries everything else the DOM
|
|
132
|
+
* accepts — `capture` included, which has to match at release time and no
|
|
133
|
+
* longer has a second place to drift from.
|
|
134
|
+
*/
|
|
135
|
+
add(target, type, handler, options) {
|
|
136
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Releases every listener of the open generation, synchronously, and opens the
|
|
140
|
+
* next one. Idempotent, and safe before anything has been added.
|
|
141
|
+
*/
|
|
142
|
+
dispose() {
|
|
143
|
+
this.#abort.abort();
|
|
144
|
+
this.#abort = new AbortController();
|
|
145
|
+
}
|
|
79
146
|
};
|
|
80
147
|
|
|
81
148
|
// src/utils/safe_timeout.ts
|
|
@@ -146,18 +213,23 @@ var AutoSubmitController = class extends Controller {
|
|
|
146
213
|
#timers = new SafeTimeout();
|
|
147
214
|
/** Id of the pending debounce timer, so a new keystroke can reset it. */
|
|
148
215
|
#pendingId = 0;
|
|
216
|
+
/** Listeners that live with the bound form rather than with the connection. */
|
|
217
|
+
#formListeners = new ListenerSet();
|
|
149
218
|
/** The form the listeners are attached to; target callbacks rebind it. */
|
|
150
219
|
#boundForm = null;
|
|
151
220
|
/** Rewinds the transient state hooks just before Turbo snapshots the page. */
|
|
152
221
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
153
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* Clears `aria-busy` and emits completion once Turbo finishes the submit.
|
|
224
|
+
*
|
|
225
|
+
* @stimeoRuntimeOnly `message` and `announce` shape the event and the announcement of one
|
|
226
|
+
* finished submit; the busy flag it clears does not depend on them.
|
|
227
|
+
*/
|
|
154
228
|
#onSubmitEnd = () => {
|
|
155
229
|
this.#boundForm?.removeAttribute("aria-busy");
|
|
156
230
|
const message = this.messageValue;
|
|
157
231
|
this.dispatch("done", { detail: { message: message || void 0 } });
|
|
158
|
-
if (this.announceValue
|
|
159
|
-
window.dispatchEvent(new CustomEvent("stimeo--announcer:announce", { detail: { message } }));
|
|
160
|
-
}
|
|
232
|
+
if (this.announceValue) announce(message);
|
|
161
233
|
};
|
|
162
234
|
/** Owns delegated IME lifecycle state and submits confirmed input text. */
|
|
163
235
|
#composition = new CompositionTracker({
|
|
@@ -194,7 +266,11 @@ var AutoSubmitController = class extends Controller {
|
|
|
194
266
|
if (event.type === "input" && this.#composition.isComposing(event)) return;
|
|
195
267
|
this.#schedule(event.target ?? null);
|
|
196
268
|
}
|
|
197
|
-
/**
|
|
269
|
+
/**
|
|
270
|
+
* Schedules (and coalesces) the debounced submit for the given trigger.
|
|
271
|
+
*
|
|
272
|
+
* @stimeoRuntimeOnly `debounce` is the delay of the one submit timer this call arms.
|
|
273
|
+
*/
|
|
198
274
|
#schedule(trigger) {
|
|
199
275
|
const form = this.#boundForm;
|
|
200
276
|
if (!form) return;
|
|
@@ -228,12 +304,12 @@ var AutoSubmitController = class extends Controller {
|
|
|
228
304
|
if (previous) {
|
|
229
305
|
this.#cancelPending();
|
|
230
306
|
previous.removeAttribute("data-auto-submit-pending");
|
|
231
|
-
|
|
307
|
+
this.#formListeners.dispose();
|
|
232
308
|
this.#composition.unobserve(previous);
|
|
233
309
|
}
|
|
234
310
|
this.#boundForm = form;
|
|
235
311
|
if (!form) return;
|
|
236
|
-
|
|
312
|
+
this.#formListeners.add(form, "turbo:submit-end", this.#onSubmitEnd);
|
|
237
313
|
this.#composition.observe(form);
|
|
238
314
|
}
|
|
239
315
|
/** Resolves the form: the explicit `form` target, else a `<form>` root, else null. */
|
|
@@ -224,7 +224,7 @@ var AvatarController = class extends Controller {
|
|
|
224
224
|
}
|
|
225
225
|
/** Applies Value precedence and returns the effective raw `src` attribute. */
|
|
226
226
|
#applySource(image) {
|
|
227
|
-
if (!this.element.hasAttribute(
|
|
227
|
+
if (!this.element.hasAttribute(`data-${this.identifier}-src-value`)) {
|
|
228
228
|
this.#src.return(image);
|
|
229
229
|
return image.getAttribute("src");
|
|
230
230
|
}
|
|
@@ -63,10 +63,15 @@ var LayoutObserver = class {
|
|
|
63
63
|
#resizeObserverFactory;
|
|
64
64
|
#resizeObserver = null;
|
|
65
65
|
#observingViewport = false;
|
|
66
|
+
#loadContainer = null;
|
|
66
67
|
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
67
68
|
#handleViewportResize = () => {
|
|
68
69
|
this.#callback();
|
|
69
70
|
};
|
|
71
|
+
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
72
|
+
#handleDescendantLoad = () => {
|
|
73
|
+
this.#callback();
|
|
74
|
+
};
|
|
70
75
|
constructor(callback, options = {}) {
|
|
71
76
|
this.#callback = callback;
|
|
72
77
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
@@ -102,14 +107,35 @@ var LayoutObserver = class {
|
|
|
102
107
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
103
108
|
}
|
|
104
109
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
110
|
+
* Starts reporting a `load` from anywhere inside `container` — an image or a
|
|
111
|
+
* frame settling changes the box it sits in, and it measures as zero high until
|
|
112
|
+
* then. `load` does not bubble, so the subscription is a capture-phase listener
|
|
113
|
+
* on the container itself and nothing the caller spells.
|
|
114
|
+
*
|
|
115
|
+
* **One container at a time.** A further call moves the observation, so a widget
|
|
116
|
+
* whose content element is swapped at runtime releases the element it let go by
|
|
117
|
+
* naming the new one — there is no second place for the release to drift from.
|
|
118
|
+
*/
|
|
119
|
+
observeDescendantLoads(container) {
|
|
120
|
+
this.unobserveDescendantLoads();
|
|
121
|
+
this.#loadContainer = container;
|
|
122
|
+
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
123
|
+
}
|
|
124
|
+
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
125
|
+
unobserveDescendantLoads() {
|
|
126
|
+
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
127
|
+
this.#loadContainer = null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Releases every observation: disconnects the {@link ResizeObserver} and removes
|
|
131
|
+
* the viewport and descendant-load listeners. Safe to call multiple times. Call
|
|
132
|
+
* this from a controller's `disconnect()`.
|
|
108
133
|
*/
|
|
109
134
|
disconnect() {
|
|
110
135
|
this.#resizeObserver?.disconnect();
|
|
111
136
|
this.#resizeObserver = null;
|
|
112
137
|
this.unobserveViewport();
|
|
138
|
+
this.unobserveDescendantLoads();
|
|
113
139
|
}
|
|
114
140
|
};
|
|
115
141
|
|
|
@@ -161,21 +187,24 @@ var BreadcrumbController = class extends Controller {
|
|
|
161
187
|
this.#resync();
|
|
162
188
|
}
|
|
163
189
|
/**
|
|
164
|
-
* Re-measures when
|
|
165
|
-
* precondition for collapsing at all, so
|
|
166
|
-
* and items get: without
|
|
167
|
-
* set would only take effect at the next resize or
|
|
168
|
-
* never come.
|
|
190
|
+
* Re-measures when an `ellipsis` target arrives. The `ellipsis` and `trigger`
|
|
191
|
+
* targets form the disclosure set, a precondition for collapsing at all, so they
|
|
192
|
+
* need the same watching the list and items get: without their callbacks a swap
|
|
193
|
+
* that breaks or completes the set would only take effect at the next resize or
|
|
194
|
+
* list mutation, which may never come.
|
|
169
195
|
*/
|
|
170
196
|
ellipsisTargetConnected() {
|
|
171
197
|
this.#resync();
|
|
172
198
|
}
|
|
199
|
+
/** Re-measures when an `ellipsis` target leaves the disclosure set. */
|
|
173
200
|
ellipsisTargetDisconnected() {
|
|
174
201
|
this.#resync();
|
|
175
202
|
}
|
|
203
|
+
/** Re-measures when a `trigger` target joins the disclosure set. */
|
|
176
204
|
triggerTargetConnected() {
|
|
177
205
|
this.#resync();
|
|
178
206
|
}
|
|
207
|
+
/** Re-measures when a `trigger` target leaves the disclosure set. */
|
|
179
208
|
triggerTargetDisconnected() {
|
|
180
209
|
this.#resync();
|
|
181
210
|
}
|
|
@@ -184,7 +213,7 @@ var BreadcrumbController = class extends Controller {
|
|
|
184
213
|
*
|
|
185
214
|
* The marker is the source of truth for what may be collapsed, so an element
|
|
186
215
|
* that loses it while the controller is live is an always-visible item again —
|
|
187
|
-
* and
|
|
216
|
+
* and rendering only walks the *current* targets, so nothing else would ever
|
|
188
217
|
* clear the `hidden` this controller put there.
|
|
189
218
|
*
|
|
190
219
|
* The `#connected` guard is load-bearing, not defensive. Stimulus fires this
|
|
@@ -289,8 +318,6 @@ var BreadcrumbController = class extends Controller {
|
|
|
289
318
|
}
|
|
290
319
|
/**
|
|
291
320
|
* Applies the collapsed/expanded state to the items, ellipsis, and trigger.
|
|
292
|
-
*
|
|
293
|
-
* @stimeoRenderRoot
|
|
294
321
|
*/
|
|
295
322
|
#render() {
|
|
296
323
|
const collapsed = this.#overflowing && !this.#expanded;
|
|
@@ -108,12 +108,8 @@ var BulkSelectController = class extends Controller {
|
|
|
108
108
|
allTargetDisconnected() {
|
|
109
109
|
this.#reconcile.schedule();
|
|
110
110
|
}
|
|
111
|
-
/** Repaints the count for a total that changed at runtime
|
|
111
|
+
/** Repaints the count for a total that changed at runtime. */
|
|
112
112
|
totalCountValueChanged() {
|
|
113
|
-
if (!Number.isFinite(this.totalCountValue)) {
|
|
114
|
-
this.totalCountValue = 0;
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
113
|
this.#reconcile.schedule();
|
|
118
114
|
}
|
|
119
115
|
/** Repaints so wording changed at runtime is used by the next announcement. */
|
|
@@ -175,6 +171,11 @@ var BulkSelectController = class extends Controller {
|
|
|
175
171
|
#announce(detail) {
|
|
176
172
|
announce(fillTemplate(this.announceTextValue, { count: detail.count }));
|
|
177
173
|
}
|
|
174
|
+
/** The declared `totalCount`, or the default `0` in place of a non-finite one. */
|
|
175
|
+
get #totalCount() {
|
|
176
|
+
const declared = this.totalCountValue;
|
|
177
|
+
return Number.isFinite(declared) ? declared : 0;
|
|
178
|
+
}
|
|
178
179
|
/**
|
|
179
180
|
* Recomputes the count, the select-all checked/indeterminate state, and the bar
|
|
180
181
|
* visibility from the current DOM. Returns the figures when the emitted count or
|
|
@@ -191,7 +192,7 @@ var BulkSelectController = class extends Controller {
|
|
|
191
192
|
this.allTarget.checked = total > 0 && checked === total;
|
|
192
193
|
this.allTarget.indeterminate = checked > 0 && checked < total;
|
|
193
194
|
}
|
|
194
|
-
const count = allPages ? this
|
|
195
|
+
const count = allPages ? this.#totalCount : checked;
|
|
195
196
|
const show = allPages || checked > 0;
|
|
196
197
|
if (this.hasBarTarget) {
|
|
197
198
|
if (!show && this.hasAllTarget && this.barTarget.contains(document.activeElement)) {
|