stimeo-ui 0.15.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 +209 -0
- data/README.md +128 -1
- data/dist/cable/index.js +392 -417
- data/dist/controllers/accordion_controller.js +228 -28
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -191
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +295 -110
- data/dist/controllers/avatar_controller.js +172 -106
- data/dist/controllers/breadcrumb_controller.js +0 -129
- data/dist/controllers/bulk_select_controller.js +264 -53
- data/dist/controllers/calendar_controller.js +346 -180
- data/dist/controllers/carousel_controller.js +587 -300
- data/dist/controllers/character_counter_controller.js +271 -143
- data/dist/controllers/checkbox_controller.js +77 -75
- data/dist/controllers/clipboard_controller.js +187 -108
- data/dist/controllers/collapsible_controller.js +374 -108
- data/dist/controllers/color_picker_controller.js +183 -127
- data/dist/controllers/combobox_controller.js +426 -109
- data/dist/controllers/command_palette_controller.js +994 -350
- data/dist/controllers/conditional_fields_controller.js +212 -125
- data/dist/controllers/confirm_controller.js +939 -245
- data/dist/controllers/context_menu_controller.js +275 -127
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +235 -97
- data/dist/controllers/currency_input_controller.js +241 -118
- data/dist/controllers/data_grid_controller.js +229 -135
- data/dist/controllers/date_range_picker_controller.js +359 -165
- data/dist/controllers/dialog_controller.js +887 -225
- data/dist/controllers/direct_upload_controller.js +80 -142
- data/dist/controllers/dirty_form_controller.js +26 -47
- data/dist/controllers/dismissible_controller.js +175 -20
- data/dist/controllers/drawer_controller.js +963 -336
- data/dist/controllers/dropdown_controller.js +342 -85
- data/dist/controllers/editable_controller.js +182 -58
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +426 -229
- data/dist/controllers/filter_controller.js +78 -36
- data/dist/controllers/flash_controller.js +328 -211
- data/dist/controllers/focus_controller.js +654 -239
- data/dist/controllers/form_field_controller.js +153 -136
- data/dist/controllers/form_validation_controller.js +29 -96
- data/dist/controllers/frame_loading_controller.js +263 -198
- data/dist/controllers/highlight_controller.js +119 -70
- data/dist/controllers/hover_card_controller.js +335 -118
- data/dist/controllers/idle_controller.js +316 -65
- data/dist/controllers/input_mask_controller.js +116 -61
- data/dist/controllers/intersection_controller.js +147 -103
- data/dist/controllers/lazy_frame_controller.js +9 -56
- data/dist/controllers/listbox_controller.js +413 -153
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -95
- data/dist/controllers/menu_controller.js +254 -167
- data/dist/controllers/menubar_controller.js +206 -331
- data/dist/controllers/meter_controller.js +159 -71
- data/dist/controllers/multi_select_controller.js +797 -388
- data/dist/controllers/navigation_menu_controller.js +156 -231
- data/dist/controllers/nested_form_controller.js +373 -174
- data/dist/controllers/network_status_controller.js +241 -45
- data/dist/controllers/number_input_controller.js +387 -227
- data/dist/controllers/optimistic_controller.js +28 -75
- data/dist/controllers/otp_controller.js +344 -196
- data/dist/controllers/overflow_indicator_controller.js +167 -93
- data/dist/controllers/overflow_menu_controller.js +408 -277
- data/dist/controllers/pagination_controller.js +342 -120
- data/dist/controllers/password_reveal_controller.js +424 -99
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +252 -96
- data/dist/controllers/pointer_drag_controller.js +263 -164
- data/dist/controllers/popover_controller.js +267 -94
- data/dist/controllers/portal_controller.js +209 -72
- data/dist/controllers/preview_guard_controller.js +243 -112
- data/dist/controllers/progress_controller.js +132 -59
- data/dist/controllers/radio_group_controller.js +138 -133
- data/dist/controllers/range_slider_controller.js +245 -112
- data/dist/controllers/rating_controller.js +429 -194
- data/dist/controllers/read_more_controller.js +185 -47
- 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 +294 -118
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -221
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -146
- data/dist/controllers/scrollspy_controller.js +181 -236
- data/dist/controllers/separator_controller.js +288 -132
- data/dist/controllers/sidebar_controller.js +1071 -375
- data/dist/controllers/skeleton_controller.js +248 -92
- data/dist/controllers/slider_controller.js +211 -96
- data/dist/controllers/smart_sticky_header_controller.js +163 -54
- data/dist/controllers/sortable_controller.js +13 -110
- data/dist/controllers/spinner_controller.js +277 -161
- data/dist/controllers/step_indicator_controller.js +107 -53
- data/dist/controllers/stepper_controller.js +171 -48
- data/dist/controllers/stick_to_bottom_controller.js +310 -116
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -192
- data/dist/controllers/switch_controller.js +107 -43
- data/dist/controllers/tabs_controller.js +50 -25
- data/dist/controllers/tags_input_controller.js +290 -149
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +148 -80
- data/dist/controllers/time_picker_controller.js +162 -81
- data/dist/controllers/toast_controller.js +365 -193
- data/dist/controllers/toggle_group_controller.js +194 -99
- data/dist/controllers/toolbar_controller.js +92 -90
- data/dist/controllers/tooltip_controller.js +185 -125
- data/dist/controllers/transition_controller.js +123 -115
- data/dist/controllers/tree_view_controller.js +248 -290
- data/dist/index.js +8649 -8572
- data/dist/positioning/index.js +148 -45
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/frame_loading_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/announce.ts
|
|
6
3
|
function announce(message, options = {}) {
|
|
7
4
|
const text = message.trim();
|
|
8
5
|
if (text.length === 0) return;
|
|
@@ -19,141 +16,214 @@ function fillTemplate(template, values) {
|
|
|
19
16
|
});
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
19
|
+
function sharedRegistry(key, create) {
|
|
20
|
+
const symbol = Symbol.for(key);
|
|
21
|
+
const scope = globalThis;
|
|
22
|
+
const existing = scope[symbol];
|
|
23
|
+
if (existing !== void 0) return existing;
|
|
24
|
+
const registry = create();
|
|
25
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
26
|
+
return registry;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isAttributeValue(value) {
|
|
30
|
+
return value === null || typeof value === "string";
|
|
31
|
+
}
|
|
32
|
+
function readLeaseRecord(element, record) {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
35
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
36
|
+
const [original, written, ...rest] = parsed;
|
|
37
|
+
const linked = rest[0] !== false;
|
|
38
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
39
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
40
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
41
|
+
} catch {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
var AttributeLease = class _AttributeLease {
|
|
46
|
+
static #registry = sharedRegistry(
|
|
47
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
48
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
49
|
+
);
|
|
50
|
+
#attribute;
|
|
51
|
+
#record;
|
|
52
|
+
#base;
|
|
53
|
+
#held = /* @__PURE__ */ new Set();
|
|
54
|
+
constructor(attribute, owner, options = {}) {
|
|
55
|
+
this.#attribute = attribute;
|
|
56
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
57
|
+
this.#base = options.base === true;
|
|
58
|
+
const names = _AttributeLease.#registry.names;
|
|
59
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
60
|
+
}
|
|
61
|
+
write(element, value) {
|
|
62
|
+
const stack = this.#stack(element);
|
|
63
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
64
|
+
this.#held.add(element);
|
|
65
|
+
const shows = !this.#covered(element, stack, hold);
|
|
66
|
+
hold.written = value;
|
|
67
|
+
this.#mark(element, stack);
|
|
68
|
+
if (shows) this.#reflect(element, value);
|
|
69
|
+
}
|
|
70
|
+
return(element) {
|
|
71
|
+
const held = this.#held.delete(element);
|
|
72
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
73
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
74
|
+
const hold = this.#take(stack);
|
|
75
|
+
if (!hold) return false;
|
|
76
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
77
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
78
|
+
const index = stack.indexOf(hold);
|
|
79
|
+
stack.splice(index, 1);
|
|
80
|
+
const above = stack[index];
|
|
81
|
+
if (above) above.linked &&= hold.linked;
|
|
82
|
+
this.#mark(element, stack);
|
|
83
|
+
if (!owns) return true;
|
|
84
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
85
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
returnAll() {
|
|
89
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
90
|
+
}
|
|
91
|
+
#stack(element) {
|
|
92
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
93
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
94
|
+
stacks.set(element, byAttribute);
|
|
95
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
96
|
+
byAttribute.set(this.#attribute, stack);
|
|
97
|
+
return stack;
|
|
98
|
+
}
|
|
99
|
+
#seed(element) {
|
|
100
|
+
const found = [];
|
|
101
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
102
|
+
const read = readLeaseRecord(element, record);
|
|
103
|
+
if (read) found.push({ ...read, record });
|
|
104
|
+
}
|
|
105
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
106
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
107
|
+
lease: null,
|
|
108
|
+
record,
|
|
109
|
+
written,
|
|
110
|
+
original,
|
|
111
|
+
linked: linked && index > 0
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
#take(stack) {
|
|
115
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
116
|
+
if (hold) hold.lease = this;
|
|
117
|
+
return hold;
|
|
118
|
+
}
|
|
119
|
+
#acquire(element, stack) {
|
|
120
|
+
const current = element.getAttribute(this.#attribute);
|
|
121
|
+
const top = stack.at(-1);
|
|
122
|
+
const bottom = stack[0];
|
|
123
|
+
const over = top?.written === current ? top : void 0;
|
|
124
|
+
const from = this.#base ? bottom : over;
|
|
125
|
+
const hold = {
|
|
126
|
+
lease: this,
|
|
127
|
+
record: this.#record,
|
|
128
|
+
written: current,
|
|
129
|
+
original: from ? from.original : current,
|
|
130
|
+
linked: !this.#base && over !== void 0
|
|
131
|
+
};
|
|
132
|
+
if (this.#base && bottom) {
|
|
133
|
+
bottom.linked = true;
|
|
134
|
+
stack.unshift(hold);
|
|
135
|
+
} else stack.push(hold);
|
|
136
|
+
return hold;
|
|
137
|
+
}
|
|
138
|
+
#covered(element, stack, hold) {
|
|
139
|
+
const current = element.getAttribute(this.#attribute);
|
|
140
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
141
|
+
}
|
|
142
|
+
#mark(element, stack) {
|
|
143
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
144
|
+
stack.forEach((hold, index) => {
|
|
145
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
146
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
147
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
148
|
+
records.set(
|
|
149
|
+
hold.record,
|
|
150
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
for (const [name, recorded] of records) {
|
|
154
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
155
|
+
if (recorded === null) element.removeAttribute(name);
|
|
156
|
+
else element.setAttribute(name, recorded);
|
|
41
157
|
}
|
|
42
158
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
159
|
+
#reflect(element, value) {
|
|
160
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
161
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
162
|
+
else element.setAttribute(this.#attribute, value);
|
|
48
163
|
}
|
|
49
164
|
};
|
|
50
165
|
|
|
51
|
-
// src/utils/detach_gate.ts
|
|
52
166
|
var DetachGate = class _DetachGate {
|
|
53
|
-
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
54
167
|
#pending = false;
|
|
55
|
-
/**
|
|
56
|
-
* True while a probe is queued — the last disconnect was ambiguous and no
|
|
57
|
-
* reconnect has cancelled it yet. Read it from `connect()` to tell the
|
|
58
|
-
* reconnect half of an in-page move from a first connect: a controller whose
|
|
59
|
-
* initialisation restarts a measurement (a min-duration floor, an elapsed
|
|
60
|
-
* counter) must skip it for the move, where nothing actually restarted.
|
|
61
|
-
*/
|
|
62
168
|
get pending() {
|
|
63
169
|
return this.#pending;
|
|
64
170
|
}
|
|
65
|
-
/**
|
|
66
|
-
* True when the disconnect is definitely a real detach — the element left
|
|
67
|
-
* the document, or `data-controller` no longer lists the identifier. False
|
|
68
|
-
* means ambiguous (in-page move or observed-root exit), NOT "alive".
|
|
69
|
-
*/
|
|
70
171
|
static isDetached(host) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return !tokens.includes(host.identifier);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Call from `disconnect()`: runs `teardown` synchronously on a definite
|
|
77
|
-
* detach (fast path), otherwise defers it one microtask — a reconnect
|
|
78
|
-
* ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
|
|
79
|
-
* One microtask is the whole probe window: Stimulus reconnects a moved
|
|
80
|
-
* element within the same mutation batch, before the checkpoint drains.
|
|
81
|
-
*/
|
|
172
|
+
return !host.element.isConnected || !_DetachGate.#listed(host);
|
|
173
|
+
}
|
|
82
174
|
disconnected(host, teardown) {
|
|
83
|
-
|
|
175
|
+
const carried = _DetachGate.#carried(host);
|
|
176
|
+
if (!carried && _DetachGate.isDetached(host)) {
|
|
84
177
|
this.#pending = false;
|
|
85
178
|
teardown();
|
|
86
179
|
return;
|
|
87
180
|
}
|
|
88
181
|
this.#pending = true;
|
|
89
|
-
|
|
182
|
+
const probe = () => {
|
|
90
183
|
if (!this.#pending) return;
|
|
91
184
|
this.#pending = false;
|
|
92
185
|
teardown();
|
|
93
|
-
}
|
|
186
|
+
};
|
|
187
|
+
if (carried) setTimeout(probe, 0);
|
|
188
|
+
else queueMicrotask(probe);
|
|
189
|
+
}
|
|
190
|
+
static #carried(host) {
|
|
191
|
+
const element = host.element;
|
|
192
|
+
return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
|
|
193
|
+
}
|
|
194
|
+
static #listed(host) {
|
|
195
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
196
|
+
return tokens.includes(host.identifier);
|
|
94
197
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Disarms a pending probe. Call from `connect()` (the reconnect that proves
|
|
97
|
-
* an in-page move) and from the head of any teardown path not routed through
|
|
98
|
-
* {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
|
|
99
|
-
* never run the teardown a second time.
|
|
100
|
-
*/
|
|
101
198
|
cancel() {
|
|
102
199
|
this.#pending = false;
|
|
103
200
|
}
|
|
104
201
|
};
|
|
105
202
|
|
|
106
|
-
// src/utils/listener_set.ts
|
|
107
203
|
var ListenerSet = class {
|
|
108
|
-
/** The generation every `add` joins until the next `dispose()`. */
|
|
109
204
|
#abort = new AbortController();
|
|
110
|
-
/**
|
|
111
|
-
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
112
|
-
*
|
|
113
|
-
* The set supplies the signal, so `options` carries everything else the DOM
|
|
114
|
-
* accepts — `capture` included, which has to match at release time and no
|
|
115
|
-
* longer has a second place to drift from.
|
|
116
|
-
*/
|
|
117
205
|
add(target, type, handler, options) {
|
|
118
206
|
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
119
207
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Releases every listener of the open generation, synchronously, and opens the
|
|
122
|
-
* next one. Idempotent, and safe before anything has been added.
|
|
123
|
-
*/
|
|
124
208
|
dispose() {
|
|
125
209
|
this.#abort.abort();
|
|
126
210
|
this.#abort = new AbortController();
|
|
127
211
|
}
|
|
128
212
|
};
|
|
129
213
|
|
|
130
|
-
// src/utils/min_duration_floor.ts
|
|
131
214
|
var MinDurationFloor = class {
|
|
132
215
|
#timers;
|
|
133
|
-
/** Pending finish timer id, or `null` when nothing is held back. */
|
|
134
216
|
#timerId = null;
|
|
135
|
-
/** Epoch ms the floor is measured from. */
|
|
136
217
|
#since = 0;
|
|
137
|
-
/** @param timers - the controller's registry; the floor schedules into it. */
|
|
138
218
|
constructor(timers) {
|
|
139
219
|
this.#timers = timers;
|
|
140
220
|
}
|
|
141
|
-
/** Starts the floor: call when the state being held becomes visible. */
|
|
142
221
|
begin() {
|
|
143
222
|
this.#since = Date.now();
|
|
144
223
|
}
|
|
145
|
-
/** True while a finish is held back waiting for the floor to elapse. */
|
|
146
224
|
get pending() {
|
|
147
225
|
return this.#timerId !== null;
|
|
148
226
|
}
|
|
149
|
-
/**
|
|
150
|
-
* Runs `finish` once the floor has elapsed, immediately when it already has.
|
|
151
|
-
*
|
|
152
|
-
* A held-back finish is **replaced**, never stacked: only the most recently
|
|
153
|
-
* queued id is cancellable, so a second timer would outlive every cancel and
|
|
154
|
-
* end a state that has since restarted. Controllers that want the first signal
|
|
155
|
-
* to win guard on {@link pending} before calling.
|
|
156
|
-
*/
|
|
157
227
|
schedule(minDuration, finish) {
|
|
158
228
|
this.cancel();
|
|
159
229
|
const remaining = minDuration - (Date.now() - this.#since);
|
|
@@ -166,7 +236,6 @@ var MinDurationFloor = class {
|
|
|
166
236
|
finish();
|
|
167
237
|
}
|
|
168
238
|
}
|
|
169
|
-
/** Drops a held-back finish. Safe when none is queued, or after a bulk clear. */
|
|
170
239
|
cancel() {
|
|
171
240
|
if (this.#timerId !== null) {
|
|
172
241
|
this.#timers.clear(this.#timerId);
|
|
@@ -175,43 +244,70 @@ var MinDurationFloor = class {
|
|
|
175
244
|
}
|
|
176
245
|
};
|
|
177
246
|
|
|
178
|
-
|
|
247
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
248
|
+
|
|
249
|
+
var NUMBER_BOUNDS = {
|
|
250
|
+
timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
|
|
251
|
+
function matchesNumberBounds(value, bounds) {
|
|
252
|
+
if (!Number.isFinite(value)) {
|
|
253
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
254
|
+
if (direction === null) return false;
|
|
255
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
256
|
+
}
|
|
257
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
258
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
259
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
260
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
261
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readNumber(raw, fallback, bounds) {
|
|
266
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
var NumberValueReader = class {
|
|
270
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
271
|
+
read(owner, name, raw, fallback, bounds) {
|
|
272
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
273
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
274
|
+
this.#lastRejected.delete(name);
|
|
275
|
+
return resolved;
|
|
276
|
+
}
|
|
277
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
278
|
+
const literal = owner.element.getAttribute(attribute);
|
|
279
|
+
if (literal === null) {
|
|
280
|
+
this.#lastRejected.delete(name);
|
|
281
|
+
return resolved;
|
|
282
|
+
}
|
|
283
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
284
|
+
this.#lastRejected.set(name, literal);
|
|
285
|
+
console.warn(
|
|
286
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return resolved;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
179
293
|
var TimerRegistry = class {
|
|
180
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
181
294
|
ids = /* @__PURE__ */ new Set();
|
|
182
|
-
/**
|
|
183
|
-
* Cancels a single tracked timer.
|
|
184
|
-
*
|
|
185
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
186
|
-
* registry), so callers can clear defensively without guarding.
|
|
187
|
-
*/
|
|
188
295
|
clear(id) {
|
|
189
296
|
if (this.ids.delete(id)) {
|
|
190
297
|
this.cancel(id);
|
|
191
298
|
}
|
|
192
299
|
}
|
|
193
|
-
/**
|
|
194
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
195
|
-
* to guarantee no timer outlives the element.
|
|
196
|
-
*/
|
|
197
300
|
clearAll() {
|
|
198
301
|
for (const id of this.ids) {
|
|
199
302
|
this.cancel(id);
|
|
200
303
|
}
|
|
201
304
|
this.ids.clear();
|
|
202
305
|
}
|
|
203
|
-
/** Number of timers currently tracked (pending). */
|
|
204
306
|
get size() {
|
|
205
307
|
return this.ids.size;
|
|
206
308
|
}
|
|
207
309
|
};
|
|
208
310
|
var SafeTimeout = class extends TimerRegistry {
|
|
209
|
-
/**
|
|
210
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
211
|
-
*
|
|
212
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
213
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
214
|
-
*/
|
|
215
311
|
set(callback, delay) {
|
|
216
312
|
const id = this.schedule(() => {
|
|
217
313
|
this.ids.delete(id);
|
|
@@ -228,8 +324,8 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
228
324
|
}
|
|
229
325
|
};
|
|
230
326
|
|
|
231
|
-
|
|
232
|
-
|
|
327
|
+
var FrameLoadingController = class _FrameLoadingController extends Controller {
|
|
328
|
+
#numbers = new NumberValueReader();
|
|
233
329
|
static targets = ["content", "skeleton", "overlay"];
|
|
234
330
|
static values = {
|
|
235
331
|
announceText: { type: String, default: "" },
|
|
@@ -237,26 +333,23 @@ var FrameLoadingController = class extends Controller {
|
|
|
237
333
|
minDuration: { type: Number, default: 0 },
|
|
238
334
|
restoreFocus: { type: Boolean, default: true }
|
|
239
335
|
};
|
|
336
|
+
static valueConstraints = {
|
|
337
|
+
minDuration: NUMBER_BOUNDS.timer
|
|
338
|
+
};
|
|
240
339
|
static events = ["start", "end", "reconcile"];
|
|
241
340
|
#timeouts = new SafeTimeout();
|
|
242
341
|
#floor = new MinDurationFloor(this.#timeouts);
|
|
243
342
|
#gate = new DetachGate();
|
|
244
|
-
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
245
343
|
#listeners = new ListenerSet();
|
|
344
|
+
#busy = new AttributeLease("aria-busy", this.identifier);
|
|
345
|
+
#loadingHook = new AttributeLease("data-frame-loading", this.identifier);
|
|
346
|
+
#hidden = new AttributeLease("hidden", this.identifier);
|
|
347
|
+
#inert = new AttributeLease("inert", this.identifier);
|
|
246
348
|
#loading = false;
|
|
247
|
-
/**
|
|
248
|
-
* The optional targets this controller revealed, and the content it marked inert.
|
|
249
|
-
* Held as references rather than re-resolved on the way out: a detach that keeps
|
|
250
|
-
* the element takes the identifier off `data-controller` first, and a scope
|
|
251
|
-
* without its identifier stops resolving targets — the elements to tidy would be
|
|
252
|
-
* unreachable exactly when the tidying matters. They double as the ownership
|
|
253
|
-
* marker, so a `hidden` or an `inert` the consumer wrote is never taken over.
|
|
254
|
-
*/
|
|
255
349
|
#revealedSkeleton = null;
|
|
256
350
|
#revealedOverlay = null;
|
|
257
351
|
#inertTarget = null;
|
|
258
352
|
#previousFocus = null;
|
|
259
|
-
/** The id of the retreated element, used to re-find it if the load replaced it. */
|
|
260
353
|
#previousFocusId = "";
|
|
261
354
|
#onStart = () => {
|
|
262
355
|
this.#floor.cancel();
|
|
@@ -264,111 +357,85 @@ var FrameLoadingController = class extends Controller {
|
|
|
264
357
|
};
|
|
265
358
|
#onEnd = () => {
|
|
266
359
|
if (!this.#loading) return;
|
|
267
|
-
this.#floor.schedule(this
|
|
360
|
+
this.#floor.schedule(this.#safeMinDuration, () => this.#finish());
|
|
268
361
|
};
|
|
269
362
|
connect() {
|
|
270
363
|
this.#gate.cancel();
|
|
271
|
-
this.#beforeCache.activate();
|
|
272
364
|
this.#listeners.add(this.element, "turbo:before-fetch-request", this.#onStart);
|
|
273
365
|
this.#listeners.add(this.element, "turbo:frame-load", this.#onEnd);
|
|
274
366
|
this.#listeners.add(this.element, "turbo:fetch-request-error", this.#onEnd);
|
|
367
|
+
if (this.#loading) return;
|
|
368
|
+
const stale = this.element.hasAttribute("data-frame-loading");
|
|
369
|
+
this.#returnCopied();
|
|
370
|
+
if (stale) this.dispatch("reconcile", { detail: {} });
|
|
371
|
+
}
|
|
372
|
+
#returnCopied() {
|
|
373
|
+
this.#busy.return(this.element);
|
|
374
|
+
this.#loadingHook.return(this.element);
|
|
375
|
+
for (const region of [...this.skeletonTargets, ...this.overlayTargets]) {
|
|
376
|
+
this.#hidden.return(region);
|
|
377
|
+
}
|
|
378
|
+
for (const content of this.contentTargets) this.#inert.return(content);
|
|
275
379
|
}
|
|
276
380
|
disconnect() {
|
|
277
381
|
this.#listeners.dispose();
|
|
278
|
-
this.#beforeCache.deactivate();
|
|
279
382
|
this.#gate.disconnected(this, () => this.#teardown());
|
|
280
383
|
}
|
|
281
|
-
/**
|
|
282
|
-
* Drops the held finish and the loading bookkeeping on a real detach, returning
|
|
283
|
-
* the frame to its idle form. No reconnect is coming, so nothing is left that
|
|
284
|
-
* could finish the load and clear the hooks — a detach that keeps the element
|
|
285
|
-
* (a morph dropping the identifier, an exit from a scoped observed root) would
|
|
286
|
-
* otherwise strand it busy and inert. Focus is left where it is: the element is
|
|
287
|
-
* leaving this controller's care, and moving it now would be an unexplained jump.
|
|
288
|
-
*/
|
|
289
384
|
#teardown() {
|
|
290
|
-
this.#gate.cancel();
|
|
291
385
|
this.#timeouts.clearAll();
|
|
292
|
-
this.#floor.cancel();
|
|
293
386
|
if (this.#loading) this.#rewindHooks();
|
|
294
387
|
this.#loading = false;
|
|
295
388
|
this.#previousFocus = null;
|
|
296
389
|
}
|
|
297
|
-
/**
|
|
298
|
-
* Returns the frame to its idle form for the snapshot Turbo is about to take, so
|
|
299
|
-
* a page reached with the Back button does not restore a frame that is busy and
|
|
300
|
-
* inert with nothing left to finish it. State only — no `end` event and no focus
|
|
301
|
-
* move, because the load did not actually complete.
|
|
302
|
-
*
|
|
303
|
-
* The load is abandoned rather than paused, so the flag and any finish the floor
|
|
304
|
-
* still holds drop along with the hooks. A kept finish would surface after the
|
|
305
|
-
* rewind as exactly the three things this pass exists to avoid — an `end`, a
|
|
306
|
-
* completion announcement, and a focus move — and a kept flag would leave the
|
|
307
|
-
* next fetch on a page that survives a cancelled visit skipping the loading
|
|
308
|
-
* state, its idempotence guard already satisfied.
|
|
309
|
-
*/
|
|
310
|
-
#rewindForCache() {
|
|
311
|
-
if (!this.#loading) return;
|
|
312
|
-
this.#loading = false;
|
|
313
|
-
this.#floor.cancel();
|
|
314
|
-
this.#rewindHooks();
|
|
315
|
-
this.dispatch("reconcile", { detail: {} });
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Clears every hook the loading state writes. Shared by the three ways a load can
|
|
319
|
-
* stop — completion, detach, snapshot — so none of them can drift into tidying
|
|
320
|
-
* only part of it.
|
|
321
|
-
*/
|
|
322
390
|
#rewindHooks() {
|
|
323
|
-
this.element
|
|
324
|
-
this.element
|
|
325
|
-
if (this.#revealedSkeleton) this.#revealedSkeleton
|
|
326
|
-
if (this.#revealedOverlay) this.#revealedOverlay
|
|
391
|
+
this.#busy.return(this.element);
|
|
392
|
+
this.#loadingHook.return(this.element);
|
|
393
|
+
if (this.#revealedSkeleton) this.#hidden.return(this.#revealedSkeleton);
|
|
394
|
+
if (this.#revealedOverlay) this.#hidden.return(this.#revealedOverlay);
|
|
327
395
|
this.#revealedSkeleton = null;
|
|
328
396
|
this.#revealedOverlay = null;
|
|
329
397
|
this.#clearInert();
|
|
330
398
|
}
|
|
331
|
-
/**
|
|
332
|
-
* Re-shows a `skeleton` that arrived mid-load. Turbo's frame renderer empties the
|
|
333
|
-
* frame and re-inserts the response's children, so a response's authored (hidden)
|
|
334
|
-
* skeleton can land while a later fetch is still running, and only the controller
|
|
335
|
-
* knows the frame is still busy.
|
|
336
|
-
*/
|
|
337
399
|
skeletonTargetConnected() {
|
|
338
400
|
if (this.#loading) this.#revealSkeleton();
|
|
339
401
|
}
|
|
340
|
-
|
|
402
|
+
skeletonTargetDisconnected(skeleton) {
|
|
403
|
+
if (this.#loading && !this.skeletonTargets.includes(skeleton)) this.#revealSkeleton();
|
|
404
|
+
}
|
|
341
405
|
overlayTargetConnected() {
|
|
342
406
|
if (this.#loading) this.#revealOverlay();
|
|
343
407
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
* an `inert` the replacement authored stays the consumer's.
|
|
348
|
-
*/
|
|
408
|
+
overlayTargetDisconnected(overlay) {
|
|
409
|
+
if (this.#loading && !this.overlayTargets.includes(overlay)) this.#revealOverlay();
|
|
410
|
+
}
|
|
349
411
|
contentTargetConnected() {
|
|
350
412
|
if (!this.#loading) return;
|
|
351
413
|
this.#clearInert();
|
|
352
414
|
this.#applyInert();
|
|
353
415
|
}
|
|
354
|
-
|
|
416
|
+
contentTargetDisconnected(content) {
|
|
417
|
+
if (!this.#loading || this.contentTargets.includes(content)) return;
|
|
418
|
+
this.#clearInert();
|
|
419
|
+
this.#applyInert();
|
|
420
|
+
}
|
|
355
421
|
#revealSkeleton() {
|
|
356
|
-
|
|
357
|
-
this.#revealedSkeleton = this
|
|
358
|
-
this.skeletonTarget.hidden = false;
|
|
422
|
+
const next = this.hasSkeletonTarget ? this.skeletonTarget : null;
|
|
423
|
+
this.#revealedSkeleton = this.#reveal(this.#revealedSkeleton, next);
|
|
359
424
|
}
|
|
360
|
-
/** Reveals the optional `overlay`, noting it as this controller's to hide again. */
|
|
361
425
|
#revealOverlay() {
|
|
362
|
-
|
|
363
|
-
this.#revealedOverlay = this
|
|
364
|
-
|
|
426
|
+
const next = this.hasOverlayTarget ? this.overlayTarget : null;
|
|
427
|
+
this.#revealedOverlay = this.#reveal(this.#revealedOverlay, next);
|
|
428
|
+
}
|
|
429
|
+
#reveal(revealed, next) {
|
|
430
|
+
if (revealed && revealed !== next) this.#hidden.return(revealed);
|
|
431
|
+
if (next) this.#hidden.write(next, null);
|
|
432
|
+
return next;
|
|
365
433
|
}
|
|
366
|
-
/** Enters the loading state: hooks, skeleton/overlay, inert content, focus retreat. */
|
|
367
434
|
#begin() {
|
|
368
435
|
this.#loading = true;
|
|
369
436
|
this.#floor.begin();
|
|
370
|
-
this.element
|
|
371
|
-
this.element
|
|
437
|
+
this.#busy.write(this.element, "true");
|
|
438
|
+
this.#loadingHook.write(this.element, "true");
|
|
372
439
|
this.#revealSkeleton();
|
|
373
440
|
this.#revealOverlay();
|
|
374
441
|
this.#applyInert();
|
|
@@ -376,7 +443,6 @@ var FrameLoadingController = class extends Controller {
|
|
|
376
443
|
this.dispatch("start", { detail: {} });
|
|
377
444
|
announce(fillTemplate(this.announceTextValue, {}));
|
|
378
445
|
}
|
|
379
|
-
/** Leaves the loading state: restore hooks, hide skeleton/overlay, restore focus. */
|
|
380
446
|
#finish() {
|
|
381
447
|
this.#loading = false;
|
|
382
448
|
this.#rewindHooks();
|
|
@@ -384,17 +450,15 @@ var FrameLoadingController = class extends Controller {
|
|
|
384
450
|
this.dispatch("end", { detail: {} });
|
|
385
451
|
announce(fillTemplate(this.announceReadyTextValue, {}));
|
|
386
452
|
}
|
|
387
|
-
/** Marks the content inert to block double-submits while stale (if we own it). */
|
|
388
453
|
#applyInert() {
|
|
389
454
|
if (!this.hasContentTarget || this.contentTarget.hasAttribute("inert")) return;
|
|
390
|
-
this.contentTarget
|
|
455
|
+
this.#inert.write(this.contentTarget, "");
|
|
391
456
|
this.#inertTarget = this.contentTarget;
|
|
392
457
|
}
|
|
393
458
|
#clearInert() {
|
|
394
|
-
this.#inertTarget
|
|
459
|
+
if (this.#inertTarget) this.#inert.return(this.#inertTarget);
|
|
395
460
|
this.#inertTarget = null;
|
|
396
461
|
}
|
|
397
|
-
/** Saves and blurs focus if it sits inside the frame about to go stale. */
|
|
398
462
|
#retreatFocus() {
|
|
399
463
|
this.#previousFocus = null;
|
|
400
464
|
this.#previousFocusId = "";
|
|
@@ -406,13 +470,6 @@ var FrameLoadingController = class extends Controller {
|
|
|
406
470
|
active.blur();
|
|
407
471
|
}
|
|
408
472
|
}
|
|
409
|
-
/**
|
|
410
|
-
* Restores focus after the load. The same node when it survived (e.g. a
|
|
411
|
-
* non-replacing update), else the element re-rendered with the same id inside the
|
|
412
|
-
* frame — Turbo frames typically re-emit the same controls. When neither is present
|
|
413
|
-
* (an anonymous control was replaced) focus is left where the browser put it, to
|
|
414
|
-
* avoid an unexpected jump (WCAG 3.2.x).
|
|
415
|
-
*/
|
|
416
473
|
#restoreFocus() {
|
|
417
474
|
const target = this.#previousFocus;
|
|
418
475
|
const id = this.#previousFocusId;
|
|
@@ -428,8 +485,16 @@ var FrameLoadingController = class extends Controller {
|
|
|
428
485
|
if (replacement && this.element.contains(replacement)) replacement.focus();
|
|
429
486
|
}
|
|
430
487
|
}
|
|
488
|
+
get #safeMinDuration() {
|
|
489
|
+
return this.#numbers.read(
|
|
490
|
+
this,
|
|
491
|
+
"minDuration",
|
|
492
|
+
this.minDurationValue,
|
|
493
|
+
_FrameLoadingController.values.minDuration.default,
|
|
494
|
+
_FrameLoadingController.valueConstraints.minDuration
|
|
495
|
+
);
|
|
496
|
+
}
|
|
431
497
|
};
|
|
432
498
|
|
|
433
499
|
export { FrameLoadingController };
|
|
434
|
-
//# sourceMappingURL=frame_loading_controller.js.map
|
|
435
500
|
//# sourceMappingURL=frame_loading_controller.js.map
|