stimeo-ui 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,30 +1,167 @@
|
|
|
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
|
+
function isAttributeValue(value) {
|
|
14
|
+
return value === null || typeof value === "string";
|
|
15
|
+
}
|
|
16
|
+
function readLeaseRecord(element, record) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
19
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
20
|
+
const [original, written, ...rest] = parsed;
|
|
21
|
+
const linked = rest[0] !== false;
|
|
22
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
23
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
24
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
25
|
+
} catch {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var AttributeLease = class _AttributeLease {
|
|
30
|
+
static #registry = sharedRegistry(
|
|
31
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
32
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
33
|
+
);
|
|
34
|
+
#attribute;
|
|
35
|
+
#record;
|
|
36
|
+
#base;
|
|
37
|
+
#held = /* @__PURE__ */ new Set();
|
|
38
|
+
constructor(attribute, owner, options = {}) {
|
|
39
|
+
this.#attribute = attribute;
|
|
40
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
41
|
+
this.#base = options.base === true;
|
|
42
|
+
const names = _AttributeLease.#registry.names;
|
|
43
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
44
|
+
}
|
|
45
|
+
write(element, value) {
|
|
46
|
+
const stack = this.#stack(element);
|
|
47
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
48
|
+
this.#held.add(element);
|
|
49
|
+
const shows = !this.#covered(element, stack, hold);
|
|
50
|
+
hold.written = value;
|
|
51
|
+
this.#mark(element, stack);
|
|
52
|
+
if (shows) this.#reflect(element, value);
|
|
53
|
+
}
|
|
54
|
+
return(element) {
|
|
55
|
+
const held = this.#held.delete(element);
|
|
56
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
57
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
58
|
+
const hold = this.#take(stack);
|
|
59
|
+
if (!hold) return false;
|
|
60
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
61
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
62
|
+
const index = stack.indexOf(hold);
|
|
63
|
+
stack.splice(index, 1);
|
|
64
|
+
const above = stack[index];
|
|
65
|
+
if (above) above.linked &&= hold.linked;
|
|
66
|
+
this.#mark(element, stack);
|
|
67
|
+
if (!owns) return true;
|
|
68
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
69
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
returnAll() {
|
|
73
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
74
|
+
}
|
|
75
|
+
#stack(element) {
|
|
76
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
77
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
78
|
+
stacks.set(element, byAttribute);
|
|
79
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
80
|
+
byAttribute.set(this.#attribute, stack);
|
|
81
|
+
return stack;
|
|
82
|
+
}
|
|
83
|
+
#seed(element) {
|
|
84
|
+
const found = [];
|
|
85
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
86
|
+
const read = readLeaseRecord(element, record);
|
|
87
|
+
if (read) found.push({ ...read, record });
|
|
88
|
+
}
|
|
89
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
90
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
91
|
+
lease: null,
|
|
92
|
+
record,
|
|
93
|
+
written,
|
|
94
|
+
original,
|
|
95
|
+
linked: linked && index > 0
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
#take(stack) {
|
|
99
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
100
|
+
if (hold) hold.lease = this;
|
|
101
|
+
return hold;
|
|
102
|
+
}
|
|
103
|
+
#acquire(element, stack) {
|
|
104
|
+
const current = element.getAttribute(this.#attribute);
|
|
105
|
+
const top = stack.at(-1);
|
|
106
|
+
const bottom = stack[0];
|
|
107
|
+
const over = top?.written === current ? top : void 0;
|
|
108
|
+
const from = this.#base ? bottom : over;
|
|
109
|
+
const hold = {
|
|
110
|
+
lease: this,
|
|
111
|
+
record: this.#record,
|
|
112
|
+
written: current,
|
|
113
|
+
original: from ? from.original : current,
|
|
114
|
+
linked: !this.#base && over !== void 0
|
|
115
|
+
};
|
|
116
|
+
if (this.#base && bottom) {
|
|
117
|
+
bottom.linked = true;
|
|
118
|
+
stack.unshift(hold);
|
|
119
|
+
} else stack.push(hold);
|
|
120
|
+
return hold;
|
|
121
|
+
}
|
|
122
|
+
#covered(element, stack, hold) {
|
|
123
|
+
const current = element.getAttribute(this.#attribute);
|
|
124
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
125
|
+
}
|
|
126
|
+
#mark(element, stack) {
|
|
127
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
128
|
+
stack.forEach((hold, index) => {
|
|
129
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
130
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
131
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
132
|
+
records.set(
|
|
133
|
+
hold.record,
|
|
134
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
for (const [name, recorded] of records) {
|
|
138
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
139
|
+
if (recorded === null) element.removeAttribute(name);
|
|
140
|
+
else element.setAttribute(name, recorded);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
#reflect(element, value) {
|
|
144
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
145
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
146
|
+
else element.setAttribute(this.#attribute, value);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
4
149
|
|
|
5
|
-
// src/utils/blur_deferral.ts
|
|
6
150
|
var BlurDeferral = class {
|
|
7
|
-
/** Elements currently holding an update back, mapped to their `blur` listener. */
|
|
8
151
|
#pending = /* @__PURE__ */ new Map();
|
|
9
|
-
/** Called after a pending element blurs and has been detached. */
|
|
10
152
|
#onRelease;
|
|
11
|
-
/** @param onRelease - Invoked once `element` actually blurs; never on `release`. */
|
|
12
153
|
constructor(onRelease) {
|
|
13
154
|
this.#onRelease = onRelease;
|
|
14
155
|
}
|
|
15
|
-
/** Number of elements currently holding an update back. */
|
|
16
156
|
get size() {
|
|
17
157
|
return this.#pending.size;
|
|
18
158
|
}
|
|
19
|
-
/** Snapshot of the pending elements, safe to iterate while releasing them. */
|
|
20
159
|
get elements() {
|
|
21
160
|
return [...this.#pending.keys()];
|
|
22
161
|
}
|
|
23
|
-
/** Whether `element` is currently holding an update back. */
|
|
24
162
|
has(element) {
|
|
25
163
|
return this.#pending.has(element);
|
|
26
164
|
}
|
|
27
|
-
/** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */
|
|
28
165
|
defer(element) {
|
|
29
166
|
if (this.#pending.has(element)) return;
|
|
30
167
|
const onBlur = () => {
|
|
@@ -34,22 +171,18 @@ var BlurDeferral = class {
|
|
|
34
171
|
this.#pending.set(element, onBlur);
|
|
35
172
|
element.addEventListener("blur", onBlur);
|
|
36
173
|
}
|
|
37
|
-
/** Defers `element` as the only pending entry, cancelling any others. */
|
|
38
174
|
deferOnly(element) {
|
|
39
175
|
for (const pending of this.elements) {
|
|
40
176
|
if (pending !== element) this.#detach(pending);
|
|
41
177
|
}
|
|
42
178
|
this.defer(element);
|
|
43
179
|
}
|
|
44
|
-
/** Cancels `element`'s deferral without completing it; no-ops when not pending. */
|
|
45
180
|
release(element) {
|
|
46
181
|
this.#detach(element);
|
|
47
182
|
}
|
|
48
|
-
/** Cancels every deferral without completing any of them. */
|
|
49
183
|
releaseAll() {
|
|
50
184
|
for (const element of this.elements) this.#detach(element);
|
|
51
185
|
}
|
|
52
|
-
/** Removes the `blur` listener for `element` and forgets it. */
|
|
53
186
|
#detach(element) {
|
|
54
187
|
const onBlur = this.#pending.get(element);
|
|
55
188
|
if (onBlur) element.removeEventListener("blur", onBlur);
|
|
@@ -57,7 +190,6 @@ var BlurDeferral = class {
|
|
|
57
190
|
}
|
|
58
191
|
};
|
|
59
192
|
|
|
60
|
-
// src/utils/declared_value.ts
|
|
61
193
|
function parseDeclared(raw, parse, fallback) {
|
|
62
194
|
try {
|
|
63
195
|
return parse(raw);
|
|
@@ -77,14 +209,8 @@ function validSelector(element, raw, fallback) {
|
|
|
77
209
|
);
|
|
78
210
|
}
|
|
79
211
|
|
|
80
|
-
// src/utils/frame_coalescer.ts
|
|
81
212
|
var FrameCoalescer = class {
|
|
82
213
|
#frame = null;
|
|
83
|
-
/**
|
|
84
|
-
* Runs `run` on the next frame, unless a frame is already pending — the first
|
|
85
|
-
* request of a burst wins and the rest are dropped. The pending frame is
|
|
86
|
-
* released before `run`, so `run` may request the next one.
|
|
87
|
-
*/
|
|
88
214
|
schedule(run) {
|
|
89
215
|
if (this.#frame !== null) return;
|
|
90
216
|
this.#frame = requestAnimationFrame(() => {
|
|
@@ -92,14 +218,6 @@ var FrameCoalescer = class {
|
|
|
92
218
|
run();
|
|
93
219
|
});
|
|
94
220
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Drops the pending frame, and reaches the platform only when there is one.
|
|
97
|
-
*
|
|
98
|
-
* There is no handle value that stands for "nothing pending":
|
|
99
|
-
* `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
|
|
100
|
-
* arrives as a large positive number that the same allocator can hand out, and
|
|
101
|
-
* an idle cancel would then drop a frame belonging to someone else.
|
|
102
|
-
*/
|
|
103
221
|
cancel() {
|
|
104
222
|
if (this.#frame === null) return;
|
|
105
223
|
cancelAnimationFrame(this.#frame);
|
|
@@ -107,12 +225,118 @@ var FrameCoalescer = class {
|
|
|
107
225
|
}
|
|
108
226
|
};
|
|
109
227
|
|
|
110
|
-
|
|
228
|
+
var ListenerSet = class {
|
|
229
|
+
#abort = new AbortController();
|
|
230
|
+
add(target, type, handler, options) {
|
|
231
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
232
|
+
}
|
|
233
|
+
dispose() {
|
|
234
|
+
this.#abort.abort();
|
|
235
|
+
this.#abort = new AbortController();
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
var MicrotaskCoalescer = class {
|
|
240
|
+
#run;
|
|
241
|
+
#queued = false;
|
|
242
|
+
#active = false;
|
|
243
|
+
#generation = 0;
|
|
244
|
+
constructor(run) {
|
|
245
|
+
this.#run = run;
|
|
246
|
+
}
|
|
247
|
+
activate() {
|
|
248
|
+
this.#active = true;
|
|
249
|
+
}
|
|
250
|
+
cancel() {
|
|
251
|
+
this.#active = false;
|
|
252
|
+
this.#queued = false;
|
|
253
|
+
this.#generation += 1;
|
|
254
|
+
}
|
|
255
|
+
schedule() {
|
|
256
|
+
if (!this.#active || this.#queued) return;
|
|
257
|
+
this.#queued = true;
|
|
258
|
+
const generation = this.#generation;
|
|
259
|
+
queueMicrotask(() => {
|
|
260
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
261
|
+
this.#queued = false;
|
|
262
|
+
this.#run();
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
var MorphRenderWatcher = class {
|
|
268
|
+
#listeners = new ListenerSet();
|
|
269
|
+
#pass;
|
|
270
|
+
#element = null;
|
|
271
|
+
#onMorph = () => this.#pass.schedule();
|
|
272
|
+
constructor(run) {
|
|
273
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
274
|
+
}
|
|
275
|
+
observe(element) {
|
|
276
|
+
if (this.#element === element) return;
|
|
277
|
+
this.disconnect();
|
|
278
|
+
this.#element = element;
|
|
279
|
+
this.#pass.activate();
|
|
280
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
281
|
+
}
|
|
282
|
+
schedule() {
|
|
283
|
+
this.#pass.schedule();
|
|
284
|
+
}
|
|
285
|
+
disconnect() {
|
|
286
|
+
this.#listeners.dispose();
|
|
287
|
+
this.#pass.cancel();
|
|
288
|
+
this.#element = null;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
var NUMBER_BOUNDS = {
|
|
293
|
+
finite: { finite: true }};
|
|
294
|
+
function matchesNumberBounds(value, bounds) {
|
|
295
|
+
if (!Number.isFinite(value)) {
|
|
296
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
297
|
+
if (direction === null) return false;
|
|
298
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
299
|
+
}
|
|
300
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
301
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
302
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
303
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
304
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function readNumber(raw, fallback, bounds) {
|
|
309
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
var NumberValueReader = class {
|
|
313
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
314
|
+
read(owner, name, raw, fallback, bounds) {
|
|
315
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
316
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
317
|
+
this.#lastRejected.delete(name);
|
|
318
|
+
return resolved;
|
|
319
|
+
}
|
|
320
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
321
|
+
const literal = owner.element.getAttribute(attribute);
|
|
322
|
+
if (literal === null) {
|
|
323
|
+
this.#lastRejected.delete(name);
|
|
324
|
+
return resolved;
|
|
325
|
+
}
|
|
326
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
327
|
+
this.#lastRejected.set(name, literal);
|
|
328
|
+
console.warn(
|
|
329
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
return resolved;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
111
336
|
function prefersReducedMotion() {
|
|
112
337
|
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
113
338
|
}
|
|
114
339
|
|
|
115
|
-
// src/utils/scroll_source.ts
|
|
116
340
|
function resolveScrollContainer(selector) {
|
|
117
341
|
const match = selector ? document.querySelector(selector) : null;
|
|
118
342
|
return match instanceof HTMLElement ? match : null;
|
|
@@ -124,69 +348,114 @@ function scrollOffset(source) {
|
|
|
124
348
|
return source === window ? window.scrollY ?? window.pageYOffset ?? 0 : source.scrollTop;
|
|
125
349
|
}
|
|
126
350
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.#rewind = rewind;
|
|
139
|
-
}
|
|
140
|
-
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
141
|
-
activate() {
|
|
142
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
143
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
144
|
-
if (first) {
|
|
145
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
351
|
+
function queryOpenDescendants(root, selector) {
|
|
352
|
+
const matches = [];
|
|
353
|
+
const roots = [root];
|
|
354
|
+
for (const scope of roots) {
|
|
355
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
356
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
357
|
+
const shadow = scope.shadowRoot;
|
|
358
|
+
if (shadow) roots.push(shadow);
|
|
359
|
+
}
|
|
360
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
361
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
146
362
|
}
|
|
147
363
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
_BeforeCacheReset.#subscribers.delete(this);
|
|
151
|
-
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
152
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
153
|
-
}
|
|
154
|
-
};
|
|
364
|
+
return matches;
|
|
365
|
+
}
|
|
155
366
|
|
|
156
|
-
|
|
157
|
-
|
|
367
|
+
var TabindexLoan = class _TabindexLoan {
|
|
368
|
+
static #registry = sharedRegistry(
|
|
369
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
370
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
371
|
+
);
|
|
158
372
|
#value;
|
|
373
|
+
#record;
|
|
159
374
|
#lent = /* @__PURE__ */ new Set();
|
|
160
|
-
|
|
161
|
-
#beforeCache = new BeforeCacheReset(() => this.returnAll());
|
|
162
|
-
/**
|
|
163
|
-
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
164
|
-
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
165
|
-
* which a scroll region with no focusable content of its own needs.
|
|
166
|
-
*/
|
|
167
|
-
constructor(value = "-1") {
|
|
375
|
+
constructor(value, owner) {
|
|
168
376
|
this.#value = value;
|
|
377
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
378
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
169
379
|
}
|
|
170
|
-
/** Lends `element` the value; no-ops when it already carries a `tabindex`. */
|
|
171
380
|
lend(element) {
|
|
172
|
-
|
|
173
|
-
element.
|
|
381
|
+
const stack = this.#stack(element);
|
|
382
|
+
const current = element.getAttribute("tabindex");
|
|
383
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
384
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
385
|
+
);
|
|
386
|
+
const linked = stack.at(-1)?.value === current;
|
|
387
|
+
if (!held && current !== null && !linked) return;
|
|
174
388
|
this.#lent.add(element);
|
|
175
|
-
|
|
389
|
+
if (held) held.loan = this;
|
|
390
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
391
|
+
this.#mark(element, stack);
|
|
392
|
+
if (current === null || !held && current !== this.#value) {
|
|
393
|
+
element.setAttribute("tabindex", this.#value);
|
|
394
|
+
}
|
|
176
395
|
}
|
|
177
|
-
/** Takes back every loan whose value is still the one that was lent. */
|
|
178
396
|
returnAll() {
|
|
179
397
|
for (const element of this.#lent) {
|
|
180
|
-
|
|
398
|
+
const stack = this.#stack(element);
|
|
399
|
+
const hold = stack.find((held) => held.loan === this);
|
|
400
|
+
if (hold) this.#release(element, stack, hold);
|
|
181
401
|
}
|
|
182
402
|
this.#lent.clear();
|
|
183
|
-
|
|
403
|
+
}
|
|
404
|
+
reclaim(element) {
|
|
405
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
406
|
+
const stack = this.#stack(element);
|
|
407
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
408
|
+
if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
|
|
409
|
+
}
|
|
410
|
+
reclaimWithin(root) {
|
|
411
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
412
|
+
this.reclaim(element);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
#stack(element) {
|
|
416
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
417
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
418
|
+
stacks.set(element, stack);
|
|
419
|
+
return stack;
|
|
420
|
+
}
|
|
421
|
+
#seed(element) {
|
|
422
|
+
const current = element.getAttribute("tabindex");
|
|
423
|
+
const holds = [];
|
|
424
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
425
|
+
const value = element.getAttribute(record);
|
|
426
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
427
|
+
}
|
|
428
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
429
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
430
|
+
return holds;
|
|
431
|
+
}
|
|
432
|
+
#release(element, stack, hold) {
|
|
433
|
+
const current = element.getAttribute("tabindex");
|
|
434
|
+
const index = stack.indexOf(hold);
|
|
435
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
436
|
+
stack.splice(index, 1);
|
|
437
|
+
const above = stack[index];
|
|
438
|
+
if (above) above.linked &&= hold.linked;
|
|
439
|
+
this.#mark(element, stack);
|
|
440
|
+
if (!owns) return;
|
|
441
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
442
|
+
if (!below) element.removeAttribute("tabindex");
|
|
443
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
444
|
+
}
|
|
445
|
+
#mark(element, stack) {
|
|
446
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
447
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
448
|
+
for (const [name, recorded] of records) {
|
|
449
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
450
|
+
if (recorded === null) element.removeAttribute(name);
|
|
451
|
+
else element.setAttribute(name, recorded);
|
|
452
|
+
}
|
|
184
453
|
}
|
|
185
454
|
};
|
|
186
455
|
|
|
187
|
-
// src/controllers/scroll_visibility_controller.ts
|
|
188
456
|
var DEFAULT_OFFSET = 400;
|
|
189
|
-
var ScrollVisibilityController = class extends Controller {
|
|
457
|
+
var ScrollVisibilityController = class _ScrollVisibilityController extends Controller {
|
|
458
|
+
#numbers = new NumberValueReader();
|
|
190
459
|
static targets = ["element"];
|
|
191
460
|
static values = {
|
|
192
461
|
offset: { type: Number, default: DEFAULT_OFFSET },
|
|
@@ -194,54 +463,38 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
194
463
|
focusSelector: { type: String, default: "" },
|
|
195
464
|
root: { type: String, default: "" }
|
|
196
465
|
};
|
|
466
|
+
static valueConstraints = {
|
|
467
|
+
offset: NUMBER_BOUNDS.finite
|
|
468
|
+
};
|
|
197
469
|
static actions = ["toTop"];
|
|
198
470
|
static events = ["change"];
|
|
199
|
-
/** Coalesces scroll bursts into one measurement per frame. */
|
|
200
471
|
#frames = new FrameCoalescer();
|
|
201
|
-
/** Previous scroll position, for `direction` mode delta detection. */
|
|
202
472
|
#lastScrollY = 0;
|
|
203
|
-
/** Current visibility, tracked to dispatch `change` only on real transitions. */
|
|
204
473
|
#visible = null;
|
|
205
|
-
/**
|
|
206
|
-
* The observed scroll source: a container element when `root` resolves, else
|
|
207
|
-
* the window. Captured on connect so teardown detaches from the same source.
|
|
208
|
-
*/
|
|
209
474
|
#scrollSource = window;
|
|
210
|
-
/**
|
|
211
|
-
* Gates the declaration callbacks to the connected window.
|
|
212
|
-
*
|
|
213
|
-
* Stimulus delivers a Value callback ahead of `connect()` and again for every
|
|
214
|
-
* runtime change; without the gate, merely connecting would evaluate — and
|
|
215
|
-
* announce — before `connect()` runs its own first reflection.
|
|
216
|
-
*/
|
|
217
475
|
#connected = false;
|
|
218
|
-
/** Validated threshold; a non-finite declaration reads as the default. */
|
|
219
476
|
#offset = DEFAULT_OFFSET;
|
|
220
|
-
/** Validated `root` selector; an unparsable declaration reads as absent. */
|
|
221
477
|
#rootSelector = "";
|
|
222
|
-
/** Validated `focusSelector`; an unparsable declaration reads as absent. */
|
|
223
478
|
#focusSelector = "";
|
|
224
|
-
|
|
225
|
-
#
|
|
226
|
-
/**
|
|
227
|
-
* The hide held back while the control itself owns focus.
|
|
228
|
-
*
|
|
229
|
-
* Completing the deferral re-runs the ordinary evaluation rather than applying
|
|
230
|
-
* the stale decision: by the time focus leaves, the scroll position may have
|
|
231
|
-
* moved back past the threshold.
|
|
232
|
-
*/
|
|
479
|
+
#tabindex = new TabindexLoan("-1", this.identifier);
|
|
480
|
+
#hidden = new AttributeLease("hidden", this.identifier);
|
|
233
481
|
#pendingHide = new BlurDeferral(() => {
|
|
234
482
|
if (this.#connected) this.#evaluate();
|
|
235
483
|
});
|
|
236
484
|
#onScroll = () => this.#frames.schedule(() => this.#evaluate());
|
|
485
|
+
#morphRender = new MorphRenderWatcher(() => {
|
|
486
|
+
this.#followSource();
|
|
487
|
+
this.#evaluate();
|
|
488
|
+
});
|
|
237
489
|
connect() {
|
|
238
|
-
this.#
|
|
239
|
-
this.#
|
|
240
|
-
this.#
|
|
490
|
+
this.#tabindex.reclaimWithin(document);
|
|
491
|
+
this.#morphRender.observe(this.element);
|
|
492
|
+
this.#listenTo(resolveScrollSource(this.#rootSelector));
|
|
241
493
|
this.#evaluate(false);
|
|
242
494
|
this.#connected = true;
|
|
243
495
|
}
|
|
244
496
|
disconnect() {
|
|
497
|
+
this.#morphRender.disconnect();
|
|
245
498
|
this.#connected = false;
|
|
246
499
|
this.#scrollSource.removeEventListener("scroll", this.#onScroll);
|
|
247
500
|
this.#frames.cancel();
|
|
@@ -249,40 +502,30 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
249
502
|
this.#tabindex.returnAll();
|
|
250
503
|
this.#visible = null;
|
|
251
504
|
}
|
|
252
|
-
/** Writes the current visibility onto a control that arrives after connect. */
|
|
253
505
|
elementTargetConnected(element) {
|
|
254
|
-
if (this.#visible !== null) element
|
|
506
|
+
if (this.#visible !== null) this.#show(element, this.#visible);
|
|
255
507
|
}
|
|
256
|
-
|
|
257
|
-
elementTargetDisconnected() {
|
|
508
|
+
elementTargetDisconnected(element) {
|
|
258
509
|
this.#pendingHide.releaseAll();
|
|
510
|
+
if (!this.elementTargets.includes(element)) this.#hidden.return(element);
|
|
511
|
+
if (this.#visible !== null && this.hasElementTarget) {
|
|
512
|
+
this.#show(this.elementTarget, this.#visible);
|
|
513
|
+
}
|
|
259
514
|
}
|
|
260
|
-
/**
|
|
261
|
-
* Validates `offset` once, then re-renders.
|
|
262
|
-
*
|
|
263
|
-
* Re-renders when application code (or a Turbo morph) changes `offset` at
|
|
264
|
-
* runtime. A declaration that is not a finite number reads as the default, so
|
|
265
|
-
* the comparison path never sees `NaN` — which would answer `false` to every
|
|
266
|
-
* comparison and strand the element (in `direction` mode, even the guarantee
|
|
267
|
-
* that the very top always reveals).
|
|
268
|
-
*/
|
|
269
515
|
offsetValueChanged() {
|
|
270
|
-
this.#offset =
|
|
271
|
-
|
|
516
|
+
this.#offset = this.#safeOffset;
|
|
517
|
+
this.#morphRender.schedule();
|
|
272
518
|
}
|
|
273
|
-
/** Re-renders when application code (or a Turbo morph) changes `mode` at runtime. */
|
|
274
519
|
modeValueChanged() {
|
|
275
|
-
|
|
520
|
+
this.#morphRender.schedule();
|
|
276
521
|
}
|
|
277
|
-
/** Validates `root` once so connect never parses a selector that throws. */
|
|
278
522
|
rootValueChanged() {
|
|
279
523
|
this.#rootSelector = validSelector(this.element, this.rootValue, "");
|
|
524
|
+
this.#morphRender.schedule();
|
|
280
525
|
}
|
|
281
|
-
/** Validates `focusSelector` once so `toTop` never parses a selector that throws. */
|
|
282
526
|
focusSelectorValueChanged() {
|
|
283
527
|
this.#focusSelector = validSelector(this.element, this.focusSelectorValue, "");
|
|
284
528
|
}
|
|
285
|
-
/** Scrolls the source to the top and, optionally, moves focus to a safe target. */
|
|
286
529
|
toTop() {
|
|
287
530
|
const behavior = prefersReducedMotion() ? "instant" : "smooth";
|
|
288
531
|
this.#scrollSource.scrollTo({ top: 0, behavior });
|
|
@@ -294,14 +537,17 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
294
537
|
}
|
|
295
538
|
}
|
|
296
539
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
540
|
+
#followSource() {
|
|
541
|
+
const source = resolveScrollSource(this.#rootSelector);
|
|
542
|
+
if (source === this.#scrollSource) return;
|
|
543
|
+
this.#scrollSource.removeEventListener("scroll", this.#onScroll);
|
|
544
|
+
this.#listenTo(source);
|
|
545
|
+
}
|
|
546
|
+
#listenTo(source) {
|
|
547
|
+
this.#scrollSource = source;
|
|
548
|
+
this.#lastScrollY = scrollOffset(source);
|
|
549
|
+
source.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
550
|
+
}
|
|
305
551
|
#evaluate(notify = true) {
|
|
306
552
|
const y = scrollOffset(this.#scrollSource);
|
|
307
553
|
let nextVisible;
|
|
@@ -309,6 +555,7 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
309
555
|
if (y <= this.#offset) {
|
|
310
556
|
nextVisible = true;
|
|
311
557
|
} else if (y === this.#lastScrollY) {
|
|
558
|
+
this.#setVisible(this.#visible ?? true, notify);
|
|
312
559
|
return;
|
|
313
560
|
} else {
|
|
314
561
|
nextVisible = y < this.#lastScrollY;
|
|
@@ -319,33 +566,36 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
319
566
|
this.#lastScrollY = y;
|
|
320
567
|
this.#setVisible(nextVisible, notify);
|
|
321
568
|
}
|
|
322
|
-
/** Applies visibility to the target, syncing `hidden`, `data-state`, `change`. */
|
|
323
569
|
#setVisible(next, notify) {
|
|
324
|
-
|
|
570
|
+
const changed = next !== this.#visible;
|
|
325
571
|
const focused = !next && this.hasElementTarget ? this.#focusedWithin() : null;
|
|
326
572
|
if (focused) {
|
|
327
573
|
this.#pendingHide.deferOnly(focused);
|
|
328
574
|
return;
|
|
329
575
|
}
|
|
330
576
|
this.#visible = next;
|
|
331
|
-
if (this.hasElementTarget) this.elementTarget
|
|
577
|
+
if (this.hasElementTarget) this.#show(this.elementTarget, next);
|
|
332
578
|
this.element.setAttribute("data-state", next ? "visible" : "hidden");
|
|
333
|
-
if (notify) this.dispatch("change", { detail: { visible: next } });
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
* `blur` does not bubble, so the deferral has to ride the focused element
|
|
339
|
-
* itself: waiting on a container that never receives the event would hold the
|
|
340
|
-
* hide forever.
|
|
341
|
-
*/
|
|
579
|
+
if (notify && changed) this.dispatch("change", { detail: { visible: next } });
|
|
580
|
+
}
|
|
581
|
+
#show(control, visible) {
|
|
582
|
+
this.#hidden.write(control, visible ? null : "");
|
|
583
|
+
}
|
|
342
584
|
#focusedWithin() {
|
|
343
585
|
const focused = document.activeElement;
|
|
344
586
|
if (focused instanceof HTMLElement && this.elementTarget.contains(focused)) return focused;
|
|
345
587
|
return null;
|
|
346
588
|
}
|
|
589
|
+
get #safeOffset() {
|
|
590
|
+
return this.#numbers.read(
|
|
591
|
+
this,
|
|
592
|
+
"offset",
|
|
593
|
+
this.offsetValue,
|
|
594
|
+
_ScrollVisibilityController.values.offset.default,
|
|
595
|
+
_ScrollVisibilityController.valueConstraints.offset
|
|
596
|
+
);
|
|
597
|
+
}
|
|
347
598
|
};
|
|
348
599
|
|
|
349
600
|
export { ScrollVisibilityController };
|
|
350
|
-
//# sourceMappingURL=scroll_visibility_controller.js.map
|
|
351
601
|
//# sourceMappingURL=scroll_visibility_controller.js.map
|