stimeo-ui 0.5.0 → 0.7.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.
- package/CHANGELOG.md +178 -0
- package/dist/controllers/alert_dialog_controller.d.ts +2 -0
- package/dist/controllers/alert_dialog_controller.js +75 -4
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.d.ts +1 -3
- package/dist/controllers/aspect_ratio_controller.js +19 -11
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +2 -0
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.d.ts +36 -15
- package/dist/controllers/avatar_controller.js +237 -40
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +2 -0
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +2 -0
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +2 -0
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +12 -3
- package/dist/controllers/carousel_controller.js +85 -9
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +52 -20
- package/dist/controllers/character_counter_controller.js +338 -63
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +34 -5
- package/dist/controllers/checkbox_controller.js +136 -25
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +10 -1
- package/dist/controllers/color_picker_controller.js +35 -9
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +2 -0
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +2 -0
- package/dist/controllers/command_palette_controller.js +75 -4
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +42 -14
- package/dist/controllers/conditional_fields_controller.js +345 -51
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +2 -0
- package/dist/controllers/confirm_controller.js +75 -4
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +2 -0
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +4 -0
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +2 -0
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +3 -0
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +23 -4
- package/dist/controllers/date_range_picker_controller.js +157 -30
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +75 -4
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +56 -24
- package/dist/controllers/direct_upload_controller.js +201 -45
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +14 -6
- package/dist/controllers/dirty_form_controller.js +192 -29
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +2 -0
- package/dist/controllers/dismissible_controller.js +83 -18
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js +75 -4
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +2 -0
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +9 -1
- package/dist/controllers/file_dropzone_controller.js +26 -3
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +2 -0
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +4 -0
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +4 -3
- package/dist/controllers/focus_controller.js +75 -4
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.d.ts +50 -10
- package/dist/controllers/form_field_controller.js +280 -62
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.d.ts +10 -8
- package/dist/controllers/form_validation_controller.js +208 -83
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +2 -0
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +2 -0
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +2 -2
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +5 -3
- package/dist/controllers/idle_controller.js +27 -5
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +2 -0
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +2 -0
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +2 -0
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +2 -0
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +2 -0
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +5 -3
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +2 -0
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +48 -12
- package/dist/controllers/multi_select_controller.js +460 -151
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +2 -0
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +2 -0
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +26 -6
- package/dist/controllers/number_input_controller.js +317 -51
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +3 -0
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +2 -0
- package/dist/controllers/overflow_menu_controller.js +6 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +2 -0
- package/dist/controllers/pagination_controller.js +35 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +2 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +5 -3
- package/dist/controllers/password_strength_controller.js +20 -2
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +33 -19
- package/dist/controllers/persist_controller.js +449 -120
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +3 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +2 -2
- package/dist/controllers/popover_controller.js +77 -4
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +3 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +2 -0
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +3 -0
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +44 -15
- package/dist/controllers/radio_group_controller.js +540 -56
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +38 -31
- package/dist/controllers/rating_controller.js +276 -89
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +2 -0
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +2 -0
- package/dist/controllers/resizable_controller.js +33 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +6 -0
- package/dist/controllers/roving_controller.js +60 -5
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +25 -11
- package/dist/controllers/scroll_area_controller.js +557 -125
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
- package/dist/controllers/scroll_visibility_controller.js +33 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +2 -0
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +54 -11
- package/dist/controllers/separator_controller.js +354 -38
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.js +83 -10
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -0
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +2 -0
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +2 -0
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +2 -0
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +2 -0
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +94 -38
- package/dist/controllers/submit_once_controller.js +399 -121
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +2 -0
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +43 -11
- package/dist/controllers/tags_input_controller.js +356 -120
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +2 -0
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +2 -0
- package/dist/controllers/theme_controller.js +8 -6
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +44 -13
- package/dist/controllers/time_picker_controller.js +296 -107
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +4 -0
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +41 -13
- package/dist/controllers/toggle_group_controller.js +378 -55
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +5 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +2 -2
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +4 -3
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +2 -0
- package/dist/controllers/tree_view_controller.js +7 -4
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +4963 -1581
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +123 -6
- package/dist/inspector/cli.js +212 -18
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +274 -50
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +15 -15
- package/dist/inspector/manifest.json +602 -80
- package/dist/positioning/index.d.ts +4 -2
- package/dist/positioning/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,154 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/scroll_area_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/attribute_lease.ts
|
|
6
|
+
var AttributeLease = class {
|
|
7
|
+
#attribute;
|
|
8
|
+
#records = /* @__PURE__ */ new Map();
|
|
9
|
+
/** @param attribute - The attribute whose temporary values this lease owns. */
|
|
10
|
+
constructor(attribute) {
|
|
11
|
+
this.#attribute = attribute;
|
|
12
|
+
}
|
|
13
|
+
/** Writes or removes the leased attribute while preserving its authored value. */
|
|
14
|
+
write(element, value) {
|
|
15
|
+
const existing = this.#records.get(element);
|
|
16
|
+
if (existing) {
|
|
17
|
+
existing.written = value;
|
|
18
|
+
} else {
|
|
19
|
+
this.#records.set(element, {
|
|
20
|
+
original: element.getAttribute(this.#attribute),
|
|
21
|
+
written: value
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
this.#reflect(element, value);
|
|
25
|
+
}
|
|
26
|
+
/** Returns one lease without overwriting a value subsequently authored by a consumer. */
|
|
27
|
+
return(element) {
|
|
28
|
+
const record = this.#records.get(element);
|
|
29
|
+
if (!record) return;
|
|
30
|
+
this.#records.delete(element);
|
|
31
|
+
const stillOwned = element.getAttribute(this.#attribute) === record.written;
|
|
32
|
+
if (stillOwned) this.#reflect(element, record.original);
|
|
33
|
+
}
|
|
34
|
+
/** Reflects only a real value transition, avoiding self-triggered mutation work. */
|
|
35
|
+
#reflect(element, value) {
|
|
36
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
37
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
38
|
+
else element.setAttribute(this.#attribute, value);
|
|
39
|
+
}
|
|
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
|
+
};
|
|
45
|
+
|
|
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
|
+
function inheritsFieldsetDisabled(control) {
|
|
77
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
78
|
+
while (fieldset) {
|
|
79
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
80
|
+
if (!legend?.contains(control)) return true;
|
|
81
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
function canTakeFocus(element) {
|
|
86
|
+
if (element.closest("[hidden], [inert]")) return false;
|
|
87
|
+
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
88
|
+
if (!("disabled" in element)) return true;
|
|
89
|
+
if (element.disabled) return false;
|
|
90
|
+
return !inheritsFieldsetDisabled(element);
|
|
91
|
+
}
|
|
92
|
+
var TAB_STOP_CANDIDATE_SELECTOR = [
|
|
93
|
+
"a[href]",
|
|
94
|
+
"area[href]",
|
|
95
|
+
"button",
|
|
96
|
+
"input",
|
|
97
|
+
"select",
|
|
98
|
+
"textarea",
|
|
99
|
+
"summary",
|
|
100
|
+
"iframe",
|
|
101
|
+
"audio[controls]",
|
|
102
|
+
"video[controls]",
|
|
103
|
+
"[tabindex]",
|
|
104
|
+
"[contenteditable]"
|
|
105
|
+
].join(",");
|
|
106
|
+
function isRenderedForFocus(element) {
|
|
107
|
+
const check = element.checkVisibility;
|
|
108
|
+
return typeof check === "function" ? check.call(element, { visibilityProperty: true }) : true;
|
|
109
|
+
}
|
|
110
|
+
function authoredTabindex(element) {
|
|
111
|
+
const value = element.getAttribute("tabindex");
|
|
112
|
+
if (value === null || !/^[+-]?\d+$/.test(value.trim())) return null;
|
|
113
|
+
return Number(value);
|
|
114
|
+
}
|
|
115
|
+
function hasNativeTabStop(element) {
|
|
116
|
+
if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {
|
|
117
|
+
return element.hasAttribute("href");
|
|
118
|
+
}
|
|
119
|
+
if (element instanceof HTMLButtonElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (element instanceof HTMLInputElement) return element.type !== "hidden";
|
|
123
|
+
if (element instanceof HTMLIFrameElement) return true;
|
|
124
|
+
if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
|
|
125
|
+
return element.hasAttribute("controls");
|
|
126
|
+
}
|
|
127
|
+
if (element instanceof HTMLElement && element.tagName === "SUMMARY") {
|
|
128
|
+
const details = element.parentElement;
|
|
129
|
+
return details instanceof HTMLDetailsElement && Array.from(details.children).find((child) => child.tagName === "SUMMARY") === element;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
function hasEditableTabStop(element) {
|
|
134
|
+
const value = element.getAttribute("contenteditable")?.toLowerCase();
|
|
135
|
+
return value === "" || value === "true" || value === "plaintext-only";
|
|
136
|
+
}
|
|
137
|
+
function isTabStop(element) {
|
|
138
|
+
if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;
|
|
139
|
+
const tabindex = authoredTabindex(element);
|
|
140
|
+
if (tabindex !== null) return tabindex >= 0;
|
|
141
|
+
return hasNativeTabStop(element) || hasEditableTabStop(element);
|
|
142
|
+
}
|
|
143
|
+
function firstTabStop(root) {
|
|
144
|
+
for (const candidate of root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)) {
|
|
145
|
+
if (isTabStop(candidate)) return candidate;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
function hasTabStop(root) {
|
|
150
|
+
return firstTabStop(root) !== null;
|
|
151
|
+
}
|
|
152
|
+
|
|
5
153
|
// src/utils/layout_observer.ts
|
|
6
154
|
var LayoutObserver = class {
|
|
7
155
|
#callback;
|
|
@@ -72,10 +220,97 @@ function logicalScrollMetrics(element, horizontal) {
|
|
|
72
220
|
return { position: Math.min(max, Math.max(0, position)), max };
|
|
73
221
|
}
|
|
74
222
|
|
|
223
|
+
// src/utils/microtask_coalescer.ts
|
|
224
|
+
var MicrotaskCoalescer = class {
|
|
225
|
+
#run;
|
|
226
|
+
#queued = false;
|
|
227
|
+
#active = false;
|
|
228
|
+
#generation = 0;
|
|
229
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
230
|
+
constructor(run) {
|
|
231
|
+
this.#run = run;
|
|
232
|
+
}
|
|
233
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
234
|
+
activate() {
|
|
235
|
+
this.#active = true;
|
|
236
|
+
}
|
|
237
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
238
|
+
cancel() {
|
|
239
|
+
this.#active = false;
|
|
240
|
+
this.#queued = false;
|
|
241
|
+
this.#generation += 1;
|
|
242
|
+
}
|
|
243
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
244
|
+
schedule() {
|
|
245
|
+
if (!this.#active || this.#queued) return;
|
|
246
|
+
this.#queued = true;
|
|
247
|
+
const generation = this.#generation;
|
|
248
|
+
queueMicrotask(() => {
|
|
249
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
250
|
+
this.#queued = false;
|
|
251
|
+
this.#run();
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// src/utils/style_property_lease.ts
|
|
257
|
+
var StylePropertyLease = class {
|
|
258
|
+
#property;
|
|
259
|
+
#records = /* @__PURE__ */ new Map();
|
|
260
|
+
/** @param property - The CSS property whose temporary values this lease owns. */
|
|
261
|
+
constructor(property) {
|
|
262
|
+
this.#property = property;
|
|
263
|
+
}
|
|
264
|
+
/** Writes or removes the leased declaration while preserving its authored value. */
|
|
265
|
+
write(element, value, priority = "") {
|
|
266
|
+
const existing = this.#records.get(element);
|
|
267
|
+
if (existing) {
|
|
268
|
+
existing.writtenValue = value;
|
|
269
|
+
existing.writtenPriority = value === null ? "" : priority;
|
|
270
|
+
} else {
|
|
271
|
+
this.#records.set(element, {
|
|
272
|
+
originalValue: element.style.getPropertyValue(this.#property),
|
|
273
|
+
originalPriority: element.style.getPropertyPriority(this.#property),
|
|
274
|
+
writtenValue: value,
|
|
275
|
+
writtenPriority: value === null ? "" : priority
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
this.#reflect(element, value, priority);
|
|
279
|
+
}
|
|
280
|
+
/** Returns one lease without overwriting a later consumer declaration. */
|
|
281
|
+
return(element) {
|
|
282
|
+
const record = this.#records.get(element);
|
|
283
|
+
if (!record) return;
|
|
284
|
+
this.#records.delete(element);
|
|
285
|
+
const style = element.style;
|
|
286
|
+
const stillOwned = style.getPropertyValue(this.#property) === (record.writtenValue ?? "") && style.getPropertyPriority(this.#property) === record.writtenPriority;
|
|
287
|
+
if (stillOwned) {
|
|
288
|
+
this.#reflect(element, record.originalValue, record.originalPriority);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** Returns every outstanding declaration lease. */
|
|
292
|
+
returnAll() {
|
|
293
|
+
for (const element of Array.from(this.#records.keys())) this.return(element);
|
|
294
|
+
}
|
|
295
|
+
/** Reflects only a real declaration transition. */
|
|
296
|
+
#reflect(element, value, priority) {
|
|
297
|
+
const style = element.style;
|
|
298
|
+
const nextValue = value ?? "";
|
|
299
|
+
const nextPriority = value === null ? "" : priority;
|
|
300
|
+
if (style.getPropertyValue(this.#property) === nextValue && style.getPropertyPriority(this.#property) === nextPriority) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (value === null) style.removeProperty(this.#property);
|
|
304
|
+
else style.setProperty(this.#property, value, priority);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
75
308
|
// src/utils/tabindex_loan.ts
|
|
76
309
|
var TabindexLoan = class {
|
|
77
310
|
#value;
|
|
78
311
|
#lent = /* @__PURE__ */ new Set();
|
|
312
|
+
/** Returns live loans before Turbo can copy them into its page snapshot. */
|
|
313
|
+
#beforeCache = new BeforeCacheReset(() => this.returnAll());
|
|
79
314
|
/**
|
|
80
315
|
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
81
316
|
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
@@ -89,6 +324,7 @@ var TabindexLoan = class {
|
|
|
89
324
|
if (element.hasAttribute("tabindex")) return;
|
|
90
325
|
element.setAttribute("tabindex", this.#value);
|
|
91
326
|
this.#lent.add(element);
|
|
327
|
+
this.#beforeCache.activate();
|
|
92
328
|
}
|
|
93
329
|
/** Takes back every loan whose value is still the one that was lent. */
|
|
94
330
|
returnAll() {
|
|
@@ -96,19 +332,11 @@ var TabindexLoan = class {
|
|
|
96
332
|
if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
|
|
97
333
|
}
|
|
98
334
|
this.#lent.clear();
|
|
335
|
+
this.#beforeCache.deactivate();
|
|
99
336
|
}
|
|
100
337
|
};
|
|
101
338
|
|
|
102
339
|
// src/controllers/scroll_area_controller.ts
|
|
103
|
-
var FOCUSABLE_SELECTOR = [
|
|
104
|
-
"a[href]",
|
|
105
|
-
"button:not([disabled])",
|
|
106
|
-
"input:not([disabled])",
|
|
107
|
-
"select:not([disabled])",
|
|
108
|
-
"textarea:not([disabled])",
|
|
109
|
-
"[tabindex]:not([tabindex='-1'])",
|
|
110
|
-
"[contenteditable='true']"
|
|
111
|
-
].join(",");
|
|
112
340
|
var EDGE_EPSILON = 1;
|
|
113
341
|
var ScrollAreaController = class extends Controller {
|
|
114
342
|
static targets = ["viewport"];
|
|
@@ -116,152 +344,356 @@ var ScrollAreaController = class extends Controller {
|
|
|
116
344
|
orientation: { type: String, default: "vertical" }
|
|
117
345
|
};
|
|
118
346
|
static events = ["reach"];
|
|
119
|
-
#layout = new LayoutObserver(() => this.#
|
|
120
|
-
|
|
347
|
+
#layout = new LayoutObserver(() => this.#refresh());
|
|
348
|
+
#rebind = new MicrotaskCoalescer(() => this.#syncViewport());
|
|
349
|
+
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
350
|
+
#tabindex = new TabindexLoan("0");
|
|
351
|
+
#role = new AttributeLease("role");
|
|
352
|
+
#overflowState = new AttributeLease("data-overflow");
|
|
353
|
+
#scrollState = new AttributeLease("data-scroll");
|
|
354
|
+
#progress = new StylePropertyLease("--stimeo--scroll-progress");
|
|
355
|
+
#viewport = null;
|
|
121
356
|
#content = null;
|
|
122
|
-
|
|
357
|
+
#nameSources = null;
|
|
358
|
+
#nameGraph = null;
|
|
359
|
+
#observedNameIds = /* @__PURE__ */ new Set();
|
|
360
|
+
#observedNameSources = [];
|
|
361
|
+
#fonts = null;
|
|
362
|
+
#scrollFrame = null;
|
|
363
|
+
#overflowing = false;
|
|
123
364
|
#lastEdge = null;
|
|
124
|
-
|
|
125
|
-
#tabindex = new TabindexLoan("0");
|
|
126
|
-
/** Whether this controller added `role="region"`, for symmetric teardown. */
|
|
127
|
-
#addedRole = false;
|
|
365
|
+
#ownedHostMutations = /* @__PURE__ */ new Map();
|
|
128
366
|
#onScroll = () => {
|
|
129
|
-
this.#
|
|
367
|
+
if (this.#scrollFrame !== null) return;
|
|
368
|
+
this.#scrollFrame = requestAnimationFrame(() => {
|
|
369
|
+
this.#scrollFrame = null;
|
|
370
|
+
const viewport = this.#viewport;
|
|
371
|
+
if (viewport) this.#syncPosition(viewport, this.#overflowing);
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
#onLoad = () => {
|
|
375
|
+
this.#refresh();
|
|
376
|
+
};
|
|
377
|
+
#onFontsSettled = () => {
|
|
378
|
+
this.#refresh();
|
|
130
379
|
};
|
|
131
380
|
connect() {
|
|
132
|
-
|
|
133
|
-
this.
|
|
134
|
-
this.#layout.observe(this.viewportTarget);
|
|
381
|
+
this.#beforeCache.activate();
|
|
382
|
+
this.#rebind.activate();
|
|
135
383
|
this.#layout.observeViewport();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!this.hasViewportTarget) return;
|
|
139
|
-
const vp = this.viewportTarget;
|
|
140
|
-
this.#syncKeyboardReach(vp, this.#syncOverflow(vp));
|
|
141
|
-
});
|
|
142
|
-
this.#content.observe(this.viewportTarget, {
|
|
143
|
-
subtree: true,
|
|
144
|
-
childList: true,
|
|
145
|
-
attributes: true
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
this.#update();
|
|
384
|
+
this.#bindFonts();
|
|
385
|
+
this.#syncViewport();
|
|
149
386
|
}
|
|
150
387
|
disconnect() {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
388
|
+
this.#beforeCache.deactivate();
|
|
389
|
+
this.#rebind.cancel();
|
|
390
|
+
this.#cancelScrollFrame();
|
|
391
|
+
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
155
392
|
this.#layout.disconnect();
|
|
393
|
+
this.#unbindFonts();
|
|
394
|
+
this.#clearHostState();
|
|
395
|
+
}
|
|
396
|
+
/** Re-measures when a retained viewport's orientation Value is morphed. */
|
|
397
|
+
orientationValueChanged() {
|
|
398
|
+
this.#refresh();
|
|
399
|
+
}
|
|
400
|
+
/** Schedules a complete resource rebind for a runtime viewport target. */
|
|
401
|
+
viewportTargetConnected() {
|
|
402
|
+
this.#rebind.schedule();
|
|
403
|
+
}
|
|
404
|
+
/** Schedules cleanup or replacement binding after a viewport leaves. */
|
|
405
|
+
viewportTargetDisconnected() {
|
|
406
|
+
this.#rebind.schedule();
|
|
407
|
+
}
|
|
408
|
+
/** Rebinds every viewport-owned resource against the final target in the mutation batch. */
|
|
409
|
+
#syncViewport() {
|
|
410
|
+
const next = this.hasViewportTarget ? this.viewportTarget : null;
|
|
411
|
+
if (next === this.#viewport) {
|
|
412
|
+
if (next) this.#refresh();
|
|
413
|
+
else this.#clearHostState();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
417
|
+
this.#viewport = next;
|
|
418
|
+
if (!next) {
|
|
419
|
+
this.#clearHostState();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
next.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
423
|
+
next.addEventListener("load", this.#onLoad, true);
|
|
424
|
+
this.#layout.observe(next);
|
|
425
|
+
this.#bindContentObserver(next);
|
|
426
|
+
this.#refresh();
|
|
427
|
+
}
|
|
428
|
+
/** Observes structural and attribute changes that can alter overflow or Tab stops. */
|
|
429
|
+
#bindContentObserver(viewport) {
|
|
430
|
+
this.#content = new MutationObserver((records) => {
|
|
431
|
+
if (this.#viewport !== viewport || !this.#hasRelevantContentMutation(records)) return;
|
|
432
|
+
this.#refresh();
|
|
433
|
+
});
|
|
434
|
+
this.#content.observe(viewport, {
|
|
435
|
+
subtree: true,
|
|
436
|
+
childList: true,
|
|
437
|
+
attributes: true,
|
|
438
|
+
characterData: true
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/** Ignores only state-hook mutations whose exact value this controller just wrote. */
|
|
442
|
+
#hasRelevantContentMutation(records) {
|
|
443
|
+
let relevant = false;
|
|
444
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
445
|
+
for (const record of records) {
|
|
446
|
+
const attribute = record.attributeName;
|
|
447
|
+
if (record.type === "attributes" && record.target === this.element && attribute !== null && this.#ownedHostMutations.has(attribute) && this.element.getAttribute(attribute) === this.#ownedHostMutations.get(attribute)) {
|
|
448
|
+
consumed.add(attribute);
|
|
449
|
+
} else {
|
|
450
|
+
relevant = true;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
for (const attribute of consumed) this.#ownedHostMutations.delete(attribute);
|
|
454
|
+
return relevant;
|
|
455
|
+
}
|
|
456
|
+
/** Releases every resource and borrowed attribute owned by one former viewport. */
|
|
457
|
+
#unbindViewport(viewport) {
|
|
458
|
+
this.#cancelScrollFrame();
|
|
459
|
+
viewport.removeEventListener("scroll", this.#onScroll);
|
|
460
|
+
viewport.removeEventListener("load", this.#onLoad, true);
|
|
461
|
+
this.#layout.unobserve(viewport);
|
|
156
462
|
this.#content?.disconnect();
|
|
157
463
|
this.#content = null;
|
|
464
|
+
this.#disconnectNameSources();
|
|
465
|
+
this.#clearViewportAttributes(viewport);
|
|
466
|
+
this.#ownedHostMutations.clear();
|
|
467
|
+
this.#viewport = null;
|
|
468
|
+
this.#overflowing = false;
|
|
158
469
|
this.#lastEdge = null;
|
|
159
470
|
}
|
|
160
|
-
/**
|
|
161
|
-
#
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.#
|
|
166
|
-
|
|
167
|
-
this
|
|
168
|
-
this
|
|
169
|
-
const edge = position === "start" ? "start" : position === "end" ? "end" : null;
|
|
170
|
-
if (overflowing && edge && edge !== this.#lastEdge) {
|
|
171
|
-
this.#lastEdge = edge;
|
|
172
|
-
this.dispatch("reach", { detail: { edge } });
|
|
173
|
-
} else if (!edge) {
|
|
174
|
-
this.#lastEdge = null;
|
|
175
|
-
}
|
|
471
|
+
/** Runs the full structural and positional measurement pass. */
|
|
472
|
+
#refresh() {
|
|
473
|
+
const viewport = this.#viewport;
|
|
474
|
+
if (!viewport) return;
|
|
475
|
+
this.#cancelScrollFrame();
|
|
476
|
+
this.#syncNameSources(viewport);
|
|
477
|
+
this.#overflowing = this.#syncOverflow(viewport);
|
|
478
|
+
this.#syncKeyboardReach(viewport, this.#overflowing);
|
|
479
|
+
this.#syncPosition(viewport, this.#overflowing);
|
|
176
480
|
}
|
|
177
|
-
/**
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
* The write is skipped when the value is unchanged. An identical `setAttribute` still
|
|
182
|
-
* queues a MutationRecord, and markup that puts the viewport target on the controller
|
|
183
|
-
* element itself would then have the content observer trigger its own next callback.
|
|
184
|
-
*/
|
|
185
|
-
#syncOverflow(vp) {
|
|
186
|
-
const overflowing = this.#measureOverflow(vp);
|
|
187
|
-
const next = overflowing ? "true" : "false";
|
|
188
|
-
if (this.element.getAttribute("data-overflow") !== next) {
|
|
189
|
-
this.element.setAttribute("data-overflow", next);
|
|
190
|
-
}
|
|
481
|
+
/** Measures overflow and reflects the state hook without identical DOM writes. */
|
|
482
|
+
#syncOverflow(viewport) {
|
|
483
|
+
const overflowing = this.#measureOverflow(viewport);
|
|
484
|
+
this.#writeHostAttribute(this.#overflowState, "data-overflow", overflowing ? "true" : "false");
|
|
191
485
|
return overflowing;
|
|
192
486
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
487
|
+
/** Measures the primary-axis position and dispatches a newly established edge. */
|
|
488
|
+
#syncPosition(viewport, overflowing) {
|
|
489
|
+
const { position, progress } = this.#measurePosition(viewport);
|
|
490
|
+
this.#writeHostAttribute(this.#scrollState, "data-scroll", position);
|
|
491
|
+
this.#writeHostProgress(String(progress));
|
|
492
|
+
const edge = overflowing && (position === "start" || position === "end") ? position : null;
|
|
493
|
+
if (edge === this.#lastEdge) return;
|
|
494
|
+
this.#lastEdge = edge;
|
|
495
|
+
if (edge) this.dispatch("reach", { detail: { edge } });
|
|
496
|
+
}
|
|
497
|
+
/** Writes one leased host attribute and records self-generated observer input. */
|
|
498
|
+
#writeHostAttribute(lease, attribute, value) {
|
|
499
|
+
const before = this.element.getAttribute(attribute);
|
|
500
|
+
lease.write(this.element, value);
|
|
501
|
+
this.#recordOwnedHostMutation(attribute, before);
|
|
502
|
+
}
|
|
503
|
+
/** Writes the leased progress property and records its serialized style mutation. */
|
|
504
|
+
#writeHostProgress(value) {
|
|
505
|
+
const before = this.element.getAttribute("style");
|
|
506
|
+
this.#progress.write(this.element, value);
|
|
507
|
+
this.#recordOwnedHostMutation("style", before);
|
|
508
|
+
}
|
|
509
|
+
/** Records an exact host mutation only when the host is also the observed viewport. */
|
|
510
|
+
#recordOwnedHostMutation(attribute, before) {
|
|
511
|
+
if (this.#viewport !== this.element) return;
|
|
512
|
+
const after = this.element.getAttribute(attribute);
|
|
513
|
+
if (after !== before) this.#ownedHostMutations.set(attribute, after);
|
|
514
|
+
}
|
|
515
|
+
/** Returns host hooks to authored values and clears edge bookkeeping. */
|
|
516
|
+
#clearHostState() {
|
|
517
|
+
this.#overflowState.returnAll();
|
|
518
|
+
this.#scrollState.returnAll();
|
|
519
|
+
this.#progress.returnAll();
|
|
520
|
+
this.#ownedHostMutations.clear();
|
|
521
|
+
this.#overflowing = false;
|
|
522
|
+
this.#lastEdge = null;
|
|
523
|
+
}
|
|
524
|
+
/** Whether the configured axis currently has a scroll range. */
|
|
525
|
+
#measureOverflow(viewport) {
|
|
526
|
+
const orientation = this.orientationValue;
|
|
527
|
+
const vertical = orientation !== "horizontal" && viewport.scrollHeight > viewport.clientHeight + EDGE_EPSILON;
|
|
528
|
+
const horizontal = orientation !== "vertical" && viewport.scrollWidth > viewport.clientWidth + EDGE_EPSILON;
|
|
197
529
|
return vertical || horizontal;
|
|
198
530
|
}
|
|
199
|
-
/**
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
531
|
+
/** Reports the primary-axis position bucket and normalized 0–1 progress. */
|
|
532
|
+
#measurePosition(viewport) {
|
|
533
|
+
const horizontalPrimary = this.orientationValue === "horizontal" || this.orientationValue === "both" && viewport.scrollHeight <= viewport.clientHeight + EDGE_EPSILON;
|
|
534
|
+
const { position: scrollPosition, max: maxScroll } = logicalScrollMetrics(
|
|
535
|
+
viewport,
|
|
536
|
+
horizontalPrimary
|
|
537
|
+
);
|
|
206
538
|
if (maxScroll <= EDGE_EPSILON) return { position: "start", progress: 0 };
|
|
207
|
-
const progress = Math.min(1, Math.max(0,
|
|
208
|
-
if (
|
|
209
|
-
if (
|
|
539
|
+
const progress = Math.min(1, Math.max(0, scrollPosition / maxScroll));
|
|
540
|
+
if (scrollPosition <= EDGE_EPSILON) return { position: "start", progress };
|
|
541
|
+
if (scrollPosition >= maxScroll - EDGE_EPSILON) return { position: "end", progress };
|
|
210
542
|
return { position: "middle", progress };
|
|
211
543
|
}
|
|
212
|
-
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (!
|
|
222
|
-
vp.setAttribute("role", "region");
|
|
223
|
-
this.#addedRole = true;
|
|
224
|
-
}
|
|
544
|
+
/** Borrows keyboard reach only when no usable descendant already owns a Tab stop. */
|
|
545
|
+
#syncKeyboardReach(viewport, overflowing) {
|
|
546
|
+
const wantsTabindex = overflowing && !hasTabStop(viewport);
|
|
547
|
+
if (!wantsTabindex) {
|
|
548
|
+
this.#clearViewportAttributes(viewport);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
this.#tabindex.lend(viewport);
|
|
552
|
+
if (this.#hasAccessibleName(viewport)) {
|
|
553
|
+
if (!viewport.hasAttribute("role")) this.#role.write(viewport, "region");
|
|
225
554
|
} else {
|
|
226
|
-
this.#
|
|
555
|
+
this.#role.return(viewport);
|
|
227
556
|
}
|
|
228
557
|
}
|
|
229
|
-
/**
|
|
230
|
-
#
|
|
558
|
+
/** Returns only the viewport attributes borrowed by this controller. */
|
|
559
|
+
#clearViewportAttributes(viewport) {
|
|
231
560
|
this.#tabindex.returnAll();
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
561
|
+
this.#role.return(viewport);
|
|
562
|
+
}
|
|
563
|
+
/** Whether the viewport already has a non-empty accessible name. */
|
|
564
|
+
#hasAccessibleName(viewport) {
|
|
565
|
+
const sources = this.#resolveNameSources(viewport);
|
|
566
|
+
if (sources.length > 0) {
|
|
567
|
+
return sources.some((source) => {
|
|
568
|
+
const label = source.getAttribute("aria-label")?.trim();
|
|
569
|
+
return Boolean(label || source.textContent?.trim());
|
|
570
|
+
});
|
|
235
571
|
}
|
|
572
|
+
return Boolean(viewport.getAttribute("aria-label")?.trim());
|
|
236
573
|
}
|
|
237
|
-
/**
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
574
|
+
/** Resolves unique, live `aria-labelledby` references in authored order. */
|
|
575
|
+
#resolveNameSources(viewport) {
|
|
576
|
+
const ids = this.#nameReferenceIds(viewport);
|
|
577
|
+
const sources = [];
|
|
578
|
+
const seen = /* @__PURE__ */ new Set();
|
|
579
|
+
for (const id of ids) {
|
|
580
|
+
const source = viewport.ownerDocument.getElementById(id);
|
|
581
|
+
if (source && !seen.has(source)) {
|
|
582
|
+
seen.add(source);
|
|
583
|
+
sources.push(source);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return sources;
|
|
587
|
+
}
|
|
588
|
+
/** Observes external name sources so a retained region never becomes unnamed. */
|
|
589
|
+
#syncNameSources(viewport) {
|
|
590
|
+
const ids = this.#nameReferenceIds(viewport);
|
|
591
|
+
const sources = this.#resolveNameSources(viewport).filter(
|
|
592
|
+
(source) => !viewport.contains(source)
|
|
248
593
|
);
|
|
594
|
+
if (ids.length === this.#observedNameIds.size && ids.every((id) => this.#observedNameIds.has(id)) && sources.length === this.#observedNameSources.length && sources.every((source, index) => source === this.#observedNameSources[index])) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
this.#disconnectNameSources();
|
|
598
|
+
this.#observedNameIds = new Set(ids);
|
|
599
|
+
this.#observedNameSources = sources;
|
|
600
|
+
if (ids.length > 0) {
|
|
601
|
+
this.#nameGraph = new MutationObserver((records) => {
|
|
602
|
+
if (this.#viewport === viewport && this.#hasRelevantNameGraphMutation(records)) {
|
|
603
|
+
this.#refresh();
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
const documentElement = viewport.ownerDocument.documentElement;
|
|
607
|
+
if (documentElement) {
|
|
608
|
+
this.#nameGraph.observe(documentElement, {
|
|
609
|
+
subtree: true,
|
|
610
|
+
childList: true,
|
|
611
|
+
attributes: true,
|
|
612
|
+
attributeFilter: ["id"],
|
|
613
|
+
attributeOldValue: true
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (sources.length === 0) return;
|
|
618
|
+
this.#nameSources = new MutationObserver(() => {
|
|
619
|
+
this.#refresh();
|
|
620
|
+
});
|
|
621
|
+
for (const source of sources) {
|
|
622
|
+
this.#nameSources.observe(source, {
|
|
623
|
+
subtree: true,
|
|
624
|
+
childList: true,
|
|
625
|
+
characterData: true,
|
|
626
|
+
attributes: true
|
|
627
|
+
});
|
|
628
|
+
}
|
|
249
629
|
}
|
|
250
|
-
/**
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
630
|
+
/** Returns normalized unique label-reference ids in authored order. */
|
|
631
|
+
#nameReferenceIds(viewport) {
|
|
632
|
+
const tokens = viewport.getAttribute("aria-labelledby")?.trim().split(/\s+/).filter(Boolean);
|
|
633
|
+
return Array.from(new Set(tokens));
|
|
634
|
+
}
|
|
635
|
+
/** Whether document-graph changes can resolve or invalidate a referenced id. */
|
|
636
|
+
#hasRelevantNameGraphMutation(records) {
|
|
637
|
+
for (const record of records) {
|
|
638
|
+
if (record.type === "attributes") {
|
|
639
|
+
const current = record.target.id;
|
|
640
|
+
if (this.#observedNameIds.has(current) || this.#observedNameIds.has(record.oldValue ?? "")) {
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
for (const node of [...record.addedNodes, ...record.removedNodes]) {
|
|
646
|
+
if (this.#nodeContainsObservedNameId(node)) return true;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
262
650
|
}
|
|
263
|
-
|
|
264
|
-
|
|
651
|
+
/** Whether a changed subtree contains one of the current label-reference ids. */
|
|
652
|
+
#nodeContainsObservedNameId(node) {
|
|
653
|
+
if (!(node instanceof Element)) return false;
|
|
654
|
+
if (this.#observedNameIds.has(node.id)) return true;
|
|
655
|
+
return Array.from(node.querySelectorAll("[id]")).some(
|
|
656
|
+
(element) => this.#observedNameIds.has(element.id)
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
/** Stops observing accessible-name sources. */
|
|
660
|
+
#disconnectNameSources() {
|
|
661
|
+
this.#nameSources?.disconnect();
|
|
662
|
+
this.#nameSources = null;
|
|
663
|
+
this.#nameGraph?.disconnect();
|
|
664
|
+
this.#nameGraph = null;
|
|
665
|
+
this.#observedNameIds.clear();
|
|
666
|
+
this.#observedNameSources = [];
|
|
667
|
+
}
|
|
668
|
+
/** Subscribes to font completion when the current engine exposes `document.fonts`. */
|
|
669
|
+
#bindFonts() {
|
|
670
|
+
const fonts = this.element.ownerDocument.fonts;
|
|
671
|
+
if (!fonts || this.#fonts === fonts) return;
|
|
672
|
+
this.#unbindFonts();
|
|
673
|
+
this.#fonts = fonts;
|
|
674
|
+
fonts.addEventListener("loadingdone", this.#onFontsSettled);
|
|
675
|
+
fonts.addEventListener("loadingerror", this.#onFontsSettled);
|
|
676
|
+
}
|
|
677
|
+
/** Removes the font completion subscriptions. */
|
|
678
|
+
#unbindFonts() {
|
|
679
|
+
this.#fonts?.removeEventListener("loadingdone", this.#onFontsSettled);
|
|
680
|
+
this.#fonts?.removeEventListener("loadingerror", this.#onFontsSettled);
|
|
681
|
+
this.#fonts = null;
|
|
682
|
+
}
|
|
683
|
+
/** Cancels a pending scroll frame. */
|
|
684
|
+
#cancelScrollFrame() {
|
|
685
|
+
if (this.#scrollFrame === null) return;
|
|
686
|
+
cancelAnimationFrame(this.#scrollFrame);
|
|
687
|
+
this.#scrollFrame = null;
|
|
688
|
+
}
|
|
689
|
+
/** Suspends live resources and returns all derived state before snapshotting. */
|
|
690
|
+
#rewindForCache() {
|
|
691
|
+
this.#rebind.cancel();
|
|
692
|
+
this.#cancelScrollFrame();
|
|
693
|
+
if (this.#viewport) this.#unbindViewport(this.#viewport);
|
|
694
|
+
this.#layout.disconnect();
|
|
695
|
+
this.#unbindFonts();
|
|
696
|
+
this.#clearHostState();
|
|
265
697
|
}
|
|
266
698
|
};
|
|
267
699
|
|