stimeo-ui 0.12.0 → 0.14.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 +71 -0
- data/dist/cable/index.js +438 -152
- data/dist/controllers/calendar_controller.js +34 -2
- data/dist/controllers/carousel_controller.js +9 -7
- data/dist/controllers/command_palette_controller.js +2 -2
- data/dist/controllers/conditional_fields_controller.js +7 -1
- data/dist/controllers/currency_input_controller.js +68 -23
- data/dist/controllers/date_range_picker_controller.js +34 -10
- data/dist/controllers/direct_upload_controller.js +21 -10
- data/dist/controllers/drawer_controller.js +2 -2
- data/dist/controllers/form_field_controller.js +7 -1
- 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 +34 -7
- data/dist/controllers/masonry_controller.js +1 -1
- 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 +141 -20
- data/dist/controllers/otp_controller.js +23 -10
- data/dist/controllers/overflow_menu_controller.js +10 -1
- 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/relative_time_controller.js +31 -5
- 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_visibility_controller.js +22 -13
- data/dist/controllers/scrollspy_controller.js +11 -6
- data/dist/controllers/smart_sticky_header_controller.js +23 -12
- data/dist/controllers/sticky_observer_controller.js +10 -5
- data/dist/controllers/theme_controller.js +23 -13
- data/dist/controllers/toast_controller.js +1 -1
- data/dist/controllers/toggle_group_controller.js +11 -3
- data/dist/controllers/toolbar_controller.js +7 -3
- data/dist/index.js +259 -202
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -103,6 +103,12 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
103
103
|
}
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
// src/utils/event_owner.ts
|
|
107
|
+
function ownerIndex(candidates, node) {
|
|
108
|
+
if (!(node instanceof Node)) return -1;
|
|
109
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
110
|
+
}
|
|
111
|
+
|
|
106
112
|
// src/utils/safe_timeout.ts
|
|
107
113
|
var TimerRegistry = class {
|
|
108
114
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -395,7 +401,7 @@ var NavigationMenuController = class extends Controller {
|
|
|
395
401
|
*/
|
|
396
402
|
#onPointerLeave = (event) => {
|
|
397
403
|
const next = event instanceof MouseEvent ? event.relatedTarget : null;
|
|
398
|
-
if (
|
|
404
|
+
if (ownerIndex(this.#hoverElements, next) !== -1) return;
|
|
399
405
|
this.#hoveredTrigger = null;
|
|
400
406
|
this.#hoverTimers.clearAll();
|
|
401
407
|
this.#hoverTimers.set(() => this.#closeFromHover(), this.hoverDelayValue);
|
|
@@ -465,7 +471,7 @@ var NavigationMenuController = class extends Controller {
|
|
|
465
471
|
*/
|
|
466
472
|
get #hoverElements() {
|
|
467
473
|
const areas = this.hoverAreaTargets;
|
|
468
|
-
const covered = (element) => areas
|
|
474
|
+
const covered = (element) => ownerIndex(areas, element) !== -1;
|
|
469
475
|
return [
|
|
470
476
|
...areas,
|
|
471
477
|
...this.triggerTargets.filter((trigger) => !covered(trigger)),
|
|
@@ -1,32 +1,115 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
// src/controllers/optimistic_controller.ts
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
// src/utils/detach_gate.ts
|
|
6
|
+
var DetachGate = class _DetachGate {
|
|
7
|
+
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
8
|
+
#pending = false;
|
|
9
|
+
/**
|
|
10
|
+
* True while a probe is queued — the last disconnect was ambiguous and no
|
|
11
|
+
* reconnect has cancelled it yet. Read it from `connect()` to tell the
|
|
12
|
+
* reconnect half of an in-page move from a first connect: a controller whose
|
|
13
|
+
* initialisation restarts a measurement (a min-duration floor, an elapsed
|
|
14
|
+
* counter) must skip it for the move, where nothing actually restarted.
|
|
15
|
+
*/
|
|
16
|
+
get pending() {
|
|
17
|
+
return this.#pending;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* True when the disconnect is definitely a real detach — the element left
|
|
21
|
+
* the document, or `data-controller` no longer lists the identifier. False
|
|
22
|
+
* means ambiguous (in-page move or observed-root exit), NOT "alive".
|
|
23
|
+
*/
|
|
24
|
+
static isDetached(host) {
|
|
25
|
+
if (!host.element.isConnected) return true;
|
|
26
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
27
|
+
return !tokens.includes(host.identifier);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Call from `disconnect()`: runs `teardown` synchronously on a definite
|
|
31
|
+
* detach (fast path), otherwise defers it one microtask — a reconnect
|
|
32
|
+
* ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
|
|
33
|
+
* One microtask is the whole probe window: Stimulus reconnects a moved
|
|
34
|
+
* element within the same mutation batch, before the checkpoint drains.
|
|
35
|
+
*/
|
|
36
|
+
disconnected(host, teardown) {
|
|
37
|
+
if (_DetachGate.isDetached(host)) {
|
|
38
|
+
this.#pending = false;
|
|
39
|
+
teardown();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.#pending = true;
|
|
43
|
+
queueMicrotask(() => {
|
|
44
|
+
if (!this.#pending) return;
|
|
45
|
+
this.#pending = false;
|
|
46
|
+
teardown();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Disarms a pending probe. Call from `connect()` (the reconnect that proves
|
|
51
|
+
* an in-page move) and from the head of any teardown path not routed through
|
|
52
|
+
* {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
|
|
53
|
+
* never run the teardown a second time.
|
|
54
|
+
*/
|
|
55
|
+
cancel() {
|
|
56
|
+
this.#pending = false;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/controllers/optimistic_controller.ts
|
|
61
|
+
var HIDDEN_MARKER = "data-optimistic-toggled";
|
|
62
|
+
var BUSY_MARKER = "data-optimistic-busy";
|
|
63
|
+
var AUTHORED_ABSENT = "absent";
|
|
64
|
+
var AUTHORED_VALUE = "value:";
|
|
65
|
+
var SYNTHETIC_SUBMISSION = {};
|
|
66
|
+
function submissionOf(event) {
|
|
67
|
+
const detail = event.detail;
|
|
68
|
+
return detail?.formSubmission ?? SYNTHETIC_SUBMISSION;
|
|
69
|
+
}
|
|
5
70
|
var OptimisticController = class extends Controller {
|
|
6
71
|
static targets = ["show", "hide"];
|
|
7
72
|
static events = ["commit", "rollback"];
|
|
73
|
+
/** The submission that owns the applied optimistic state, if any. */
|
|
74
|
+
#pending = null;
|
|
75
|
+
#gate = new DetachGate();
|
|
76
|
+
/** True between `connect()` and `disconnect()`, so a repeat connect rewinds nothing. */
|
|
77
|
+
#connected = false;
|
|
8
78
|
connect() {
|
|
9
|
-
|
|
79
|
+
const restored = !this.#connected && !this.#gate.pending;
|
|
80
|
+
this.#gate.cancel();
|
|
81
|
+
this.#connected = true;
|
|
82
|
+
if (restored && this.element.hasAttribute("data-optimistic")) this.#revert();
|
|
10
83
|
this.element.addEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
11
84
|
this.element.addEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
12
85
|
}
|
|
13
86
|
disconnect() {
|
|
87
|
+
this.#connected = false;
|
|
14
88
|
this.element.removeEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
15
89
|
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
90
|
+
this.#gate.disconnected(this, () => {
|
|
91
|
+
this.#pending = null;
|
|
92
|
+
});
|
|
16
93
|
}
|
|
17
|
-
#onSubmitStart = () => {
|
|
94
|
+
#onSubmitStart = (event) => {
|
|
95
|
+
this.#pending = submissionOf(event);
|
|
96
|
+
if (this.element instanceof HTMLFormElement && !this.element.hasAttribute(BUSY_MARKER)) {
|
|
97
|
+
this.element.setAttribute(BUSY_MARKER, AUTHORED_ABSENT);
|
|
98
|
+
}
|
|
99
|
+
this.#write(this.element, "aria-busy", "true", BUSY_MARKER);
|
|
18
100
|
this.element.setAttribute("data-optimistic", "true");
|
|
19
|
-
this.
|
|
20
|
-
for (const target of this.
|
|
21
|
-
for (const target of this.hideTargets) this.#toggleHidden(target, true);
|
|
101
|
+
for (const target of this.showTargets) this.#setHidden(target, false);
|
|
102
|
+
for (const target of this.hideTargets) this.#setHidden(target, true);
|
|
22
103
|
};
|
|
23
104
|
#onSubmitEnd = (event) => {
|
|
105
|
+
if (submissionOf(event) !== this.#pending) return;
|
|
106
|
+
this.#pending = null;
|
|
24
107
|
const success = event.detail?.success === true;
|
|
25
|
-
this.element.removeAttribute("aria-busy");
|
|
26
108
|
if (success) {
|
|
27
109
|
this.element.removeAttribute("data-optimistic");
|
|
110
|
+
this.#restore(this.element, "aria-busy", BUSY_MARKER, "true");
|
|
28
111
|
for (const target of [...this.showTargets, ...this.hideTargets]) {
|
|
29
|
-
target.removeAttribute(
|
|
112
|
+
target.removeAttribute(HIDDEN_MARKER);
|
|
30
113
|
}
|
|
31
114
|
this.dispatch("commit");
|
|
32
115
|
} else {
|
|
@@ -34,21 +117,59 @@ var OptimisticController = class extends Controller {
|
|
|
34
117
|
this.dispatch("rollback");
|
|
35
118
|
}
|
|
36
119
|
};
|
|
37
|
-
/**
|
|
38
|
-
#
|
|
39
|
-
if (target.hidden === hidden) return;
|
|
40
|
-
target
|
|
41
|
-
|
|
120
|
+
/** Hides or reveals a target, recording the authored `hidden` on first write. */
|
|
121
|
+
#setHidden(target, hidden) {
|
|
122
|
+
if (target.hasAttribute("hidden") === hidden) return;
|
|
123
|
+
this.#write(target, "hidden", hidden ? "" : null, HIDDEN_MARKER);
|
|
124
|
+
}
|
|
125
|
+
/** Writes an attribute, recording what it displaced under `marker` once. */
|
|
126
|
+
#write(element, attribute, value, marker) {
|
|
127
|
+
if (!element.hasAttribute(marker)) {
|
|
128
|
+
const authored = element.getAttribute(attribute);
|
|
129
|
+
element.setAttribute(
|
|
130
|
+
marker,
|
|
131
|
+
authored === null ? AUTHORED_ABSENT : `${AUTHORED_VALUE}${authored}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (value === null) element.removeAttribute(attribute);
|
|
135
|
+
else element.setAttribute(attribute, value);
|
|
42
136
|
}
|
|
43
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Puts back the value `marker` recorded and drops the record.
|
|
139
|
+
*
|
|
140
|
+
* `written` is what this controller last put in the attribute. Anything else
|
|
141
|
+
* there now belongs to whoever wrote it — Turbo owns `aria-busy` on a form and
|
|
142
|
+
* clears it before the terminal arrives, and a consumer may have moved a face
|
|
143
|
+
* mid-flight — so the record is dropped without touching the attribute. A
|
|
144
|
+
* marker this build did not write names no authored value, so the attribute
|
|
145
|
+
* goes back to being absent.
|
|
146
|
+
*/
|
|
147
|
+
#restore(element, attribute, marker, written) {
|
|
148
|
+
const recorded = element.getAttribute(marker);
|
|
149
|
+
if (recorded === null) return;
|
|
150
|
+
element.removeAttribute(marker);
|
|
151
|
+
if (element.getAttribute(attribute) !== written) return;
|
|
152
|
+
if (recorded.startsWith(AUTHORED_VALUE)) {
|
|
153
|
+
element.setAttribute(attribute, recorded.slice(AUTHORED_VALUE.length));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
element.removeAttribute(attribute);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Puts back exactly the markup this controller displaced (record-owned only).
|
|
160
|
+
*
|
|
161
|
+
* A face can be registered on both sides, and the hide pass runs last, so what
|
|
162
|
+
* this controller wrote there is `hidden` — matching it against the reveal's
|
|
163
|
+
* absent value instead would read as someone else's write and leave the face
|
|
164
|
+
* hidden. Each target is therefore restored once, against the write it last
|
|
165
|
+
* received.
|
|
166
|
+
*/
|
|
44
167
|
#revert() {
|
|
45
168
|
this.element.removeAttribute("data-optimistic");
|
|
46
|
-
this.element
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
target.removeAttribute(TOGGLED_MARKER);
|
|
51
|
-
}
|
|
169
|
+
this.#restore(this.element, "aria-busy", BUSY_MARKER, "true");
|
|
170
|
+
const hidden = new Set(this.hideTargets);
|
|
171
|
+
for (const target of /* @__PURE__ */ new Set([...this.showTargets, ...this.hideTargets])) {
|
|
172
|
+
this.#restore(target, "hidden", HIDDEN_MARKER, hidden.has(target) ? "" : null);
|
|
52
173
|
}
|
|
53
174
|
}
|
|
54
175
|
};
|
|
@@ -152,6 +152,25 @@ var CompositionTracker = class {
|
|
|
152
152
|
};
|
|
153
153
|
};
|
|
154
154
|
|
|
155
|
+
// src/utils/declared_value.ts
|
|
156
|
+
function parseDeclared(raw, parse, fallback) {
|
|
157
|
+
try {
|
|
158
|
+
return parse(raw);
|
|
159
|
+
} catch {
|
|
160
|
+
return fallback;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function compileRegExp(source, anchor = "none") {
|
|
164
|
+
return parseDeclared(
|
|
165
|
+
source,
|
|
166
|
+
(text) => {
|
|
167
|
+
const bare = new RegExp(text);
|
|
168
|
+
return anchor === "exact" ? new RegExp(`^(?:${text})$`) : bare;
|
|
169
|
+
},
|
|
170
|
+
null
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
155
174
|
// src/utils/half_width.ts
|
|
156
175
|
var FULL_WIDTH_SHIFT = 65248;
|
|
157
176
|
function halfWidthChar(char) {
|
|
@@ -201,13 +220,7 @@ var MicrotaskCoalescer = class {
|
|
|
201
220
|
|
|
202
221
|
// src/controllers/otp_controller.ts
|
|
203
222
|
var DEFAULT_PATTERN = "[0-9]";
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
return new RegExp(`^${source}$`);
|
|
207
|
-
} catch {
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
223
|
+
var DEFAULT_MATCHER = new RegExp(`^(?:${DEFAULT_PATTERN})$`);
|
|
211
224
|
function hasModifier(event) {
|
|
212
225
|
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
213
226
|
}
|
|
@@ -222,7 +235,7 @@ var OtpController = class extends Controller {
|
|
|
222
235
|
static actions = ["onInput", "onKeydown", "onPaste", "onPointerDown", "clear"];
|
|
223
236
|
static events = ["change", "complete", "invalid", "reconcile"];
|
|
224
237
|
/** Validated matcher; the hot path never compiles a raw declaration. */
|
|
225
|
-
#pattern =
|
|
238
|
+
#pattern = DEFAULT_MATCHER;
|
|
226
239
|
/** Source of {@link #pattern}, reported in `invalid` so consumers can word it. */
|
|
227
240
|
#patternSource = DEFAULT_PATTERN;
|
|
228
241
|
/** Public state carried by the last dispatch; keeps a no-op sync silent. */
|
|
@@ -295,9 +308,9 @@ var OtpController = class extends Controller {
|
|
|
295
308
|
* the new pattern no longer accepts, so the combined value stays interpretable.
|
|
296
309
|
*/
|
|
297
310
|
patternValueChanged() {
|
|
298
|
-
const compiled =
|
|
311
|
+
const compiled = compileRegExp(this.patternValue, "exact");
|
|
299
312
|
this.#patternSource = compiled ? this.patternValue : DEFAULT_PATTERN;
|
|
300
|
-
this.#pattern = compiled ??
|
|
313
|
+
this.#pattern = compiled ?? DEFAULT_MATCHER;
|
|
301
314
|
if (!this.#connected) return;
|
|
302
315
|
let dropped = false;
|
|
303
316
|
for (const field of this.fieldTargets) {
|
|
@@ -31,6 +31,15 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// src/utils/event_owner.ts
|
|
35
|
+
function ownerIndex(candidates, node) {
|
|
36
|
+
if (!(node instanceof Node)) return -1;
|
|
37
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
38
|
+
}
|
|
39
|
+
function ownerOf(candidates, node) {
|
|
40
|
+
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
// src/utils/focus_candidate.ts
|
|
35
44
|
function inheritsFieldsetDisabled(control) {
|
|
36
45
|
let fieldset = control.closest("fieldset[disabled]");
|
|
@@ -502,7 +511,7 @@ var OverflowMenuController = class extends Controller {
|
|
|
502
511
|
}
|
|
503
512
|
/** The managed item that is, or contains, `el` — focus rescue works on either. */
|
|
504
513
|
#ownerOf(el) {
|
|
505
|
-
return this.#items
|
|
514
|
+
return ownerOf(this.#items, el);
|
|
506
515
|
}
|
|
507
516
|
/** Whether the item takes up room in the bar; an authored `hidden` one does not. */
|
|
508
517
|
#rendered(item) {
|
|
@@ -63,6 +63,15 @@ var DetachGate = class _DetachGate {
|
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
// src/utils/event_owner.ts
|
|
67
|
+
function ownerIndex(candidates, node) {
|
|
68
|
+
if (!(node instanceof Node)) return -1;
|
|
69
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
70
|
+
}
|
|
71
|
+
function ownerOf(candidates, node) {
|
|
72
|
+
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
// src/controllers/pointer_drag_controller.ts
|
|
67
76
|
var NATIVE_KEY_OWNERS = "input, textarea, select, button, a[href], summary, [contenteditable]";
|
|
68
77
|
var PointerDragController = class _PointerDragController extends Controller {
|
|
@@ -406,9 +415,7 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
406
415
|
}
|
|
407
416
|
/** Resolves the handle owning an event target (the handle or a descendant). */
|
|
408
417
|
#handleFor(target) {
|
|
409
|
-
|
|
410
|
-
if (!node) return null;
|
|
411
|
-
return this.#handles().find((handle) => handle === node || handle.contains(node)) ?? null;
|
|
418
|
+
return ownerOf(this.#handles(), target);
|
|
412
419
|
}
|
|
413
420
|
#pointerTypeOf(event) {
|
|
414
421
|
const type = event.pointerType;
|
|
@@ -2,6 +2,26 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/portal_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
|
+
function validSelector(element, raw, fallback) {
|
|
14
|
+
if (raw.length === 0) return fallback;
|
|
15
|
+
return parseDeclared(
|
|
16
|
+
raw,
|
|
17
|
+
(selector) => {
|
|
18
|
+
element.matches(selector);
|
|
19
|
+
return selector;
|
|
20
|
+
},
|
|
21
|
+
fallback
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
// src/utils/detach_gate.ts
|
|
6
26
|
var DetachGate = class _DetachGate {
|
|
7
27
|
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
@@ -81,16 +101,7 @@ var PortalController = class extends Controller {
|
|
|
81
101
|
* instead, which reads as a mistake and can be traced back to the declaration.
|
|
82
102
|
*/
|
|
83
103
|
toValueChanged() {
|
|
84
|
-
|
|
85
|
-
if (selector.length > 0) {
|
|
86
|
-
try {
|
|
87
|
-
this.element.matches(selector);
|
|
88
|
-
this.#toSelector = selector;
|
|
89
|
-
return;
|
|
90
|
-
} catch {
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
this.#toSelector = "body";
|
|
104
|
+
this.#toSelector = validSelector(this.element, this.toValue, "body");
|
|
94
105
|
}
|
|
95
106
|
connect() {
|
|
96
107
|
this.#gate.cancel();
|
|
@@ -20,6 +20,15 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
20
20
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// src/utils/event_owner.ts
|
|
24
|
+
function ownerIndex(candidates, node) {
|
|
25
|
+
if (!(node instanceof Node)) return -1;
|
|
26
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
27
|
+
}
|
|
28
|
+
function ownerOf(candidates, node) {
|
|
29
|
+
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
// src/utils/focus_candidate.ts
|
|
24
33
|
function inheritsFieldsetDisabled(control) {
|
|
25
34
|
let fieldset = control.closest("fieldset[disabled]");
|
|
@@ -479,9 +488,8 @@ var RadioGroupController = class extends Controller {
|
|
|
479
488
|
}
|
|
480
489
|
/** Finds this group's radio containing an event target, excluding nested groups. */
|
|
481
490
|
#radioForEventTarget(target) {
|
|
482
|
-
if (!this.#ownsEventTarget(target)) return
|
|
483
|
-
|
|
484
|
-
return this.radioTargets.find((radio) => radio === node || radio.contains(node));
|
|
491
|
+
if (!this.#ownsEventTarget(target)) return null;
|
|
492
|
+
return ownerOf(this.radioTargets, target);
|
|
485
493
|
}
|
|
486
494
|
/** Whether the closest Radio Group scope around a target is this instance. */
|
|
487
495
|
#ownsEventTarget(target) {
|
|
@@ -31,6 +31,35 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// src/utils/intl_format.ts
|
|
35
|
+
var formatters = /* @__PURE__ */ new WeakMap();
|
|
36
|
+
function intlFormatter(factory, locale, options, fallbackLocale) {
|
|
37
|
+
const cache = cacheFor(factory);
|
|
38
|
+
const key = cacheKey(locale, options, fallbackLocale);
|
|
39
|
+
if (cache.has(key)) return cache.get(key);
|
|
40
|
+
const formatter = construct(factory, locale, options);
|
|
41
|
+
cache.set(key, formatter);
|
|
42
|
+
return formatter;
|
|
43
|
+
}
|
|
44
|
+
function cacheFor(factory) {
|
|
45
|
+
const existing = formatters.get(factory);
|
|
46
|
+
if (existing) return existing;
|
|
47
|
+
const created = /* @__PURE__ */ new Map();
|
|
48
|
+
formatters.set(factory, created);
|
|
49
|
+
return created;
|
|
50
|
+
}
|
|
51
|
+
function cacheKey(locale, options, fallbackLocale) {
|
|
52
|
+
return JSON.stringify([locale, options, fallbackLocale]);
|
|
53
|
+
}
|
|
54
|
+
function construct(factory, locale, options, fallbackLocale) {
|
|
55
|
+
try {
|
|
56
|
+
return new factory(locale, options);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (!(error instanceof RangeError)) throw error;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
34
63
|
// src/utils/microtask_coalescer.ts
|
|
35
64
|
var MicrotaskCoalescer = class {
|
|
36
65
|
#run;
|
|
@@ -128,6 +157,7 @@ var UNITS = [
|
|
|
128
157
|
{ limit: 31557600, unit: "month", ms: 26298e5 },
|
|
129
158
|
YEAR_SCALE
|
|
130
159
|
];
|
|
160
|
+
var RELATIVE_OPTIONS = { numeric: "auto" };
|
|
131
161
|
var RelativeTimeController = class extends Controller {
|
|
132
162
|
static values = {
|
|
133
163
|
locale: { type: String, default: "" },
|
|
@@ -235,11 +265,7 @@ var RelativeTimeController = class extends Controller {
|
|
|
235
265
|
* when the runtime rejects that locale.
|
|
236
266
|
*/
|
|
237
267
|
get #formatter() {
|
|
238
|
-
|
|
239
|
-
return new Intl.RelativeTimeFormat(this.#locale, { numeric: "auto" });
|
|
240
|
-
} catch {
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
268
|
+
return intlFormatter(Intl.RelativeTimeFormat, this.#locale, RELATIVE_OPTIONS);
|
|
243
269
|
}
|
|
244
270
|
/** Locale precedence: the value, then the nearest `lang` up the ancestor chain. */
|
|
245
271
|
get #locale() {
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
// src/controllers/reset_before_cache_controller.ts
|
|
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
|
+
function validSelector(element, raw, fallback) {
|
|
14
|
+
if (raw.length === 0) return fallback;
|
|
15
|
+
return parseDeclared(
|
|
16
|
+
raw,
|
|
17
|
+
(selector) => {
|
|
18
|
+
element.matches(selector);
|
|
19
|
+
return selector;
|
|
20
|
+
},
|
|
21
|
+
fallback
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
3
25
|
// src/controllers/reset_before_cache_controller.ts
|
|
4
26
|
var STATELESS_INPUT_TYPES = /* @__PURE__ */ new Set(["hidden", "submit", "reset", "button", "image"]);
|
|
5
27
|
function restoreField(element) {
|
|
@@ -43,16 +65,7 @@ var ResetBeforeCacheController = class extends Controller {
|
|
|
43
65
|
* silently taking the sweep down.
|
|
44
66
|
*/
|
|
45
67
|
scopeValueChanged() {
|
|
46
|
-
|
|
47
|
-
if (selector.length > 0) {
|
|
48
|
-
try {
|
|
49
|
-
this.element.matches(selector);
|
|
50
|
-
this.#scopeSelector = selector;
|
|
51
|
-
return;
|
|
52
|
-
} catch {
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
this.#scopeSelector = "";
|
|
68
|
+
this.#scopeSelector = validSelector(this.element, this.scopeValue, "");
|
|
56
69
|
}
|
|
57
70
|
connect() {
|
|
58
71
|
document.addEventListener("turbo:before-cache", this.#onBeforeCache);
|
|
@@ -8,6 +8,12 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
8
8
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// src/utils/event_owner.ts
|
|
12
|
+
function ownerIndex(candidates, node) {
|
|
13
|
+
if (!(node instanceof Node)) return -1;
|
|
14
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
// src/utils/microtask_coalescer.ts
|
|
12
18
|
var MicrotaskCoalescer = class {
|
|
13
19
|
#run;
|
|
@@ -154,8 +160,7 @@ var ResizableController = class extends Controller {
|
|
|
154
160
|
const panes = [];
|
|
155
161
|
if (this.hasPrimaryTarget) panes.push(this.primaryTarget);
|
|
156
162
|
if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);
|
|
157
|
-
const
|
|
158
|
-
const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));
|
|
163
|
+
const current = ownerIndex(panes, event.target);
|
|
159
164
|
const next = panes[(current + 1) % panes.length];
|
|
160
165
|
if (!next) return;
|
|
161
166
|
event.preventDefault();
|
|
@@ -16,6 +16,12 @@ function hasModifierChord(event) {
|
|
|
16
16
|
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// src/utils/event_owner.ts
|
|
20
|
+
function ownerIndex(candidates, node) {
|
|
21
|
+
if (!(node instanceof Node)) return -1;
|
|
22
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
// src/utils/focus_candidate.ts
|
|
20
26
|
function inheritsFieldsetDisabled(control) {
|
|
21
27
|
let fieldset = control.closest("fieldset[disabled]");
|
|
@@ -182,9 +188,7 @@ var RovingController = class extends Controller {
|
|
|
182
188
|
};
|
|
183
189
|
/** Resolves the item index owning an event target (the item or a descendant). */
|
|
184
190
|
#indexOf(target) {
|
|
185
|
-
|
|
186
|
-
if (!node) return -1;
|
|
187
|
-
return this.itemTargets.findIndex((item) => item === node || item.contains(node));
|
|
191
|
+
return ownerIndex(this.itemTargets, target);
|
|
188
192
|
}
|
|
189
193
|
/** Makes `index` the tab stop (optionally focusing it), emitting `change` once. */
|
|
190
194
|
#activate(index, focus) {
|
|
@@ -57,6 +57,26 @@ var BlurDeferral = class {
|
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
// src/utils/declared_value.ts
|
|
61
|
+
function parseDeclared(raw, parse, fallback) {
|
|
62
|
+
try {
|
|
63
|
+
return parse(raw);
|
|
64
|
+
} catch {
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function validSelector(element, raw, fallback) {
|
|
69
|
+
if (raw.length === 0) return fallback;
|
|
70
|
+
return parseDeclared(
|
|
71
|
+
raw,
|
|
72
|
+
(selector) => {
|
|
73
|
+
element.matches(selector);
|
|
74
|
+
return selector;
|
|
75
|
+
},
|
|
76
|
+
fallback
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
60
80
|
// src/utils/reduced_motion.ts
|
|
61
81
|
function prefersReducedMotion() {
|
|
62
82
|
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
@@ -223,11 +243,11 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
223
243
|
}
|
|
224
244
|
/** Validates `root` once so connect never parses a selector that throws. */
|
|
225
245
|
rootValueChanged() {
|
|
226
|
-
this.#rootSelector =
|
|
246
|
+
this.#rootSelector = validSelector(this.element, this.rootValue, "");
|
|
227
247
|
}
|
|
228
248
|
/** Validates `focusSelector` once so `toTop` never parses a selector that throws. */
|
|
229
249
|
focusSelectorValueChanged() {
|
|
230
|
-
this.#focusSelector =
|
|
250
|
+
this.#focusSelector = validSelector(this.element, this.focusSelectorValue, "");
|
|
231
251
|
}
|
|
232
252
|
/** Scrolls the source to the top and, optionally, moves focus to a safe target. */
|
|
233
253
|
toTop() {
|
|
@@ -299,17 +319,6 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
299
319
|
if (focused instanceof HTMLElement && this.elementTarget.contains(focused)) return focused;
|
|
300
320
|
return null;
|
|
301
321
|
}
|
|
302
|
-
/** Returns `declared` when it parses as a selector, and `""` when it does not. */
|
|
303
|
-
#validSelector(declared) {
|
|
304
|
-
if (declared.length > 0) {
|
|
305
|
-
try {
|
|
306
|
-
this.element.matches(declared);
|
|
307
|
-
return declared;
|
|
308
|
-
} catch {
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return "";
|
|
312
|
-
}
|
|
313
322
|
#scrollY() {
|
|
314
323
|
if (this.#scrollSource === window) {
|
|
315
324
|
return window.scrollY ?? window.pageYOffset ?? 0;
|
|
@@ -2,14 +2,19 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/scrollspy_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;
|
|
@@ -239,7 +244,7 @@ var ScrollspyController = class extends Controller {
|
|
|
239
244
|
* only rewrites attributes, so Stimulus fires no target callback: without
|
|
240
245
|
* this, a link re-pointed from `#intro` to `#faq` would keep the controller
|
|
241
246
|
* observing `#intro` for the rest of the page's life. The filter is exactly
|
|
242
|
-
*
|
|
247
|
+
* `href` and `data-href`; guarding on `MutationObserver` keeps the
|
|
243
248
|
* controller usable where the API is absent, matching `IntersectionWatcher`'s
|
|
244
249
|
* own support guard.
|
|
245
250
|
*/
|