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,78 +1,152 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function sharedRegistry(key, create) {
|
|
4
|
+
const symbol = Symbol.for(key);
|
|
5
|
+
const scope = globalThis;
|
|
6
|
+
const existing = scope[symbol];
|
|
7
|
+
if (existing !== void 0) return existing;
|
|
8
|
+
const registry = create();
|
|
9
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
10
|
+
return registry;
|
|
11
|
+
}
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
);
|
|
7
34
|
#attribute;
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
35
|
+
#record;
|
|
36
|
+
#base;
|
|
37
|
+
#held = /* @__PURE__ */ new Set();
|
|
38
|
+
constructor(attribute, owner, options = {}) {
|
|
11
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));
|
|
12
44
|
}
|
|
13
|
-
/** Writes or removes the leased attribute while preserving its authored value. */
|
|
14
45
|
write(element, value) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
this.#reflect(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);
|
|
25
53
|
}
|
|
26
|
-
/** Returns one lease without overwriting a value subsequently authored by a consumer. */
|
|
27
54
|
return(element) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
this.#
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
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
|
+
}
|
|
33
142
|
}
|
|
34
|
-
/** Reflects only a real value transition, avoiding self-triggered mutation work. */
|
|
35
143
|
#reflect(element, value) {
|
|
36
144
|
if (element.getAttribute(this.#attribute) === value) return;
|
|
37
145
|
if (value === null) element.removeAttribute(this.#attribute);
|
|
38
146
|
else element.setAttribute(this.#attribute, value);
|
|
39
147
|
}
|
|
40
|
-
/** Returns every outstanding lease using the same ownership check as {@link return}. */
|
|
41
|
-
returnAll() {
|
|
42
|
-
for (const element of Array.from(this.#records.keys())) this.return(element);
|
|
43
|
-
}
|
|
44
148
|
};
|
|
45
149
|
|
|
46
|
-
// src/utils/before_cache_reset.ts
|
|
47
|
-
var BeforeCacheReset = class _BeforeCacheReset {
|
|
48
|
-
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
49
|
-
static #subscribers = /* @__PURE__ */ new Set();
|
|
50
|
-
/** The shared listener; installed while at least one instance is subscribed. */
|
|
51
|
-
static #onBeforeCache = () => {
|
|
52
|
-
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
53
|
-
};
|
|
54
|
-
#rewind;
|
|
55
|
-
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
56
|
-
constructor(rewind) {
|
|
57
|
-
this.#rewind = rewind;
|
|
58
|
-
}
|
|
59
|
-
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
60
|
-
activate() {
|
|
61
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
62
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
63
|
-
if (first) {
|
|
64
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
68
|
-
deactivate() {
|
|
69
|
-
_BeforeCacheReset.#subscribers.delete(this);
|
|
70
|
-
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
71
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// src/utils/focus_candidate.ts
|
|
76
150
|
function inheritsFieldsetDisabled(control) {
|
|
77
151
|
let fieldset = control.closest("fieldset[disabled]");
|
|
78
152
|
while (fieldset) {
|
|
@@ -82,8 +156,19 @@ function inheritsFieldsetDisabled(control) {
|
|
|
82
156
|
}
|
|
83
157
|
return false;
|
|
84
158
|
}
|
|
159
|
+
function unslotted(element) {
|
|
160
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
161
|
+
}
|
|
162
|
+
function flatTreeParent(element) {
|
|
163
|
+
if (unslotted(element)) return null;
|
|
164
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
165
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
166
|
+
return parent instanceof Element ? parent : null;
|
|
167
|
+
}
|
|
85
168
|
function canTakeFocus(element) {
|
|
86
|
-
|
|
169
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
170
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
171
|
+
}
|
|
87
172
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
88
173
|
if (!("disabled" in element)) return true;
|
|
89
174
|
if (element.disabled) return false;
|
|
@@ -150,14 +235,8 @@ function hasTabStop(root) {
|
|
|
150
235
|
return firstTabStop(root) !== null;
|
|
151
236
|
}
|
|
152
237
|
|
|
153
|
-
// src/utils/frame_coalescer.ts
|
|
154
238
|
var FrameCoalescer = class {
|
|
155
239
|
#frame = null;
|
|
156
|
-
/**
|
|
157
|
-
* Runs `run` on the next frame, unless a frame is already pending — the first
|
|
158
|
-
* request of a burst wins and the rest are dropped. The pending frame is
|
|
159
|
-
* released before `run`, so `run` may request the next one.
|
|
160
|
-
*/
|
|
161
240
|
schedule(run) {
|
|
162
241
|
if (this.#frame !== null) return;
|
|
163
242
|
this.#frame = requestAnimationFrame(() => {
|
|
@@ -165,14 +244,6 @@ var FrameCoalescer = class {
|
|
|
165
244
|
run();
|
|
166
245
|
});
|
|
167
246
|
}
|
|
168
|
-
/**
|
|
169
|
-
* Drops the pending frame, and reaches the platform only when there is one.
|
|
170
|
-
*
|
|
171
|
-
* There is no handle value that stands for "nothing pending":
|
|
172
|
-
* `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
|
|
173
|
-
* arrives as a large positive number that the same allocator can hand out, and
|
|
174
|
-
* an idle cancel would then drop a frame belonging to someone else.
|
|
175
|
-
*/
|
|
176
247
|
cancel() {
|
|
177
248
|
if (this.#frame === null) return;
|
|
178
249
|
cancelAnimationFrame(this.#frame);
|
|
@@ -180,18 +251,15 @@ var FrameCoalescer = class {
|
|
|
180
251
|
}
|
|
181
252
|
};
|
|
182
253
|
|
|
183
|
-
// src/utils/layout_observer.ts
|
|
184
254
|
var LayoutObserver = class {
|
|
185
255
|
#callback;
|
|
186
256
|
#resizeObserverFactory;
|
|
187
257
|
#resizeObserver = null;
|
|
188
258
|
#observingViewport = false;
|
|
189
259
|
#loadContainer = null;
|
|
190
|
-
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
191
260
|
#handleViewportResize = () => {
|
|
192
261
|
this.#callback();
|
|
193
262
|
};
|
|
194
|
-
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
195
263
|
#handleDescendantLoad = () => {
|
|
196
264
|
this.#callback();
|
|
197
265
|
};
|
|
@@ -199,11 +267,6 @@ var LayoutObserver = class {
|
|
|
199
267
|
this.#callback = callback;
|
|
200
268
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
201
269
|
}
|
|
202
|
-
/**
|
|
203
|
-
* Starts observing an element's size. Repeated calls observe additional
|
|
204
|
-
* elements through the same shared observer. No-ops when no
|
|
205
|
-
* `ResizeObserver` implementation is available.
|
|
206
|
-
*/
|
|
207
270
|
observe(element) {
|
|
208
271
|
if (!this.#resizeObserverFactory) return;
|
|
209
272
|
if (!this.#resizeObserver) {
|
|
@@ -213,47 +276,28 @@ var LayoutObserver = class {
|
|
|
213
276
|
}
|
|
214
277
|
this.#resizeObserver.observe(element);
|
|
215
278
|
}
|
|
216
|
-
/** Stops observing a single element while leaving any others in place. */
|
|
217
279
|
unobserve(element) {
|
|
218
280
|
this.#resizeObserver?.unobserve(element);
|
|
219
281
|
}
|
|
220
|
-
/** Starts observing viewport resizes. Idempotent: the listener is added once. */
|
|
221
282
|
observeViewport() {
|
|
222
283
|
if (this.#observingViewport) return;
|
|
223
284
|
this.#observingViewport = true;
|
|
224
285
|
window.addEventListener("resize", this.#handleViewportResize);
|
|
225
286
|
}
|
|
226
|
-
/** Stops observing viewport resizes without affecting element observation. */
|
|
227
287
|
unobserveViewport() {
|
|
228
288
|
if (!this.#observingViewport) return;
|
|
229
289
|
this.#observingViewport = false;
|
|
230
290
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
231
291
|
}
|
|
232
|
-
/**
|
|
233
|
-
* Starts reporting a `load` from anywhere inside `container` — an image or a
|
|
234
|
-
* frame settling changes the box it sits in, and it measures as zero high until
|
|
235
|
-
* then. `load` does not bubble, so the subscription is a capture-phase listener
|
|
236
|
-
* on the container itself and nothing the caller spells.
|
|
237
|
-
*
|
|
238
|
-
* **One container at a time.** A further call moves the observation, so a widget
|
|
239
|
-
* whose content element is swapped at runtime releases the element it let go by
|
|
240
|
-
* naming the new one — there is no second place for the release to drift from.
|
|
241
|
-
*/
|
|
242
292
|
observeDescendantLoads(container) {
|
|
243
293
|
this.unobserveDescendantLoads();
|
|
244
294
|
this.#loadContainer = container;
|
|
245
295
|
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
246
296
|
}
|
|
247
|
-
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
248
297
|
unobserveDescendantLoads() {
|
|
249
298
|
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
250
299
|
this.#loadContainer = null;
|
|
251
300
|
}
|
|
252
|
-
/**
|
|
253
|
-
* Releases every observation: disconnects the {@link ResizeObserver} and removes
|
|
254
|
-
* the viewport and descendant-load listeners. Safe to call multiple times. Call
|
|
255
|
-
* this from a controller's `disconnect()`.
|
|
256
|
-
*/
|
|
257
301
|
disconnect() {
|
|
258
302
|
this.#resizeObserver?.disconnect();
|
|
259
303
|
this.#resizeObserver = null;
|
|
@@ -262,7 +306,6 @@ var LayoutObserver = class {
|
|
|
262
306
|
}
|
|
263
307
|
};
|
|
264
308
|
|
|
265
|
-
// src/utils/logical_scroll.ts
|
|
266
309
|
function isRtl(element) {
|
|
267
310
|
return window.getComputedStyle(element).direction === "rtl";
|
|
268
311
|
}
|
|
@@ -276,27 +319,33 @@ function logicalScrollMetrics(element, horizontal) {
|
|
|
276
319
|
return { position: Math.min(max, Math.max(0, position)), max };
|
|
277
320
|
}
|
|
278
321
|
|
|
279
|
-
|
|
322
|
+
var ListenerSet = class {
|
|
323
|
+
#abort = new AbortController();
|
|
324
|
+
add(target, type, handler, options) {
|
|
325
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
326
|
+
}
|
|
327
|
+
dispose() {
|
|
328
|
+
this.#abort.abort();
|
|
329
|
+
this.#abort = new AbortController();
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
280
333
|
var MicrotaskCoalescer = class {
|
|
281
334
|
#run;
|
|
282
335
|
#queued = false;
|
|
283
336
|
#active = false;
|
|
284
337
|
#generation = 0;
|
|
285
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
286
338
|
constructor(run) {
|
|
287
339
|
this.#run = run;
|
|
288
340
|
}
|
|
289
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
290
341
|
activate() {
|
|
291
342
|
this.#active = true;
|
|
292
343
|
}
|
|
293
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
294
344
|
cancel() {
|
|
295
345
|
this.#active = false;
|
|
296
346
|
this.#queued = false;
|
|
297
347
|
this.#generation += 1;
|
|
298
348
|
}
|
|
299
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
300
349
|
schedule() {
|
|
301
350
|
if (!this.#active || this.#queued) return;
|
|
302
351
|
this.#queued = true;
|
|
@@ -309,90 +358,284 @@ var MicrotaskCoalescer = class {
|
|
|
309
358
|
}
|
|
310
359
|
};
|
|
311
360
|
|
|
312
|
-
|
|
313
|
-
|
|
361
|
+
var MorphRenderWatcher = class {
|
|
362
|
+
#listeners = new ListenerSet();
|
|
363
|
+
#pass;
|
|
364
|
+
#element = null;
|
|
365
|
+
#onMorph = () => this.#pass.schedule();
|
|
366
|
+
constructor(run) {
|
|
367
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
368
|
+
}
|
|
369
|
+
observe(element) {
|
|
370
|
+
if (this.#element === element) return;
|
|
371
|
+
this.disconnect();
|
|
372
|
+
this.#element = element;
|
|
373
|
+
this.#pass.activate();
|
|
374
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
375
|
+
}
|
|
376
|
+
schedule() {
|
|
377
|
+
this.#pass.schedule();
|
|
378
|
+
}
|
|
379
|
+
disconnect() {
|
|
380
|
+
this.#listeners.dispose();
|
|
381
|
+
this.#pass.cancel();
|
|
382
|
+
this.#element = null;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
var StylePropertyLease = class _StylePropertyLease {
|
|
387
|
+
static #registry = sharedRegistry(
|
|
388
|
+
"stimeo-ui.style-property-lease.registry.v1",
|
|
389
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
390
|
+
);
|
|
314
391
|
#property;
|
|
315
|
-
#
|
|
316
|
-
|
|
317
|
-
constructor(property) {
|
|
392
|
+
#record;
|
|
393
|
+
#held = /* @__PURE__ */ new Set();
|
|
394
|
+
constructor(property, owner) {
|
|
318
395
|
this.#property = property;
|
|
396
|
+
this.#record = `data-${owner}-style-${property}-lease`;
|
|
397
|
+
const names = _StylePropertyLease.#registry.names;
|
|
398
|
+
names.set(property, (names.get(property) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
319
399
|
}
|
|
320
|
-
/** Writes or removes the leased declaration while preserving its authored value. */
|
|
321
400
|
write(element, value, priority = "") {
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
writtenValue: value,
|
|
331
|
-
writtenPriority: value === null ? "" : priority
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
this.#reflect(element, value, priority);
|
|
401
|
+
const stack = this.#stack(element);
|
|
402
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
403
|
+
this.#held.add(element);
|
|
404
|
+
const shows = !this.#covered(element, stack, hold);
|
|
405
|
+
hold.writtenValue = value;
|
|
406
|
+
hold.writtenPriority = value === null ? "" : priority;
|
|
407
|
+
this.#mark(element, stack);
|
|
408
|
+
if (shows) this.#show(element, value, hold.writtenPriority);
|
|
335
409
|
}
|
|
336
|
-
/** Returns one lease without overwriting a later consumer declaration. */
|
|
337
410
|
return(element) {
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
this.#
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
411
|
+
const held = this.#held.delete(element);
|
|
412
|
+
const known = _StylePropertyLease.#registry.stacks.get(element)?.has(this.#property);
|
|
413
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
414
|
+
const hold = this.#take(stack);
|
|
415
|
+
if (!hold) return;
|
|
416
|
+
const owns = this.#carries(element, hold) && !this.#covered(element, stack, hold);
|
|
417
|
+
const index = stack.indexOf(hold);
|
|
418
|
+
stack.splice(index, 1);
|
|
419
|
+
const above = stack[index];
|
|
420
|
+
if (above) above.linked &&= hold.linked;
|
|
421
|
+
this.#mark(element, stack);
|
|
422
|
+
if (!owns) return;
|
|
423
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
424
|
+
if (below) this.#show(element, below.writtenValue, below.writtenPriority);
|
|
425
|
+
else this.#show(element, hold.originalValue, hold.originalPriority);
|
|
346
426
|
}
|
|
347
|
-
/** Returns every outstanding declaration lease. */
|
|
348
427
|
returnAll() {
|
|
349
|
-
for (const element of Array.from(this.#
|
|
428
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
429
|
+
}
|
|
430
|
+
#recorded(element, name) {
|
|
431
|
+
try {
|
|
432
|
+
const parsed = JSON.parse(element.getAttribute(name) ?? "null");
|
|
433
|
+
if (!Array.isArray(parsed)) return null;
|
|
434
|
+
const [originalValue, originalPriority, writtenValue, writtenPriority, ...rest] = parsed;
|
|
435
|
+
if (typeof originalValue !== "string" || typeof originalPriority !== "string") return null;
|
|
436
|
+
if (typeof writtenValue !== "string" && writtenValue !== null) return null;
|
|
437
|
+
if (typeof writtenPriority !== "string") return null;
|
|
438
|
+
const linked = rest[0] !== false;
|
|
439
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
440
|
+
if (!beneath.every((under) => typeof under === "string")) return null;
|
|
441
|
+
return { originalValue, originalPriority, writtenValue, writtenPriority, linked, beneath };
|
|
442
|
+
} catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
350
445
|
}
|
|
351
|
-
|
|
352
|
-
|
|
446
|
+
#stack(element) {
|
|
447
|
+
const stacks = _StylePropertyLease.#registry.stacks;
|
|
448
|
+
const byProperty = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
449
|
+
stacks.set(element, byProperty);
|
|
450
|
+
const stack = byProperty.get(this.#property) ?? this.#seed(element);
|
|
451
|
+
byProperty.set(this.#property, stack);
|
|
452
|
+
return stack;
|
|
453
|
+
}
|
|
454
|
+
#seed(element) {
|
|
455
|
+
const found = [];
|
|
456
|
+
for (const record of _StylePropertyLease.#registry.names.get(this.#property) ?? []) {
|
|
457
|
+
const read = this.#recorded(element, record);
|
|
458
|
+
if (read) found.push({ ...read, record });
|
|
459
|
+
}
|
|
460
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
461
|
+
return found.sort((one, other) => depth(one) - depth(other)).map((entry, index) => ({
|
|
462
|
+
lease: null,
|
|
463
|
+
record: entry.record,
|
|
464
|
+
writtenValue: entry.writtenValue,
|
|
465
|
+
writtenPriority: entry.writtenPriority,
|
|
466
|
+
originalValue: entry.originalValue,
|
|
467
|
+
originalPriority: entry.originalPriority,
|
|
468
|
+
linked: entry.linked && index > 0
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
#take(stack) {
|
|
472
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
473
|
+
if (hold) hold.lease = this;
|
|
474
|
+
return hold;
|
|
475
|
+
}
|
|
476
|
+
#acquire(element, stack) {
|
|
477
|
+
const top = stack.at(-1);
|
|
478
|
+
const over = top && this.#carries(element, top) ? top : void 0;
|
|
353
479
|
const style = element.style;
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
480
|
+
const hold = {
|
|
481
|
+
lease: this,
|
|
482
|
+
record: this.#record,
|
|
483
|
+
writtenValue: null,
|
|
484
|
+
writtenPriority: "",
|
|
485
|
+
originalValue: over ? over.originalValue : style.getPropertyValue(this.#property),
|
|
486
|
+
originalPriority: over ? over.originalPriority : style.getPropertyPriority(this.#property),
|
|
487
|
+
linked: over !== void 0
|
|
488
|
+
};
|
|
489
|
+
stack.push(hold);
|
|
490
|
+
return hold;
|
|
491
|
+
}
|
|
492
|
+
#covered(element, stack, hold) {
|
|
493
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => this.#carries(element, above));
|
|
494
|
+
}
|
|
495
|
+
#mark(element, stack) {
|
|
496
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
497
|
+
stack.forEach((hold, index) => {
|
|
498
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
499
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
500
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
501
|
+
const { originalValue, originalPriority, writtenValue, writtenPriority } = hold;
|
|
502
|
+
records.set(
|
|
503
|
+
hold.record,
|
|
504
|
+
(writtenValue ?? "") === originalValue && writtenPriority === originalPriority && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([
|
|
505
|
+
originalValue,
|
|
506
|
+
originalPriority,
|
|
507
|
+
writtenValue,
|
|
508
|
+
writtenPriority,
|
|
509
|
+
...marker,
|
|
510
|
+
...beneath
|
|
511
|
+
])
|
|
512
|
+
);
|
|
513
|
+
});
|
|
514
|
+
for (const [name, recorded] of records) {
|
|
515
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
516
|
+
if (recorded === null) element.removeAttribute(name);
|
|
517
|
+
else element.setAttribute(name, recorded);
|
|
358
518
|
}
|
|
359
|
-
|
|
360
|
-
|
|
519
|
+
}
|
|
520
|
+
#carries(element, hold) {
|
|
521
|
+
return this.#holds(element, hold.writtenValue ?? "", hold.writtenPriority);
|
|
522
|
+
}
|
|
523
|
+
#holds(element, value, priority) {
|
|
524
|
+
const style = element.style;
|
|
525
|
+
return style.getPropertyValue(this.#property) === value && style.getPropertyPriority(this.#property) === priority;
|
|
526
|
+
}
|
|
527
|
+
#show(element, value, priority) {
|
|
528
|
+
if (this.#holds(element, value ?? "", priority)) return;
|
|
529
|
+
if (value === null) element.style.removeProperty(this.#property);
|
|
530
|
+
else element.style.setProperty(this.#property, value, priority);
|
|
361
531
|
}
|
|
362
532
|
};
|
|
363
533
|
|
|
364
|
-
|
|
365
|
-
|
|
534
|
+
function queryOpenDescendants(root, selector) {
|
|
535
|
+
const matches = [];
|
|
536
|
+
const roots = [root];
|
|
537
|
+
for (const scope of roots) {
|
|
538
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
539
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
540
|
+
const shadow = scope.shadowRoot;
|
|
541
|
+
if (shadow) roots.push(shadow);
|
|
542
|
+
}
|
|
543
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
544
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return matches;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
var TabindexLoan = class _TabindexLoan {
|
|
551
|
+
static #registry = sharedRegistry(
|
|
552
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
553
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
554
|
+
);
|
|
366
555
|
#value;
|
|
556
|
+
#record;
|
|
367
557
|
#lent = /* @__PURE__ */ new Set();
|
|
368
|
-
|
|
369
|
-
#beforeCache = new BeforeCacheReset(() => this.returnAll());
|
|
370
|
-
/**
|
|
371
|
-
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
372
|
-
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
373
|
-
* which a scroll region with no focusable content of its own needs.
|
|
374
|
-
*/
|
|
375
|
-
constructor(value = "-1") {
|
|
558
|
+
constructor(value, owner) {
|
|
376
559
|
this.#value = value;
|
|
560
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
561
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
377
562
|
}
|
|
378
|
-
/** Lends `element` the value; no-ops when it already carries a `tabindex`. */
|
|
379
563
|
lend(element) {
|
|
380
|
-
|
|
381
|
-
element.
|
|
564
|
+
const stack = this.#stack(element);
|
|
565
|
+
const current = element.getAttribute("tabindex");
|
|
566
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
567
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
568
|
+
);
|
|
569
|
+
const linked = stack.at(-1)?.value === current;
|
|
570
|
+
if (!held && current !== null && !linked) return;
|
|
382
571
|
this.#lent.add(element);
|
|
383
|
-
|
|
572
|
+
if (held) held.loan = this;
|
|
573
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
574
|
+
this.#mark(element, stack);
|
|
575
|
+
if (current === null || !held && current !== this.#value) {
|
|
576
|
+
element.setAttribute("tabindex", this.#value);
|
|
577
|
+
}
|
|
384
578
|
}
|
|
385
|
-
/** Takes back every loan whose value is still the one that was lent. */
|
|
386
579
|
returnAll() {
|
|
387
580
|
for (const element of this.#lent) {
|
|
388
|
-
|
|
581
|
+
const stack = this.#stack(element);
|
|
582
|
+
const hold = stack.find((held) => held.loan === this);
|
|
583
|
+
if (hold) this.#release(element, stack, hold);
|
|
389
584
|
}
|
|
390
585
|
this.#lent.clear();
|
|
391
|
-
|
|
586
|
+
}
|
|
587
|
+
reclaim(element) {
|
|
588
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
589
|
+
const stack = this.#stack(element);
|
|
590
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
591
|
+
if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
|
|
592
|
+
}
|
|
593
|
+
reclaimWithin(root) {
|
|
594
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
595
|
+
this.reclaim(element);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
#stack(element) {
|
|
599
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
600
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
601
|
+
stacks.set(element, stack);
|
|
602
|
+
return stack;
|
|
603
|
+
}
|
|
604
|
+
#seed(element) {
|
|
605
|
+
const current = element.getAttribute("tabindex");
|
|
606
|
+
const holds = [];
|
|
607
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
608
|
+
const value = element.getAttribute(record);
|
|
609
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
610
|
+
}
|
|
611
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
612
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
613
|
+
return holds;
|
|
614
|
+
}
|
|
615
|
+
#release(element, stack, hold) {
|
|
616
|
+
const current = element.getAttribute("tabindex");
|
|
617
|
+
const index = stack.indexOf(hold);
|
|
618
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
619
|
+
stack.splice(index, 1);
|
|
620
|
+
const above = stack[index];
|
|
621
|
+
if (above) above.linked &&= hold.linked;
|
|
622
|
+
this.#mark(element, stack);
|
|
623
|
+
if (!owns) return;
|
|
624
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
625
|
+
if (!below) element.removeAttribute("tabindex");
|
|
626
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
627
|
+
}
|
|
628
|
+
#mark(element, stack) {
|
|
629
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
630
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
631
|
+
for (const [name, recorded] of records) {
|
|
632
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
633
|
+
if (recorded === null) element.removeAttribute(name);
|
|
634
|
+
else element.setAttribute(name, recorded);
|
|
635
|
+
}
|
|
392
636
|
}
|
|
393
637
|
};
|
|
394
638
|
|
|
395
|
-
// src/controllers/scroll_area_controller.ts
|
|
396
639
|
var EDGE_EPSILON = 1;
|
|
397
640
|
var ScrollAreaController = class extends Controller {
|
|
398
641
|
static targets = ["viewport"];
|
|
@@ -401,13 +644,15 @@ var ScrollAreaController = class extends Controller {
|
|
|
401
644
|
};
|
|
402
645
|
static events = ["reach"];
|
|
403
646
|
#layout = new LayoutObserver(() => this.#refresh());
|
|
404
|
-
#rebind = new
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
#
|
|
408
|
-
#
|
|
409
|
-
#
|
|
410
|
-
|
|
647
|
+
#rebind = new MorphRenderWatcher(() => this.#syncViewport());
|
|
648
|
+
#tabindex = new TabindexLoan("0", this.identifier);
|
|
649
|
+
#role = new AttributeLease("role", this.identifier);
|
|
650
|
+
#overflowState = new AttributeLease("data-overflow", this.identifier);
|
|
651
|
+
#scrollState = new AttributeLease("data-scroll", this.identifier);
|
|
652
|
+
#progress = new StylePropertyLease(
|
|
653
|
+
"--stimeo--scroll-progress",
|
|
654
|
+
this.identifier
|
|
655
|
+
);
|
|
411
656
|
#viewport = null;
|
|
412
657
|
#content = null;
|
|
413
658
|
#nameSources = null;
|
|
@@ -415,7 +660,6 @@ var ScrollAreaController = class extends Controller {
|
|
|
415
660
|
#observedNameIds = /* @__PURE__ */ new Set();
|
|
416
661
|
#observedNameSources = [];
|
|
417
662
|
#fonts = null;
|
|
418
|
-
/** Coalesces scroll bursts into one position sync per frame. */
|
|
419
663
|
#frames = new FrameCoalescer();
|
|
420
664
|
#overflowing = false;
|
|
421
665
|
#lastEdge = null;
|
|
@@ -428,39 +672,31 @@ var ScrollAreaController = class extends Controller {
|
|
|
428
672
|
this.#refresh();
|
|
429
673
|
};
|
|
430
674
|
connect() {
|
|
431
|
-
this.#
|
|
432
|
-
this.#rebind.activate();
|
|
675
|
+
this.#rebind.observe(this.element);
|
|
433
676
|
this.#layout.observeViewport();
|
|
434
677
|
this.#bindFonts();
|
|
435
678
|
this.#syncViewport();
|
|
436
679
|
}
|
|
437
680
|
disconnect() {
|
|
438
|
-
this.#
|
|
439
|
-
this.#rebind.cancel();
|
|
440
|
-
this.#frames.cancel();
|
|
681
|
+
this.#rebind.disconnect();
|
|
441
682
|
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
442
683
|
this.#layout.disconnect();
|
|
443
684
|
this.#unbindFonts();
|
|
444
685
|
this.#clearHostState();
|
|
445
686
|
}
|
|
446
|
-
/** Re-measures when a retained viewport's orientation Value is morphed. */
|
|
447
687
|
orientationValueChanged() {
|
|
448
688
|
this.#refresh();
|
|
449
689
|
}
|
|
450
|
-
/** Schedules a complete resource rebind for a runtime viewport target. */
|
|
451
690
|
viewportTargetConnected() {
|
|
452
691
|
this.#rebind.schedule();
|
|
453
692
|
}
|
|
454
|
-
/** Schedules cleanup or replacement binding after a viewport leaves. */
|
|
455
693
|
viewportTargetDisconnected() {
|
|
456
694
|
this.#rebind.schedule();
|
|
457
695
|
}
|
|
458
|
-
/** Rebinds every viewport-owned resource against the final target in the mutation batch. */
|
|
459
696
|
#syncViewport() {
|
|
460
697
|
const next = this.hasViewportTarget ? this.viewportTarget : null;
|
|
461
698
|
if (next === this.#viewport) {
|
|
462
699
|
if (next) this.#refresh();
|
|
463
|
-
else this.#clearHostState();
|
|
464
700
|
return;
|
|
465
701
|
}
|
|
466
702
|
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
@@ -475,7 +711,6 @@ var ScrollAreaController = class extends Controller {
|
|
|
475
711
|
this.#bindContentObserver(next);
|
|
476
712
|
this.#refresh();
|
|
477
713
|
}
|
|
478
|
-
/** Observes structural and attribute changes that can alter overflow or Tab stops. */
|
|
479
714
|
#bindContentObserver(viewport) {
|
|
480
715
|
this.#content = new MutationObserver((records) => {
|
|
481
716
|
if (this.#viewport !== viewport || !this.#hasRelevantContentMutation(records)) return;
|
|
@@ -488,7 +723,6 @@ var ScrollAreaController = class extends Controller {
|
|
|
488
723
|
characterData: true
|
|
489
724
|
});
|
|
490
725
|
}
|
|
491
|
-
/** Ignores only state-hook mutations whose exact value this controller just wrote. */
|
|
492
726
|
#hasRelevantContentMutation(records) {
|
|
493
727
|
let relevant = false;
|
|
494
728
|
const consumed = /* @__PURE__ */ new Set();
|
|
@@ -503,7 +737,6 @@ var ScrollAreaController = class extends Controller {
|
|
|
503
737
|
for (const attribute of consumed) this.#ownedHostMutations.delete(attribute);
|
|
504
738
|
return relevant;
|
|
505
739
|
}
|
|
506
|
-
/** Releases every resource and borrowed attribute owned by one former viewport. */
|
|
507
740
|
#unbindViewport(viewport) {
|
|
508
741
|
this.#frames.cancel();
|
|
509
742
|
viewport.removeEventListener("scroll", this.#onScroll);
|
|
@@ -518,11 +751,6 @@ var ScrollAreaController = class extends Controller {
|
|
|
518
751
|
this.#overflowing = false;
|
|
519
752
|
this.#lastEdge = null;
|
|
520
753
|
}
|
|
521
|
-
/**
|
|
522
|
-
* Runs the full structural and positional measurement pass.
|
|
523
|
-
*
|
|
524
|
-
* @stimeoRenderRoot
|
|
525
|
-
*/
|
|
526
754
|
#refresh() {
|
|
527
755
|
const viewport = this.#viewport;
|
|
528
756
|
if (!viewport) return;
|
|
@@ -532,13 +760,11 @@ var ScrollAreaController = class extends Controller {
|
|
|
532
760
|
this.#syncKeyboardReach(viewport, this.#overflowing);
|
|
533
761
|
this.#syncPosition(viewport, this.#overflowing);
|
|
534
762
|
}
|
|
535
|
-
/** Measures overflow and reflects the state hook without identical DOM writes. */
|
|
536
763
|
#syncOverflow(viewport) {
|
|
537
764
|
const overflowing = this.#measureOverflow(viewport);
|
|
538
765
|
this.#writeHostAttribute(this.#overflowState, "data-overflow", overflowing ? "true" : "false");
|
|
539
766
|
return overflowing;
|
|
540
767
|
}
|
|
541
|
-
/** Measures the primary-axis position and dispatches a newly established edge. */
|
|
542
768
|
#syncPosition(viewport, overflowing) {
|
|
543
769
|
const { position, progress } = this.#measurePosition(viewport);
|
|
544
770
|
this.#writeHostAttribute(this.#scrollState, "data-scroll", position);
|
|
@@ -548,41 +774,40 @@ var ScrollAreaController = class extends Controller {
|
|
|
548
774
|
this.#lastEdge = edge;
|
|
549
775
|
if (edge) this.dispatch("reach", { detail: { edge } });
|
|
550
776
|
}
|
|
551
|
-
/** Writes one leased host attribute and records self-generated observer input. */
|
|
552
777
|
#writeHostAttribute(lease, attribute, value) {
|
|
778
|
+
const record = `data-${this.identifier}-${attribute}-lease`;
|
|
553
779
|
const before = this.element.getAttribute(attribute);
|
|
780
|
+
const recordBefore = this.element.getAttribute(record);
|
|
554
781
|
lease.write(this.element, value);
|
|
555
782
|
this.#recordOwnedHostMutation(attribute, before);
|
|
783
|
+
this.#recordOwnedHostMutation(record, recordBefore);
|
|
556
784
|
}
|
|
557
|
-
/** Writes the leased progress property and records its serialized style mutation. */
|
|
558
785
|
#writeHostProgress(value) {
|
|
786
|
+
const record = `data-${this.identifier}-style---stimeo--scroll-progress-lease`;
|
|
559
787
|
const before = this.element.getAttribute("style");
|
|
788
|
+
const recordBefore = this.element.getAttribute(record);
|
|
560
789
|
this.#progress.write(this.element, value);
|
|
561
790
|
this.#recordOwnedHostMutation("style", before);
|
|
791
|
+
this.#recordOwnedHostMutation(record, recordBefore);
|
|
562
792
|
}
|
|
563
|
-
/** Records an exact host mutation only when the host is also the observed viewport. */
|
|
564
793
|
#recordOwnedHostMutation(attribute, before) {
|
|
565
794
|
if (this.#viewport !== this.element) return;
|
|
566
795
|
const after = this.element.getAttribute(attribute);
|
|
567
796
|
if (after !== before) this.#ownedHostMutations.set(attribute, after);
|
|
568
797
|
}
|
|
569
|
-
/** Returns host hooks to authored values and clears edge bookkeeping. */
|
|
570
798
|
#clearHostState() {
|
|
571
799
|
this.#overflowState.returnAll();
|
|
572
800
|
this.#scrollState.returnAll();
|
|
573
801
|
this.#progress.returnAll();
|
|
574
|
-
this.#ownedHostMutations.clear();
|
|
575
802
|
this.#overflowing = false;
|
|
576
803
|
this.#lastEdge = null;
|
|
577
804
|
}
|
|
578
|
-
/** Whether the configured axis currently has a scroll range. */
|
|
579
805
|
#measureOverflow(viewport) {
|
|
580
806
|
const orientation = this.orientationValue;
|
|
581
807
|
const vertical = orientation !== "horizontal" && viewport.scrollHeight > viewport.clientHeight + EDGE_EPSILON;
|
|
582
808
|
const horizontal = orientation !== "vertical" && viewport.scrollWidth > viewport.clientWidth + EDGE_EPSILON;
|
|
583
809
|
return vertical || horizontal;
|
|
584
810
|
}
|
|
585
|
-
/** Reports the primary-axis position bucket and normalized 0–1 progress. */
|
|
586
811
|
#measurePosition(viewport) {
|
|
587
812
|
const horizontalPrimary = this.orientationValue === "horizontal" || this.orientationValue === "both" && viewport.scrollHeight <= viewport.clientHeight + EDGE_EPSILON;
|
|
588
813
|
const { position: scrollPosition, max: maxScroll } = logicalScrollMetrics(
|
|
@@ -595,7 +820,6 @@ var ScrollAreaController = class extends Controller {
|
|
|
595
820
|
if (scrollPosition >= maxScroll - EDGE_EPSILON) return { position: "end", progress };
|
|
596
821
|
return { position: "middle", progress };
|
|
597
822
|
}
|
|
598
|
-
/** Borrows keyboard reach only when no usable descendant already owns a Tab stop. */
|
|
599
823
|
#syncKeyboardReach(viewport, overflowing) {
|
|
600
824
|
const wantsTabindex = overflowing && !hasTabStop(viewport);
|
|
601
825
|
if (!wantsTabindex) {
|
|
@@ -606,15 +830,17 @@ var ScrollAreaController = class extends Controller {
|
|
|
606
830
|
if (this.#hasAccessibleName(viewport)) {
|
|
607
831
|
if (!viewport.hasAttribute("role")) this.#role.write(viewport, "region");
|
|
608
832
|
} else {
|
|
609
|
-
this.#
|
|
833
|
+
this.#returnRole(viewport);
|
|
610
834
|
}
|
|
611
835
|
}
|
|
612
|
-
/** Returns only the viewport attributes borrowed by this controller. */
|
|
613
836
|
#clearViewportAttributes(viewport) {
|
|
614
837
|
this.#tabindex.returnAll();
|
|
838
|
+
this.#tabindex.reclaim(viewport);
|
|
839
|
+
this.#returnRole(viewport);
|
|
840
|
+
}
|
|
841
|
+
#returnRole(viewport) {
|
|
615
842
|
this.#role.return(viewport);
|
|
616
843
|
}
|
|
617
|
-
/** Whether the viewport already has a non-empty accessible name. */
|
|
618
844
|
#hasAccessibleName(viewport) {
|
|
619
845
|
const sources = this.#resolveNameSources(viewport);
|
|
620
846
|
if (sources.length > 0) {
|
|
@@ -625,21 +851,15 @@ var ScrollAreaController = class extends Controller {
|
|
|
625
851
|
}
|
|
626
852
|
return Boolean(viewport.getAttribute("aria-label")?.trim());
|
|
627
853
|
}
|
|
628
|
-
/** Resolves unique, live `aria-labelledby` references in authored order. */
|
|
629
854
|
#resolveNameSources(viewport) {
|
|
630
855
|
const ids = this.#nameReferenceIds(viewport);
|
|
631
856
|
const sources = [];
|
|
632
|
-
const seen = /* @__PURE__ */ new Set();
|
|
633
857
|
for (const id of ids) {
|
|
634
858
|
const source = viewport.ownerDocument.getElementById(id);
|
|
635
|
-
if (source
|
|
636
|
-
seen.add(source);
|
|
637
|
-
sources.push(source);
|
|
638
|
-
}
|
|
859
|
+
if (source) sources.push(source);
|
|
639
860
|
}
|
|
640
861
|
return sources;
|
|
641
862
|
}
|
|
642
|
-
/** Observes external name sources so a retained region never becomes unnamed. */
|
|
643
863
|
#syncNameSources(viewport) {
|
|
644
864
|
const ids = this.#nameReferenceIds(viewport);
|
|
645
865
|
const sources = this.#resolveNameSources(viewport).filter(
|
|
@@ -681,12 +901,10 @@ var ScrollAreaController = class extends Controller {
|
|
|
681
901
|
});
|
|
682
902
|
}
|
|
683
903
|
}
|
|
684
|
-
/** Returns normalized unique label-reference ids in authored order. */
|
|
685
904
|
#nameReferenceIds(viewport) {
|
|
686
905
|
const tokens = viewport.getAttribute("aria-labelledby")?.trim().split(/\s+/).filter(Boolean);
|
|
687
906
|
return Array.from(new Set(tokens));
|
|
688
907
|
}
|
|
689
|
-
/** Whether document-graph changes can resolve or invalidate a referenced id. */
|
|
690
908
|
#hasRelevantNameGraphMutation(records) {
|
|
691
909
|
for (const record of records) {
|
|
692
910
|
if (record.type === "attributes") {
|
|
@@ -702,7 +920,6 @@ var ScrollAreaController = class extends Controller {
|
|
|
702
920
|
}
|
|
703
921
|
return false;
|
|
704
922
|
}
|
|
705
|
-
/** Whether a changed subtree contains one of the current label-reference ids. */
|
|
706
923
|
#nodeContainsObservedNameId(node) {
|
|
707
924
|
if (!(node instanceof Element)) return false;
|
|
708
925
|
if (this.#observedNameIds.has(node.id)) return true;
|
|
@@ -710,7 +927,6 @@ var ScrollAreaController = class extends Controller {
|
|
|
710
927
|
(element) => this.#observedNameIds.has(element.id)
|
|
711
928
|
);
|
|
712
929
|
}
|
|
713
|
-
/** Stops observing accessible-name sources. */
|
|
714
930
|
#disconnectNameSources() {
|
|
715
931
|
this.#nameSources?.disconnect();
|
|
716
932
|
this.#nameSources = null;
|
|
@@ -719,32 +935,19 @@ var ScrollAreaController = class extends Controller {
|
|
|
719
935
|
this.#observedNameIds.clear();
|
|
720
936
|
this.#observedNameSources = [];
|
|
721
937
|
}
|
|
722
|
-
/** Subscribes to font completion when the current engine exposes `document.fonts`. */
|
|
723
938
|
#bindFonts() {
|
|
724
939
|
const fonts = this.element.ownerDocument.fonts;
|
|
725
940
|
if (!fonts || this.#fonts === fonts) return;
|
|
726
|
-
this.#unbindFonts();
|
|
727
941
|
this.#fonts = fonts;
|
|
728
942
|
fonts.addEventListener("loadingdone", this.#onFontsSettled);
|
|
729
943
|
fonts.addEventListener("loadingerror", this.#onFontsSettled);
|
|
730
944
|
}
|
|
731
|
-
/** Removes the font completion subscriptions. */
|
|
732
945
|
#unbindFonts() {
|
|
733
946
|
this.#fonts?.removeEventListener("loadingdone", this.#onFontsSettled);
|
|
734
947
|
this.#fonts?.removeEventListener("loadingerror", this.#onFontsSettled);
|
|
735
948
|
this.#fonts = null;
|
|
736
949
|
}
|
|
737
|
-
/** Suspends live resources and returns all derived state before snapshotting. */
|
|
738
|
-
#rewindForCache() {
|
|
739
|
-
this.#rebind.cancel();
|
|
740
|
-
this.#frames.cancel();
|
|
741
|
-
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
742
|
-
this.#layout.disconnect();
|
|
743
|
-
this.#unbindFonts();
|
|
744
|
-
this.#clearHostState();
|
|
745
|
-
}
|
|
746
950
|
};
|
|
747
951
|
|
|
748
952
|
export { ScrollAreaController };
|
|
749
|
-
//# sourceMappingURL=scroll_area_controller.js.map
|
|
750
953
|
//# sourceMappingURL=scroll_area_controller.js.map
|