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/spinner_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,124 +16,209 @@ 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/default_attribute.ts
|
|
52
166
|
function setDefaultAttribute(element, name, value) {
|
|
53
167
|
if (element.hasAttribute(name)) return false;
|
|
54
168
|
element.setAttribute(name, value);
|
|
55
169
|
return true;
|
|
56
170
|
}
|
|
57
171
|
|
|
58
|
-
// src/utils/detach_gate.ts
|
|
59
172
|
var DetachGate = class _DetachGate {
|
|
60
|
-
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
61
173
|
#pending = false;
|
|
62
|
-
/**
|
|
63
|
-
* True while a probe is queued — the last disconnect was ambiguous and no
|
|
64
|
-
* reconnect has cancelled it yet. Read it from `connect()` to tell the
|
|
65
|
-
* reconnect half of an in-page move from a first connect: a controller whose
|
|
66
|
-
* initialisation restarts a measurement (a min-duration floor, an elapsed
|
|
67
|
-
* counter) must skip it for the move, where nothing actually restarted.
|
|
68
|
-
*/
|
|
69
174
|
get pending() {
|
|
70
175
|
return this.#pending;
|
|
71
176
|
}
|
|
72
|
-
/**
|
|
73
|
-
* True when the disconnect is definitely a real detach — the element left
|
|
74
|
-
* the document, or `data-controller` no longer lists the identifier. False
|
|
75
|
-
* means ambiguous (in-page move or observed-root exit), NOT "alive".
|
|
76
|
-
*/
|
|
77
177
|
static isDetached(host) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return !tokens.includes(host.identifier);
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Call from `disconnect()`: runs `teardown` synchronously on a definite
|
|
84
|
-
* detach (fast path), otherwise defers it one microtask — a reconnect
|
|
85
|
-
* ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
|
|
86
|
-
* One microtask is the whole probe window: Stimulus reconnects a moved
|
|
87
|
-
* element within the same mutation batch, before the checkpoint drains.
|
|
88
|
-
*/
|
|
178
|
+
return !host.element.isConnected || !_DetachGate.#listed(host);
|
|
179
|
+
}
|
|
89
180
|
disconnected(host, teardown) {
|
|
90
|
-
|
|
181
|
+
const carried = _DetachGate.#carried(host);
|
|
182
|
+
if (!carried && _DetachGate.isDetached(host)) {
|
|
91
183
|
this.#pending = false;
|
|
92
184
|
teardown();
|
|
93
185
|
return;
|
|
94
186
|
}
|
|
95
187
|
this.#pending = true;
|
|
96
|
-
|
|
188
|
+
const probe = () => {
|
|
97
189
|
if (!this.#pending) return;
|
|
98
190
|
this.#pending = false;
|
|
99
191
|
teardown();
|
|
100
|
-
}
|
|
192
|
+
};
|
|
193
|
+
if (carried) setTimeout(probe, 0);
|
|
194
|
+
else queueMicrotask(probe);
|
|
195
|
+
}
|
|
196
|
+
static #carried(host) {
|
|
197
|
+
const element = host.element;
|
|
198
|
+
return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
|
|
199
|
+
}
|
|
200
|
+
static #listed(host) {
|
|
201
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
202
|
+
return tokens.includes(host.identifier);
|
|
101
203
|
}
|
|
102
|
-
/**
|
|
103
|
-
* Disarms a pending probe. Call from `connect()` (the reconnect that proves
|
|
104
|
-
* an in-page move) and from the head of any teardown path not routed through
|
|
105
|
-
* {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
|
|
106
|
-
* never run the teardown a second time.
|
|
107
|
-
*/
|
|
108
204
|
cancel() {
|
|
109
205
|
this.#pending = false;
|
|
110
206
|
}
|
|
111
207
|
};
|
|
112
208
|
|
|
113
|
-
// src/utils/min_duration_floor.ts
|
|
114
209
|
var MinDurationFloor = class {
|
|
115
210
|
#timers;
|
|
116
|
-
/** Pending finish timer id, or `null` when nothing is held back. */
|
|
117
211
|
#timerId = null;
|
|
118
|
-
/** Epoch ms the floor is measured from. */
|
|
119
212
|
#since = 0;
|
|
120
|
-
/** @param timers - the controller's registry; the floor schedules into it. */
|
|
121
213
|
constructor(timers) {
|
|
122
214
|
this.#timers = timers;
|
|
123
215
|
}
|
|
124
|
-
/** Starts the floor: call when the state being held becomes visible. */
|
|
125
216
|
begin() {
|
|
126
217
|
this.#since = Date.now();
|
|
127
218
|
}
|
|
128
|
-
/** True while a finish is held back waiting for the floor to elapse. */
|
|
129
219
|
get pending() {
|
|
130
220
|
return this.#timerId !== null;
|
|
131
221
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Runs `finish` once the floor has elapsed, immediately when it already has.
|
|
134
|
-
*
|
|
135
|
-
* A held-back finish is **replaced**, never stacked: only the most recently
|
|
136
|
-
* queued id is cancellable, so a second timer would outlive every cancel and
|
|
137
|
-
* end a state that has since restarted. Controllers that want the first signal
|
|
138
|
-
* to win guard on {@link pending} before calling.
|
|
139
|
-
*/
|
|
140
222
|
schedule(minDuration, finish) {
|
|
141
223
|
this.cancel();
|
|
142
224
|
const remaining = minDuration - (Date.now() - this.#since);
|
|
@@ -149,7 +231,6 @@ var MinDurationFloor = class {
|
|
|
149
231
|
finish();
|
|
150
232
|
}
|
|
151
233
|
}
|
|
152
|
-
/** Drops a held-back finish. Safe when none is queued, or after a bulk clear. */
|
|
153
234
|
cancel() {
|
|
154
235
|
if (this.#timerId !== null) {
|
|
155
236
|
this.#timers.clear(this.#timerId);
|
|
@@ -158,43 +239,70 @@ var MinDurationFloor = class {
|
|
|
158
239
|
}
|
|
159
240
|
};
|
|
160
241
|
|
|
161
|
-
|
|
242
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
243
|
+
|
|
244
|
+
var NUMBER_BOUNDS = {
|
|
245
|
+
timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
|
|
246
|
+
function matchesNumberBounds(value, bounds) {
|
|
247
|
+
if (!Number.isFinite(value)) {
|
|
248
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
249
|
+
if (direction === null) return false;
|
|
250
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
251
|
+
}
|
|
252
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
253
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
254
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
255
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
256
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function readNumber(raw, fallback, bounds) {
|
|
261
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
var NumberValueReader = class {
|
|
265
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
266
|
+
read(owner, name, raw, fallback, bounds) {
|
|
267
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
268
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
269
|
+
this.#lastRejected.delete(name);
|
|
270
|
+
return resolved;
|
|
271
|
+
}
|
|
272
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
273
|
+
const literal = owner.element.getAttribute(attribute);
|
|
274
|
+
if (literal === null) {
|
|
275
|
+
this.#lastRejected.delete(name);
|
|
276
|
+
return resolved;
|
|
277
|
+
}
|
|
278
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
279
|
+
this.#lastRejected.set(name, literal);
|
|
280
|
+
console.warn(
|
|
281
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return resolved;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
162
288
|
var TimerRegistry = class {
|
|
163
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
164
289
|
ids = /* @__PURE__ */ new Set();
|
|
165
|
-
/**
|
|
166
|
-
* Cancels a single tracked timer.
|
|
167
|
-
*
|
|
168
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
169
|
-
* registry), so callers can clear defensively without guarding.
|
|
170
|
-
*/
|
|
171
290
|
clear(id) {
|
|
172
291
|
if (this.ids.delete(id)) {
|
|
173
292
|
this.cancel(id);
|
|
174
293
|
}
|
|
175
294
|
}
|
|
176
|
-
/**
|
|
177
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
178
|
-
* to guarantee no timer outlives the element.
|
|
179
|
-
*/
|
|
180
295
|
clearAll() {
|
|
181
296
|
for (const id of this.ids) {
|
|
182
297
|
this.cancel(id);
|
|
183
298
|
}
|
|
184
299
|
this.ids.clear();
|
|
185
300
|
}
|
|
186
|
-
/** Number of timers currently tracked (pending). */
|
|
187
301
|
get size() {
|
|
188
302
|
return this.ids.size;
|
|
189
303
|
}
|
|
190
304
|
};
|
|
191
305
|
var SafeTimeout = class extends TimerRegistry {
|
|
192
|
-
/**
|
|
193
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
194
|
-
*
|
|
195
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
196
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
197
|
-
*/
|
|
198
306
|
set(callback, delay) {
|
|
199
307
|
const id = this.schedule(() => {
|
|
200
308
|
this.ids.delete(id);
|
|
@@ -211,8 +319,8 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
211
319
|
}
|
|
212
320
|
};
|
|
213
321
|
|
|
214
|
-
|
|
215
|
-
|
|
322
|
+
var SpinnerController = class _SpinnerController extends Controller {
|
|
323
|
+
#numbers = new NumberValueReader();
|
|
216
324
|
static targets = ["indicator", "region", "message"];
|
|
217
325
|
static values = {
|
|
218
326
|
announceText: { type: String, default: "" },
|
|
@@ -221,42 +329,52 @@ var SpinnerController = class extends Controller {
|
|
|
221
329
|
minDuration: { type: Number, default: 0 },
|
|
222
330
|
timeout: { type: Number, default: 0 }
|
|
223
331
|
};
|
|
332
|
+
static valueConstraints = {
|
|
333
|
+
delay: NUMBER_BOUNDS.timer,
|
|
334
|
+
minDuration: NUMBER_BOUNDS.timer,
|
|
335
|
+
timeout: NUMBER_BOUNDS.timer
|
|
336
|
+
};
|
|
224
337
|
static actions = ["start", "stop"];
|
|
225
338
|
static events = ["hide", "show", "timeout", "reconcile"];
|
|
226
339
|
#timers = new SafeTimeout();
|
|
227
340
|
#floor = new MinDurationFloor(this.#timers);
|
|
228
341
|
#gate = new DetachGate();
|
|
229
|
-
#
|
|
230
|
-
|
|
342
|
+
#busyLease = new AttributeLease("aria-busy", this.identifier);
|
|
343
|
+
#hiddenLease = new AttributeLease("hidden", this.identifier);
|
|
344
|
+
#connected = false;
|
|
345
|
+
#cycling = false;
|
|
231
346
|
#delayTimerId = null;
|
|
232
|
-
/** Pending safety-net timer id, or `null` when `timeout` is off or not armed. */
|
|
233
347
|
#timeoutTimerId = null;
|
|
234
348
|
connect() {
|
|
349
|
+
this.#connected = true;
|
|
235
350
|
this.#gate.cancel();
|
|
236
|
-
this.#
|
|
237
|
-
if (this.#state === "pending" && this.#delayTimerId === null) {
|
|
351
|
+
if (this.#state !== "idle" && !this.#cycling) {
|
|
238
352
|
this.#setBusy(false);
|
|
353
|
+
this.#showIndicator(false);
|
|
239
354
|
this.element.setAttribute("data-state", "idle");
|
|
355
|
+
this.dispatch("reconcile", { detail: {} });
|
|
240
356
|
return;
|
|
241
357
|
}
|
|
242
358
|
setDefaultAttribute(this.element, "data-state", "idle");
|
|
243
359
|
}
|
|
244
|
-
/**
|
|
245
|
-
* Re-applies the current phase to an indicator that arrived after `connect()`.
|
|
246
|
-
*
|
|
247
|
-
* A Turbo Stream can swap the indicator for a fresh node mid-load, and that node
|
|
248
|
-
* carries the markup contract's `hidden`. Without this the spinner would vanish
|
|
249
|
-
* while `data-state` still says `loading`, and nothing but the next cycle would
|
|
250
|
-
* bring it back.
|
|
251
|
-
*/
|
|
252
360
|
indicatorTargetConnected(target) {
|
|
253
|
-
target
|
|
361
|
+
this.#hiddenLease.write(target, this.#state === "loading" ? null : "");
|
|
362
|
+
}
|
|
363
|
+
indicatorTargetDisconnected(indicator) {
|
|
364
|
+
if (!this.indicatorTargets.includes(indicator)) this.#hiddenLease.return(indicator);
|
|
365
|
+
if (this.#connected) this.#showIndicator(this.#state === "loading");
|
|
366
|
+
}
|
|
367
|
+
regionTargetConnected(region) {
|
|
368
|
+
if (this.#connected) this.#busyLease.write(region, String(this.#busy));
|
|
369
|
+
}
|
|
370
|
+
regionTargetDisconnected(region) {
|
|
371
|
+
if (!this.regionTargets.includes(region)) this.#busyLease.return(region);
|
|
372
|
+
if (this.#connected) this.#setBusy(this.#busy);
|
|
254
373
|
}
|
|
255
374
|
disconnect() {
|
|
256
|
-
this.#
|
|
375
|
+
this.#connected = false;
|
|
257
376
|
this.#gate.disconnected(this, () => this.#teardown());
|
|
258
377
|
}
|
|
259
|
-
/** Begins loading. Honors `delay` before the spinner actually appears. */
|
|
260
378
|
start() {
|
|
261
379
|
if (this.#state === "loading") {
|
|
262
380
|
this.#setBusy(true);
|
|
@@ -265,99 +383,68 @@ var SpinnerController = class extends Controller {
|
|
|
265
383
|
return;
|
|
266
384
|
}
|
|
267
385
|
if (this.#state !== "idle") return;
|
|
386
|
+
this.#cycling = true;
|
|
268
387
|
this.#setBusy(true);
|
|
269
388
|
this.#floor.cancel();
|
|
270
389
|
this.#armTimeout();
|
|
271
|
-
if (this
|
|
390
|
+
if (this.#safeDelay > 0) {
|
|
272
391
|
this.element.setAttribute("data-state", "pending");
|
|
273
392
|
this.#delayTimerId = this.#timers.set(() => {
|
|
274
393
|
this.#delayTimerId = null;
|
|
275
394
|
this.#show();
|
|
276
|
-
}, this
|
|
395
|
+
}, this.#safeDelay);
|
|
277
396
|
} else {
|
|
278
397
|
this.#show();
|
|
279
398
|
}
|
|
280
399
|
}
|
|
281
|
-
/** Ends loading. Honors `minDuration` so a shown spinner does not flicker. */
|
|
282
400
|
stop() {
|
|
283
401
|
const state = this.#state;
|
|
284
402
|
this.#cancelTimeout();
|
|
285
403
|
if (state === "pending") {
|
|
286
404
|
this.#cancelDelay();
|
|
405
|
+
this.#cycling = false;
|
|
287
406
|
this.#setBusy(false);
|
|
288
407
|
this.element.setAttribute("data-state", "idle");
|
|
289
408
|
return;
|
|
290
409
|
}
|
|
291
410
|
if (state !== "loading") return;
|
|
292
411
|
this.#setBusy(false);
|
|
293
|
-
this.#floor.schedule(this
|
|
412
|
+
this.#floor.schedule(this.#safeMinDuration, () => this.#hide());
|
|
294
413
|
}
|
|
295
|
-
/** Reveals the indicator, marks the moment shown, and announces via the live region. */
|
|
296
414
|
#show() {
|
|
297
415
|
this.#floor.begin();
|
|
298
|
-
this.#
|
|
299
|
-
if (this.hasIndicatorTarget) this.indicatorTarget.hidden = false;
|
|
416
|
+
this.#showIndicator(true);
|
|
300
417
|
this.element.setAttribute("data-state", "loading");
|
|
301
418
|
this.dispatch("show", { detail: {} });
|
|
302
419
|
announce(fillTemplate(this.announceTextValue, {}));
|
|
303
420
|
}
|
|
304
|
-
/** Hides the indicator and returns to the idle state. */
|
|
305
421
|
#hide() {
|
|
306
|
-
|
|
422
|
+
this.#cycling = false;
|
|
423
|
+
this.#showIndicator(false);
|
|
307
424
|
this.element.setAttribute("data-state", "idle");
|
|
308
425
|
this.dispatch("hide", { detail: {} });
|
|
309
426
|
announce(fillTemplate(this.announceReadyTextValue, {}));
|
|
310
427
|
}
|
|
311
|
-
/**
|
|
312
|
-
* Drops both timers on a real detach. The markup keeps whatever it last held: an
|
|
313
|
-
* element on its way out of the document has no reader left, and one whose
|
|
314
|
-
* `data-controller` dropped the identifier no longer resolves its own targets, so
|
|
315
|
-
* the rollback could only ever be partial. The snapshot is rewound where it is
|
|
316
|
-
* still whole, on `turbo:before-cache`.
|
|
317
|
-
*/
|
|
318
428
|
#teardown() {
|
|
319
|
-
this.#gate.cancel();
|
|
320
429
|
this.#timers.clearAll();
|
|
321
430
|
this.#delayTimerId = null;
|
|
322
431
|
this.#timeoutTimerId = null;
|
|
323
|
-
this.#
|
|
432
|
+
this.#cycling = false;
|
|
324
433
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
* so a page reached with the Back button is not restored mid-load with a
|
|
328
|
-
* spinner nothing can stop. State only: `data-state`, the indicator's `hidden`,
|
|
329
|
-
* and `aria-busy`. No `hide` is dispatched — that would claim the load finished.
|
|
330
|
-
* The rewind reports itself as `reconcile` instead, so a consumer painting from
|
|
331
|
-
* `show` can drop it before the snapshot is taken. The live page keeps its
|
|
332
|
-
* timers, so a navigation that never completes leaves the running cycle intact.
|
|
333
|
-
*/
|
|
334
|
-
#rewindForCache() {
|
|
335
|
-
const running = this.element.getAttribute("data-state") !== "idle";
|
|
336
|
-
this.#cancelTimeout();
|
|
337
|
-
this.#setBusy(false);
|
|
338
|
-
if (this.hasIndicatorTarget) this.indicatorTarget.hidden = true;
|
|
339
|
-
this.element.setAttribute("data-state", "idle");
|
|
340
|
-
if (running) this.dispatch("reconcile", { detail: {} });
|
|
434
|
+
#showIndicator(shown) {
|
|
435
|
+
if (this.hasIndicatorTarget) this.#hiddenLease.write(this.indicatorTarget, shown ? null : "");
|
|
341
436
|
}
|
|
342
|
-
/** Reflects busy state onto the controlled region (if present). */
|
|
343
437
|
#setBusy(busy) {
|
|
344
|
-
if (this.hasRegionTarget)
|
|
345
|
-
this.regionTarget.setAttribute("aria-busy", String(busy));
|
|
346
|
-
}
|
|
438
|
+
if (this.hasRegionTarget) this.#busyLease.write(this.regionTarget, String(busy));
|
|
347
439
|
}
|
|
348
|
-
/**
|
|
349
|
-
* Arms the safety net so a `stop` that never arrives cannot strand the spinner.
|
|
350
|
-
* Off by default: the consumer owns the async work, so only it knows whether a
|
|
351
|
-
* ceiling makes sense. Re-arming on a restart measures from the newest start.
|
|
352
|
-
*/
|
|
353
440
|
#armTimeout() {
|
|
354
441
|
this.#cancelTimeout();
|
|
355
|
-
if (this
|
|
442
|
+
if (this.#safeTimeout <= 0) return;
|
|
356
443
|
this.#timeoutTimerId = this.#timers.set(() => {
|
|
357
444
|
this.#timeoutTimerId = null;
|
|
358
445
|
this.dispatch("timeout", { detail: {} });
|
|
359
446
|
this.stop();
|
|
360
|
-
}, this
|
|
447
|
+
}, this.#safeTimeout);
|
|
361
448
|
}
|
|
362
449
|
#cancelTimeout() {
|
|
363
450
|
if (this.#timeoutTimerId !== null) {
|
|
@@ -371,12 +458,41 @@ var SpinnerController = class extends Controller {
|
|
|
371
458
|
this.#delayTimerId = null;
|
|
372
459
|
}
|
|
373
460
|
}
|
|
374
|
-
/** Current lifecycle phase as reflected on `data-state`. */
|
|
375
461
|
get #state() {
|
|
376
462
|
return this.element.getAttribute("data-state") ?? "idle";
|
|
377
463
|
}
|
|
464
|
+
get #busy() {
|
|
465
|
+
const state = this.#state;
|
|
466
|
+
return state === "pending" || state === "loading" && !this.#floor.pending;
|
|
467
|
+
}
|
|
468
|
+
get #safeDelay() {
|
|
469
|
+
return this.#numbers.read(
|
|
470
|
+
this,
|
|
471
|
+
"delay",
|
|
472
|
+
this.delayValue,
|
|
473
|
+
_SpinnerController.values.delay.default,
|
|
474
|
+
_SpinnerController.valueConstraints.delay
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
get #safeMinDuration() {
|
|
478
|
+
return this.#numbers.read(
|
|
479
|
+
this,
|
|
480
|
+
"minDuration",
|
|
481
|
+
this.minDurationValue,
|
|
482
|
+
_SpinnerController.values.minDuration.default,
|
|
483
|
+
_SpinnerController.valueConstraints.minDuration
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
get #safeTimeout() {
|
|
487
|
+
return this.#numbers.read(
|
|
488
|
+
this,
|
|
489
|
+
"timeout",
|
|
490
|
+
this.timeoutValue,
|
|
491
|
+
_SpinnerController.values.timeout.default,
|
|
492
|
+
_SpinnerController.valueConstraints.timeout
|
|
493
|
+
);
|
|
494
|
+
}
|
|
378
495
|
};
|
|
379
496
|
|
|
380
497
|
export { SpinnerController };
|
|
381
|
-
//# sourceMappingURL=spinner_controller.js.map
|
|
382
498
|
//# sourceMappingURL=spinner_controller.js.map
|