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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/dist/cable/index.js +438 -152
  4. data/dist/controllers/calendar_controller.js +34 -2
  5. data/dist/controllers/carousel_controller.js +9 -7
  6. data/dist/controllers/command_palette_controller.js +2 -2
  7. data/dist/controllers/conditional_fields_controller.js +7 -1
  8. data/dist/controllers/currency_input_controller.js +68 -23
  9. data/dist/controllers/date_range_picker_controller.js +34 -10
  10. data/dist/controllers/direct_upload_controller.js +21 -10
  11. data/dist/controllers/drawer_controller.js +2 -2
  12. data/dist/controllers/form_field_controller.js +7 -1
  13. data/dist/controllers/input_mask_controller.js +33 -12
  14. data/dist/controllers/intersection_controller.js +10 -5
  15. data/dist/controllers/lazy_frame_controller.js +10 -5
  16. data/dist/controllers/local_time_controller.js +34 -7
  17. data/dist/controllers/masonry_controller.js +1 -1
  18. data/dist/controllers/menu_controller.js +10 -2
  19. data/dist/controllers/menubar_controller.js +10 -2
  20. data/dist/controllers/navigation_menu_controller.js +8 -2
  21. data/dist/controllers/optimistic_controller.js +141 -20
  22. data/dist/controllers/otp_controller.js +23 -10
  23. data/dist/controllers/overflow_menu_controller.js +10 -1
  24. data/dist/controllers/pointer_drag_controller.js +10 -3
  25. data/dist/controllers/portal_controller.js +21 -10
  26. data/dist/controllers/radio_group_controller.js +11 -3
  27. data/dist/controllers/relative_time_controller.js +31 -5
  28. data/dist/controllers/reset_before_cache_controller.js +23 -10
  29. data/dist/controllers/resizable_controller.js +7 -2
  30. data/dist/controllers/roving_controller.js +7 -3
  31. data/dist/controllers/scroll_visibility_controller.js +22 -13
  32. data/dist/controllers/scrollspy_controller.js +11 -6
  33. data/dist/controllers/smart_sticky_header_controller.js +23 -12
  34. data/dist/controllers/sticky_observer_controller.js +10 -5
  35. data/dist/controllers/theme_controller.js +23 -13
  36. data/dist/controllers/toast_controller.js +1 -1
  37. data/dist/controllers/toggle_group_controller.js +11 -3
  38. data/dist/controllers/toolbar_controller.js +7 -3
  39. data/dist/index.js +259 -202
  40. data/lib/stimeo/ui/version.rb +1 -1
  41. 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 (next instanceof Node && this.#hoverElements.some((el) => el.contains(next))) return;
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.some((area) => area.contains(element));
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
- var TOGGLED_MARKER = "data-optimistic-toggled";
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
- if (this.element.hasAttribute("data-optimistic")) this.#revert();
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.element.setAttribute("aria-busy", "true");
20
- for (const target of this.showTargets) this.#toggleHidden(target, false);
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(TOGGLED_MARKER);
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
- /** Sets `hidden` only when it actually changes, marker-tracking the mutation. */
38
- #toggleHidden(target, hidden) {
39
- if (target.hidden === hidden) return;
40
- target.hidden = hidden;
41
- target.setAttribute(TOGGLED_MARKER, "true");
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
- /** Reverts exactly the mutations this controller made (marker-owned only). */
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.removeAttribute("aria-busy");
47
- for (const target of [...this.showTargets, ...this.hideTargets]) {
48
- if (target.hasAttribute(TOGGLED_MARKER)) {
49
- target.hidden = !target.hidden;
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
- function compilePattern(source) {
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 = new RegExp(`^${DEFAULT_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 = compilePattern(this.patternValue);
311
+ const compiled = compileRegExp(this.patternValue, "exact");
299
312
  this.#patternSource = compiled ? this.patternValue : DEFAULT_PATTERN;
300
- this.#pattern = compiled ?? new RegExp(`^${DEFAULT_PATTERN}$`);
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.find((item) => item === el || item.contains(el)) ?? null;
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
- const node = target;
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
- const selector = this.toValue;
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 void 0;
483
- const node = target;
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
- try {
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
- const selector = this.scopeValue;
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 target = event.target;
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
- const node = target;
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 = this.#validSelector(this.rootValue);
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 = this.#validSelector(this.focusSelectorValue);
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/intersection_watcher.ts
6
- function queryRoot(selector) {
7
- if (!selector) return null;
5
+ // src/utils/declared_value.ts
6
+ function parseDeclared(raw, parse, fallback) {
8
7
  try {
9
- return document.querySelector(selector);
8
+ return parse(raw);
10
9
  } catch {
10
+ return fallback;
11
11
  }
12
- return null;
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
- * {@link ANCHOR_ATTRIBUTES}; guarding on `MutationObserver` keeps the
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
  */