stimeo-ui 0.16.0 → 0.17.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 +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/conditional_fields_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/event_owner.ts
|
|
6
3
|
function ownerIndex(candidates, node) {
|
|
7
4
|
if (!(node instanceof Node)) return -1;
|
|
8
5
|
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
9
6
|
}
|
|
10
7
|
|
|
11
|
-
// src/utils/focus_candidate.ts
|
|
12
8
|
function inheritsFieldsetDisabled(control) {
|
|
13
9
|
let fieldset = control.closest("fieldset[disabled]");
|
|
14
10
|
while (fieldset) {
|
|
@@ -18,33 +14,37 @@ function inheritsFieldsetDisabled(control) {
|
|
|
18
14
|
}
|
|
19
15
|
return false;
|
|
20
16
|
}
|
|
17
|
+
function unslotted(element) {
|
|
18
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
19
|
+
}
|
|
20
|
+
function flatTreeParent(element) {
|
|
21
|
+
if (unslotted(element)) return null;
|
|
22
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
23
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
24
|
+
return parent instanceof Element ? parent : null;
|
|
25
|
+
}
|
|
21
26
|
function canTakeFocus(element) {
|
|
22
|
-
|
|
27
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
28
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
29
|
+
}
|
|
23
30
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
24
31
|
if (!("disabled" in element)) return true;
|
|
25
32
|
if (element.disabled) return false;
|
|
26
33
|
return !inheritsFieldsetDisabled(element);
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
// src/utils/form_reset_watcher.ts
|
|
30
36
|
var FormResetWatcher = class {
|
|
31
37
|
#owns;
|
|
32
38
|
#onReset;
|
|
33
39
|
#frames = /* @__PURE__ */ new Set();
|
|
34
40
|
#generation = 0;
|
|
35
|
-
/**
|
|
36
|
-
* @param owns - whether `form` owns a control this controller derives from.
|
|
37
|
-
* @param onReset - the reconciliation pass, run once per honoured reset.
|
|
38
|
-
*/
|
|
39
41
|
constructor(owns, onReset) {
|
|
40
42
|
this.#owns = owns;
|
|
41
43
|
this.#onReset = onReset;
|
|
42
44
|
}
|
|
43
|
-
/** Subscribes to `reset` at the document; call from `connect()`. Idempotent. */
|
|
44
45
|
observe() {
|
|
45
46
|
document.addEventListener("reset", this.#handleReset, true);
|
|
46
47
|
}
|
|
47
|
-
/** Unsubscribes and drops every pending pass; call from `disconnect()`. */
|
|
48
48
|
disconnect() {
|
|
49
49
|
document.removeEventListener("reset", this.#handleReset, true);
|
|
50
50
|
this.#generation += 1;
|
|
@@ -68,57 +68,38 @@ var FormResetWatcher = class {
|
|
|
68
68
|
this.#frames.add(frame);
|
|
69
69
|
});
|
|
70
70
|
};
|
|
71
|
-
/** Runs the pass unless a listener cancelled the reset before it landed. */
|
|
72
71
|
#settle(event) {
|
|
73
72
|
if (!event.defaultPrevented) this.#onReset();
|
|
74
73
|
}
|
|
75
74
|
};
|
|
76
75
|
|
|
77
|
-
// src/utils/listener_set.ts
|
|
78
76
|
var ListenerSet = class {
|
|
79
|
-
/** The generation every `add` joins until the next `dispose()`. */
|
|
80
77
|
#abort = new AbortController();
|
|
81
|
-
/**
|
|
82
|
-
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
83
|
-
*
|
|
84
|
-
* The set supplies the signal, so `options` carries everything else the DOM
|
|
85
|
-
* accepts — `capture` included, which has to match at release time and no
|
|
86
|
-
* longer has a second place to drift from.
|
|
87
|
-
*/
|
|
88
78
|
add(target, type, handler, options) {
|
|
89
79
|
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
90
80
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Releases every listener of the open generation, synchronously, and opens the
|
|
93
|
-
* next one. Idempotent, and safe before anything has been added.
|
|
94
|
-
*/
|
|
95
81
|
dispose() {
|
|
96
82
|
this.#abort.abort();
|
|
97
83
|
this.#abort = new AbortController();
|
|
98
84
|
}
|
|
99
85
|
};
|
|
100
86
|
|
|
101
|
-
// src/utils/microtask_coalescer.ts
|
|
102
87
|
var MicrotaskCoalescer = class {
|
|
103
88
|
#run;
|
|
104
89
|
#queued = false;
|
|
105
90
|
#active = false;
|
|
106
91
|
#generation = 0;
|
|
107
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
108
92
|
constructor(run) {
|
|
109
93
|
this.#run = run;
|
|
110
94
|
}
|
|
111
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
112
95
|
activate() {
|
|
113
96
|
this.#active = true;
|
|
114
97
|
}
|
|
115
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
116
98
|
cancel() {
|
|
117
99
|
this.#active = false;
|
|
118
100
|
this.#queued = false;
|
|
119
101
|
this.#generation += 1;
|
|
120
102
|
}
|
|
121
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
122
103
|
schedule() {
|
|
123
104
|
if (!this.#active || this.#queued) return;
|
|
124
105
|
this.#queued = true;
|
|
@@ -131,68 +112,148 @@ var MicrotaskCoalescer = class {
|
|
|
131
112
|
}
|
|
132
113
|
};
|
|
133
114
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
};
|
|
142
|
-
#rewind;
|
|
143
|
-
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
144
|
-
constructor(rewind) {
|
|
145
|
-
this.#rewind = rewind;
|
|
115
|
+
var MorphRenderWatcher = class {
|
|
116
|
+
#listeners = new ListenerSet();
|
|
117
|
+
#pass;
|
|
118
|
+
#element = null;
|
|
119
|
+
#onMorph = () => this.#pass.schedule();
|
|
120
|
+
constructor(run) {
|
|
121
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
146
122
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
123
|
+
observe(element) {
|
|
124
|
+
if (this.#element === element) return;
|
|
125
|
+
this.disconnect();
|
|
126
|
+
this.#element = element;
|
|
127
|
+
this.#pass.activate();
|
|
128
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
154
129
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
130
|
+
schedule() {
|
|
131
|
+
this.#pass.schedule();
|
|
132
|
+
}
|
|
133
|
+
disconnect() {
|
|
134
|
+
this.#listeners.dispose();
|
|
135
|
+
this.#pass.cancel();
|
|
136
|
+
this.#element = null;
|
|
160
137
|
}
|
|
161
138
|
};
|
|
162
139
|
|
|
163
|
-
|
|
164
|
-
|
|
140
|
+
function queryOpenDescendants(root, selector) {
|
|
141
|
+
const matches = [];
|
|
142
|
+
const roots = [root];
|
|
143
|
+
for (const scope of roots) {
|
|
144
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
145
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
146
|
+
const shadow = scope.shadowRoot;
|
|
147
|
+
if (shadow) roots.push(shadow);
|
|
148
|
+
}
|
|
149
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
150
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return matches;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sharedRegistry(key, create) {
|
|
157
|
+
const symbol = Symbol.for(key);
|
|
158
|
+
const scope = globalThis;
|
|
159
|
+
const existing = scope[symbol];
|
|
160
|
+
if (existing !== void 0) return existing;
|
|
161
|
+
const registry = create();
|
|
162
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
163
|
+
return registry;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var TabindexLoan = class _TabindexLoan {
|
|
167
|
+
static #registry = sharedRegistry(
|
|
168
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
169
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
170
|
+
);
|
|
165
171
|
#value;
|
|
172
|
+
#record;
|
|
166
173
|
#lent = /* @__PURE__ */ new Set();
|
|
167
|
-
|
|
168
|
-
#beforeCache = new BeforeCacheReset(() => this.returnAll());
|
|
169
|
-
/**
|
|
170
|
-
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
171
|
-
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
172
|
-
* which a scroll region with no focusable content of its own needs.
|
|
173
|
-
*/
|
|
174
|
-
constructor(value = "-1") {
|
|
174
|
+
constructor(value, owner) {
|
|
175
175
|
this.#value = value;
|
|
176
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
177
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
176
178
|
}
|
|
177
|
-
/** Lends `element` the value; no-ops when it already carries a `tabindex`. */
|
|
178
179
|
lend(element) {
|
|
179
|
-
|
|
180
|
-
element.
|
|
180
|
+
const stack = this.#stack(element);
|
|
181
|
+
const current = element.getAttribute("tabindex");
|
|
182
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
183
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
184
|
+
);
|
|
185
|
+
const linked = stack.at(-1)?.value === current;
|
|
186
|
+
if (!held && current !== null && !linked) return;
|
|
181
187
|
this.#lent.add(element);
|
|
182
|
-
|
|
188
|
+
if (held) held.loan = this;
|
|
189
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
190
|
+
this.#mark(element, stack);
|
|
191
|
+
if (current === null || !held && current !== this.#value) {
|
|
192
|
+
element.setAttribute("tabindex", this.#value);
|
|
193
|
+
}
|
|
183
194
|
}
|
|
184
|
-
/** Takes back every loan whose value is still the one that was lent. */
|
|
185
195
|
returnAll() {
|
|
186
196
|
for (const element of this.#lent) {
|
|
187
|
-
|
|
197
|
+
const stack = this.#stack(element);
|
|
198
|
+
const hold = stack.find((held) => held.loan === this);
|
|
199
|
+
if (hold) this.#release(element, stack, hold);
|
|
188
200
|
}
|
|
189
201
|
this.#lent.clear();
|
|
190
|
-
|
|
202
|
+
}
|
|
203
|
+
reclaim(element) {
|
|
204
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
205
|
+
const stack = this.#stack(element);
|
|
206
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
207
|
+
if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
|
|
208
|
+
}
|
|
209
|
+
reclaimWithin(root) {
|
|
210
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
211
|
+
this.reclaim(element);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
#stack(element) {
|
|
215
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
216
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
217
|
+
stacks.set(element, stack);
|
|
218
|
+
return stack;
|
|
219
|
+
}
|
|
220
|
+
#seed(element) {
|
|
221
|
+
const current = element.getAttribute("tabindex");
|
|
222
|
+
const holds = [];
|
|
223
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
224
|
+
const value = element.getAttribute(record);
|
|
225
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
226
|
+
}
|
|
227
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
228
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
229
|
+
return holds;
|
|
230
|
+
}
|
|
231
|
+
#release(element, stack, hold) {
|
|
232
|
+
const current = element.getAttribute("tabindex");
|
|
233
|
+
const index = stack.indexOf(hold);
|
|
234
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
235
|
+
stack.splice(index, 1);
|
|
236
|
+
const above = stack[index];
|
|
237
|
+
if (above) above.linked &&= hold.linked;
|
|
238
|
+
this.#mark(element, stack);
|
|
239
|
+
if (!owns) return;
|
|
240
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
241
|
+
if (!below) element.removeAttribute("tabindex");
|
|
242
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
243
|
+
}
|
|
244
|
+
#mark(element, stack) {
|
|
245
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
246
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
247
|
+
for (const [name, recorded] of records) {
|
|
248
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
249
|
+
if (recorded === null) element.removeAttribute(name);
|
|
250
|
+
else element.setAttribute(name, recorded);
|
|
251
|
+
}
|
|
191
252
|
}
|
|
192
253
|
};
|
|
193
254
|
|
|
194
|
-
// src/controllers/conditional_fields_controller.ts
|
|
195
255
|
var DISABLED_MARKER = "data-conditional-disabled";
|
|
256
|
+
var CONTROLS = "input, textarea, select, button";
|
|
196
257
|
var OBSERVED_ATTRIBUTES = [
|
|
197
258
|
"hidden",
|
|
198
259
|
"aria-hidden",
|
|
@@ -215,23 +276,19 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
215
276
|
};
|
|
216
277
|
static actions = ["evaluate"];
|
|
217
278
|
static events = ["change", "reconcile"];
|
|
218
|
-
/** Logical state last committed for each current region; rebuilt on every connect. */
|
|
219
279
|
#lastVisible = /* @__PURE__ */ new WeakMap();
|
|
220
|
-
/** Controller writes awaiting an observer callback, separated from authored morphs. */
|
|
221
280
|
#internalWrites = /* @__PURE__ */ new WeakMap();
|
|
222
281
|
#observing = false;
|
|
223
|
-
|
|
224
|
-
#
|
|
282
|
+
#departed = /* @__PURE__ */ new Set();
|
|
283
|
+
#concealed = /* @__PURE__ */ new WeakSet();
|
|
284
|
+
#reconcile = new MorphRenderWatcher(() => this.#reconcileDom());
|
|
225
285
|
#listeners = new ListenerSet();
|
|
226
286
|
#formReset = new FormResetWatcher(
|
|
227
287
|
(form) => this.#hasTriggerOwnedBy(form),
|
|
228
288
|
() => this.#reconcile.schedule()
|
|
229
289
|
);
|
|
230
|
-
|
|
231
|
-
#rootTabindex = new TabindexLoan();
|
|
232
|
-
/** Watches retained targets and controls whose declarative or reflected state changed. */
|
|
290
|
+
#rootTabindex = new TabindexLoan("-1", this.identifier);
|
|
233
291
|
#observer = new MutationObserver((records) => this.#onMutations(records));
|
|
234
|
-
/** Evaluates only events whose source is one of this instance's live trigger targets. */
|
|
235
292
|
#onTriggerInput = (event) => {
|
|
236
293
|
const target = event.target;
|
|
237
294
|
const triggers = this.triggerTargets;
|
|
@@ -239,16 +296,12 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
239
296
|
this.#commitChange(triggers);
|
|
240
297
|
}
|
|
241
298
|
};
|
|
242
|
-
/** Reconciles property-only retained-element changes after Turbo morphs. */
|
|
243
|
-
#onMorph = () => {
|
|
244
|
-
this.#reconcile.schedule();
|
|
245
|
-
};
|
|
246
|
-
/** Reflects initial live state and opens every retained-DOM reconciliation path. */
|
|
247
299
|
connect() {
|
|
300
|
+
this.#rootTabindex.reclaim(this.element);
|
|
248
301
|
this.#lastVisible = /* @__PURE__ */ new WeakMap();
|
|
249
302
|
this.#internalWrites = /* @__PURE__ */ new WeakMap();
|
|
250
|
-
this.#reconcile.activate();
|
|
251
303
|
this.#settle();
|
|
304
|
+
this.#reconcile.observe(this.element);
|
|
252
305
|
this.#observer.observe(this.element, {
|
|
253
306
|
attributes: true,
|
|
254
307
|
attributeFilter: OBSERVED_ATTRIBUTES,
|
|
@@ -258,56 +311,46 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
258
311
|
this.#observing = true;
|
|
259
312
|
this.#listeners.add(this.element, "change", this.#onTriggerInput);
|
|
260
313
|
this.#listeners.add(this.element, "input", this.#onTriggerInput);
|
|
261
|
-
this.#listeners.add(this.element, "turbo:morph-element", this.#onMorph);
|
|
262
314
|
this.#formReset.observe();
|
|
263
315
|
}
|
|
264
|
-
/** Releases all external resources and returns the temporary focus landmark loan. */
|
|
265
316
|
disconnect() {
|
|
266
|
-
this.#reconcile.
|
|
317
|
+
this.#reconcile.disconnect();
|
|
267
318
|
this.#observing = false;
|
|
268
319
|
this.#observer.disconnect();
|
|
269
320
|
this.#listeners.dispose();
|
|
270
321
|
this.#formReset.disconnect();
|
|
271
322
|
this.#rootTabindex.returnAll();
|
|
272
323
|
}
|
|
273
|
-
/** Reconciles after a trigger target is inserted or restored. */
|
|
274
324
|
triggerTargetConnected() {
|
|
275
325
|
this.#reconcile.schedule();
|
|
276
326
|
}
|
|
277
|
-
/** Reconciles after a trigger target is removed or replaced. */
|
|
278
327
|
triggerTargetDisconnected() {
|
|
279
328
|
this.#reconcile.schedule();
|
|
280
329
|
}
|
|
281
|
-
/** Initializes a region target inserted or restored at runtime. */
|
|
282
330
|
regionTargetConnected() {
|
|
283
331
|
this.#reconcile.schedule();
|
|
284
332
|
}
|
|
285
|
-
|
|
286
|
-
|
|
333
|
+
regionTargetDisconnected(region) {
|
|
334
|
+
this.#departed.add(region);
|
|
287
335
|
this.#reconcile.schedule();
|
|
288
336
|
}
|
|
289
|
-
/** Reconciles when application code or a morph changes the match policy. */
|
|
290
337
|
matchValueChanged() {
|
|
291
338
|
this.#reconcile.schedule();
|
|
292
339
|
}
|
|
293
|
-
/** Reconciles control ownership when hidden controls become enabled or excluded. */
|
|
294
340
|
disableHiddenValueChanged() {
|
|
295
341
|
this.#reconcile.schedule();
|
|
296
342
|
}
|
|
297
|
-
/** Re-evaluates every region and repairs all reflected DOM state. */
|
|
298
343
|
evaluate() {
|
|
299
344
|
this.#commitChange();
|
|
300
345
|
}
|
|
301
|
-
/**
|
|
302
|
-
* Computes one settled plan, shows destinations before hiding sources, and then
|
|
303
|
-
* returns logical transitions in DOM order after every region is internally coherent.
|
|
304
|
-
*
|
|
305
|
-
* @stimeoRenderRoot
|
|
306
|
-
*/
|
|
307
346
|
#settle(triggers = this.triggerTargets) {
|
|
308
347
|
const regions = this.regionTargets;
|
|
309
348
|
const plan = /* @__PURE__ */ new Map();
|
|
310
349
|
for (const region of regions) plan.set(region, this.#isVisible(region, triggers));
|
|
350
|
+
for (const departed of this.#departed) {
|
|
351
|
+
if (!plan.has(departed)) this.#release(departed, plan);
|
|
352
|
+
}
|
|
353
|
+
this.#departed.clear();
|
|
311
354
|
const changed = /* @__PURE__ */ new Set();
|
|
312
355
|
const applicationOrder = [
|
|
313
356
|
...regions.filter((region) => plan.get(region) === true),
|
|
@@ -321,19 +364,16 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
321
364
|
}
|
|
322
365
|
return regions.filter((region) => changed.has(region)).map((region) => ({ region, visible: plan.get(region) === true }));
|
|
323
366
|
}
|
|
324
|
-
/** Reports logical transitions caused by user input or the public action. */
|
|
325
367
|
#commitChange(triggers = this.triggerTargets) {
|
|
326
368
|
for (const detail of this.#settle(triggers)) {
|
|
327
369
|
this.dispatch("change", { detail });
|
|
328
370
|
}
|
|
329
371
|
}
|
|
330
|
-
/** Reports logical transitions derived from DOM, lifecycle, reset, or Value state. */
|
|
331
372
|
#reconcileDom() {
|
|
332
373
|
for (const detail of this.#settle()) {
|
|
333
374
|
this.dispatch("reconcile", { detail });
|
|
334
375
|
}
|
|
335
376
|
}
|
|
336
|
-
/** Applies one planned state while keeping event history separate from DOM repair. */
|
|
337
377
|
#applyRegion(region, visible, plan) {
|
|
338
378
|
const previous = this.#lastVisible.get(region) ?? !region.hidden;
|
|
339
379
|
const changed = visible !== previous;
|
|
@@ -343,17 +383,16 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
343
383
|
this.#syncRegionControls(region, visible);
|
|
344
384
|
return changed;
|
|
345
385
|
}
|
|
346
|
-
/** Reflects visibility idempotently, recording writes so the observer ignores its own work. */
|
|
347
386
|
#syncRegionState(region, visible) {
|
|
387
|
+
if (!visible && this.#predicateFor(region) !== null) this.#concealed.add(region);
|
|
388
|
+
else this.#concealed.delete(region);
|
|
348
389
|
this.#writeAttribute(region, "hidden", visible ? null : "");
|
|
349
390
|
this.#writeAttribute(region, "aria-hidden", visible ? null : "true");
|
|
350
391
|
this.#writeAttribute(region, "data-visible", visible ? "true" : null);
|
|
351
392
|
}
|
|
352
|
-
/** Enables/disables descendant controls, touching only state carrying our marker. */
|
|
353
393
|
#syncRegionControls(region, visible) {
|
|
354
394
|
const shouldDisable = !visible && this.disableHiddenValue;
|
|
355
|
-
const
|
|
356
|
-
for (const control of controls) {
|
|
395
|
+
for (const control of region.querySelectorAll(CONTROLS)) {
|
|
357
396
|
const marked = control.hasAttribute(DISABLED_MARKER);
|
|
358
397
|
if (shouldDisable) {
|
|
359
398
|
if (!control.disabled) {
|
|
@@ -363,18 +402,30 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
363
402
|
this.#writeAttribute(control, DISABLED_MARKER, "true");
|
|
364
403
|
}
|
|
365
404
|
} else if (marked) {
|
|
366
|
-
this.#
|
|
367
|
-
|
|
405
|
+
this.#enable(control);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
#enable(control) {
|
|
410
|
+
this.#writeAttribute(control, "disabled", null);
|
|
411
|
+
this.#writeAttribute(control, DISABLED_MARKER, null);
|
|
412
|
+
}
|
|
413
|
+
#release(element, plan) {
|
|
414
|
+
this.#lastVisible.delete(element);
|
|
415
|
+
if (this.#concealed.delete(element)) this.#writeAttribute(element, "hidden", null);
|
|
416
|
+
this.#writeAttribute(element, "aria-hidden", null);
|
|
417
|
+
this.#writeAttribute(element, "data-visible", null);
|
|
418
|
+
for (const control of element.querySelectorAll(CONTROLS)) {
|
|
419
|
+
if (control.hasAttribute(DISABLED_MARKER) && this.#survivesPlan(control, plan)) {
|
|
420
|
+
this.#enable(control);
|
|
368
421
|
}
|
|
369
422
|
}
|
|
370
423
|
}
|
|
371
|
-
/** Whether a region's declared condition holds across `triggers` per `match`. */
|
|
372
424
|
#isVisible(region, triggers) {
|
|
373
425
|
const predicate = this.#predicateFor(region);
|
|
374
426
|
if (predicate === null) return !region.hidden;
|
|
375
427
|
return triggers.length > 0 && (this.matchValue === "all" ? triggers.every(predicate) : triggers.some(predicate));
|
|
376
428
|
}
|
|
377
|
-
/** Builds the deterministic per-trigger predicate from the region's condition. */
|
|
378
429
|
#predicateFor(region) {
|
|
379
430
|
if (region.hasAttribute("data-when-checked")) {
|
|
380
431
|
return (trigger) => this.#isChecked(trigger);
|
|
@@ -391,19 +442,16 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
391
442
|
#isChecked(trigger) {
|
|
392
443
|
return trigger instanceof HTMLInputElement && trigger.checked;
|
|
393
444
|
}
|
|
394
|
-
/** A trigger has a value when a radio/checkbox is selected, or its control value matches. */
|
|
395
445
|
#matchesValue(trigger, wanted) {
|
|
396
446
|
if (trigger instanceof HTMLInputElement && (trigger.type === "checkbox" || trigger.type === "radio")) {
|
|
397
447
|
return trigger.checked && trigger.value === wanted;
|
|
398
448
|
}
|
|
399
449
|
return trigger.value === wanted;
|
|
400
450
|
}
|
|
401
|
-
/** Moves focus to the first safe trigger, or to the controller landmark as a fallback. */
|
|
402
451
|
#retreatFocus(region, plan) {
|
|
403
452
|
const active = document.activeElement;
|
|
404
453
|
if (active && region.contains(active)) this.#moveFocus(plan);
|
|
405
454
|
}
|
|
406
|
-
/** Moves focus after the caller establishes that the active node is being hidden. */
|
|
407
455
|
#moveFocus(plan) {
|
|
408
456
|
const destination = this.triggerTargets.find(
|
|
409
457
|
(trigger) => canTakeFocus(trigger) && this.#survivesPlan(trigger, plan)
|
|
@@ -416,11 +464,9 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
416
464
|
this.#rootTabindex.lend(this.element);
|
|
417
465
|
this.element.focus();
|
|
418
466
|
}
|
|
419
|
-
/** Whether a focus candidate sits outside every region this pass will hide. */
|
|
420
467
|
#survivesPlan(candidate, plan) {
|
|
421
468
|
return [...plan].every(([region, visible]) => visible || !region.contains(candidate));
|
|
422
469
|
}
|
|
423
|
-
/** Records one idempotent attribute write before MutationObserver sees it. */
|
|
424
470
|
#writeAttribute(element, name, value) {
|
|
425
471
|
if (element.getAttribute(name) !== value) {
|
|
426
472
|
if (this.#observing) {
|
|
@@ -435,7 +481,6 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
435
481
|
else element.setAttribute(name, value);
|
|
436
482
|
}
|
|
437
483
|
}
|
|
438
|
-
/** Separates authored records from controller writes and queues one relevant repair. */
|
|
439
484
|
#onMutations(records) {
|
|
440
485
|
const triggers = this.triggerTargets;
|
|
441
486
|
const regions = this.regionTargets;
|
|
@@ -465,16 +510,13 @@ var ConditionalFieldsController = class extends Controller {
|
|
|
465
510
|
}
|
|
466
511
|
if (needsReconcile) this.#reconcile.schedule();
|
|
467
512
|
}
|
|
468
|
-
/** Whether an authored mutation target belongs to a trigger, region, or managed control. */
|
|
469
513
|
#elementAffectsState(target, triggers, regions) {
|
|
470
514
|
return ownerIndex(triggers, target) !== -1 || ownerIndex(regions, target) !== -1;
|
|
471
515
|
}
|
|
472
|
-
/** Whether a form owns at least one current trigger, including `form=` associations. */
|
|
473
516
|
#hasTriggerOwnedBy(form) {
|
|
474
517
|
return this.triggerTargets.some((trigger) => trigger.form === form);
|
|
475
518
|
}
|
|
476
519
|
};
|
|
477
520
|
|
|
478
521
|
export { ConditionalFieldsController };
|
|
479
|
-
//# sourceMappingURL=conditional_fields_controller.js.map
|
|
480
522
|
//# sourceMappingURL=conditional_fields_controller.js.map
|