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
|
@@ -162,6 +162,16 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
162
162
|
}
|
|
163
163
|
};
|
|
164
164
|
|
|
165
|
+
// src/utils/state_reason.ts
|
|
166
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
167
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
168
|
+
function stateReasonFor(event) {
|
|
169
|
+
if (!event) return "api";
|
|
170
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
171
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
172
|
+
return "user";
|
|
173
|
+
}
|
|
174
|
+
|
|
165
175
|
// src/controllers/navigation_menu_controller.ts
|
|
166
176
|
var NavigationMenuController = class extends Controller {
|
|
167
177
|
static targets = ["trigger", "panel", "hoverArea"];
|
|
@@ -170,6 +180,7 @@ var NavigationMenuController = class extends Controller {
|
|
|
170
180
|
hoverDelay: { type: Number, default: 150 }
|
|
171
181
|
};
|
|
172
182
|
static actions = ["onTriggerKeydown", "toggle"];
|
|
183
|
+
static events = ["close", "open"];
|
|
173
184
|
/** Open/close delay timers for hover mode; cleared together on disconnect. */
|
|
174
185
|
#hoverTimers = new SafeTimeout();
|
|
175
186
|
/**
|
|
@@ -190,19 +201,23 @@ var NavigationMenuController = class extends Controller {
|
|
|
190
201
|
* rebuild would re-wire hover listeners onto an already-disconnected nav.
|
|
191
202
|
*/
|
|
192
203
|
#connected = false;
|
|
204
|
+
/** Whether state moves are reported: set once `connect()` settled the baseline. */
|
|
205
|
+
#reporting = false;
|
|
193
206
|
/** Escape-stack membership while any panel is open; the shared resolver dismisses via it. */
|
|
194
207
|
#escapeLayer = new EscapeLayer();
|
|
195
208
|
/** Establishes the closed baseline and the dismissal listeners. */
|
|
196
209
|
connect() {
|
|
197
210
|
this.#connected = true;
|
|
198
|
-
this.#closeAll();
|
|
211
|
+
this.#closeAll("api");
|
|
199
212
|
document.addEventListener("click", this.#onOutsideClick, true);
|
|
200
213
|
this.element.addEventListener("focusout", this.#onFocusOut);
|
|
201
214
|
if (this.openOnHoverValue) this.#addHoverListeners();
|
|
215
|
+
this.#reporting = true;
|
|
202
216
|
}
|
|
203
217
|
/** Removes every listener, pending hover timer, and stack membership taken while connected. */
|
|
204
218
|
disconnect() {
|
|
205
219
|
this.#connected = false;
|
|
220
|
+
this.#reporting = false;
|
|
206
221
|
this.#escapeLayer.deactivate();
|
|
207
222
|
document.removeEventListener("click", this.#onOutsideClick, true);
|
|
208
223
|
this.element.removeEventListener("focusout", this.#onFocusOut);
|
|
@@ -259,13 +274,14 @@ var NavigationMenuController = class extends Controller {
|
|
|
259
274
|
*/
|
|
260
275
|
toggle(event) {
|
|
261
276
|
const trigger = event.currentTarget;
|
|
277
|
+
const reason = stateReasonFor(event);
|
|
262
278
|
this.#hoverTimers.clearAll();
|
|
263
279
|
if (!this.#isExpanded(trigger)) {
|
|
264
|
-
this.#openPanel(trigger);
|
|
280
|
+
this.#openPanel(trigger, reason);
|
|
265
281
|
return;
|
|
266
282
|
}
|
|
267
283
|
if (this.openOnHoverValue && this.#hoveredTrigger === trigger) return;
|
|
268
|
-
this.#closePanel(trigger);
|
|
284
|
+
this.#closePanel(trigger, reason);
|
|
269
285
|
}
|
|
270
286
|
/**
|
|
271
287
|
* `ArrowLeft`/`ArrowRight` move focus between triggers (keeping Tab order).
|
|
@@ -297,24 +313,27 @@ var NavigationMenuController = class extends Controller {
|
|
|
297
313
|
* already-open panel only re-asserts the Escape layer: the close/open
|
|
298
314
|
* round-trip would rewrite `aria-expanded` and `hidden` for no state change.
|
|
299
315
|
*/
|
|
300
|
-
#openPanel(trigger) {
|
|
316
|
+
#openPanel(trigger, reason) {
|
|
301
317
|
if (trigger.getAttribute("aria-disabled") === "true") return;
|
|
302
318
|
if (this.#isExpanded(trigger)) {
|
|
303
319
|
this.#syncEscapeLayer();
|
|
304
320
|
return;
|
|
305
321
|
}
|
|
306
|
-
this.#closeAll();
|
|
322
|
+
this.#closeAll(reason);
|
|
307
323
|
const panel = this.#panelFor(trigger);
|
|
308
324
|
if (!panel) return;
|
|
309
325
|
panel.hidden = false;
|
|
310
326
|
trigger.setAttribute("aria-expanded", "true");
|
|
327
|
+
this.#report("open", trigger, panel, reason);
|
|
311
328
|
this.#syncEscapeLayer();
|
|
312
329
|
}
|
|
313
|
-
/** Closes `trigger`'s panel
|
|
314
|
-
#closePanel(trigger) {
|
|
330
|
+
/** Closes `trigger`'s panel, reflects the collapsed state, and reports a move. */
|
|
331
|
+
#closePanel(trigger, reason) {
|
|
332
|
+
const was = this.#isExpanded(trigger);
|
|
315
333
|
const panel = this.#panelFor(trigger);
|
|
316
334
|
if (panel) panel.hidden = true;
|
|
317
335
|
trigger.setAttribute("aria-expanded", "false");
|
|
336
|
+
if (was && panel) this.#report("close", trigger, panel, reason);
|
|
318
337
|
this.#syncEscapeLayer();
|
|
319
338
|
}
|
|
320
339
|
/**
|
|
@@ -322,8 +341,16 @@ var NavigationMenuController = class extends Controller {
|
|
|
322
341
|
* triggers, so a trigger whose panel was hidden by other means cannot keep
|
|
323
342
|
* advertising an open panel.
|
|
324
343
|
*/
|
|
325
|
-
#closeAll() {
|
|
326
|
-
for (const trigger of this.triggerTargets) this.#closePanel(trigger);
|
|
344
|
+
#closeAll(reason) {
|
|
345
|
+
for (const trigger of this.triggerTargets) this.#closePanel(trigger, reason);
|
|
346
|
+
}
|
|
347
|
+
/** Names the panel that moved, so a subscriber does not have to re-derive it. */
|
|
348
|
+
#report(name, trigger, panel, reason) {
|
|
349
|
+
if (!this.#reporting) return;
|
|
350
|
+
const index = this.triggerTargets.indexOf(trigger);
|
|
351
|
+
const detail = { reason, index, panel };
|
|
352
|
+
if (name === "open") this.dispatch("open", { detail, cancelable: false });
|
|
353
|
+
else this.dispatch("close", { detail, cancelable: false });
|
|
327
354
|
}
|
|
328
355
|
/**
|
|
329
356
|
* Whether a trigger can receive arrow-key focus: not `hidden`, not a natively
|
|
@@ -365,12 +392,12 @@ var NavigationMenuController = class extends Controller {
|
|
|
365
392
|
const open = this.#openTrigger;
|
|
366
393
|
if (!open) return;
|
|
367
394
|
this.#hoverTimers.clearAll();
|
|
368
|
-
this.#closePanel(open);
|
|
395
|
+
this.#closePanel(open, "escape");
|
|
369
396
|
open.focus();
|
|
370
397
|
}
|
|
371
398
|
/** Closes panels when a click lands outside the nav element. */
|
|
372
399
|
#onOutsideClick = (event) => {
|
|
373
|
-
if (this.#isAnyOpen && !this.element.contains(event.target)) this.#closeAll();
|
|
400
|
+
if (this.#isAnyOpen && !this.element.contains(event.target)) this.#closeAll("outside");
|
|
374
401
|
};
|
|
375
402
|
/**
|
|
376
403
|
* Closes (without restoring focus) when focus leaves the nav for a known
|
|
@@ -383,9 +410,13 @@ var NavigationMenuController = class extends Controller {
|
|
|
383
410
|
#onFocusOut = (event) => {
|
|
384
411
|
const next = event.relatedTarget;
|
|
385
412
|
if (!(next instanceof Node) || this.element.contains(next)) return;
|
|
386
|
-
this.#closeAll();
|
|
413
|
+
this.#closeAll("focus");
|
|
387
414
|
};
|
|
388
|
-
/**
|
|
415
|
+
/**
|
|
416
|
+
* Opens a trigger's panel after the hover delay (hover mode).
|
|
417
|
+
*
|
|
418
|
+
* @stimeoRuntimeOnly `hoverDelay` is the delay of the one hover timer this entry arms.
|
|
419
|
+
*/
|
|
389
420
|
#onPointerEnter = (event) => {
|
|
390
421
|
const trigger = this.#triggerForHover(event.currentTarget);
|
|
391
422
|
if (!trigger) return;
|
|
@@ -398,6 +429,8 @@ var NavigationMenuController = class extends Controller {
|
|
|
398
429
|
* pointer moved directly into another part of the hover region (an adjacent
|
|
399
430
|
* trigger, panel, or hoverArea): crossing a shared edge must not schedule a
|
|
400
431
|
* spurious close, e.g. a hoverArea whose panel sits outside it as a sibling.
|
|
432
|
+
*
|
|
433
|
+
* @stimeoRuntimeOnly `hoverDelay` is the delay of the one hover timer this exit arms.
|
|
401
434
|
*/
|
|
402
435
|
#onPointerLeave = (event) => {
|
|
403
436
|
const next = event instanceof MouseEvent ? event.relatedTarget : null;
|
|
@@ -416,7 +449,7 @@ var NavigationMenuController = class extends Controller {
|
|
|
416
449
|
if (trigger.getAttribute("aria-disabled") === "true") return;
|
|
417
450
|
const open = this.#openTrigger;
|
|
418
451
|
if (open && open !== trigger && this.#holdsFocus(this.#panelFor(open))) open.focus();
|
|
419
|
-
this.#openPanel(trigger);
|
|
452
|
+
this.#openPanel(trigger, "pointer");
|
|
420
453
|
}
|
|
421
454
|
/**
|
|
422
455
|
* Hover-driven close: skipped while focus sits inside the panel it would hide,
|
|
@@ -427,7 +460,7 @@ var NavigationMenuController = class extends Controller {
|
|
|
427
460
|
#closeFromHover() {
|
|
428
461
|
const open = this.#openTrigger;
|
|
429
462
|
if (open && this.#holdsFocus(this.#panelFor(open))) return;
|
|
430
|
-
this.#closeAll();
|
|
463
|
+
this.#closeAll("pointer");
|
|
431
464
|
}
|
|
432
465
|
/** Whether `panel` exists and currently contains the focused element. */
|
|
433
466
|
#holdsFocus(panel) {
|
|
@@ -228,12 +228,28 @@ var TabindexLoan = class {
|
|
|
228
228
|
}
|
|
229
229
|
};
|
|
230
230
|
|
|
231
|
+
// src/utils/target_selector.ts
|
|
232
|
+
function targetSelector(identifier, name) {
|
|
233
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
234
|
+
}
|
|
235
|
+
|
|
231
236
|
// src/controllers/nested_form_controller.ts
|
|
232
|
-
var
|
|
233
|
-
var
|
|
234
|
-
var DESTROY_FLAG_SELECTOR = '[data-stimeo--nested-form-target="destroyFlag"]';
|
|
237
|
+
var REMOVE_PART = "remove";
|
|
238
|
+
var DESTROY_FLAG_PART = "destroyFlag";
|
|
235
239
|
var DESTROYED_VALUES = /* @__PURE__ */ new Set(["1", "true"]);
|
|
236
240
|
var NestedFormController = class extends Controller {
|
|
241
|
+
/** Selects any root of this controller; the nearest one owns an element. */
|
|
242
|
+
get #rootSelector() {
|
|
243
|
+
return `[data-controller~="${this.identifier}"]`;
|
|
244
|
+
}
|
|
245
|
+
/** Selects per-row remove buttons, in the namespace this controller is registered under. */
|
|
246
|
+
get #removeSelector() {
|
|
247
|
+
return targetSelector(this.identifier, REMOVE_PART);
|
|
248
|
+
}
|
|
249
|
+
/** Selects the hidden `_destroy` input a persisted row carries. */
|
|
250
|
+
get #destroyFlagSelector() {
|
|
251
|
+
return targetSelector(this.identifier, DESTROY_FLAG_PART);
|
|
252
|
+
}
|
|
237
253
|
static targets = ["list", "template", "add", "remove", "destroyFlag"];
|
|
238
254
|
static values = {
|
|
239
255
|
min: { type: Number, default: 0 },
|
|
@@ -265,7 +281,7 @@ var NestedFormController = class extends Controller {
|
|
|
265
281
|
*/
|
|
266
282
|
#onClick = (event) => {
|
|
267
283
|
const target = event.target;
|
|
268
|
-
const button = target?.closest(
|
|
284
|
+
const button = target?.closest(this.#removeSelector);
|
|
269
285
|
if (!button || this.#ownerOf(button) !== this.element) return;
|
|
270
286
|
const row = this.#rowContaining(button);
|
|
271
287
|
if (row) this.#removeRow(row);
|
|
@@ -362,6 +378,9 @@ var NestedFormController = class extends Controller {
|
|
|
362
378
|
return;
|
|
363
379
|
}
|
|
364
380
|
const row = added[0];
|
|
381
|
+
for (const node of Array.from(list.childNodes).slice(beforeNodes)) {
|
|
382
|
+
if (node !== row) node.remove();
|
|
383
|
+
}
|
|
365
384
|
this.#refresh();
|
|
366
385
|
firstTabStop(row)?.focus();
|
|
367
386
|
this.dispatch("add", { detail: { index, element: row } });
|
|
@@ -371,6 +390,8 @@ var NestedFormController = class extends Controller {
|
|
|
371
390
|
* Removes a row: a persisted row (one carrying its own `destroyFlag`) has the
|
|
372
391
|
* flag set to `1` and is hidden so Rails destroys it on submit; an unsaved row
|
|
373
392
|
* is dropped from the DOM. Returns focus to a surviving row. No-ops at `min`.
|
|
393
|
+
*
|
|
394
|
+
* @stimeoRuntimeOnly `min` decides whether this one removal is allowed.
|
|
374
395
|
*/
|
|
375
396
|
#removeRow(row) {
|
|
376
397
|
const rows = this.#effectiveRows;
|
|
@@ -426,7 +447,11 @@ var NestedFormController = class extends Controller {
|
|
|
426
447
|
this.#tabindex.lend(this.element);
|
|
427
448
|
this.element.focus();
|
|
428
449
|
}
|
|
429
|
-
/**
|
|
450
|
+
/**
|
|
451
|
+
* Recomputes the count and min/max hooks from the DOM and records them as published.
|
|
452
|
+
*
|
|
453
|
+
* @stimeoRenderRoot
|
|
454
|
+
*/
|
|
430
455
|
#refresh() {
|
|
431
456
|
if (!this.hasListTarget) return;
|
|
432
457
|
const count = this.#effectiveRows.length;
|
|
@@ -455,7 +480,11 @@ var NestedFormController = class extends Controller {
|
|
|
455
480
|
const moved = previous.count !== current.count || previous.atMin !== current.atMin || previous.atMax !== current.atMax;
|
|
456
481
|
if (moved) this.dispatch("reconcile", { detail: { ...current } });
|
|
457
482
|
}
|
|
458
|
-
/**
|
|
483
|
+
/**
|
|
484
|
+
* Bridges the count change to the shared announcer when configured.
|
|
485
|
+
*
|
|
486
|
+
* @stimeoRuntimeOnly `announce` and `countMessage` word the one announcement of this change.
|
|
487
|
+
*/
|
|
459
488
|
#announce() {
|
|
460
489
|
if (!this.announceValue || this.countMessageValue === "") return;
|
|
461
490
|
announce(fillTemplate(this.countMessageValue, { count: this.#effectiveRows.length }));
|
|
@@ -514,14 +543,14 @@ var NestedFormController = class extends Controller {
|
|
|
514
543
|
}
|
|
515
544
|
/** The row's own destroy flag, skipping flags owned by a nested inner form. */
|
|
516
545
|
#destroyFlagOf(row) {
|
|
517
|
-
for (const flag of row.querySelectorAll(
|
|
546
|
+
for (const flag of row.querySelectorAll(this.#destroyFlagSelector)) {
|
|
518
547
|
if (this.#ownerOf(flag) === this.element) return flag;
|
|
519
548
|
}
|
|
520
549
|
return null;
|
|
521
550
|
}
|
|
522
551
|
/** The nearest nested-form root that owns `el`. */
|
|
523
552
|
#ownerOf(el) {
|
|
524
|
-
return el.closest(
|
|
553
|
+
return el.closest(this.#rootSelector);
|
|
525
554
|
}
|
|
526
555
|
/** The nearest ancestor of `el` that is a direct child of the list, else null. */
|
|
527
556
|
#rowContaining(el) {
|
|
@@ -105,6 +105,9 @@ var NetworkStatusController = class extends Controller {
|
|
|
105
105
|
*
|
|
106
106
|
* The event goes out last, so a listener reading `data-state` or a banner's
|
|
107
107
|
* visibility sees the state the transition landed on rather than the previous one.
|
|
108
|
+
*
|
|
109
|
+
* @stimeoRuntimeOnly The texts word the one announcement of this transition; the state it shows
|
|
110
|
+
* comes from the browser.
|
|
108
111
|
*/
|
|
109
112
|
#update(online) {
|
|
110
113
|
if (online === this.#online) return;
|
|
@@ -126,7 +129,12 @@ var NetworkStatusController = class extends Controller {
|
|
|
126
129
|
if (this.hasOnlineTarget) this.onlineTarget.hidden = true;
|
|
127
130
|
if (this.hasOfflineTarget) this.offlineTarget.hidden = false;
|
|
128
131
|
}
|
|
129
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Shows the recovery banner, optionally auto-hiding it after `onlineAutoHide`.
|
|
134
|
+
*
|
|
135
|
+
* @stimeoRuntimeOnly `onlineAutoHide` is the delay of the one timer that hides the banner this
|
|
136
|
+
* transition shows.
|
|
137
|
+
*/
|
|
130
138
|
#showOnline() {
|
|
131
139
|
if (this.hasOfflineTarget) this.offlineTarget.hidden = true;
|
|
132
140
|
if (!this.hasOnlineTarget) return;
|
|
@@ -79,11 +79,19 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
// src/utils/composition_tracker.ts
|
|
82
|
+
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
83
|
+
"insertCompositionText",
|
|
84
|
+
"insertFromComposition",
|
|
85
|
+
"deleteCompositionText",
|
|
86
|
+
"deleteByComposition"
|
|
87
|
+
]);
|
|
82
88
|
var CompositionTracker = class {
|
|
83
89
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
84
90
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
85
91
|
#onStart;
|
|
86
92
|
#onEnd;
|
|
93
|
+
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
94
|
+
#confirmedTarget = null;
|
|
87
95
|
constructor(options = {}) {
|
|
88
96
|
this.#onStart = options.onStart;
|
|
89
97
|
this.#onEnd = options.onEnd;
|
|
@@ -93,6 +101,7 @@ var CompositionTracker = class {
|
|
|
93
101
|
if (this.#observedTargets.has(target)) return;
|
|
94
102
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
95
103
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
104
|
+
target.addEventListener("keydown", this.#handleKeydown);
|
|
96
105
|
this.#observedTargets.add(target);
|
|
97
106
|
}
|
|
98
107
|
/** Stops tracking one target and clears any active composition it owned. */
|
|
@@ -100,31 +109,74 @@ var CompositionTracker = class {
|
|
|
100
109
|
if (!this.#observedTargets.delete(target)) return;
|
|
101
110
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
102
111
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
112
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
103
113
|
this.#activeTargets.delete(target);
|
|
114
|
+
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
104
115
|
}
|
|
105
116
|
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
106
117
|
disconnect() {
|
|
107
118
|
for (const target of this.#observedTargets) {
|
|
108
119
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
109
120
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
121
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
110
122
|
}
|
|
111
123
|
this.#observedTargets.clear();
|
|
112
124
|
this.#activeTargets.clear();
|
|
125
|
+
this.#confirmedTarget = null;
|
|
113
126
|
}
|
|
114
127
|
/** True when lifecycle tracking or the current event reports composition. */
|
|
115
128
|
isComposing(event) {
|
|
116
129
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
117
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
133
|
+
*
|
|
134
|
+
* Asking closes the window either way, so one confirmation is folded at most
|
|
135
|
+
* once and a consumer asks once per `input`.
|
|
136
|
+
*/
|
|
137
|
+
consumesConfirmedInput(event) {
|
|
138
|
+
const confirmed = this.#confirmedTarget;
|
|
139
|
+
this.#confirmedTarget = null;
|
|
140
|
+
if (confirmed === null || confirmed !== event.target) return false;
|
|
141
|
+
const inputType = event.inputType;
|
|
142
|
+
return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
|
|
143
|
+
}
|
|
118
144
|
#handleStart = (event) => {
|
|
145
|
+
this.#confirmedTarget = null;
|
|
119
146
|
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
120
147
|
this.#onStart?.(event);
|
|
121
148
|
};
|
|
122
149
|
#handleEnd = (event) => {
|
|
123
150
|
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
151
|
+
this.#confirmedTarget = event.target;
|
|
124
152
|
this.#onEnd?.(event);
|
|
125
153
|
};
|
|
154
|
+
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
155
|
+
#handleKeydown = () => {
|
|
156
|
+
this.#confirmedTarget = null;
|
|
157
|
+
};
|
|
126
158
|
};
|
|
127
159
|
|
|
160
|
+
// src/utils/field_mirror.ts
|
|
161
|
+
function commitEdit(control) {
|
|
162
|
+
control.dispatchEvent(new Event("input", { bubbles: true }));
|
|
163
|
+
control.dispatchEvent(new Event("change", { bubbles: true }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/utils/half_width.ts
|
|
167
|
+
var FULL_WIDTH_SHIFT = 65248;
|
|
168
|
+
function halfWidthChar(char) {
|
|
169
|
+
if (char >= "\uFF01" && char <= "\uFF5E") {
|
|
170
|
+
return String.fromCharCode(char.charCodeAt(0) - FULL_WIDTH_SHIFT);
|
|
171
|
+
}
|
|
172
|
+
return char === "\u3000" ? " " : char;
|
|
173
|
+
}
|
|
174
|
+
function toHalfWidth(text) {
|
|
175
|
+
let out = "";
|
|
176
|
+
for (const char of text) out += halfWidthChar(char);
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
128
180
|
// src/utils/microtask_coalescer.ts
|
|
129
181
|
var MicrotaskCoalescer = class {
|
|
130
182
|
#run;
|
|
@@ -358,15 +410,34 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
358
410
|
#globalGuards = null;
|
|
359
411
|
/** Per-target guards allow an old button to be released without touching its replacement. */
|
|
360
412
|
#buttonGuards = /* @__PURE__ */ new Map();
|
|
361
|
-
/**
|
|
362
|
-
|
|
413
|
+
/**
|
|
414
|
+
* Tracks IME lifecycle on the current input, including confirming keys without a
|
|
415
|
+
* signal, and runs a reconciliation the composition held back once it ends.
|
|
416
|
+
*/
|
|
417
|
+
#composition = new CompositionTracker({
|
|
418
|
+
onEnd: () => {
|
|
419
|
+
if (this.#reconcileHeld) this.#repaint.schedule();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
/** A reconciliation that arrived while the input was composing. */
|
|
423
|
+
#reconcileHeld = false;
|
|
363
424
|
/** Restores authored custom-spinbutton ARIA when a target leaves or the controller stops. */
|
|
364
425
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
365
426
|
#ariaValueNow = new AttributeLease("aria-valuenow");
|
|
366
427
|
#ariaValueMin = new AttributeLease("aria-valuemin");
|
|
367
428
|
#ariaValueMax = new AttributeLease("aria-valuemax");
|
|
368
|
-
/**
|
|
429
|
+
/**
|
|
430
|
+
* The number shown last — reconciled or committed by the user — which the next
|
|
431
|
+
* move is measured from; `null` while the field is blank, and before this
|
|
432
|
+
* instance has shown one. A disconnect keeps it for the same instance
|
|
433
|
+
* connecting again.
|
|
434
|
+
*/
|
|
369
435
|
#lastValue = null;
|
|
436
|
+
/**
|
|
437
|
+
* Whether `connect()` has run and `disconnect()` has not since. The pass
|
|
438
|
+
* `connect()` runs takes the baseline without reporting it.
|
|
439
|
+
*/
|
|
440
|
+
#connected = false;
|
|
370
441
|
/** Timers for the hold delay and the suppress-reset safety net. */
|
|
371
442
|
#holdTimeouts = new SafeTimeout();
|
|
372
443
|
/** The running auto-repeat interval (one at a time). */
|
|
@@ -394,9 +465,16 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
394
465
|
window.addEventListener("pointercancel", this.#onPointerEnd, { signal });
|
|
395
466
|
window.addEventListener("blur", this.#onWindowBlur, { signal });
|
|
396
467
|
this.#reconcile();
|
|
468
|
+
this.#connected = true;
|
|
397
469
|
}
|
|
398
|
-
/**
|
|
470
|
+
/**
|
|
471
|
+
* Releases listeners, derived attributes, timers, and the hold state. The number
|
|
472
|
+
* shown last stays: the pass the next `connect()` runs takes a new one without a
|
|
473
|
+
* report from an input that is present, and with none it stays the number an
|
|
474
|
+
* input arriving later is compared with.
|
|
475
|
+
*/
|
|
399
476
|
disconnect() {
|
|
477
|
+
this.#connected = false;
|
|
400
478
|
this.#repaint.cancel();
|
|
401
479
|
this.#composition.disconnect();
|
|
402
480
|
this.#beforeCache.deactivate();
|
|
@@ -418,7 +496,6 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
418
496
|
this.#ariaValueNow.returnAll();
|
|
419
497
|
this.#ariaValueMin.returnAll();
|
|
420
498
|
this.#ariaValueMax.returnAll();
|
|
421
|
-
this.#lastValue = null;
|
|
422
499
|
}
|
|
423
500
|
/** Reconciles the value against a minimum changed by application code or a Turbo morph. */
|
|
424
501
|
minValueChanged() {
|
|
@@ -437,12 +514,15 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
437
514
|
this.#composition.observe(input);
|
|
438
515
|
this.#repaint.schedule();
|
|
439
516
|
}
|
|
440
|
-
/**
|
|
517
|
+
/**
|
|
518
|
+
* Releases ARIA and composition state owned by an input that left the controller.
|
|
519
|
+
* The number it showed stays the baseline, so an input swapped in holding another
|
|
520
|
+
* number is reported as `reconcile`.
|
|
521
|
+
*/
|
|
441
522
|
inputTargetDisconnected(input) {
|
|
442
523
|
this.#composition.unobserve(input);
|
|
443
524
|
this.#releaseInputAria(input);
|
|
444
525
|
this.#stopHold(false);
|
|
445
|
-
this.#lastValue = null;
|
|
446
526
|
this.#repaint.schedule();
|
|
447
527
|
}
|
|
448
528
|
/** Wires focus preservation and hold behavior on a runtime increment target. */
|
|
@@ -520,7 +600,7 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
520
600
|
return;
|
|
521
601
|
}
|
|
522
602
|
event.preventDefault();
|
|
523
|
-
this.#commit(next);
|
|
603
|
+
this.#commit(next, true);
|
|
524
604
|
}
|
|
525
605
|
/**
|
|
526
606
|
* Wires a step button: `pointerdown` keeps focus on the input and arms the
|
|
@@ -624,42 +704,58 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
624
704
|
*
|
|
625
705
|
* @returns Whether the value changed (drives the auto-repeat's bound stop).
|
|
626
706
|
*/
|
|
627
|
-
#commit(raw) {
|
|
707
|
+
#commit(raw, report = false) {
|
|
628
708
|
const value = this.#normalize(raw);
|
|
629
709
|
const changed = this.#lastValue === null || value !== this.#lastValue;
|
|
710
|
+
const shown = this.inputTarget.value;
|
|
630
711
|
this.#write(value);
|
|
631
712
|
this.#lastValue = value;
|
|
713
|
+
if (report && shown !== this.inputTarget.value) commitEdit(this.inputTarget);
|
|
632
714
|
if (changed) this.dispatch("change", { detail: { value } });
|
|
633
715
|
return changed;
|
|
634
716
|
}
|
|
635
717
|
/** Commits an adjacent endpoint/grid value and reports whether it moved. */
|
|
636
718
|
#commitStep(count) {
|
|
637
719
|
if (!this.hasInputTarget) return false;
|
|
638
|
-
return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange));
|
|
720
|
+
return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange), true);
|
|
639
721
|
}
|
|
640
722
|
/**
|
|
641
|
-
* Reflects the current value after a range or
|
|
642
|
-
*
|
|
723
|
+
* Reflects the current value after a range, step or input change, reporting a
|
|
724
|
+
* number that moved from the one shown last as `reconcile`. While the input is
|
|
725
|
+
* composing, its text is the IME's: the pass waits for the composition to end
|
|
726
|
+
* instead of reading and rewriting it. With no input there is nothing to show,
|
|
727
|
+
* and the number shown last stays the baseline for the input that comes next.
|
|
643
728
|
*
|
|
644
729
|
* @stimeoRenderRoot
|
|
645
730
|
*/
|
|
646
731
|
#reconcile() {
|
|
647
|
-
if (!this.hasInputTarget)
|
|
648
|
-
|
|
732
|
+
if (!this.hasInputTarget) return;
|
|
733
|
+
if (this.#composition.isComposing()) {
|
|
734
|
+
this.#reconcileHeld = true;
|
|
649
735
|
return;
|
|
650
736
|
}
|
|
737
|
+
this.#reconcileHeld = false;
|
|
738
|
+
const previous = this.#lastValue;
|
|
651
739
|
if (this.inputTarget.value.trim() !== "") {
|
|
652
|
-
const previous = this.#lastValue;
|
|
653
740
|
const value = this.#normalize(this.#currentValue());
|
|
654
741
|
this.#write(value);
|
|
655
742
|
this.#lastValue = value;
|
|
656
|
-
if (previous !== null && value !== previous) {
|
|
657
|
-
this.dispatch("reconcile", { detail: { value } });
|
|
658
|
-
}
|
|
659
743
|
} else {
|
|
660
744
|
this.#lastValue = null;
|
|
661
745
|
this.#reflectEmpty();
|
|
662
746
|
}
|
|
747
|
+
this.#reportMove(previous);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Reports the number shown as `reconcile` when a change the page made moved it
|
|
751
|
+
* from `previous`. The page moved it, so it is reported apart from `change`,
|
|
752
|
+
* which stays reserved for user edits. A blank field reports nothing, the way a
|
|
753
|
+
* user blanking it does: both events carry a number.
|
|
754
|
+
*/
|
|
755
|
+
#reportMove(previous) {
|
|
756
|
+
const value = this.#lastValue;
|
|
757
|
+
if (!this.#connected || value === null || value === previous) return;
|
|
758
|
+
this.dispatch("reconcile", { detail: { value } });
|
|
663
759
|
}
|
|
664
760
|
/** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */
|
|
665
761
|
#write(value) {
|
|
@@ -719,10 +815,17 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
719
815
|
button.disabled = false;
|
|
720
816
|
button.removeAttribute(OWNED_DISABLED);
|
|
721
817
|
}
|
|
722
|
-
/**
|
|
818
|
+
/**
|
|
819
|
+
* The current numeric value, falling back to a finite min (else 0) when blank.
|
|
820
|
+
* Every read of the field's number comes through here, and the text is read
|
|
821
|
+
* through the shared half-width mapping: a text-type spinbutton holds whatever an
|
|
822
|
+
* IME confirmed, full-width digits and signs included (`34`, `-5`), while a
|
|
823
|
+
* native number field's value is already half-width or empty.
|
|
824
|
+
*/
|
|
723
825
|
#currentValue() {
|
|
724
|
-
const
|
|
725
|
-
|
|
826
|
+
const text = toHalfWidth(this.inputTarget.value);
|
|
827
|
+
const parsed = Number(text);
|
|
828
|
+
if (Number.isFinite(parsed) && text.trim() !== "") return parsed;
|
|
726
829
|
return Number.isFinite(this.minValue) ? this.minValue : 0;
|
|
727
830
|
}
|
|
728
831
|
/** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */
|
|
@@ -57,7 +57,44 @@ var DetachGate = class _DetachGate {
|
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
// src/utils/listener_set.ts
|
|
61
|
+
var ListenerSet = class {
|
|
62
|
+
/** The generation every `add` joins until the next `dispose()`. */
|
|
63
|
+
#abort = new AbortController();
|
|
64
|
+
/**
|
|
65
|
+
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
66
|
+
*
|
|
67
|
+
* The set supplies the signal, so `options` carries everything else the DOM
|
|
68
|
+
* accepts — `capture` included, which has to match at release time and no
|
|
69
|
+
* longer has a second place to drift from.
|
|
70
|
+
*/
|
|
71
|
+
add(target, type, handler, options) {
|
|
72
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Releases every listener of the open generation, synchronously, and opens the
|
|
76
|
+
* next one. Idempotent, and safe before anything has been added.
|
|
77
|
+
*/
|
|
78
|
+
dispose() {
|
|
79
|
+
this.#abort.abort();
|
|
80
|
+
this.#abort = new AbortController();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/utils/transient_hooks.ts
|
|
85
|
+
var TransientHooks = class {
|
|
86
|
+
#attributes;
|
|
87
|
+
constructor(options) {
|
|
88
|
+
this.#attributes = options.attributes;
|
|
89
|
+
}
|
|
90
|
+
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
91
|
+
reset(element) {
|
|
92
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
60
96
|
// src/controllers/optimistic_controller.ts
|
|
97
|
+
var TRANSIENT = new TransientHooks({ attributes: ["data-optimistic"] });
|
|
61
98
|
var HIDDEN_MARKER = "data-optimistic-toggled";
|
|
62
99
|
var BUSY_MARKER = "data-optimistic-busy";
|
|
63
100
|
var AUTHORED_ABSENT = "absent";
|
|
@@ -73,6 +110,7 @@ var OptimisticController = class extends Controller {
|
|
|
73
110
|
/** The submission that owns the applied optimistic state, if any. */
|
|
74
111
|
#pending = null;
|
|
75
112
|
#gate = new DetachGate();
|
|
113
|
+
#listeners = new ListenerSet();
|
|
76
114
|
/** True between `connect()` and `disconnect()`, so a repeat connect rewinds nothing. */
|
|
77
115
|
#connected = false;
|
|
78
116
|
connect() {
|
|
@@ -80,13 +118,12 @@ var OptimisticController = class extends Controller {
|
|
|
80
118
|
this.#gate.cancel();
|
|
81
119
|
this.#connected = true;
|
|
82
120
|
if (restored && this.element.hasAttribute("data-optimistic")) this.#revert();
|
|
83
|
-
this.element
|
|
84
|
-
this.element
|
|
121
|
+
this.#listeners.add(this.element, "turbo:submit-start", this.#onSubmitStart);
|
|
122
|
+
this.#listeners.add(this.element, "turbo:submit-end", this.#onSubmitEnd);
|
|
85
123
|
}
|
|
86
124
|
disconnect() {
|
|
87
125
|
this.#connected = false;
|
|
88
|
-
this.
|
|
89
|
-
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
126
|
+
this.#listeners.dispose();
|
|
90
127
|
this.#gate.disconnected(this, () => {
|
|
91
128
|
this.#pending = null;
|
|
92
129
|
});
|
|
@@ -165,7 +202,7 @@ var OptimisticController = class extends Controller {
|
|
|
165
202
|
* received.
|
|
166
203
|
*/
|
|
167
204
|
#revert() {
|
|
168
|
-
this.element
|
|
205
|
+
TRANSIENT.reset(this.element);
|
|
169
206
|
this.#restore(this.element, "aria-busy", BUSY_MARKER, "true");
|
|
170
207
|
const hidden = new Set(this.hideTargets);
|
|
171
208
|
for (const target of /* @__PURE__ */ new Set([...this.showTargets, ...this.hideTargets])) {
|