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,49 +1,195 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function sharedRegistry(key, create) {
|
|
4
|
+
const symbol = Symbol.for(key);
|
|
5
|
+
const scope = globalThis;
|
|
6
|
+
const existing = scope[symbol];
|
|
7
|
+
if (existing !== void 0) return existing;
|
|
8
|
+
const registry = create();
|
|
9
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
10
|
+
return registry;
|
|
11
|
+
}
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
function isAttributeValue(value) {
|
|
14
|
+
return value === null || typeof value === "string";
|
|
15
|
+
}
|
|
16
|
+
function readLeaseRecord(element, record) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
19
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
20
|
+
const [original, written, ...rest] = parsed;
|
|
21
|
+
const linked = rest[0] !== false;
|
|
22
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
23
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
24
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
25
|
+
} catch {
|
|
26
|
+
return void 0;
|
|
17
27
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
}
|
|
29
|
+
var AttributeLease = class _AttributeLease {
|
|
30
|
+
static #registry = sharedRegistry(
|
|
31
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
32
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
33
|
+
);
|
|
34
|
+
#attribute;
|
|
35
|
+
#record;
|
|
36
|
+
#base;
|
|
37
|
+
#held = /* @__PURE__ */ new Set();
|
|
38
|
+
constructor(attribute, owner, options = {}) {
|
|
39
|
+
this.#attribute = attribute;
|
|
40
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
41
|
+
this.#base = options.base === true;
|
|
42
|
+
const names = _AttributeLease.#registry.names;
|
|
43
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
44
|
+
}
|
|
45
|
+
write(element, value) {
|
|
46
|
+
const stack = this.#stack(element);
|
|
47
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
48
|
+
this.#held.add(element);
|
|
49
|
+
const shows = !this.#covered(element, stack, hold);
|
|
50
|
+
hold.written = value;
|
|
51
|
+
this.#mark(element, stack);
|
|
52
|
+
if (shows) this.#reflect(element, value);
|
|
53
|
+
}
|
|
54
|
+
return(element) {
|
|
55
|
+
const held = this.#held.delete(element);
|
|
56
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
57
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
58
|
+
const hold = this.#take(stack);
|
|
59
|
+
if (!hold) return false;
|
|
60
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
61
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
62
|
+
const index = stack.indexOf(hold);
|
|
63
|
+
stack.splice(index, 1);
|
|
64
|
+
const above = stack[index];
|
|
65
|
+
if (above) above.linked &&= hold.linked;
|
|
66
|
+
this.#mark(element, stack);
|
|
67
|
+
if (!owns) return true;
|
|
68
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
69
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
returnAll() {
|
|
73
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
74
|
+
}
|
|
75
|
+
#stack(element) {
|
|
76
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
77
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
78
|
+
stacks.set(element, byAttribute);
|
|
79
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
80
|
+
byAttribute.set(this.#attribute, stack);
|
|
81
|
+
return stack;
|
|
82
|
+
}
|
|
83
|
+
#seed(element) {
|
|
84
|
+
const found = [];
|
|
85
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
86
|
+
const read = readLeaseRecord(element, record);
|
|
87
|
+
if (read) found.push({ ...read, record });
|
|
24
88
|
}
|
|
89
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
90
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
91
|
+
lease: null,
|
|
92
|
+
record,
|
|
93
|
+
written,
|
|
94
|
+
original,
|
|
95
|
+
linked: linked && index > 0
|
|
96
|
+
}));
|
|
25
97
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
98
|
+
#take(stack) {
|
|
99
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
100
|
+
if (hold) hold.lease = this;
|
|
101
|
+
return hold;
|
|
102
|
+
}
|
|
103
|
+
#acquire(element, stack) {
|
|
104
|
+
const current = element.getAttribute(this.#attribute);
|
|
105
|
+
const top = stack.at(-1);
|
|
106
|
+
const bottom = stack[0];
|
|
107
|
+
const over = top?.written === current ? top : void 0;
|
|
108
|
+
const from = this.#base ? bottom : over;
|
|
109
|
+
const hold = {
|
|
110
|
+
lease: this,
|
|
111
|
+
record: this.#record,
|
|
112
|
+
written: current,
|
|
113
|
+
original: from ? from.original : current,
|
|
114
|
+
linked: !this.#base && over !== void 0
|
|
115
|
+
};
|
|
116
|
+
if (this.#base && bottom) {
|
|
117
|
+
bottom.linked = true;
|
|
118
|
+
stack.unshift(hold);
|
|
119
|
+
} else stack.push(hold);
|
|
120
|
+
return hold;
|
|
121
|
+
}
|
|
122
|
+
#covered(element, stack, hold) {
|
|
123
|
+
const current = element.getAttribute(this.#attribute);
|
|
124
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
125
|
+
}
|
|
126
|
+
#mark(element, stack) {
|
|
127
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
128
|
+
stack.forEach((hold, index) => {
|
|
129
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
130
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
131
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
132
|
+
records.set(
|
|
133
|
+
hold.record,
|
|
134
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
for (const [name, recorded] of records) {
|
|
138
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
139
|
+
if (recorded === null) element.removeAttribute(name);
|
|
140
|
+
else element.setAttribute(name, recorded);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
#reflect(element, value) {
|
|
144
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
145
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
146
|
+
else element.setAttribute(this.#attribute, value);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
var DetachGate = class _DetachGate {
|
|
151
|
+
#pending = false;
|
|
152
|
+
get pending() {
|
|
153
|
+
return this.#pending;
|
|
154
|
+
}
|
|
155
|
+
static isDetached(host) {
|
|
156
|
+
return !host.element.isConnected || !_DetachGate.#listed(host);
|
|
157
|
+
}
|
|
158
|
+
disconnected(host, teardown) {
|
|
159
|
+
const carried = _DetachGate.#carried(host);
|
|
160
|
+
if (!carried && _DetachGate.isDetached(host)) {
|
|
161
|
+
this.#pending = false;
|
|
162
|
+
teardown();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.#pending = true;
|
|
166
|
+
const probe = () => {
|
|
167
|
+
if (!this.#pending) return;
|
|
168
|
+
this.#pending = false;
|
|
169
|
+
teardown();
|
|
170
|
+
};
|
|
171
|
+
if (carried) setTimeout(probe, 0);
|
|
172
|
+
else queueMicrotask(probe);
|
|
173
|
+
}
|
|
174
|
+
static #carried(host) {
|
|
175
|
+
const element = host.element;
|
|
176
|
+
return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
|
|
177
|
+
}
|
|
178
|
+
static #listed(host) {
|
|
179
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
180
|
+
return tokens.includes(host.identifier);
|
|
181
|
+
}
|
|
182
|
+
cancel() {
|
|
183
|
+
this.#pending = false;
|
|
31
184
|
}
|
|
32
185
|
};
|
|
33
186
|
|
|
34
|
-
// src/utils/escape_layer.ts
|
|
35
187
|
var EscapeLayer = class _EscapeLayer {
|
|
36
|
-
static #registries =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
#claims = null;
|
|
42
|
-
/**
|
|
43
|
-
* Activates this layer at the top of its document's Escape stack, installing
|
|
44
|
-
* the document's shared resolver listener if this is its first layer.
|
|
45
|
-
* Re-activating an already-active layer moves it to the top.
|
|
46
|
-
*/
|
|
188
|
+
static #registries = sharedRegistry(
|
|
189
|
+
"stimeo-ui.escape-layer.registry.v1",
|
|
190
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
191
|
+
);
|
|
192
|
+
#active = null;
|
|
47
193
|
activate(ownerDocument = document, options) {
|
|
48
194
|
this.deactivate();
|
|
49
195
|
let registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
@@ -52,44 +198,31 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
52
198
|
_EscapeLayer.#registries.set(ownerDocument, registry);
|
|
53
199
|
ownerDocument.addEventListener("keydown", registry.onKeydown);
|
|
54
200
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.#
|
|
58
|
-
this.#claims = options.claims ?? null;
|
|
201
|
+
const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
|
|
202
|
+
registry.stack.push(entry);
|
|
203
|
+
this.#active = { document: ownerDocument, entry };
|
|
59
204
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Removes this layer from its document's Escape stack, uninstalling the
|
|
62
|
-
* shared listener when the stack empties. Safe to call when inactive.
|
|
63
|
-
*/
|
|
64
205
|
deactivate() {
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
67
|
-
const registry = _EscapeLayer.#registries.get(
|
|
206
|
+
const active = this.#active;
|
|
207
|
+
if (!active) return;
|
|
208
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
68
209
|
if (registry) {
|
|
69
|
-
const index = registry.stack.lastIndexOf(
|
|
210
|
+
const index = registry.stack.lastIndexOf(active.entry);
|
|
70
211
|
if (index >= 0) registry.stack.splice(index, 1);
|
|
71
212
|
if (registry.stack.length === 0) {
|
|
72
|
-
|
|
73
|
-
_EscapeLayer.#registries.delete(
|
|
213
|
+
active.document.removeEventListener("keydown", registry.onKeydown);
|
|
214
|
+
_EscapeLayer.#registries.delete(active.document);
|
|
74
215
|
}
|
|
75
216
|
}
|
|
76
|
-
this.#
|
|
77
|
-
this.#onDismiss = null;
|
|
78
|
-
this.#claims = null;
|
|
217
|
+
this.#active = null;
|
|
79
218
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Whether this active layer would own a press right now: it is the topmost
|
|
82
|
-
* layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
|
|
83
|
-
* and diagnostics — production dismissal goes through the shared listener.
|
|
84
|
-
*/
|
|
85
219
|
get ownsEscape() {
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
88
|
-
const registry = _EscapeLayer.#registries.get(
|
|
220
|
+
const active = this.#active;
|
|
221
|
+
if (!active) return false;
|
|
222
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
89
223
|
if (!registry) return false;
|
|
90
|
-
return _EscapeLayer.#resolveOwner(registry.stack) ===
|
|
224
|
+
return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
|
|
91
225
|
}
|
|
92
|
-
/** Builds a document's registry with its shared resolver listener. */
|
|
93
226
|
static #createRegistry() {
|
|
94
227
|
const registry = {
|
|
95
228
|
stack: [],
|
|
@@ -98,24 +231,22 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
98
231
|
const owner = _EscapeLayer.#resolveOwner(registry.stack);
|
|
99
232
|
if (!owner) return;
|
|
100
233
|
event.preventDefault();
|
|
101
|
-
owner
|
|
234
|
+
owner.onDismiss();
|
|
102
235
|
}
|
|
103
236
|
};
|
|
104
237
|
return registry;
|
|
105
238
|
}
|
|
106
|
-
/** The topmost stack layer whose claims predicate passes, or `null`. */
|
|
107
239
|
static #resolveOwner(stack) {
|
|
108
240
|
for (let index = stack.length - 1; index >= 0; index--) {
|
|
109
241
|
const layer = stack[index];
|
|
110
242
|
if (!layer) continue;
|
|
111
|
-
if (layer
|
|
243
|
+
if (layer.claims && !layer.claims()) continue;
|
|
112
244
|
return layer;
|
|
113
245
|
}
|
|
114
246
|
return null;
|
|
115
247
|
}
|
|
116
248
|
};
|
|
117
249
|
|
|
118
|
-
// src/utils/focus_candidate.ts
|
|
119
250
|
function inheritsFieldsetDisabled(control) {
|
|
120
251
|
let fieldset = control.closest("fieldset[disabled]");
|
|
121
252
|
while (fieldset) {
|
|
@@ -125,8 +256,34 @@ function inheritsFieldsetDisabled(control) {
|
|
|
125
256
|
}
|
|
126
257
|
return false;
|
|
127
258
|
}
|
|
259
|
+
function unslotted(element) {
|
|
260
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
261
|
+
}
|
|
262
|
+
function flatTreeParent(element) {
|
|
263
|
+
if (unslotted(element)) return null;
|
|
264
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
265
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
266
|
+
return parent instanceof Element ? parent : null;
|
|
267
|
+
}
|
|
268
|
+
function flatTreeContains(ancestor, element) {
|
|
269
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
270
|
+
if (node === ancestor) return true;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
function closestInFlatTree(element, selector) {
|
|
275
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
276
|
+
if (node.matches(selector)) return node;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
function flatTreeChildren(element) {
|
|
281
|
+
return ownedScope(element) ?? Array.from(element.children);
|
|
282
|
+
}
|
|
128
283
|
function canTakeFocus(element) {
|
|
129
|
-
|
|
284
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
285
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
286
|
+
}
|
|
130
287
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
131
288
|
if (!("disabled" in element)) return true;
|
|
132
289
|
if (element.disabled) return false;
|
|
@@ -183,190 +340,649 @@ function isTabStop(element) {
|
|
|
183
340
|
if (tabindex !== null) return tabindex >= 0;
|
|
184
341
|
return hasNativeTabStop(element) || hasEditableTabStop(element);
|
|
185
342
|
}
|
|
186
|
-
function
|
|
187
|
-
return
|
|
188
|
-
|
|
343
|
+
function isRadio(element) {
|
|
344
|
+
return element instanceof HTMLInputElement && element.type === "radio";
|
|
345
|
+
}
|
|
346
|
+
function sameRadioGroup(a, b) {
|
|
347
|
+
return a.name !== "" && a.name === b.name && a.form === b.form && a.getRootNode() === b.getRootNode();
|
|
348
|
+
}
|
|
349
|
+
function isInEditableContent(element) {
|
|
350
|
+
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
351
|
+
if (node.getAttribute("contenteditable")?.toLowerCase() === "false") return false;
|
|
352
|
+
if (hasEditableTabStop(node)) return true;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
var XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
|
|
357
|
+
function isSvgLink(element) {
|
|
358
|
+
return element instanceof SVGElement && element.localName === "a" && (element.hasAttribute("href") || element.hasAttributeNS(XLINK_NAMESPACE, "href"));
|
|
359
|
+
}
|
|
360
|
+
function hasRenderedImage(area) {
|
|
361
|
+
const map = area.closest("map");
|
|
362
|
+
const key = map?.name || map?.id;
|
|
363
|
+
if (!map || !key) return false;
|
|
364
|
+
const root = map.getRootNode();
|
|
365
|
+
return Array.from(root.querySelectorAll("img[usemap]")).some(
|
|
366
|
+
(image) => image.getAttribute("usemap") === `#${key}` && isRenderedForFocus(image)
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
function isFocusableInOrder(element) {
|
|
370
|
+
const tabindex = authoredTabindex(element);
|
|
371
|
+
return (tabindex === null || tabindex >= 0) && canTakeFocus(element);
|
|
372
|
+
}
|
|
373
|
+
function extraStop(element) {
|
|
374
|
+
if (element instanceof HTMLAreaElement) {
|
|
375
|
+
const focusable = element.hasAttribute("href") || authoredTabindex(element) !== null;
|
|
376
|
+
return focusable && isFocusableInOrder(element) && hasRenderedImage(element);
|
|
377
|
+
}
|
|
378
|
+
if (element instanceof HTMLObjectElement) {
|
|
379
|
+
return isFocusableInOrder(element) && isRenderedForFocus(element) && element.contentWindow !== null;
|
|
380
|
+
}
|
|
381
|
+
if (isSvgLink(element))
|
|
382
|
+
return isFocusableInOrder(element) && isRenderedForFocus(element);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
function isSequentialStop(element) {
|
|
386
|
+
const stop = extraStop(element) ?? (element.matches(TAB_STOP_CANDIDATE_SELECTOR) && isTabStop(element));
|
|
387
|
+
if (!stop) return false;
|
|
388
|
+
const link = element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement || isSvgLink(element);
|
|
389
|
+
return !(link && authoredTabindex(element) === null && isInEditableContent(element));
|
|
390
|
+
}
|
|
391
|
+
function ownedScope(element) {
|
|
392
|
+
if (element.shadowRoot) return Array.from(element.shadowRoot.children);
|
|
393
|
+
if (element instanceof HTMLSlotElement) {
|
|
394
|
+
const assigned = element.assignedElements();
|
|
395
|
+
return assigned.length > 0 ? assigned : Array.from(element.children);
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
function orderScope(elements, walk) {
|
|
400
|
+
const entries = [];
|
|
401
|
+
const visit = (element) => {
|
|
402
|
+
if (element === walk.from) walk.fromAt = walk.tree.length;
|
|
403
|
+
const tabindex = authoredTabindex(element) ?? 0;
|
|
404
|
+
const stop = (element instanceof HTMLElement || element instanceof SVGElement) && isSequentialStop(element) ? element : null;
|
|
405
|
+
if (stop) walk.tree.push(stop);
|
|
406
|
+
const owned = ownedScope(element);
|
|
407
|
+
if (owned) {
|
|
408
|
+
if (tabindex >= 0) {
|
|
409
|
+
entries.push({ tabindex, stops: [...stop ? [stop] : [], ...orderScope(owned, walk)] });
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (stop) entries.push({ tabindex, stops: [stop] });
|
|
414
|
+
for (const child of Array.from(element.children)) visit(child);
|
|
415
|
+
};
|
|
416
|
+
for (const element of elements) visit(element);
|
|
417
|
+
const positive = entries.filter((entry) => entry.tabindex > 0);
|
|
418
|
+
positive.sort((a, b) => a.tabindex - b.tabindex);
|
|
419
|
+
const rest = entries.filter((entry) => entry.tabindex === 0);
|
|
420
|
+
return [...positive, ...rest].flatMap((entry) => entry.stops);
|
|
421
|
+
}
|
|
422
|
+
function passesRadioRule(element, tree, focusedRadio) {
|
|
423
|
+
if (!isRadio(element) || element.checked) return true;
|
|
424
|
+
const group = tree.filter(
|
|
425
|
+
(stop) => stop === element || isRadio(stop) && sameRadioGroup(stop, element)
|
|
189
426
|
);
|
|
427
|
+
if (group.some((radio) => radio.checked)) return false;
|
|
428
|
+
if (group[0] !== element) return false;
|
|
429
|
+
return focusedRadio === null || !sameRadioGroup(focusedRadio, element);
|
|
430
|
+
}
|
|
431
|
+
function startIndex(order, walk, backward, isStop) {
|
|
432
|
+
if (walk.fromAt < 0) return backward ? order.length - 1 : 0;
|
|
433
|
+
const at = order.indexOf(walk.from);
|
|
434
|
+
if (at >= 0) return backward ? at - 1 : at + 1;
|
|
435
|
+
const neighbour = backward ? walk.tree.slice(0, walk.fromAt).reverse().find(isStop) : walk.tree.slice(walk.fromAt).find(isStop);
|
|
436
|
+
if (neighbour) return order.indexOf(neighbour);
|
|
437
|
+
return backward ? order.length - 1 : 0;
|
|
438
|
+
}
|
|
439
|
+
function sequentialTabStops(root, from = null, backward = false) {
|
|
440
|
+
const walk = { from, tree: [], fromAt: from === root ? 0 : -1 };
|
|
441
|
+
const order = orderScope(ownedScope(root) ?? Array.from(root.children), walk);
|
|
442
|
+
const focusedRadio = walk.fromAt >= 0 && isRadio(from) ? from : null;
|
|
443
|
+
const isStop = (element) => passesRadioRule(element, walk.tree, focusedRadio);
|
|
444
|
+
const start = startIndex(order, walk, backward, isStop);
|
|
445
|
+
const visited = backward ? [...order.slice(0, start + 1).reverse(), ...order.slice(start + 1).reverse()] : [...order.slice(start), ...order.slice(0, start)];
|
|
446
|
+
return visited.filter((element) => element !== from && isStop(element));
|
|
447
|
+
}
|
|
448
|
+
function deepActiveElement(ownerDocument) {
|
|
449
|
+
let active = ownerDocument.activeElement;
|
|
450
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
451
|
+
return active;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
var MicrotaskCoalescer = class {
|
|
455
|
+
#run;
|
|
456
|
+
#queued = false;
|
|
457
|
+
#active = false;
|
|
458
|
+
#generation = 0;
|
|
459
|
+
constructor(run) {
|
|
460
|
+
this.#run = run;
|
|
461
|
+
}
|
|
462
|
+
activate() {
|
|
463
|
+
this.#active = true;
|
|
464
|
+
}
|
|
465
|
+
cancel() {
|
|
466
|
+
this.#active = false;
|
|
467
|
+
this.#queued = false;
|
|
468
|
+
this.#generation += 1;
|
|
469
|
+
}
|
|
470
|
+
schedule() {
|
|
471
|
+
if (!this.#active || this.#queued) return;
|
|
472
|
+
this.#queued = true;
|
|
473
|
+
const generation = this.#generation;
|
|
474
|
+
queueMicrotask(() => {
|
|
475
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
476
|
+
this.#queued = false;
|
|
477
|
+
this.#run();
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
function queryOpenDescendants(root, selector) {
|
|
483
|
+
const matches = [];
|
|
484
|
+
const roots = [root];
|
|
485
|
+
for (const scope of roots) {
|
|
486
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
487
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
488
|
+
const shadow = scope.shadowRoot;
|
|
489
|
+
if (shadow) roots.push(shadow);
|
|
490
|
+
}
|
|
491
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
492
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return matches;
|
|
190
496
|
}
|
|
191
497
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
#
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
498
|
+
var TabindexLoan = class _TabindexLoan {
|
|
499
|
+
static #registry = sharedRegistry(
|
|
500
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
501
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
502
|
+
);
|
|
503
|
+
#value;
|
|
504
|
+
#record;
|
|
505
|
+
#lent = /* @__PURE__ */ new Set();
|
|
506
|
+
constructor(value, owner) {
|
|
507
|
+
this.#value = value;
|
|
508
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
509
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
510
|
+
}
|
|
511
|
+
lend(element) {
|
|
512
|
+
const stack = this.#stack(element);
|
|
513
|
+
const current = element.getAttribute("tabindex");
|
|
514
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
515
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
516
|
+
);
|
|
517
|
+
const linked = stack.at(-1)?.value === current;
|
|
518
|
+
if (!held && current !== null && !linked) return;
|
|
519
|
+
this.#lent.add(element);
|
|
520
|
+
if (held) held.loan = this;
|
|
521
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
522
|
+
this.#mark(element, stack);
|
|
523
|
+
if (current === null || !held && current !== this.#value) {
|
|
524
|
+
element.setAttribute("tabindex", this.#value);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
returnAll() {
|
|
528
|
+
for (const element of this.#lent) {
|
|
529
|
+
const stack = this.#stack(element);
|
|
530
|
+
const hold = stack.find((held) => held.loan === this);
|
|
531
|
+
if (hold) this.#release(element, stack, hold);
|
|
532
|
+
}
|
|
533
|
+
this.#lent.clear();
|
|
534
|
+
}
|
|
535
|
+
reclaim(element) {
|
|
536
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
537
|
+
const stack = this.#stack(element);
|
|
538
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
539
|
+
if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
|
|
540
|
+
}
|
|
541
|
+
reclaimWithin(root) {
|
|
542
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
543
|
+
this.reclaim(element);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
#stack(element) {
|
|
547
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
548
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
549
|
+
stacks.set(element, stack);
|
|
550
|
+
return stack;
|
|
551
|
+
}
|
|
552
|
+
#seed(element) {
|
|
553
|
+
const current = element.getAttribute("tabindex");
|
|
554
|
+
const holds = [];
|
|
555
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
556
|
+
const value = element.getAttribute(record);
|
|
557
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
558
|
+
}
|
|
559
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
560
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
561
|
+
return holds;
|
|
562
|
+
}
|
|
563
|
+
#release(element, stack, hold) {
|
|
564
|
+
const current = element.getAttribute("tabindex");
|
|
565
|
+
const index = stack.indexOf(hold);
|
|
566
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
567
|
+
stack.splice(index, 1);
|
|
568
|
+
const above = stack[index];
|
|
569
|
+
if (above) above.linked &&= hold.linked;
|
|
570
|
+
this.#mark(element, stack);
|
|
571
|
+
if (!owns) return;
|
|
572
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
573
|
+
if (!below) element.removeAttribute("tabindex");
|
|
574
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
575
|
+
}
|
|
576
|
+
#mark(element, stack) {
|
|
577
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
578
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
579
|
+
for (const [name, recorded] of records) {
|
|
580
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
581
|
+
if (recorded === null) element.removeAttribute(name);
|
|
582
|
+
else element.setAttribute(name, recorded);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
var FOCUS_TRAP_OWNER = "stimeo-focus-trap";
|
|
588
|
+
var INERT_MARK = `data-${FOCUS_TRAP_OWNER}-inert`;
|
|
589
|
+
var OVERFLOW_MARK = `data-${FOCUS_TRAP_OWNER}-overflow`;
|
|
590
|
+
var FocusTrap = class _FocusTrap {
|
|
591
|
+
static #registries = sharedRegistry(
|
|
592
|
+
"stimeo-ui.focus-trap.registry.v2",
|
|
593
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
594
|
+
);
|
|
595
|
+
static #TEXT_ENTRY_TYPES = /* @__PURE__ */ new Set([
|
|
596
|
+
"text",
|
|
597
|
+
"search",
|
|
598
|
+
"email",
|
|
599
|
+
"url",
|
|
600
|
+
"tel",
|
|
601
|
+
"password",
|
|
602
|
+
"number"
|
|
603
|
+
]);
|
|
604
|
+
#openers = [];
|
|
605
|
+
#entry;
|
|
606
|
+
#tabindexLoans = /* @__PURE__ */ new Map();
|
|
203
607
|
#activeState = false;
|
|
204
|
-
/** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */
|
|
205
608
|
#escapeLayer = new EscapeLayer();
|
|
206
|
-
|
|
609
|
+
#gate = new DetachGate();
|
|
207
610
|
#getContainer;
|
|
208
|
-
/** Closing/focus hooks; see {@link FocusTrapOptions}. */
|
|
209
611
|
#options;
|
|
210
|
-
/**
|
|
211
|
-
* @param getContainer - Returns the trapped element. Called on every operation
|
|
212
|
-
* so the live target is always used.
|
|
213
|
-
* @param options - Closing/focus hooks; see {@link FocusTrapOptions}.
|
|
214
|
-
*/
|
|
215
612
|
constructor(getContainer, options = {}) {
|
|
216
613
|
this.#getContainer = getContainer;
|
|
217
614
|
this.#options = options;
|
|
615
|
+
this.#entry = {
|
|
616
|
+
container: () => this.#readContainer(),
|
|
617
|
+
moveFocus: (backward) => this.#moveFocus(backward),
|
|
618
|
+
refocus: () => this.#refocus(),
|
|
619
|
+
isolated: false,
|
|
620
|
+
lockedBody: null,
|
|
621
|
+
openedFrom: null,
|
|
622
|
+
handedBack: []
|
|
623
|
+
};
|
|
218
624
|
}
|
|
219
|
-
/** Whether the trap is currently active. */
|
|
220
625
|
get active() {
|
|
221
626
|
return this.#activeState;
|
|
222
627
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Applies the trap: records the current focus, optionally locks background scroll
|
|
225
|
-
* and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless
|
|
226
|
-
* `autoFocus` is off) moves focus inside. No-ops if already active.
|
|
227
|
-
*/
|
|
228
628
|
activate() {
|
|
229
629
|
if (this.#activeState) return;
|
|
630
|
+
_FocusTrap.#sweep();
|
|
230
631
|
this.#activeState = true;
|
|
231
|
-
|
|
232
|
-
this.#
|
|
233
|
-
if (this.#flag(this.#options.lockScroll, true))
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
|
|
239
|
-
document.addEventListener("keydown", this.#onKeydown);
|
|
240
|
-
this.#beforeCache.activate();
|
|
632
|
+
this.#openers = _FocusTrap.#focusPath();
|
|
633
|
+
this.#entry.openedFrom = _FocusTrap.#trapHolding(this.#openers[0] ?? null);
|
|
634
|
+
if (this.#flag(this.#options.lockScroll, true)) this.#lockScroll();
|
|
635
|
+
this.#entry.isolated = this.#flag(this.#options.isolate, true);
|
|
636
|
+
this.#joinStack();
|
|
637
|
+
_FocusTrap.#reconcileBackground(document);
|
|
241
638
|
const onEscape = this.#options.onEscape;
|
|
242
639
|
if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });
|
|
243
640
|
if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
|
|
244
641
|
}
|
|
245
|
-
/**
|
|
246
|
-
* Reverts every side effect applied by {@link activate}. No-ops if inactive, so
|
|
247
|
-
* a controller can call it defensively from both `close()` and `disconnect()`.
|
|
248
|
-
*
|
|
249
|
-
* @param restoreFocus - Move focus back to the opener (default `true`). Pass
|
|
250
|
-
* `false` on teardown (`disconnect`), where yanking focus is undesirable.
|
|
251
|
-
*/
|
|
252
642
|
deactivate({ restoreFocus = true } = {}) {
|
|
253
|
-
if (!this.#activeState)
|
|
643
|
+
if (!this.#activeState) {
|
|
644
|
+
this.#returnTabindexes();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
254
647
|
this.#activeState = false;
|
|
255
648
|
this.#escapeLayer.deactivate();
|
|
256
|
-
|
|
257
|
-
this.#
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
649
|
+
const above = this.#leaveStack();
|
|
650
|
+
this.#unlockScroll();
|
|
651
|
+
_FocusTrap.#reconcileBackground(document);
|
|
652
|
+
const wayBack = this.#handOver(above);
|
|
653
|
+
if (restoreFocus) this.#restore(wayBack, above);
|
|
654
|
+
this.#returnTabindexes();
|
|
655
|
+
}
|
|
656
|
+
connect() {
|
|
657
|
+
this.#gate.cancel();
|
|
658
|
+
_FocusTrap.#sweep();
|
|
659
|
+
new TabindexLoan("-1", FOCUS_TRAP_OWNER).reclaimWithin(document);
|
|
660
|
+
}
|
|
661
|
+
disconnect(host) {
|
|
662
|
+
if (this.#activeState) this.deactivate({ restoreFocus: false });
|
|
663
|
+
this.#gate.disconnected(host, () => this.#returnTabindexes());
|
|
664
|
+
}
|
|
665
|
+
refreshIsolation() {
|
|
666
|
+
if (!this.#activeState) return;
|
|
667
|
+
const isolate = this.#flag(this.#options.isolate, true);
|
|
668
|
+
if (isolate === this.#entry.isolated) return;
|
|
669
|
+
this.#entry.isolated = isolate;
|
|
670
|
+
const stranded = _FocusTrap.#reconcileBackground(document);
|
|
671
|
+
const active = deepActiveElement(document);
|
|
672
|
+
if (stranded.some((element) => flatTreeContains(element, active))) this.#focusInitial();
|
|
673
|
+
}
|
|
674
|
+
refreshContainer() {
|
|
675
|
+
if (!this.#activeState) return;
|
|
676
|
+
_FocusTrap.#reconcileBackground(document);
|
|
677
|
+
this.#returnTabindexes();
|
|
678
|
+
if (this.#ownsTab()) this.#refocus();
|
|
267
679
|
}
|
|
268
|
-
/** Resolves a boolean-or-getter option, defaulting when it was not provided. */
|
|
269
680
|
#flag(option, fallback) {
|
|
270
681
|
if (option === void 0) return fallback;
|
|
271
682
|
return typeof option === "function" ? option() : option;
|
|
272
683
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
* is left alone mid-navigation. The listener lives only while active.
|
|
280
|
-
*/
|
|
281
|
-
#beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));
|
|
282
|
-
/**
|
|
283
|
-
* Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the
|
|
284
|
-
* shared {@link EscapeLayer} resolver, so Tab trapping stays independent of
|
|
285
|
-
* which layer currently owns Escape.
|
|
286
|
-
*/
|
|
287
|
-
#onKeydown = (event) => {
|
|
288
|
-
if (event.key === "Tab") this.#trapTab(event);
|
|
289
|
-
};
|
|
290
|
-
/** Keeps `Tab` focus cycling within the container's focusable elements. */
|
|
291
|
-
#trapTab(event) {
|
|
292
|
-
const focusable = this.#focusableElements();
|
|
293
|
-
if (focusable.length === 0) {
|
|
294
|
-
event.preventDefault();
|
|
295
|
-
return;
|
|
684
|
+
#joinStack() {
|
|
685
|
+
const registry = _FocusTrap.#registry(document);
|
|
686
|
+
if (registry.stack.length === 0) {
|
|
687
|
+
document.addEventListener("keydown", registry.onKeydown);
|
|
688
|
+
document.addEventListener("turbo:morph", registry.onMorph);
|
|
689
|
+
document.addEventListener("turbo:morph-element", registry.onMorph);
|
|
296
690
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
691
|
+
registry.stack.push(this.#entry);
|
|
692
|
+
}
|
|
693
|
+
#leaveStack() {
|
|
694
|
+
const registry = _FocusTrap.#registry(document);
|
|
695
|
+
const index = registry.stack.indexOf(this.#entry);
|
|
696
|
+
const above = registry.stack.slice(index + 1);
|
|
697
|
+
registry.stack.splice(index, 1);
|
|
698
|
+
if (registry.stack.length === 0) {
|
|
699
|
+
document.removeEventListener("keydown", registry.onKeydown);
|
|
700
|
+
document.removeEventListener("turbo:morph", registry.onMorph);
|
|
701
|
+
document.removeEventListener("turbo:morph-element", registry.onMorph);
|
|
304
702
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
703
|
+
return above;
|
|
704
|
+
}
|
|
705
|
+
static #focusPath() {
|
|
706
|
+
const path = [];
|
|
707
|
+
for (let active = document.activeElement; active; active = active.shadowRoot?.activeElement ?? null) {
|
|
708
|
+
if (active instanceof HTMLElement && active !== document.body) path.unshift(active);
|
|
709
|
+
}
|
|
710
|
+
return path;
|
|
711
|
+
}
|
|
712
|
+
static #trapHolding(element) {
|
|
713
|
+
for (const trap of [..._FocusTrap.#registry(document).stack].reverse()) {
|
|
714
|
+
const container = trap.container();
|
|
715
|
+
if (container && flatTreeContains(container, element)) return trap;
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
#handOver(above) {
|
|
720
|
+
const entry = this.#entry;
|
|
721
|
+
const wayBack = [
|
|
722
|
+
...this.#openers,
|
|
723
|
+
this.#options.fallbackFocus?.() ?? null,
|
|
724
|
+
...entry.handedBack
|
|
725
|
+
].filter((element) => element !== null);
|
|
726
|
+
for (const trap of above) {
|
|
727
|
+
if (trap.openedFrom !== entry) continue;
|
|
728
|
+
trap.handedBack = [...trap.handedBack, ...wayBack];
|
|
729
|
+
trap.openedFrom = entry.openedFrom;
|
|
730
|
+
}
|
|
731
|
+
this.#openers = [];
|
|
732
|
+
entry.openedFrom = null;
|
|
733
|
+
entry.handedBack = [];
|
|
734
|
+
return wayBack;
|
|
735
|
+
}
|
|
736
|
+
#restore(wayBack, above) {
|
|
737
|
+
const active = deepActiveElement(document);
|
|
738
|
+
const released = this.#readContainer();
|
|
739
|
+
const holds = (trap) => {
|
|
740
|
+
const container = trap.container();
|
|
741
|
+
if (!container || !_FocusTrap.#takesTab(trap) || !flatTreeContains(container, active)) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
return !released || !flatTreeContains(released, active) || flatTreeContains(released, container);
|
|
745
|
+
};
|
|
746
|
+
if (above.some(holds)) return;
|
|
747
|
+
for (const element of wayBack) {
|
|
748
|
+
if (this.#activeState) return;
|
|
749
|
+
if (!element.isConnected || !canTakeFocus(element) || !isRenderedForFocus(element)) continue;
|
|
750
|
+
const before = deepActiveElement(document);
|
|
751
|
+
element.focus();
|
|
752
|
+
const after = deepActiveElement(document);
|
|
753
|
+
if (after === element || after !== before) return;
|
|
754
|
+
}
|
|
755
|
+
_FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack)?.refocus();
|
|
756
|
+
}
|
|
757
|
+
#refocus() {
|
|
758
|
+
if (!this.#flag(this.#options.autoFocus, true)) return;
|
|
759
|
+
if (flatTreeContains(this.#getContainer(), deepActiveElement(document))) return;
|
|
760
|
+
this.#focusInitial();
|
|
761
|
+
}
|
|
762
|
+
#lendTabindex(container) {
|
|
763
|
+
const loan = this.#tabindexLoans.get(container) ?? new TabindexLoan("-1", FOCUS_TRAP_OWNER);
|
|
764
|
+
loan.lend(container);
|
|
765
|
+
this.#tabindexLoans.set(container, loan);
|
|
766
|
+
}
|
|
767
|
+
#returnTabindexes() {
|
|
768
|
+
const focused = deepActiveElement(document);
|
|
769
|
+
for (const [element, loan] of [...this.#tabindexLoans]) {
|
|
770
|
+
if (element === focused) continue;
|
|
771
|
+
loan.returnAll();
|
|
772
|
+
this.#tabindexLoans.delete(element);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
#lockScroll() {
|
|
776
|
+
const registry = _FocusTrap.#registry(document);
|
|
777
|
+
const body = document.body;
|
|
778
|
+
if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
|
|
779
|
+
registry.scrollLocks.add(this.#entry);
|
|
780
|
+
this.#entry.lockedBody = body;
|
|
781
|
+
body.style.overflow = "hidden";
|
|
782
|
+
}
|
|
783
|
+
#unlockScroll() {
|
|
784
|
+
const registry = _FocusTrap.#registry(document);
|
|
785
|
+
const body = this.#entry.lockedBody;
|
|
786
|
+
this.#entry.lockedBody = null;
|
|
787
|
+
if (!registry.scrollLocks.delete(this.#entry) || !body) return;
|
|
788
|
+
if (!_FocusTrap.#isLocked(registry, body)) _FocusTrap.#unlock(body);
|
|
789
|
+
}
|
|
790
|
+
static #isLocked(registry, body) {
|
|
791
|
+
return [...registry.scrollLocks].some((trap) => trap.lockedBody === body);
|
|
792
|
+
}
|
|
793
|
+
static #unlock(body) {
|
|
794
|
+
const saved = body.getAttribute(OVERFLOW_MARK);
|
|
795
|
+
if (saved === null) return;
|
|
796
|
+
body.removeAttribute(OVERFLOW_MARK);
|
|
797
|
+
body.style.overflow = saved;
|
|
798
|
+
}
|
|
799
|
+
static #sweep() {
|
|
800
|
+
const registry = _FocusTrap.#registry(document);
|
|
801
|
+
for (const element of queryOpenDescendants(document, `[${INERT_MARK}]`)) {
|
|
802
|
+
if (registry.inerted.has(element)) continue;
|
|
803
|
+
element.removeAttribute(INERT_MARK);
|
|
804
|
+
element.inert = false;
|
|
805
|
+
}
|
|
806
|
+
if (!_FocusTrap.#isLocked(registry, document.body)) _FocusTrap.#unlock(document.body);
|
|
807
|
+
}
|
|
808
|
+
static #registry(ownerDocument) {
|
|
809
|
+
const existing = _FocusTrap.#registries.get(ownerDocument);
|
|
810
|
+
if (existing) return existing;
|
|
811
|
+
const morphs = new MicrotaskCoalescer(() => _FocusTrap.#reapply(registry));
|
|
812
|
+
morphs.activate();
|
|
813
|
+
const registry = {
|
|
814
|
+
stack: [],
|
|
815
|
+
onKeydown: (event) => _FocusTrap.#resolveTab(registry.stack, event),
|
|
816
|
+
onMorph: () => morphs.schedule(),
|
|
817
|
+
inerted: /* @__PURE__ */ new Set(),
|
|
818
|
+
scrollLocks: /* @__PURE__ */ new Set()
|
|
819
|
+
};
|
|
820
|
+
_FocusTrap.#registries.set(ownerDocument, registry);
|
|
821
|
+
return registry;
|
|
822
|
+
}
|
|
823
|
+
static #reapply(registry) {
|
|
824
|
+
_FocusTrap.#reconcileBackground(document);
|
|
825
|
+
const body = document.body;
|
|
826
|
+
if (!_FocusTrap.#isLocked(registry, body)) return;
|
|
827
|
+
if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
|
|
828
|
+
body.style.overflow = "hidden";
|
|
829
|
+
}
|
|
830
|
+
static #reconcileBackground(ownerDocument) {
|
|
831
|
+
const registry = _FocusTrap.#registry(ownerDocument);
|
|
832
|
+
for (const element of registry.inerted) {
|
|
833
|
+
if (!element.hasAttribute(INERT_MARK)) registry.inerted.delete(element);
|
|
834
|
+
}
|
|
835
|
+
const background = _FocusTrap.#backgroundOf(registry);
|
|
836
|
+
for (const element of registry.inerted) {
|
|
837
|
+
if (background.has(element)) continue;
|
|
838
|
+
element.removeAttribute(INERT_MARK);
|
|
839
|
+
element.inert = false;
|
|
840
|
+
registry.inerted.delete(element);
|
|
841
|
+
}
|
|
842
|
+
const added = [];
|
|
843
|
+
for (const element of background) {
|
|
844
|
+
if (registry.inerted.has(element)) continue;
|
|
845
|
+
element.setAttribute(INERT_MARK, "");
|
|
846
|
+
element.inert = true;
|
|
847
|
+
registry.inerted.add(element);
|
|
848
|
+
added.push(element);
|
|
849
|
+
}
|
|
850
|
+
return added;
|
|
851
|
+
}
|
|
852
|
+
static #backgroundOf(registry) {
|
|
853
|
+
const containers = registry.stack.map((trap) => trap.container());
|
|
854
|
+
let top = -1;
|
|
855
|
+
registry.stack.forEach((trap, index) => {
|
|
856
|
+
if (trap.isolated && containers[index]) top = index;
|
|
857
|
+
});
|
|
858
|
+
const background = /* @__PURE__ */ new Set();
|
|
859
|
+
if (top < 0) return background;
|
|
860
|
+
const kept = /* @__PURE__ */ new Set();
|
|
861
|
+
const keep = (path) => {
|
|
862
|
+
for (const node of path) {
|
|
863
|
+
kept.add(node);
|
|
864
|
+
background.delete(node);
|
|
335
865
|
}
|
|
336
|
-
node
|
|
866
|
+
for (const node of path) {
|
|
867
|
+
const parent = flatTreeParent(node);
|
|
868
|
+
for (const sibling of parent ? flatTreeChildren(parent) : []) {
|
|
869
|
+
if (!(sibling instanceof HTMLElement) || kept.has(sibling)) continue;
|
|
870
|
+
if (sibling.inert && !registry.inerted.has(sibling)) continue;
|
|
871
|
+
background.add(sibling);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
containers.slice(top).forEach((container, index) => {
|
|
876
|
+
const path = [];
|
|
877
|
+
for (let node = container; node && node !== document.body && !kept.has(node); ) {
|
|
878
|
+
path.push(node);
|
|
879
|
+
node = flatTreeParent(node);
|
|
880
|
+
}
|
|
881
|
+
if (index === 0 || path.some((node) => background.has(node))) keep(path);
|
|
882
|
+
});
|
|
883
|
+
return background;
|
|
884
|
+
}
|
|
885
|
+
#readContainer() {
|
|
886
|
+
try {
|
|
887
|
+
return this.#getContainer();
|
|
888
|
+
} catch {
|
|
889
|
+
return null;
|
|
337
890
|
}
|
|
338
891
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
892
|
+
static #resolveTab(stack, event) {
|
|
893
|
+
if (event.key !== "Tab" || event.defaultPrevented) return;
|
|
894
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
895
|
+
if (event.ctrlKey || event.metaKey) return;
|
|
896
|
+
const owner = _FocusTrap.#tabOwner(stack);
|
|
897
|
+
if (!owner) return;
|
|
898
|
+
event.preventDefault();
|
|
899
|
+
owner.moveFocus(event.shiftKey);
|
|
900
|
+
}
|
|
901
|
+
static #tabOwner(stack) {
|
|
902
|
+
for (const trap of [...stack].reverse()) {
|
|
903
|
+
if (_FocusTrap.#takesTab(trap)) return trap;
|
|
904
|
+
}
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
static #takesTab(trap) {
|
|
908
|
+
const container = trap.container();
|
|
909
|
+
if (!container) return false;
|
|
910
|
+
return container.isConnected && !closestInFlatTree(container, "[hidden]") && isRenderedForFocus(container);
|
|
911
|
+
}
|
|
912
|
+
#ownsTab() {
|
|
913
|
+
return _FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack) === this.#entry;
|
|
914
|
+
}
|
|
915
|
+
#moveFocus(backward) {
|
|
916
|
+
const from = deepActiveElement(document);
|
|
917
|
+
for (const stop of sequentialTabStops(this.#getContainer(), from, backward)) {
|
|
918
|
+
if (!this.#ownsTab()) return;
|
|
919
|
+
const before = deepActiveElement(document);
|
|
920
|
+
stop.focus();
|
|
921
|
+
const after = deepActiveElement(document);
|
|
922
|
+
if (after === before) continue;
|
|
923
|
+
if (after === stop) _FocusTrap.#selectOnEntry(stop);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
static #selectOnEntry(stop) {
|
|
928
|
+
if (stop instanceof HTMLInputElement && _FocusTrap.#TEXT_ENTRY_TYPES.has(stop.type)) {
|
|
929
|
+
stop.select();
|
|
343
930
|
}
|
|
344
|
-
this.#inertedSiblings = [];
|
|
345
931
|
}
|
|
346
|
-
/** Moves focus to the initial target, the first focusable, or the container. */
|
|
347
932
|
#focusInitial() {
|
|
348
933
|
const preferred = this.#options.initialFocus?.();
|
|
349
934
|
if (preferred) {
|
|
350
935
|
preferred.focus();
|
|
351
936
|
return;
|
|
352
937
|
}
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
938
|
+
const container = this.#getContainer();
|
|
939
|
+
const [first] = sequentialTabStops(container);
|
|
940
|
+
if (first) {
|
|
941
|
+
first.focus();
|
|
356
942
|
return;
|
|
357
943
|
}
|
|
358
|
-
|
|
359
|
-
container.tabIndex = -1;
|
|
944
|
+
this.#lendTabindex(container);
|
|
360
945
|
container.focus();
|
|
361
946
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
var ListenerSet = class {
|
|
950
|
+
#abort = new AbortController();
|
|
951
|
+
add(target, type, handler, options) {
|
|
952
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
953
|
+
}
|
|
954
|
+
dispose() {
|
|
955
|
+
this.#abort.abort();
|
|
956
|
+
this.#abort = new AbortController();
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
var MorphRenderWatcher = class {
|
|
961
|
+
#listeners = new ListenerSet();
|
|
962
|
+
#pass;
|
|
963
|
+
#element = null;
|
|
964
|
+
#onMorph = () => this.#pass.schedule();
|
|
965
|
+
constructor(run) {
|
|
966
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
967
|
+
}
|
|
968
|
+
observe(element) {
|
|
969
|
+
if (this.#element === element) return;
|
|
970
|
+
this.disconnect();
|
|
971
|
+
this.#element = element;
|
|
972
|
+
this.#pass.activate();
|
|
973
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
974
|
+
}
|
|
975
|
+
schedule() {
|
|
976
|
+
this.#pass.schedule();
|
|
977
|
+
}
|
|
978
|
+
disconnect() {
|
|
979
|
+
this.#listeners.dispose();
|
|
980
|
+
this.#pass.cancel();
|
|
981
|
+
this.#element = null;
|
|
365
982
|
}
|
|
366
983
|
};
|
|
367
984
|
|
|
368
|
-
|
|
369
|
-
var ConfirmController = class extends Controller {
|
|
985
|
+
var ConfirmController = class _ConfirmController extends Controller {
|
|
370
986
|
static targets = ["dialog", "title", "message", "confirm", "cancel"];
|
|
371
987
|
static values = {
|
|
372
988
|
confirmLabel: { type: String, default: "OK" },
|
|
@@ -375,112 +991,187 @@ var ConfirmController = class extends Controller {
|
|
|
375
991
|
};
|
|
376
992
|
static actions = ["confirm", "cancel", "request"];
|
|
377
993
|
static events = ["open", "resolve"];
|
|
378
|
-
/** Resolver for the in-flight confirmation Promise (one dialog at a time). */
|
|
379
994
|
#pending = null;
|
|
380
|
-
/** Turbo's forms config and its original confirm method, for restore. */
|
|
381
995
|
#turboForms = null;
|
|
382
996
|
#previousConfirm = void 0;
|
|
997
|
+
#continuing = null;
|
|
998
|
+
#connected = false;
|
|
999
|
+
#asked = 0;
|
|
1000
|
+
#dialog = null;
|
|
1001
|
+
#hidden = new AttributeLease("hidden", this.identifier);
|
|
1002
|
+
#wording = null;
|
|
383
1003
|
#trap = new FocusTrap(() => this.dialogTarget, {
|
|
384
1004
|
onEscape: () => this.#resolve(false),
|
|
385
1005
|
initialFocus: () => this.#initialFocusElement()
|
|
386
1006
|
});
|
|
1007
|
+
#morphRender = new MorphRenderWatcher(() => this.#repair());
|
|
387
1008
|
connect() {
|
|
388
|
-
|
|
1009
|
+
this.#trap.connect();
|
|
1010
|
+
this.#connected = true;
|
|
1011
|
+
if (this.hasDialogTarget) this.#hidden.write(this.dialogTarget, "");
|
|
1012
|
+
this.#dialog = this.hasDialogTarget ? this.dialogTarget : null;
|
|
389
1013
|
this.#installTurboHook();
|
|
1014
|
+
this.#morphRender.observe(this.element);
|
|
1015
|
+
}
|
|
1016
|
+
#repair() {
|
|
1017
|
+
if (!this.hasDialogTarget) return;
|
|
1018
|
+
this.#hidden.write(this.dialogTarget, this.#pending ? null : "");
|
|
1019
|
+
this.#applyWording();
|
|
390
1020
|
}
|
|
391
1021
|
disconnect() {
|
|
1022
|
+
this.#connected = false;
|
|
392
1023
|
if (this.#turboForms) {
|
|
393
1024
|
this.#turboForms.confirm = this.#previousConfirm;
|
|
394
1025
|
this.#turboForms = null;
|
|
395
1026
|
}
|
|
396
1027
|
this.#resolve(false, false);
|
|
397
|
-
this.#trap.
|
|
1028
|
+
this.#trap.disconnect(this);
|
|
1029
|
+
this.#morphRender.disconnect();
|
|
1030
|
+
}
|
|
1031
|
+
dialogTargetConnected() {
|
|
1032
|
+
if (this.#connected) this.#adoptDialog();
|
|
1033
|
+
}
|
|
1034
|
+
dialogTargetDisconnected(dialog) {
|
|
1035
|
+
if (this.dialogTargets.includes(dialog)) return;
|
|
1036
|
+
this.#hidden.return(dialog);
|
|
1037
|
+
if (this.#connected) this.#adoptDialog();
|
|
1038
|
+
}
|
|
1039
|
+
messageTargetConnected() {
|
|
1040
|
+
this.#applyWording();
|
|
1041
|
+
}
|
|
1042
|
+
messageTargetDisconnected() {
|
|
1043
|
+
this.#applyWording();
|
|
1044
|
+
}
|
|
1045
|
+
confirmTargetConnected() {
|
|
1046
|
+
this.#applyWording();
|
|
1047
|
+
}
|
|
1048
|
+
confirmTargetDisconnected() {
|
|
1049
|
+
this.#applyWording();
|
|
1050
|
+
}
|
|
1051
|
+
cancelTargetConnected() {
|
|
1052
|
+
this.#applyWording();
|
|
1053
|
+
}
|
|
1054
|
+
cancelTargetDisconnected() {
|
|
1055
|
+
this.#applyWording();
|
|
398
1056
|
}
|
|
399
|
-
/** Confirms the pending request (resolves `true`). Bound via `data-action`. */
|
|
400
1057
|
confirm() {
|
|
401
1058
|
this.#resolve(true);
|
|
402
1059
|
}
|
|
403
|
-
/** Cancels the pending request (resolves `false`). Bound via `data-action`. */
|
|
404
1060
|
cancel() {
|
|
405
1061
|
this.#resolve(false);
|
|
406
1062
|
}
|
|
407
|
-
/**
|
|
408
|
-
* Intercepts a link/button click, shows the confirm dialog, and continues the
|
|
409
|
-
* original action (form submit or navigation) only when confirmed. The message
|
|
410
|
-
* comes from the `message` action param or the element's `data-turbo-confirm`.
|
|
411
|
-
*/
|
|
412
1063
|
request(event) {
|
|
413
1064
|
const element = event.currentTarget ?? event.target;
|
|
414
|
-
if (!element) return;
|
|
1065
|
+
if (!element || element === this.#continuing) return;
|
|
415
1066
|
event.preventDefault();
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
void this.#prompt(message).then((confirmed) => {
|
|
420
|
-
if (confirmed) this.#continue(element);
|
|
1067
|
+
const submitter = event.submitter ?? null;
|
|
1068
|
+
void this.#prompt(this.#messageOf(element, submitter)).then((confirmed) => {
|
|
1069
|
+
if (confirmed) this.#continue(element, submitter);
|
|
421
1070
|
});
|
|
422
1071
|
}
|
|
423
|
-
/**
|
|
424
|
-
* Opens the dialog for `message` and resolves once the user confirms or cancels.
|
|
425
|
-
* Degrades to native `window.confirm` when no dialog target is present.
|
|
426
|
-
*
|
|
427
|
-
* @stimeoRuntimeOnly The labels word the one dialog this call opens; every opening writes both
|
|
428
|
-
* labels again, and the dialog stays hidden between prompts.
|
|
429
|
-
*/
|
|
430
1072
|
#prompt(message) {
|
|
1073
|
+
if (!this.#connected) return Promise.resolve(false);
|
|
431
1074
|
if (!this.hasDialogTarget) return Promise.resolve(window.confirm(message));
|
|
1075
|
+
const asked = ++this.#asked;
|
|
432
1076
|
this.#resolve(false);
|
|
433
|
-
if (this
|
|
434
|
-
|
|
435
|
-
|
|
1077
|
+
if (asked !== this.#asked) return Promise.resolve(false);
|
|
1078
|
+
this.#wording = { message, confirm: this.confirmLabelValue, cancel: this.cancelLabelValue };
|
|
1079
|
+
this.#applyWording();
|
|
436
1080
|
return new Promise((resolve) => {
|
|
437
1081
|
this.#pending = resolve;
|
|
438
|
-
this.dialogTarget
|
|
1082
|
+
this.#hidden.write(this.dialogTarget, null);
|
|
439
1083
|
this.dispatch("open", { detail: { message } });
|
|
1084
|
+
if (this.#pending !== resolve) return;
|
|
440
1085
|
this.#trap.activate();
|
|
441
1086
|
});
|
|
442
1087
|
}
|
|
443
|
-
/**
|
|
444
|
-
* Settles the pending Promise, closes the dialog, and emits `resolve`.
|
|
445
|
-
* `restoreFocus` is forwarded to the trap so teardown (disconnect) can settle a
|
|
446
|
-
* pending confirmation without moving focus.
|
|
447
|
-
*/
|
|
448
1088
|
#resolve(confirmed, restoreFocus = true) {
|
|
449
1089
|
const pending = this.#pending;
|
|
450
1090
|
if (!pending) return;
|
|
451
1091
|
this.#pending = null;
|
|
452
|
-
|
|
1092
|
+
this.#wording = null;
|
|
1093
|
+
if (this.hasDialogTarget) this.#hidden.write(this.dialogTarget, "");
|
|
453
1094
|
this.#trap.deactivate({ restoreFocus });
|
|
454
1095
|
this.dispatch("resolve", { detail: { confirmed } });
|
|
455
1096
|
pending(confirmed);
|
|
456
1097
|
}
|
|
457
|
-
|
|
458
|
-
|
|
1098
|
+
#applyWording() {
|
|
1099
|
+
const wording = this.#wording;
|
|
1100
|
+
if (!wording) return;
|
|
1101
|
+
this.#word(this.messageTargets, wording.message);
|
|
1102
|
+
this.#word(this.confirmTargets, wording.confirm);
|
|
1103
|
+
this.#word(this.cancelTargets, wording.cancel);
|
|
1104
|
+
}
|
|
1105
|
+
#word(targets, text) {
|
|
1106
|
+
const slot = this.#inDialog(targets);
|
|
1107
|
+
if (slot) slot.textContent = text;
|
|
1108
|
+
}
|
|
1109
|
+
#inDialog(targets) {
|
|
1110
|
+
if (!this.hasDialogTarget) return null;
|
|
1111
|
+
const dialog = this.dialogTarget;
|
|
1112
|
+
return targets.find((element) => dialog.contains(element)) ?? null;
|
|
1113
|
+
}
|
|
1114
|
+
#adoptDialog() {
|
|
1115
|
+
const dialog = this.hasDialogTarget ? this.dialogTarget : null;
|
|
1116
|
+
if (dialog === this.#dialog) return;
|
|
1117
|
+
this.#dialog = dialog;
|
|
1118
|
+
if (!this.#pending) {
|
|
1119
|
+
if (dialog) this.#hidden.write(dialog, "");
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (!dialog) {
|
|
1123
|
+
this.#resolve(false);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
this.#hidden.write(dialog, null);
|
|
1127
|
+
this.#applyWording();
|
|
1128
|
+
this.#trap.refreshContainer();
|
|
1129
|
+
}
|
|
1130
|
+
#messageOf(element, submitter) {
|
|
1131
|
+
return element.getAttribute(`data-${this.identifier}-message-param`) || (submitter ?? element).getAttribute("data-turbo-confirm") || _ConfirmController.#formOf(element)?.getAttribute("data-turbo-confirm") || "";
|
|
1132
|
+
}
|
|
1133
|
+
#continue(element, submitter) {
|
|
459
1134
|
if (element instanceof HTMLAnchorElement && element.href) {
|
|
460
1135
|
window.location.href = element.href;
|
|
461
1136
|
return;
|
|
462
1137
|
}
|
|
463
|
-
const form = element
|
|
464
|
-
if (form)
|
|
465
|
-
|
|
1138
|
+
const form = _ConfirmController.#formOf(element);
|
|
1139
|
+
if (form) this.#submit(form, element === form ? submitter : element);
|
|
1140
|
+
}
|
|
1141
|
+
static #formOf(element) {
|
|
1142
|
+
if (element instanceof HTMLFormElement) return element;
|
|
1143
|
+
return _ConfirmController.#isSubmitButton(element) ? element.form : null;
|
|
1144
|
+
}
|
|
1145
|
+
#submit(form, submitter) {
|
|
1146
|
+
const button = _ConfirmController.#isSubmitButton(submitter) && submitter.form === form ? submitter : void 0;
|
|
1147
|
+
this.#continuing = form;
|
|
1148
|
+
try {
|
|
1149
|
+
form.requestSubmit(button);
|
|
1150
|
+
} finally {
|
|
1151
|
+
this.#continuing = null;
|
|
466
1152
|
}
|
|
467
1153
|
}
|
|
468
|
-
|
|
1154
|
+
static #isSubmitButton(element) {
|
|
1155
|
+
if (element instanceof HTMLButtonElement) return element.type === "submit";
|
|
1156
|
+
return element instanceof HTMLInputElement && (element.type === "submit" || element.type === "image");
|
|
1157
|
+
}
|
|
469
1158
|
#installTurboHook() {
|
|
470
1159
|
const turbo = window.Turbo;
|
|
471
1160
|
const forms = turbo?.config?.forms;
|
|
472
1161
|
if (!forms) return;
|
|
473
1162
|
this.#turboForms = forms;
|
|
474
1163
|
this.#previousConfirm = forms.confirm;
|
|
475
|
-
forms.confirm = (message) => this.#
|
|
1164
|
+
forms.confirm = (message, form) => this.#askForTurbo(message, form);
|
|
1165
|
+
}
|
|
1166
|
+
#askForTurbo(message, form) {
|
|
1167
|
+
if (this.#continuing && form === this.#continuing) return Promise.resolve(true);
|
|
1168
|
+
return this.#prompt(message);
|
|
476
1169
|
}
|
|
477
|
-
/** The button to focus on open — the least-destructive one (cancel) by default. */
|
|
478
1170
|
#initialFocusElement() {
|
|
479
|
-
|
|
480
|
-
return
|
|
1171
|
+
const confirm = this.initialFocusValue === "confirm" ? this.#inDialog(this.confirmTargets) : null;
|
|
1172
|
+
return confirm ?? this.#inDialog(this.cancelTargets);
|
|
481
1173
|
}
|
|
482
1174
|
};
|
|
483
1175
|
|
|
484
1176
|
export { ConfirmController };
|
|
485
|
-
//# sourceMappingURL=confirm_controller.js.map
|
|
486
1177
|
//# sourceMappingURL=confirm_controller.js.map
|