stimeo-ui 0.16.0 → 0.18.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 +136 -0
- data/README.md +27 -11
- data/dist/cable/index.js +384 -449
- data/dist/controllers/accordion_controller.js +174 -55
- data/dist/controllers/alert_dialog_controller.js +903 -235
- data/dist/controllers/announcer_controller.js +238 -205
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +414 -145
- data/dist/controllers/avatar_controller.js +185 -107
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +282 -58
- data/dist/controllers/calendar_controller.js +228 -260
- data/dist/controllers/carousel_controller.js +536 -436
- data/dist/controllers/character_counter_controller.js +493 -207
- data/dist/controllers/checkbox_controller.js +38 -74
- data/dist/controllers/clipboard_controller.js +261 -149
- data/dist/controllers/collapsible_controller.js +304 -120
- data/dist/controllers/color_picker_controller.js +178 -146
- data/dist/controllers/combobox_controller.js +522 -156
- data/dist/controllers/command_palette_controller.js +1156 -395
- data/dist/controllers/conditional_fields_controller.js +300 -171
- data/dist/controllers/confirm_controller.js +980 -262
- data/dist/controllers/context_menu_controller.js +346 -153
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +281 -158
- data/dist/controllers/currency_input_controller.js +575 -225
- data/dist/controllers/data_grid_controller.js +248 -214
- data/dist/controllers/date_range_picker_controller.js +338 -235
- data/dist/controllers/dialog_controller.js +899 -237
- data/dist/controllers/direct_upload_controller.js +97 -167
- data/dist/controllers/dirty_form_controller.js +72 -69
- data/dist/controllers/dismissible_controller.js +196 -24
- data/dist/controllers/drawer_controller.js +974 -355
- data/dist/controllers/dropdown_controller.js +339 -96
- data/dist/controllers/editable_controller.js +315 -88
- data/dist/controllers/empty_state_controller.js +79 -58
- data/dist/controllers/file_dropzone_controller.js +322 -211
- data/dist/controllers/filter_controller.js +77 -42
- data/dist/controllers/flash_controller.js +238 -347
- data/dist/controllers/focus_controller.js +679 -240
- data/dist/controllers/form_field_controller.js +156 -132
- data/dist/controllers/form_validation_controller.js +31 -97
- data/dist/controllers/frame_loading_controller.js +275 -217
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +327 -130
- data/dist/controllers/idle_controller.js +271 -101
- data/dist/controllers/input_mask_controller.js +483 -97
- data/dist/controllers/intersection_controller.js +150 -106
- data/dist/controllers/lazy_frame_controller.js +24 -68
- data/dist/controllers/listbox_controller.js +523 -240
- data/dist/controllers/local_time_controller.js +102 -77
- data/dist/controllers/masonry_controller.js +80 -96
- data/dist/controllers/menu_controller.js +302 -188
- data/dist/controllers/menubar_controller.js +232 -345
- data/dist/controllers/meter_controller.js +417 -87
- data/dist/controllers/multi_select_controller.js +785 -393
- data/dist/controllers/navigation_menu_controller.js +190 -250
- data/dist/controllers/nested_form_controller.js +396 -192
- data/dist/controllers/network_status_controller.js +268 -62
- data/dist/controllers/number_input_controller.js +610 -303
- data/dist/controllers/optimistic_controller.js +45 -87
- data/dist/controllers/otp_controller.js +658 -255
- data/dist/controllers/overflow_indicator_controller.js +122 -113
- data/dist/controllers/overflow_menu_controller.js +470 -295
- data/dist/controllers/pagination_controller.js +332 -123
- data/dist/controllers/password_reveal_controller.js +392 -121
- data/dist/controllers/password_strength_controller.js +111 -158
- data/dist/controllers/persist_controller.js +165 -111
- data/dist/controllers/pointer_drag_controller.js +434 -188
- data/dist/controllers/popover_controller.js +269 -113
- data/dist/controllers/portal_controller.js +215 -76
- data/dist/controllers/preview_guard_controller.js +243 -116
- data/dist/controllers/progress_controller.js +391 -78
- data/dist/controllers/radio_group_controller.js +278 -211
- data/dist/controllers/range_slider_controller.js +220 -113
- data/dist/controllers/rating_controller.js +458 -211
- data/dist/controllers/read_more_controller.js +138 -63
- data/dist/controllers/reading_progress_controller.js +166 -137
- data/dist/controllers/relative_time_controller.js +211 -112
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
- data/dist/controllers/resizable_controller.js +299 -150
- data/dist/controllers/roving_controller.js +49 -88
- data/dist/controllers/scroll_area_controller.js +490 -246
- data/dist/controllers/scroll_restore_controller.js +14 -55
- data/dist/controllers/scroll_visibility_controller.js +432 -149
- data/dist/controllers/scrollspy_controller.js +193 -248
- data/dist/controllers/separator_controller.js +285 -142
- data/dist/controllers/sidebar_controller.js +1069 -404
- data/dist/controllers/skeleton_controller.js +259 -101
- data/dist/controllers/slider_controller.js +187 -86
- data/dist/controllers/smart_sticky_header_controller.js +173 -66
- data/dist/controllers/sortable_controller.js +26 -125
- data/dist/controllers/spinner_controller.js +288 -173
- data/dist/controllers/step_indicator_controller.js +105 -46
- data/dist/controllers/stepper_controller.js +120 -73
- data/dist/controllers/stick_to_bottom_controller.js +275 -140
- data/dist/controllers/sticky_observer_controller.js +220 -49
- data/dist/controllers/submit_once_controller.js +376 -227
- data/dist/controllers/switch_controller.js +201 -84
- data/dist/controllers/tabs_controller.js +41 -30
- data/dist/controllers/tags_input_controller.js +293 -173
- data/dist/controllers/textarea_autosize_controller.js +108 -74
- data/dist/controllers/theme_controller.js +140 -116
- data/dist/controllers/time_picker_controller.js +331 -127
- data/dist/controllers/toast_controller.js +244 -324
- data/dist/controllers/toggle_group_controller.js +252 -191
- data/dist/controllers/toolbar_controller.js +219 -120
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +283 -369
- data/dist/index.js +8573 -10410
- data/dist/positioning/index.js +154 -47
- data/lib/generators/stimeo/install/install_generator.rb +7 -2
- data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,68 +1,64 @@
|
|
|
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
|
+
}
|
|
12
|
+
|
|
13
|
+
var PREFIX = "__stimeo_raw_v1_";
|
|
14
|
+
sharedRegistry(
|
|
15
|
+
"stimeo-ui.form-data-projection.registry.v1",
|
|
16
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
17
|
+
);
|
|
18
|
+
function decodedName(key) {
|
|
19
|
+
if (!key.startsWith(PREFIX)) return null;
|
|
20
|
+
const separator = PREFIX.length + 32;
|
|
21
|
+
if (key[separator] !== ":") return null;
|
|
22
|
+
try {
|
|
23
|
+
const name = JSON.parse(key.slice(separator + 1));
|
|
24
|
+
return typeof name === "string" ? name : null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function projectedFieldName(name) {
|
|
30
|
+
return decodedName(name) ?? name;
|
|
31
|
+
}
|
|
4
32
|
|
|
5
|
-
// src/utils/listener_set.ts
|
|
6
33
|
var ListenerSet = class {
|
|
7
|
-
/** The generation every `add` joins until the next `dispose()`. */
|
|
8
34
|
#abort = new AbortController();
|
|
9
|
-
/**
|
|
10
|
-
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
11
|
-
*
|
|
12
|
-
* The set supplies the signal, so `options` carries everything else the DOM
|
|
13
|
-
* accepts — `capture` included, which has to match at release time and no
|
|
14
|
-
* longer has a second place to drift from.
|
|
15
|
-
*/
|
|
16
35
|
add(target, type, handler, options) {
|
|
17
36
|
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
18
37
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Releases every listener of the open generation, synchronously, and opens the
|
|
21
|
-
* next one. Idempotent, and safe before anything has been added.
|
|
22
|
-
*/
|
|
23
38
|
dispose() {
|
|
24
39
|
this.#abort.abort();
|
|
25
40
|
this.#abort = new AbortController();
|
|
26
41
|
}
|
|
27
42
|
};
|
|
28
43
|
|
|
29
|
-
// src/utils/safe_timeout.ts
|
|
30
44
|
var TimerRegistry = class {
|
|
31
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
32
45
|
ids = /* @__PURE__ */ new Set();
|
|
33
|
-
/**
|
|
34
|
-
* Cancels a single tracked timer.
|
|
35
|
-
*
|
|
36
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
37
|
-
* registry), so callers can clear defensively without guarding.
|
|
38
|
-
*/
|
|
39
46
|
clear(id) {
|
|
40
47
|
if (this.ids.delete(id)) {
|
|
41
48
|
this.cancel(id);
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
46
|
-
* to guarantee no timer outlives the element.
|
|
47
|
-
*/
|
|
48
51
|
clearAll() {
|
|
49
52
|
for (const id of this.ids) {
|
|
50
53
|
this.cancel(id);
|
|
51
54
|
}
|
|
52
55
|
this.ids.clear();
|
|
53
56
|
}
|
|
54
|
-
/** Number of timers currently tracked (pending). */
|
|
55
57
|
get size() {
|
|
56
58
|
return this.ids.size;
|
|
57
59
|
}
|
|
58
60
|
};
|
|
59
61
|
var SafeTimeout = class extends TimerRegistry {
|
|
60
|
-
/**
|
|
61
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
62
|
-
*
|
|
63
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
64
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
65
|
-
*/
|
|
66
62
|
set(callback, delay) {
|
|
67
63
|
const id = this.schedule(() => {
|
|
68
64
|
this.ids.delete(id);
|
|
@@ -79,21 +75,45 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
79
75
|
}
|
|
80
76
|
};
|
|
81
77
|
|
|
82
|
-
|
|
78
|
+
var SubmissionOwner = class {
|
|
79
|
+
#synthetic = /* @__PURE__ */ Symbol("synthetic submission");
|
|
80
|
+
#active = null;
|
|
81
|
+
begin(event) {
|
|
82
|
+
this.#active = { source: event.target, token: this.#tokenOf(event) };
|
|
83
|
+
}
|
|
84
|
+
resolve(event) {
|
|
85
|
+
if (this.#active === null || this.#active.source !== event.target || this.#active.token !== this.#tokenOf(event)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
this.clear();
|
|
89
|
+
return this.#detailOf(event)?.success === true;
|
|
90
|
+
}
|
|
91
|
+
clear() {
|
|
92
|
+
this.#active = null;
|
|
93
|
+
}
|
|
94
|
+
#tokenOf(event) {
|
|
95
|
+
return this.#detailOf(event)?.formSubmission ?? this.#synthetic;
|
|
96
|
+
}
|
|
97
|
+
#detailOf(event) {
|
|
98
|
+
return event.detail;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
83
102
|
var TransientHooks = class {
|
|
84
103
|
#attributes;
|
|
85
104
|
constructor(options) {
|
|
86
105
|
this.#attributes = options.attributes;
|
|
87
106
|
}
|
|
88
|
-
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
89
107
|
reset(element) {
|
|
90
108
|
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
91
109
|
}
|
|
92
110
|
};
|
|
93
111
|
|
|
94
|
-
// src/controllers/dirty_form_controller.ts
|
|
95
112
|
var TRANSIENT = new TransientHooks({ attributes: ["data-dirty"] });
|
|
96
|
-
var visitCoordinators =
|
|
113
|
+
var visitCoordinators = sharedRegistry(
|
|
114
|
+
"stimeo-ui.dirty-form.registry.v1",
|
|
115
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
116
|
+
);
|
|
97
117
|
var TurboVisitCoordinator = class {
|
|
98
118
|
#document;
|
|
99
119
|
#participants = /* @__PURE__ */ new Set();
|
|
@@ -127,7 +147,6 @@ var TurboVisitCoordinator = class {
|
|
|
127
147
|
}
|
|
128
148
|
this.#participants.add(participant);
|
|
129
149
|
}
|
|
130
|
-
/** Removes a participant and reports whether the coordinator became empty. */
|
|
131
150
|
remove(participant) {
|
|
132
151
|
this.#participants.delete(participant);
|
|
133
152
|
if (this.#participants.size > 0) return false;
|
|
@@ -160,14 +179,13 @@ var DirtyFormController = class extends Controller {
|
|
|
160
179
|
static events = ["dirty", "guard"];
|
|
161
180
|
#baseline = "";
|
|
162
181
|
#dirty = false;
|
|
163
|
-
#beforeunloadBound = false;
|
|
164
182
|
#timeouts = new SafeTimeout();
|
|
165
183
|
#listeners = new ListenerSet();
|
|
166
|
-
|
|
184
|
+
#unloadListeners = new ListenerSet();
|
|
185
|
+
#submission = new SubmissionOwner();
|
|
167
186
|
#submitting = false;
|
|
168
187
|
#submitAttempt = null;
|
|
169
188
|
#submittedBaseline = null;
|
|
170
|
-
#activeSubmission = null;
|
|
171
189
|
#unregisterVisit = null;
|
|
172
190
|
#onFieldChange = () => {
|
|
173
191
|
this.#submitting = false;
|
|
@@ -187,26 +205,19 @@ var DirtyFormController = class extends Controller {
|
|
|
187
205
|
this.#submitAttempt.snapshot = this.#serialize();
|
|
188
206
|
};
|
|
189
207
|
#onSubmitStart = (event) => {
|
|
190
|
-
|
|
191
|
-
this.#
|
|
208
|
+
if (!this.#ownsSource(event.target)) return;
|
|
209
|
+
this.#submission.begin(event);
|
|
192
210
|
this.#submittedBaseline = this.#submitAttempt?.snapshot ?? this.#serialize();
|
|
193
211
|
this.#submitAttempt = null;
|
|
194
212
|
this.#submitting = this.#serialize() === this.#submittedBaseline;
|
|
195
213
|
};
|
|
196
214
|
#onSubmitEnd = (event) => {
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
215
|
+
const success = this.#submission.resolve(event);
|
|
216
|
+
if (success === null) return;
|
|
201
217
|
const submittedBaseline = this.#submittedBaseline;
|
|
202
218
|
this.#submitting = false;
|
|
203
219
|
this.#submittedBaseline = null;
|
|
204
|
-
|
|
205
|
-
if (detail?.success === false) return;
|
|
206
|
-
if (submittedBaseline === null) {
|
|
207
|
-
this.markClean();
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
220
|
+
if (!success || submittedBaseline === null) return;
|
|
210
221
|
this.#baseline = submittedBaseline;
|
|
211
222
|
this.#evaluate();
|
|
212
223
|
};
|
|
@@ -216,7 +227,6 @@ var DirtyFormController = class extends Controller {
|
|
|
216
227
|
event.returnValue = this.messageValue;
|
|
217
228
|
};
|
|
218
229
|
connect() {
|
|
219
|
-
this.#resetSubmission();
|
|
220
230
|
this.#baseline = this.#serialize();
|
|
221
231
|
this.#setDirty(false);
|
|
222
232
|
TRANSIENT.reset(this.element);
|
|
@@ -226,6 +236,7 @@ var DirtyFormController = class extends Controller {
|
|
|
226
236
|
this.#listeners.add(this.element, "formdata", this.#onFormData);
|
|
227
237
|
this.#listeners.add(this.element, "turbo:submit-start", this.#onSubmitStart);
|
|
228
238
|
this.#listeners.add(this.element, "turbo:submit-end", this.#onSubmitEnd);
|
|
239
|
+
this.#unregisterVisit?.();
|
|
229
240
|
this.#unregisterVisit = registerVisitParticipant(this.element.ownerDocument, {
|
|
230
241
|
form: this.element,
|
|
231
242
|
evaluate: (event) => this.#evaluateVisit(event)
|
|
@@ -238,17 +249,14 @@ var DirtyFormController = class extends Controller {
|
|
|
238
249
|
this.#unbindBeforeUnload();
|
|
239
250
|
this.#resetSubmission();
|
|
240
251
|
}
|
|
241
|
-
/** Re-baselines to the current values and clears the dirty state (e.g. after a save). */
|
|
242
252
|
markClean() {
|
|
243
253
|
this.#baseline = this.#serialize();
|
|
244
254
|
this.#setDirty(false);
|
|
245
255
|
}
|
|
246
|
-
/** Adopts restored values without erasing an already-dirty user revision. */
|
|
247
256
|
acceptRestore() {
|
|
248
257
|
if (this.#dirty) return;
|
|
249
258
|
this.markClean();
|
|
250
259
|
}
|
|
251
|
-
/** Recomputes dirty against the baseline and flips state when it changes. */
|
|
252
260
|
#evaluate() {
|
|
253
261
|
this.#setDirty(this.#serialize() !== this.#baseline);
|
|
254
262
|
}
|
|
@@ -264,7 +272,6 @@ var DirtyFormController = class extends Controller {
|
|
|
264
272
|
}
|
|
265
273
|
this.dispatch("dirty", { detail: { dirty } });
|
|
266
274
|
}
|
|
267
|
-
/** Evaluates this form for the document-level Turbo visit coordinator. */
|
|
268
275
|
#evaluateVisit(event) {
|
|
269
276
|
if (!this.#dirty || this.#guardSuppressed()) return null;
|
|
270
277
|
const guard = this.dispatch("guard", { detail: { event }, cancelable: true });
|
|
@@ -274,14 +281,10 @@ var DirtyFormController = class extends Controller {
|
|
|
274
281
|
return { kind: "confirm", message: this.messageValue };
|
|
275
282
|
}
|
|
276
283
|
#bindBeforeUnload() {
|
|
277
|
-
|
|
278
|
-
window.addEventListener("beforeunload", this.#onBeforeUnload);
|
|
279
|
-
this.#beforeunloadBound = true;
|
|
284
|
+
this.#unloadListeners.add(window, "beforeunload", this.#onBeforeUnload);
|
|
280
285
|
}
|
|
281
286
|
#unbindBeforeUnload() {
|
|
282
|
-
|
|
283
|
-
window.removeEventListener("beforeunload", this.#onBeforeUnload);
|
|
284
|
-
this.#beforeunloadBound = false;
|
|
287
|
+
this.#unloadListeners.dispose();
|
|
285
288
|
}
|
|
286
289
|
#guardSuppressed() {
|
|
287
290
|
return this.#submitting && (this.#submitAttempt === null || !this.#submitAttempt.event.defaultPrevented);
|
|
@@ -291,9 +294,11 @@ var DirtyFormController = class extends Controller {
|
|
|
291
294
|
this.#submitting = false;
|
|
292
295
|
this.#submitAttempt = null;
|
|
293
296
|
this.#submittedBaseline = null;
|
|
294
|
-
this.#
|
|
297
|
+
this.#submission.clear();
|
|
298
|
+
}
|
|
299
|
+
#ownsSource(target) {
|
|
300
|
+
return target === this.element || target instanceof Element && target.closest("form") === this.element;
|
|
295
301
|
}
|
|
296
|
-
/** Stable serialization of the form's controls for change detection. */
|
|
297
302
|
#serialize() {
|
|
298
303
|
const parts = [];
|
|
299
304
|
for (const el of Array.from(this.element.elements)) {
|
|
@@ -310,15 +315,13 @@ var DirtyFormController = class extends Controller {
|
|
|
310
315
|
}
|
|
311
316
|
return JSON.stringify(parts);
|
|
312
317
|
}
|
|
313
|
-
/** A stable key for a control, or null for elements without value semantics. */
|
|
314
318
|
#nameOf(el) {
|
|
315
319
|
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
316
|
-
return el.name || el.id || "";
|
|
320
|
+
return projectedFieldName(el.name) || el.id || "";
|
|
317
321
|
}
|
|
318
322
|
return null;
|
|
319
323
|
}
|
|
320
324
|
};
|
|
321
325
|
|
|
322
326
|
export { DirtyFormController };
|
|
323
|
-
//# sourceMappingURL=dirty_form_controller.js.map
|
|
324
327
|
//# sourceMappingURL=dirty_form_controller.js.map
|
|
@@ -1,8 +1,158 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function setAttributeIfChanged(element, name, value, beforeWrite) {
|
|
4
|
+
if (element.getAttribute(name) === value) return false;
|
|
5
|
+
if (value === null) element.removeAttribute(name);
|
|
6
|
+
else element.setAttribute(name, value);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function sharedRegistry(key, create) {
|
|
11
|
+
const symbol = Symbol.for(key);
|
|
12
|
+
const scope = globalThis;
|
|
13
|
+
const existing = scope[symbol];
|
|
14
|
+
if (existing !== void 0) return existing;
|
|
15
|
+
const registry2 = create();
|
|
16
|
+
Object.defineProperty(globalThis, symbol, { value: registry2 });
|
|
17
|
+
return registry2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var registry = sharedRegistry(
|
|
21
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
22
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
23
|
+
);
|
|
24
|
+
function isAttributeValue(value) {
|
|
25
|
+
return value === null || typeof value === "string";
|
|
26
|
+
}
|
|
27
|
+
function readLeaseRecord(element, record) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
30
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
31
|
+
const [original, written, ...rest] = parsed;
|
|
32
|
+
const linked = rest[0] !== false;
|
|
33
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
34
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
35
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
36
|
+
} catch {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function attributeStack(element, attribute) {
|
|
41
|
+
const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
42
|
+
registry.stacks.set(element, byAttribute);
|
|
43
|
+
const known = byAttribute.get(attribute);
|
|
44
|
+
if (known) return known;
|
|
45
|
+
const found = [];
|
|
46
|
+
for (const record of registry.names.get(attribute) ?? []) {
|
|
47
|
+
const read = readLeaseRecord(element, record);
|
|
48
|
+
if (read) found.push({ ...read, record });
|
|
49
|
+
}
|
|
50
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
51
|
+
const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
52
|
+
lease: null,
|
|
53
|
+
record,
|
|
54
|
+
written,
|
|
55
|
+
original,
|
|
56
|
+
linked: linked && index > 0
|
|
57
|
+
}));
|
|
58
|
+
byAttribute.set(attribute, stack);
|
|
59
|
+
return stack;
|
|
60
|
+
}
|
|
61
|
+
function findHold(stack, lease, record) {
|
|
62
|
+
return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
|
|
63
|
+
}
|
|
64
|
+
var AttributeLease = class {
|
|
65
|
+
#attribute;
|
|
66
|
+
#record;
|
|
67
|
+
#base;
|
|
68
|
+
#preserveAuthor;
|
|
69
|
+
#held = /* @__PURE__ */ new Set();
|
|
70
|
+
constructor(attribute, owner, options = {}) {
|
|
71
|
+
this.#attribute = attribute;
|
|
72
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
73
|
+
this.#base = options.base === true;
|
|
74
|
+
this.#preserveAuthor = options.preserveAuthor === true;
|
|
75
|
+
const names = registry.names;
|
|
76
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
77
|
+
}
|
|
78
|
+
write(element, value) {
|
|
79
|
+
const stack = attributeStack(element, this.#attribute);
|
|
80
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
81
|
+
this.#held.add(element);
|
|
82
|
+
const shows = !this.#covered(element, stack, hold);
|
|
83
|
+
hold.written = value;
|
|
84
|
+
this.#mark(element, stack);
|
|
85
|
+
if (shows) this.#reflect(element, value);
|
|
86
|
+
}
|
|
87
|
+
return(element) {
|
|
88
|
+
const held = this.#held.delete(element);
|
|
89
|
+
const known = registry.stacks.get(element)?.has(this.#attribute);
|
|
90
|
+
const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
|
|
91
|
+
const hold = this.#take(stack);
|
|
92
|
+
if (!hold) return false;
|
|
93
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
94
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
95
|
+
const index = stack.indexOf(hold);
|
|
96
|
+
stack.splice(index, 1);
|
|
97
|
+
const above = stack[index];
|
|
98
|
+
if (above) above.linked &&= hold.linked;
|
|
99
|
+
this.#mark(element, stack);
|
|
100
|
+
if (!owns) return true;
|
|
101
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
102
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
returnAll() {
|
|
106
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
107
|
+
}
|
|
108
|
+
#take(stack) {
|
|
109
|
+
const hold = findHold(stack, this, this.#record);
|
|
110
|
+
if (hold) hold.lease = this;
|
|
111
|
+
return hold;
|
|
112
|
+
}
|
|
113
|
+
#acquire(element, stack) {
|
|
114
|
+
const current = element.getAttribute(this.#attribute);
|
|
115
|
+
const top = stack.at(-1);
|
|
116
|
+
const bottom = stack[0];
|
|
117
|
+
const over = top?.written === current ? top : void 0;
|
|
118
|
+
const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
|
|
119
|
+
const hold = {
|
|
120
|
+
lease: this,
|
|
121
|
+
record: this.#record,
|
|
122
|
+
written: current,
|
|
123
|
+
original: from ? from.original : current,
|
|
124
|
+
linked: !this.#base && over !== void 0
|
|
125
|
+
};
|
|
126
|
+
if (this.#base && bottom) {
|
|
127
|
+
bottom.linked = true;
|
|
128
|
+
stack.unshift(hold);
|
|
129
|
+
} else stack.push(hold);
|
|
130
|
+
return hold;
|
|
131
|
+
}
|
|
132
|
+
#covered(element, stack, hold) {
|
|
133
|
+
const current = element.getAttribute(this.#attribute);
|
|
134
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
135
|
+
}
|
|
136
|
+
#mark(element, stack) {
|
|
137
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
138
|
+
stack.forEach((hold, index) => {
|
|
139
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
140
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
141
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
142
|
+
records.set(
|
|
143
|
+
hold.record,
|
|
144
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
for (const [name, recorded] of records) {
|
|
148
|
+
setAttributeIfChanged(element, name, recorded);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
#reflect(element, value) {
|
|
152
|
+
setAttributeIfChanged(element, this.#attribute, value);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
4
155
|
|
|
5
|
-
// src/utils/focus_candidate.ts
|
|
6
156
|
function inheritsFieldsetDisabled(control) {
|
|
7
157
|
let fieldset = control.closest("fieldset[disabled]");
|
|
8
158
|
while (fieldset) {
|
|
@@ -12,8 +162,19 @@ function inheritsFieldsetDisabled(control) {
|
|
|
12
162
|
}
|
|
13
163
|
return false;
|
|
14
164
|
}
|
|
165
|
+
function unslotted(element) {
|
|
166
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
167
|
+
}
|
|
168
|
+
function flatTreeParent(element) {
|
|
169
|
+
if (unslotted(element)) return null;
|
|
170
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
171
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
172
|
+
return parent instanceof Element ? parent : null;
|
|
173
|
+
}
|
|
15
174
|
function canTakeFocus(element) {
|
|
16
|
-
|
|
175
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
176
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
177
|
+
}
|
|
17
178
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
18
179
|
if (!("disabled" in element)) return true;
|
|
19
180
|
if (element.disabled) return false;
|
|
@@ -76,7 +237,17 @@ function tabStopsWithin(root) {
|
|
|
76
237
|
);
|
|
77
238
|
}
|
|
78
239
|
|
|
79
|
-
|
|
240
|
+
var ListenerSet = class {
|
|
241
|
+
#abort = new AbortController();
|
|
242
|
+
add(target, type, handler, options) {
|
|
243
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
244
|
+
}
|
|
245
|
+
dispose() {
|
|
246
|
+
this.#abort.abort();
|
|
247
|
+
this.#abort = new AbortController();
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
80
251
|
var DismissibleController = class extends Controller {
|
|
81
252
|
static targets = ["root", "fallback"];
|
|
82
253
|
static values = {
|
|
@@ -85,25 +256,33 @@ var DismissibleController = class extends Controller {
|
|
|
85
256
|
};
|
|
86
257
|
static actions = ["dismiss"];
|
|
87
258
|
static events = ["dismiss"];
|
|
259
|
+
#openDefault = new AttributeLease("data-state", this.identifier);
|
|
260
|
+
#connected = false;
|
|
261
|
+
#escapeListeners = new ListenerSet();
|
|
88
262
|
connect() {
|
|
89
|
-
|
|
90
|
-
if (!root.hasAttribute("data-state")) {
|
|
91
|
-
root.setAttribute("data-state", "open");
|
|
92
|
-
}
|
|
263
|
+
this.#establishOpen(this.#root);
|
|
93
264
|
this.#syncEscapeListener();
|
|
265
|
+
this.#connected = true;
|
|
94
266
|
}
|
|
95
267
|
disconnect() {
|
|
96
|
-
this
|
|
268
|
+
this.#connected = false;
|
|
269
|
+
this.#escapeListeners.dispose();
|
|
270
|
+
}
|
|
271
|
+
rootTargetConnected(root) {
|
|
272
|
+
if (!this.#connected) return;
|
|
273
|
+
if (!this.rootTargets.includes(this.element)) this.#openDefault.return(this.element);
|
|
274
|
+
this.#establishOpen(root);
|
|
275
|
+
}
|
|
276
|
+
rootTargetDisconnected(root) {
|
|
277
|
+
if (!this.rootTargets.includes(root)) this.#openDefault.return(root);
|
|
278
|
+
if (this.#connected && this.hasRootTarget) this.#establishOpen(this.rootTarget);
|
|
97
279
|
}
|
|
98
|
-
/** Keeps the Escape listener aligned with a live `closeOnEscape` Value. */
|
|
99
280
|
closeOnEscapeValueChanged() {
|
|
100
281
|
this.#syncEscapeListener();
|
|
101
282
|
}
|
|
102
|
-
/** Dismisses the element. Bound via `data-action` (click on the close button). */
|
|
103
283
|
dismiss() {
|
|
104
284
|
this.#performDismiss();
|
|
105
285
|
}
|
|
106
|
-
/** Dismisses on Escape when `closeOnEscape` is set and focus is inside. */
|
|
107
286
|
#onKeydown = (event) => {
|
|
108
287
|
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
109
288
|
if (!this.closeOnEscapeValue) return;
|
|
@@ -112,18 +291,18 @@ var DismissibleController = class extends Controller {
|
|
|
112
291
|
event.preventDefault();
|
|
113
292
|
this.#performDismiss();
|
|
114
293
|
};
|
|
115
|
-
/** Adds or removes the one stable listener reference without duplicating it. */
|
|
116
294
|
#syncEscapeListener() {
|
|
117
|
-
this.
|
|
295
|
+
this.#escapeListeners.dispose();
|
|
118
296
|
if (this.closeOnEscapeValue) {
|
|
119
|
-
this.element
|
|
297
|
+
this.#escapeListeners.add(this.element, "keydown", this.#onKeydown);
|
|
120
298
|
}
|
|
121
299
|
}
|
|
122
|
-
|
|
300
|
+
#establishOpen(root) {
|
|
301
|
+
if (!root.hasAttribute("data-state")) this.#openDefault.write(root, "open");
|
|
302
|
+
}
|
|
123
303
|
get #root() {
|
|
124
304
|
return this.hasRootTarget ? this.rootTarget : this.element;
|
|
125
305
|
}
|
|
126
|
-
/** @stimeoRuntimeOnly `mode` decides how this one dismissal takes the element away. */
|
|
127
306
|
#performDismiss() {
|
|
128
307
|
const root = this.#root;
|
|
129
308
|
const mode = this.modeValue === "hide" ? "hide" : "remove";
|
|
@@ -136,10 +315,6 @@ var DismissibleController = class extends Controller {
|
|
|
136
315
|
root.remove();
|
|
137
316
|
}
|
|
138
317
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Moves focus out of `root` *before* it is removed, but only when focus is
|
|
141
|
-
* actually inside it — otherwise the user's place elsewhere is left undisturbed.
|
|
142
|
-
*/
|
|
143
318
|
#retreatFocus(root) {
|
|
144
319
|
const active = document.activeElement;
|
|
145
320
|
if (!(active instanceof HTMLElement) || !root.contains(active)) return;
|
|
@@ -149,7 +324,6 @@ var DismissibleController = class extends Controller {
|
|
|
149
324
|
}
|
|
150
325
|
document.body.focus();
|
|
151
326
|
}
|
|
152
|
-
/** Yields available focus fallbacks in precedence order, stopping after focus succeeds. */
|
|
153
327
|
*#focusFallbacks(root) {
|
|
154
328
|
const explicitFallback = this.hasFallbackTarget ? this.fallbackTarget : null;
|
|
155
329
|
if (explicitFallback && !root.contains(explicitFallback) && this.#isExplicitFocusAvailable(explicitFallback)) {
|
|
@@ -171,7 +345,6 @@ var DismissibleController = class extends Controller {
|
|
|
171
345
|
yield element;
|
|
172
346
|
}
|
|
173
347
|
}
|
|
174
|
-
/** Whether an explicit fallback can receive programmatic focus right now. */
|
|
175
348
|
#isExplicitFocusAvailable(element) {
|
|
176
349
|
const programmaticallyFocusable = element.hasAttribute("tabindex") || isTabStop(element);
|
|
177
350
|
return programmaticallyFocusable && canTakeFocus(element) && isRenderedForFocus(element);
|
|
@@ -179,5 +352,4 @@ var DismissibleController = class extends Controller {
|
|
|
179
352
|
};
|
|
180
353
|
|
|
181
354
|
export { DismissibleController };
|
|
182
|
-
//# sourceMappingURL=dismissible_controller.js.map
|
|
183
355
|
//# sourceMappingURL=dismissible_controller.js.map
|