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
|
@@ -2,6 +2,17 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/flash_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. */
|
|
@@ -84,6 +95,121 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
84
95
|
}
|
|
85
96
|
};
|
|
86
97
|
|
|
98
|
+
// src/utils/keyed_timers.ts
|
|
99
|
+
var KeyedTimers = class {
|
|
100
|
+
#timers = new SafeTimeout();
|
|
101
|
+
/** The pending timer of each key; an entry lives exactly as long as its timer. */
|
|
102
|
+
#pending = /* @__PURE__ */ new Map();
|
|
103
|
+
/**
|
|
104
|
+
* Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had
|
|
105
|
+
* pending. The entry is dropped before the callback runs, so the callback sees
|
|
106
|
+
* the key unarmed and may arm it again for the next round.
|
|
107
|
+
*/
|
|
108
|
+
set(key, callback, delay) {
|
|
109
|
+
this.clear(key);
|
|
110
|
+
const id = this.#timers.set(() => {
|
|
111
|
+
this.#pending.delete(key);
|
|
112
|
+
callback();
|
|
113
|
+
}, delay);
|
|
114
|
+
this.#pending.set(key, id);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Cancels `key`'s pending timer, if it has one.
|
|
118
|
+
*
|
|
119
|
+
* A timer id is a positive integer, so `-1` stands for "nothing pending" and
|
|
120
|
+
* the registry ignores an id it does not own — the unarmed case needs no
|
|
121
|
+
* branch of its own, and no other key's timer can be reached from here.
|
|
122
|
+
*/
|
|
123
|
+
clear(key) {
|
|
124
|
+
this.#timers.clear(this.#pending.get(key) ?? -1);
|
|
125
|
+
this.#pending.delete(key);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Cancels every pending timer and forgets every key. Call this from a
|
|
129
|
+
* controller's `disconnect()` so no timer, and no entry, outlives the element.
|
|
130
|
+
*/
|
|
131
|
+
clearAll() {
|
|
132
|
+
this.#timers.clearAll();
|
|
133
|
+
this.#pending.clear();
|
|
134
|
+
}
|
|
135
|
+
/** Whether `key` has a timer pending. */
|
|
136
|
+
has(key) {
|
|
137
|
+
return this.#pending.has(key);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// src/utils/pausable_timers.ts
|
|
142
|
+
var PausableTimers = class {
|
|
143
|
+
#timers = new KeyedTimers();
|
|
144
|
+
/** Every key armed or held; an entry goes as its timer fires, or with the key. */
|
|
145
|
+
#entries = /* @__PURE__ */ new Map();
|
|
146
|
+
/**
|
|
147
|
+
* Arms `callback` after `delay` ms for `key`, replacing whatever `key` had.
|
|
148
|
+
* A held key stays held and banks `delay` for its resume.
|
|
149
|
+
*/
|
|
150
|
+
set(key, callback, delay) {
|
|
151
|
+
const reasons = this.#entries.get(key)?.reasons ?? /* @__PURE__ */ new Set();
|
|
152
|
+
const entry = { callback, startedAt: 0, remaining: delay, reasons };
|
|
153
|
+
this.#entries.set(key, entry);
|
|
154
|
+
if (reasons.size === 0) this.#arm(key, entry);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Holds `key` for `reason` and banks the time left, with a floor of one
|
|
158
|
+
* millisecond. Reports whether this call is the one that stopped a running
|
|
159
|
+
* timer, so the caller can write its held-state hook exactly once.
|
|
160
|
+
*/
|
|
161
|
+
pause(key, reason) {
|
|
162
|
+
const entry = this.#entries.get(key);
|
|
163
|
+
if (entry === void 0) return false;
|
|
164
|
+
entry.reasons.add(reason);
|
|
165
|
+
if (!this.#timers.has(key)) return false;
|
|
166
|
+
this.#timers.clear(key);
|
|
167
|
+
entry.remaining = Math.max(1, entry.remaining - (Date.now() - entry.startedAt));
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Releases `reason` on `key`, arming the banked time again once no reason is
|
|
172
|
+
* left. Reports whether this call is the one that started the timer again.
|
|
173
|
+
*/
|
|
174
|
+
resume(key, reason) {
|
|
175
|
+
const entry = this.#entries.get(key);
|
|
176
|
+
if (entry === void 0) return false;
|
|
177
|
+
entry.reasons.delete(reason);
|
|
178
|
+
if (entry.reasons.size > 0 || this.#timers.has(key)) return false;
|
|
179
|
+
this.#arm(key, entry);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
/** Whether `key` is armed or held — that is, whether this registry drives it at all. */
|
|
183
|
+
tracks(key) {
|
|
184
|
+
return this.#entries.has(key);
|
|
185
|
+
}
|
|
186
|
+
/** Cancels `key`'s timer and drops its hold. */
|
|
187
|
+
clear(key) {
|
|
188
|
+
this.#timers.clear(key);
|
|
189
|
+
this.#entries.delete(key);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Cancels every timer and forgets every key. Call this from a controller's
|
|
193
|
+
* `disconnect()` so neither a timer nor a hold outlives the element.
|
|
194
|
+
*/
|
|
195
|
+
clearAll() {
|
|
196
|
+
this.#timers.clearAll();
|
|
197
|
+
this.#entries.clear();
|
|
198
|
+
}
|
|
199
|
+
/** Starts `entry`'s banked time running for `key`, and drops it as it fires. */
|
|
200
|
+
#arm(key, entry) {
|
|
201
|
+
entry.startedAt = Date.now();
|
|
202
|
+
this.#timers.set(
|
|
203
|
+
key,
|
|
204
|
+
() => {
|
|
205
|
+
this.#entries.delete(key);
|
|
206
|
+
entry.callback();
|
|
207
|
+
},
|
|
208
|
+
entry.remaining
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
87
213
|
// src/utils/transition_completion.ts
|
|
88
214
|
function timeMs(value) {
|
|
89
215
|
const trimmed = value.trim();
|
|
@@ -130,12 +256,13 @@ var FlashController = class extends Controller {
|
|
|
130
256
|
};
|
|
131
257
|
static actions = ["dismiss"];
|
|
132
258
|
static events = ["show", "dismiss", "reconcile"];
|
|
259
|
+
/** Removal timers for the leaving transition; the auto-dismiss ones live below. */
|
|
133
260
|
#timers = new SafeTimeout();
|
|
261
|
+
/** Per-message auto-dismiss, held open while the message is hovered or focused. */
|
|
262
|
+
#dismiss = new PausableTimers();
|
|
134
263
|
#observer = null;
|
|
135
264
|
/** Whether the controller is between `connect()` and `disconnect()`. */
|
|
136
265
|
#connected = false;
|
|
137
|
-
/** Auto-dismiss timer state keyed by message element. */
|
|
138
|
-
#state = /* @__PURE__ */ new Map();
|
|
139
266
|
/** Messages already processed, in insertion order, to enforce `max` and avoid double work. */
|
|
140
267
|
#order = [];
|
|
141
268
|
/**
|
|
@@ -161,8 +288,8 @@ var FlashController = class extends Controller {
|
|
|
161
288
|
this.#beforeCache.deactivate();
|
|
162
289
|
this.#stopObserving();
|
|
163
290
|
this.#timers.clearAll();
|
|
291
|
+
this.#dismiss.clearAll();
|
|
164
292
|
for (const message of this.#order) this.#unbindPause(message);
|
|
165
|
-
this.#state.clear();
|
|
166
293
|
this.#order.length = 0;
|
|
167
294
|
this.#leaving.clear();
|
|
168
295
|
}
|
|
@@ -291,8 +418,7 @@ var FlashController = class extends Controller {
|
|
|
291
418
|
* own role to do the announcing (dynamic inserts). Idempotent per message.
|
|
292
419
|
*/
|
|
293
420
|
#process(message, bridge) {
|
|
294
|
-
if (this.#
|
|
295
|
-
if (this.#leaving.has(message)) return;
|
|
421
|
+
if (this.#order.includes(message) || this.#leaving.has(message)) return;
|
|
296
422
|
const type = message.getAttribute("data-flash-type") ?? "";
|
|
297
423
|
const assertive = ASSERTIVE_TYPES.has(type);
|
|
298
424
|
if (!message.hasAttribute("role")) {
|
|
@@ -303,11 +429,7 @@ var FlashController = class extends Controller {
|
|
|
303
429
|
this.#bindPause(message);
|
|
304
430
|
const text = message.textContent?.trim() ?? "";
|
|
305
431
|
this.dispatch("show", { target: message, detail: { type, message: text } });
|
|
306
|
-
if (bridge
|
|
307
|
-
window.dispatchEvent(
|
|
308
|
-
new CustomEvent("stimeo--announcer:announce", { detail: { message: text, assertive } })
|
|
309
|
-
);
|
|
310
|
-
}
|
|
432
|
+
if (bridge) announce(text, { assertive });
|
|
311
433
|
this.#startTimer(message);
|
|
312
434
|
this.#enforceMax();
|
|
313
435
|
}
|
|
@@ -320,53 +442,33 @@ var FlashController = class extends Controller {
|
|
|
320
442
|
this.#beginDismiss(oldest, "limit");
|
|
321
443
|
}
|
|
322
444
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const id = this.#timers.set(() => this.#beginDismiss(message, "timeout"), duration);
|
|
328
|
-
this.#state.set(message, {
|
|
329
|
-
id,
|
|
330
|
-
startedAt: Date.now(),
|
|
331
|
-
remaining: duration,
|
|
332
|
-
paused: existing?.paused ?? /* @__PURE__ */ new Set()
|
|
333
|
-
});
|
|
445
|
+
/** Arms a message's auto-dismiss; a non-positive `duration` means it never expires. */
|
|
446
|
+
#startTimer(message) {
|
|
447
|
+
if (this.durationValue <= 0) return;
|
|
448
|
+
this.#dismiss.set(message, () => this.#beginDismiss(message, "timeout"), this.durationValue);
|
|
334
449
|
}
|
|
335
450
|
/**
|
|
336
|
-
*
|
|
337
|
-
* Hover and focus are independent reasons
|
|
338
|
-
* first of them
|
|
451
|
+
* Holds a message's auto-dismiss open while it is hovered or focused
|
|
452
|
+
* (WCAG 2.2 2.2.1). Hover and focus are independent reasons, and the registry
|
|
453
|
+
* banks the time left on the first of them and waits for the last.
|
|
339
454
|
*/
|
|
340
455
|
#pause(message, reason) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (timer.id === 0) return;
|
|
345
|
-
this.#timers.clear(timer.id);
|
|
346
|
-
const remaining = Math.max(1, timer.remaining - (Date.now() - timer.startedAt));
|
|
347
|
-
this.#state.set(message, { id: 0, startedAt: 0, remaining, paused: timer.paused });
|
|
348
|
-
}
|
|
349
|
-
/** Resumes a paused message's auto-dismiss with the banked time. */
|
|
456
|
+
this.#dismiss.pause(message, reason);
|
|
457
|
+
}
|
|
458
|
+
/** Releases one reason, resuming the banked time once no reason is left. */
|
|
350
459
|
#resume(message, reason) {
|
|
351
|
-
|
|
352
|
-
if (!timer) return;
|
|
353
|
-
timer.paused.delete(reason);
|
|
354
|
-
if (timer.paused.size > 0) return;
|
|
355
|
-
if (timer.id !== 0) return;
|
|
356
|
-
this.#startTimer(message, timer.remaining);
|
|
460
|
+
this.#dismiss.resume(message, reason);
|
|
357
461
|
}
|
|
358
462
|
/** Releases every per-message resource: timer, stacking slot, pause listeners. */
|
|
359
463
|
#forget(message) {
|
|
360
|
-
|
|
361
|
-
if (timer?.id) this.#timers.clear(timer.id);
|
|
362
|
-
this.#state.delete(message);
|
|
464
|
+
this.#dismiss.clear(message);
|
|
363
465
|
const index = this.#order.indexOf(message);
|
|
364
466
|
if (index !== -1) this.#order.splice(index, 1);
|
|
365
467
|
this.#unbindPause(message);
|
|
366
468
|
}
|
|
367
469
|
/** Marks a message leaving, then removes it after its CSS transition and emits dismiss. */
|
|
368
470
|
#beginDismiss(message, reason) {
|
|
369
|
-
if (!this.#
|
|
471
|
+
if (!this.#order.includes(message)) return;
|
|
370
472
|
this.#forget(message);
|
|
371
473
|
this.#leaving.add(message);
|
|
372
474
|
message.setAttribute("data-flash-state", "leaving");
|
|
@@ -100,6 +100,12 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
100
100
|
}
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
+
// src/utils/event_owner.ts
|
|
104
|
+
function ownerIndex(candidates, node) {
|
|
105
|
+
if (!(node instanceof Node)) return -1;
|
|
106
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
107
|
+
}
|
|
108
|
+
|
|
103
109
|
// src/utils/microtask_coalescer.ts
|
|
104
110
|
var MicrotaskCoalescer = class {
|
|
105
111
|
#run;
|
|
@@ -357,7 +363,7 @@ var FormFieldController = class _FormFieldController extends Controller {
|
|
|
357
363
|
return this.errorTargets.includes(target);
|
|
358
364
|
return record.attributeName === "id" && [...this.descriptionTargets, ...this.errorTargets].includes(target);
|
|
359
365
|
}
|
|
360
|
-
return this.errorTargets
|
|
366
|
+
return ownerIndex(this.errorTargets, target) !== -1;
|
|
361
367
|
}
|
|
362
368
|
/** Returns borrowed control ARIA before Turbo snapshots the page. */
|
|
363
369
|
#rewindForCache() {
|
|
@@ -103,6 +103,30 @@ var DetachGate = class _DetachGate {
|
|
|
103
103
|
}
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
// src/utils/listener_set.ts
|
|
107
|
+
var ListenerSet = class {
|
|
108
|
+
/** The generation every `add` joins until the next `dispose()`. */
|
|
109
|
+
#abort = new AbortController();
|
|
110
|
+
/**
|
|
111
|
+
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
112
|
+
*
|
|
113
|
+
* The set supplies the signal, so `options` carries everything else the DOM
|
|
114
|
+
* accepts — `capture` included, which has to match at release time and no
|
|
115
|
+
* longer has a second place to drift from.
|
|
116
|
+
*/
|
|
117
|
+
add(target, type, handler, options) {
|
|
118
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Releases every listener of the open generation, synchronously, and opens the
|
|
122
|
+
* next one. Idempotent, and safe before anything has been added.
|
|
123
|
+
*/
|
|
124
|
+
dispose() {
|
|
125
|
+
this.#abort.abort();
|
|
126
|
+
this.#abort = new AbortController();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
106
130
|
// src/utils/min_duration_floor.ts
|
|
107
131
|
var MinDurationFloor = class {
|
|
108
132
|
#timers;
|
|
@@ -218,6 +242,7 @@ var FrameLoadingController = class extends Controller {
|
|
|
218
242
|
#floor = new MinDurationFloor(this.#timeouts);
|
|
219
243
|
#gate = new DetachGate();
|
|
220
244
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
245
|
+
#listeners = new ListenerSet();
|
|
221
246
|
#loading = false;
|
|
222
247
|
/**
|
|
223
248
|
* The optional targets this controller revealed, and the content it marked inert.
|
|
@@ -244,14 +269,12 @@ var FrameLoadingController = class extends Controller {
|
|
|
244
269
|
connect() {
|
|
245
270
|
this.#gate.cancel();
|
|
246
271
|
this.#beforeCache.activate();
|
|
247
|
-
this.element
|
|
248
|
-
this.element
|
|
249
|
-
this.element
|
|
272
|
+
this.#listeners.add(this.element, "turbo:before-fetch-request", this.#onStart);
|
|
273
|
+
this.#listeners.add(this.element, "turbo:frame-load", this.#onEnd);
|
|
274
|
+
this.#listeners.add(this.element, "turbo:fetch-request-error", this.#onEnd);
|
|
250
275
|
}
|
|
251
276
|
disconnect() {
|
|
252
|
-
this.
|
|
253
|
-
this.element.removeEventListener("turbo:frame-load", this.#onEnd);
|
|
254
|
-
this.element.removeEventListener("turbo:fetch-request-error", this.#onEnd);
|
|
277
|
+
this.#listeners.dispose();
|
|
255
278
|
this.#beforeCache.deactivate();
|
|
256
279
|
this.#gate.disconnected(this, () => this.#teardown());
|
|
257
280
|
}
|
|
@@ -2,11 +2,6 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/highlight_controller.ts
|
|
4
4
|
|
|
5
|
-
// src/utils/reduced_motion.ts
|
|
6
|
-
function prefersReducedMotion() {
|
|
7
|
-
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
5
|
// src/utils/safe_timeout.ts
|
|
11
6
|
var TimerRegistry = class {
|
|
12
7
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -60,6 +55,54 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
60
55
|
}
|
|
61
56
|
};
|
|
62
57
|
|
|
58
|
+
// src/utils/keyed_timers.ts
|
|
59
|
+
var KeyedTimers = class {
|
|
60
|
+
#timers = new SafeTimeout();
|
|
61
|
+
/** The pending timer of each key; an entry lives exactly as long as its timer. */
|
|
62
|
+
#pending = /* @__PURE__ */ new Map();
|
|
63
|
+
/**
|
|
64
|
+
* Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had
|
|
65
|
+
* pending. The entry is dropped before the callback runs, so the callback sees
|
|
66
|
+
* the key unarmed and may arm it again for the next round.
|
|
67
|
+
*/
|
|
68
|
+
set(key, callback, delay) {
|
|
69
|
+
this.clear(key);
|
|
70
|
+
const id = this.#timers.set(() => {
|
|
71
|
+
this.#pending.delete(key);
|
|
72
|
+
callback();
|
|
73
|
+
}, delay);
|
|
74
|
+
this.#pending.set(key, id);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Cancels `key`'s pending timer, if it has one.
|
|
78
|
+
*
|
|
79
|
+
* A timer id is a positive integer, so `-1` stands for "nothing pending" and
|
|
80
|
+
* the registry ignores an id it does not own — the unarmed case needs no
|
|
81
|
+
* branch of its own, and no other key's timer can be reached from here.
|
|
82
|
+
*/
|
|
83
|
+
clear(key) {
|
|
84
|
+
this.#timers.clear(this.#pending.get(key) ?? -1);
|
|
85
|
+
this.#pending.delete(key);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Cancels every pending timer and forgets every key. Call this from a
|
|
89
|
+
* controller's `disconnect()` so no timer, and no entry, outlives the element.
|
|
90
|
+
*/
|
|
91
|
+
clearAll() {
|
|
92
|
+
this.#timers.clearAll();
|
|
93
|
+
this.#pending.clear();
|
|
94
|
+
}
|
|
95
|
+
/** Whether `key` has a timer pending. */
|
|
96
|
+
has(key) {
|
|
97
|
+
return this.#pending.has(key);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// src/utils/reduced_motion.ts
|
|
102
|
+
function prefersReducedMotion() {
|
|
103
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
104
|
+
}
|
|
105
|
+
|
|
63
106
|
// src/controllers/highlight_controller.ts
|
|
64
107
|
var hookOwners = /* @__PURE__ */ new WeakMap();
|
|
65
108
|
var HighlightController = class extends Controller {
|
|
@@ -68,14 +111,11 @@ var HighlightController = class extends Controller {
|
|
|
68
111
|
observe: { type: Boolean, default: false }
|
|
69
112
|
};
|
|
70
113
|
static events = ["start", "end"];
|
|
71
|
-
#timeouts = new SafeTimeout();
|
|
72
114
|
/**
|
|
73
|
-
* The removal timer this connection has outstanding for
|
|
74
|
-
*
|
|
75
|
-
* so a cleared id can never be matched against a recycled one. Which connection owns
|
|
76
|
-
* an element's hook is answered by the shared owner registry above.
|
|
115
|
+
* The removal timer this connection has outstanding for each element. Which
|
|
116
|
+
* connection owns an element's hook is answered by the shared owner registry above.
|
|
77
117
|
*/
|
|
78
|
-
#
|
|
118
|
+
#removal = new KeyedTimers();
|
|
79
119
|
#observer = null;
|
|
80
120
|
connect() {
|
|
81
121
|
this.#clearArrivedHook(this.element);
|
|
@@ -92,8 +132,7 @@ var HighlightController = class extends Controller {
|
|
|
92
132
|
disconnect() {
|
|
93
133
|
this.#observer?.disconnect();
|
|
94
134
|
this.#observer = null;
|
|
95
|
-
this.#
|
|
96
|
-
this.#pending = /* @__PURE__ */ new WeakMap();
|
|
135
|
+
this.#removal.clearAll();
|
|
97
136
|
}
|
|
98
137
|
/** Drops a hook that arrived with the DOM, along with this connection's claim on it. */
|
|
99
138
|
#clearArrivedHook(el) {
|
|
@@ -114,13 +153,15 @@ var HighlightController = class extends Controller {
|
|
|
114
153
|
this.#releasePending(el);
|
|
115
154
|
el.setAttribute("data-highlight", "true");
|
|
116
155
|
this.dispatch("start", { target: el, detail: { element: el } });
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
156
|
+
this.#removal.set(
|
|
157
|
+
el,
|
|
158
|
+
() => {
|
|
159
|
+
hookOwners.delete(el);
|
|
160
|
+
el.removeAttribute("data-highlight");
|
|
161
|
+
this.dispatch("end", { target: el, detail: { element: el } });
|
|
162
|
+
},
|
|
163
|
+
this.durationValue
|
|
164
|
+
);
|
|
124
165
|
hookOwners.set(el, this);
|
|
125
166
|
}
|
|
126
167
|
/**
|
|
@@ -133,10 +174,9 @@ var HighlightController = class extends Controller {
|
|
|
133
174
|
if (owner !== void 0 && owner !== this) owner.#cancelPending(el);
|
|
134
175
|
this.#cancelPending(el);
|
|
135
176
|
}
|
|
136
|
-
/** Releases `el`'s pending removal timer, if it has one. */
|
|
177
|
+
/** Releases `el`'s pending removal timer, if it has one, and this connection's claim. */
|
|
137
178
|
#cancelPending(el) {
|
|
138
|
-
this.#
|
|
139
|
-
this.#pending.delete(el);
|
|
179
|
+
this.#removal.clear(el);
|
|
140
180
|
hookOwners.delete(el);
|
|
141
181
|
}
|
|
142
182
|
};
|
|
@@ -49,6 +49,30 @@ var CompositionTracker = class {
|
|
|
49
49
|
};
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
// src/utils/declared_value.ts
|
|
53
|
+
function parseDeclared(raw, parse, fallback) {
|
|
54
|
+
try {
|
|
55
|
+
return parse(raw);
|
|
56
|
+
} catch {
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function compileRegExp(source, anchor = "none") {
|
|
61
|
+
return parseDeclared(
|
|
62
|
+
source,
|
|
63
|
+
(text) => {
|
|
64
|
+
const bare = new RegExp(text);
|
|
65
|
+
return anchor === "exact" ? new RegExp(`^(?:${text})$`) : bare;
|
|
66
|
+
},
|
|
67
|
+
null
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
function parseJsonObject(raw) {
|
|
71
|
+
const parsed = parseDeclared(raw, (text) => JSON.parse(text), null);
|
|
72
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
// src/utils/focus_candidate.ts
|
|
53
77
|
function inheritsFieldsetDisabled(control) {
|
|
54
78
|
let fieldset = control.closest("fieldset[disabled]");
|
|
@@ -118,22 +142,19 @@ function applyMask(value, pattern, tokens) {
|
|
|
118
142
|
}
|
|
119
143
|
function compileTokens(declaration) {
|
|
120
144
|
const map = /* @__PURE__ */ new Map();
|
|
121
|
-
for (const [key, source] of Object.entries(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
for (const [key, source] of Object.entries(DEFAULT_TOKENS)) {
|
|
146
|
+
const compiled = compileRegExp(source, "exact");
|
|
147
|
+
if (compiled) map.set(key, compiled);
|
|
148
|
+
}
|
|
149
|
+
for (const [key, source] of Object.entries(parseTokens(declaration))) {
|
|
150
|
+
const compiled = compileRegExp(source, "exact");
|
|
151
|
+
if (compiled) map.set(key, compiled);
|
|
126
152
|
}
|
|
127
153
|
return map;
|
|
128
154
|
}
|
|
129
155
|
function parseTokens(declaration) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
parsed = JSON.parse(declaration);
|
|
133
|
-
} catch {
|
|
134
|
-
return {};
|
|
135
|
-
}
|
|
136
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
156
|
+
const parsed = parseJsonObject(declaration);
|
|
157
|
+
if (parsed === null) return {};
|
|
137
158
|
const tokens = {};
|
|
138
159
|
for (const [key, source] of Object.entries(parsed)) {
|
|
139
160
|
if (typeof source === "string") tokens[key] = source;
|
|
@@ -2,6 +2,15 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/intersection_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/declared_value.ts
|
|
6
|
+
function parseDeclared(raw, parse, fallback) {
|
|
7
|
+
try {
|
|
8
|
+
return parse(raw);
|
|
9
|
+
} catch {
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
// src/utils/intersection_watcher.ts
|
|
6
15
|
function isBeforeRootStart(entry) {
|
|
7
16
|
const rect = entry.boundingClientRect;
|
|
@@ -11,11 +20,7 @@ function isBeforeRootStart(entry) {
|
|
|
11
20
|
}
|
|
12
21
|
function queryRoot(selector) {
|
|
13
22
|
if (!selector) return null;
|
|
14
|
-
|
|
15
|
-
return document.querySelector(selector);
|
|
16
|
-
} catch {
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
23
|
+
return parseDeclared(selector, (raw) => document.querySelector(raw), null);
|
|
19
24
|
}
|
|
20
25
|
var IntersectionWatcher = class {
|
|
21
26
|
#onEntries;
|
|
@@ -2,14 +2,19 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/lazy_frame_controller.ts
|
|
4
4
|
|
|
5
|
-
// src/utils/
|
|
6
|
-
function
|
|
7
|
-
if (!selector) return null;
|
|
5
|
+
// src/utils/declared_value.ts
|
|
6
|
+
function parseDeclared(raw, parse, fallback) {
|
|
8
7
|
try {
|
|
9
|
-
return
|
|
8
|
+
return parse(raw);
|
|
10
9
|
} catch {
|
|
10
|
+
return fallback;
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/utils/intersection_watcher.ts
|
|
15
|
+
function queryRoot(selector) {
|
|
16
|
+
if (!selector) return null;
|
|
17
|
+
return parseDeclared(selector, (raw) => document.querySelector(raw), null);
|
|
13
18
|
}
|
|
14
19
|
var IntersectionWatcher = class {
|
|
15
20
|
#onEntries;
|
|
@@ -2,6 +2,40 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/local_time_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/intl_format.ts
|
|
6
|
+
var formatters = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
function intlFormatter(factory, locale, options, fallbackLocale) {
|
|
8
|
+
const cache = cacheFor(factory);
|
|
9
|
+
const key = cacheKey(locale, options, fallbackLocale);
|
|
10
|
+
if (cache.has(key)) return cache.get(key);
|
|
11
|
+
const formatter = construct(factory, locale, options);
|
|
12
|
+
cache.set(key, formatter);
|
|
13
|
+
return formatter;
|
|
14
|
+
}
|
|
15
|
+
function cacheFor(factory) {
|
|
16
|
+
const existing = formatters.get(factory);
|
|
17
|
+
if (existing) return existing;
|
|
18
|
+
const created = /* @__PURE__ */ new Map();
|
|
19
|
+
formatters.set(factory, created);
|
|
20
|
+
return created;
|
|
21
|
+
}
|
|
22
|
+
function cacheKey(locale, options, fallbackLocale) {
|
|
23
|
+
return JSON.stringify([locale, options, fallbackLocale]);
|
|
24
|
+
}
|
|
25
|
+
function construct(factory, locale, options, fallbackLocale) {
|
|
26
|
+
try {
|
|
27
|
+
return new factory(locale, options);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (!(error instanceof RangeError)) throw error;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/utils/locale.ts
|
|
35
|
+
function resolveLocale(element, declared = "") {
|
|
36
|
+
return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
5
39
|
// src/utils/microtask_coalescer.ts
|
|
6
40
|
var MicrotaskCoalescer = class {
|
|
7
41
|
#run;
|
|
@@ -150,8 +184,9 @@ var LocalTimeController = class extends Controller {
|
|
|
150
184
|
/**
|
|
151
185
|
* Formats `date` with `Intl.DateTimeFormat`, including each style only when it
|
|
152
186
|
* is a valid keyword (so a consumer can show date-only or time-only by clearing
|
|
153
|
-
* the other). Returns `null` when neither style is usable or
|
|
154
|
-
* the caller can leave the
|
|
187
|
+
* the other). Returns `null` when neither style is usable or the declaration
|
|
188
|
+
* names a locale or time zone `Intl` rejects, so the caller can leave the
|
|
189
|
+
* authored text untouched.
|
|
155
190
|
*/
|
|
156
191
|
#applyFormat(date, dateStyle, timeStyle) {
|
|
157
192
|
const options = {
|
|
@@ -160,15 +195,12 @@ var LocalTimeController = class extends Controller {
|
|
|
160
195
|
};
|
|
161
196
|
if (options.dateStyle === void 0 && options.timeStyle === void 0) return null;
|
|
162
197
|
if (this.timeZoneValue.length > 0) options.timeZone = this.timeZoneValue;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
/** Locale precedence: the value, then the nearest `lang` up the ancestor chain. */
|
|
170
|
-
get #locale() {
|
|
171
|
-
return this.localeValue || this.element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
198
|
+
const formatter = intlFormatter(
|
|
199
|
+
Intl.DateTimeFormat,
|
|
200
|
+
resolveLocale(this.element, this.localeValue),
|
|
201
|
+
options
|
|
202
|
+
);
|
|
203
|
+
return formatter === null ? null : formatter.format(date);
|
|
172
204
|
}
|
|
173
205
|
};
|
|
174
206
|
|