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,37 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/overflow_menu_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/before_cache_reset.ts
|
|
6
|
-
var BeforeCacheReset = class _BeforeCacheReset {
|
|
7
|
-
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
8
|
-
static #subscribers = /* @__PURE__ */ new Set();
|
|
9
|
-
/** The shared listener; installed while at least one instance is subscribed. */
|
|
10
|
-
static #onBeforeCache = () => {
|
|
11
|
-
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
12
|
-
};
|
|
13
|
-
#rewind;
|
|
14
|
-
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
15
|
-
constructor(rewind) {
|
|
16
|
-
this.#rewind = rewind;
|
|
17
|
-
}
|
|
18
|
-
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
19
|
-
activate() {
|
|
20
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
21
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
22
|
-
if (first) {
|
|
23
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
27
|
-
deactivate() {
|
|
28
|
-
_BeforeCacheReset.#subscribers.delete(this);
|
|
29
|
-
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
30
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// src/utils/event_owner.ts
|
|
35
3
|
function ownerIndex(candidates, node) {
|
|
36
4
|
if (!(node instanceof Node)) return -1;
|
|
37
5
|
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
@@ -40,7 +8,6 @@ function ownerOf(candidates, node) {
|
|
|
40
8
|
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
41
9
|
}
|
|
42
10
|
|
|
43
|
-
// src/utils/focus_candidate.ts
|
|
44
11
|
function inheritsFieldsetDisabled(control) {
|
|
45
12
|
let fieldset = control.closest("fieldset[disabled]");
|
|
46
13
|
while (fieldset) {
|
|
@@ -50,26 +17,34 @@ function inheritsFieldsetDisabled(control) {
|
|
|
50
17
|
}
|
|
51
18
|
return false;
|
|
52
19
|
}
|
|
20
|
+
function unslotted(element) {
|
|
21
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
22
|
+
}
|
|
23
|
+
function flatTreeParent(element) {
|
|
24
|
+
if (unslotted(element)) return null;
|
|
25
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
26
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
27
|
+
return parent instanceof Element ? parent : null;
|
|
28
|
+
}
|
|
53
29
|
function canTakeFocus(element) {
|
|
54
|
-
|
|
30
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
31
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
32
|
+
}
|
|
55
33
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
56
34
|
if (!("disabled" in element)) return true;
|
|
57
35
|
if (element.disabled) return false;
|
|
58
36
|
return !inheritsFieldsetDisabled(element);
|
|
59
37
|
}
|
|
60
38
|
|
|
61
|
-
// src/utils/layout_observer.ts
|
|
62
39
|
var LayoutObserver = class {
|
|
63
40
|
#callback;
|
|
64
41
|
#resizeObserverFactory;
|
|
65
42
|
#resizeObserver = null;
|
|
66
43
|
#observingViewport = false;
|
|
67
44
|
#loadContainer = null;
|
|
68
|
-
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
69
45
|
#handleViewportResize = () => {
|
|
70
46
|
this.#callback();
|
|
71
47
|
};
|
|
72
|
-
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
73
48
|
#handleDescendantLoad = () => {
|
|
74
49
|
this.#callback();
|
|
75
50
|
};
|
|
@@ -77,11 +52,6 @@ var LayoutObserver = class {
|
|
|
77
52
|
this.#callback = callback;
|
|
78
53
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
79
54
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Starts observing an element's size. Repeated calls observe additional
|
|
82
|
-
* elements through the same shared observer. No-ops when no
|
|
83
|
-
* `ResizeObserver` implementation is available.
|
|
84
|
-
*/
|
|
85
55
|
observe(element) {
|
|
86
56
|
if (!this.#resizeObserverFactory) return;
|
|
87
57
|
if (!this.#resizeObserver) {
|
|
@@ -91,47 +61,28 @@ var LayoutObserver = class {
|
|
|
91
61
|
}
|
|
92
62
|
this.#resizeObserver.observe(element);
|
|
93
63
|
}
|
|
94
|
-
/** Stops observing a single element while leaving any others in place. */
|
|
95
64
|
unobserve(element) {
|
|
96
65
|
this.#resizeObserver?.unobserve(element);
|
|
97
66
|
}
|
|
98
|
-
/** Starts observing viewport resizes. Idempotent: the listener is added once. */
|
|
99
67
|
observeViewport() {
|
|
100
68
|
if (this.#observingViewport) return;
|
|
101
69
|
this.#observingViewport = true;
|
|
102
70
|
window.addEventListener("resize", this.#handleViewportResize);
|
|
103
71
|
}
|
|
104
|
-
/** Stops observing viewport resizes without affecting element observation. */
|
|
105
72
|
unobserveViewport() {
|
|
106
73
|
if (!this.#observingViewport) return;
|
|
107
74
|
this.#observingViewport = false;
|
|
108
75
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
109
76
|
}
|
|
110
|
-
/**
|
|
111
|
-
* Starts reporting a `load` from anywhere inside `container` — an image or a
|
|
112
|
-
* frame settling changes the box it sits in, and it measures as zero high until
|
|
113
|
-
* then. `load` does not bubble, so the subscription is a capture-phase listener
|
|
114
|
-
* on the container itself and nothing the caller spells.
|
|
115
|
-
*
|
|
116
|
-
* **One container at a time.** A further call moves the observation, so a widget
|
|
117
|
-
* whose content element is swapped at runtime releases the element it let go by
|
|
118
|
-
* naming the new one — there is no second place for the release to drift from.
|
|
119
|
-
*/
|
|
120
77
|
observeDescendantLoads(container) {
|
|
121
78
|
this.unobserveDescendantLoads();
|
|
122
79
|
this.#loadContainer = container;
|
|
123
80
|
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
124
81
|
}
|
|
125
|
-
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
126
82
|
unobserveDescendantLoads() {
|
|
127
83
|
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
128
84
|
this.#loadContainer = null;
|
|
129
85
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Releases every observation: disconnects the {@link ResizeObserver} and removes
|
|
132
|
-
* the viewport and descendant-load listeners. Safe to call multiple times. Call
|
|
133
|
-
* this from a controller's `disconnect()`.
|
|
134
|
-
*/
|
|
135
86
|
disconnect() {
|
|
136
87
|
this.#resizeObserver?.disconnect();
|
|
137
88
|
this.#resizeObserver = null;
|
|
@@ -140,43 +91,134 @@ var LayoutObserver = class {
|
|
|
140
91
|
}
|
|
141
92
|
};
|
|
142
93
|
|
|
143
|
-
|
|
94
|
+
var ListenerSet = class {
|
|
95
|
+
#abort = new AbortController();
|
|
96
|
+
add(target, type, handler, options) {
|
|
97
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
98
|
+
}
|
|
99
|
+
dispose() {
|
|
100
|
+
this.#abort.abort();
|
|
101
|
+
this.#abort = new AbortController();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
var MicrotaskCoalescer = class {
|
|
106
|
+
#run;
|
|
107
|
+
#queued = false;
|
|
108
|
+
#active = false;
|
|
109
|
+
#generation = 0;
|
|
110
|
+
constructor(run) {
|
|
111
|
+
this.#run = run;
|
|
112
|
+
}
|
|
113
|
+
activate() {
|
|
114
|
+
this.#active = true;
|
|
115
|
+
}
|
|
116
|
+
cancel() {
|
|
117
|
+
this.#active = false;
|
|
118
|
+
this.#queued = false;
|
|
119
|
+
this.#generation += 1;
|
|
120
|
+
}
|
|
121
|
+
schedule() {
|
|
122
|
+
if (!this.#active || this.#queued) return;
|
|
123
|
+
this.#queued = true;
|
|
124
|
+
const generation = this.#generation;
|
|
125
|
+
queueMicrotask(() => {
|
|
126
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
127
|
+
this.#queued = false;
|
|
128
|
+
this.#run();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
var MorphRenderWatcher = class {
|
|
134
|
+
#listeners = new ListenerSet();
|
|
135
|
+
#pass;
|
|
136
|
+
#element = null;
|
|
137
|
+
#onMorph = () => this.#pass.schedule();
|
|
138
|
+
constructor(run) {
|
|
139
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
140
|
+
}
|
|
141
|
+
observe(element) {
|
|
142
|
+
if (this.#element === element) return;
|
|
143
|
+
this.disconnect();
|
|
144
|
+
this.#element = element;
|
|
145
|
+
this.#pass.activate();
|
|
146
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
147
|
+
}
|
|
148
|
+
schedule() {
|
|
149
|
+
this.#pass.schedule();
|
|
150
|
+
}
|
|
151
|
+
disconnect() {
|
|
152
|
+
this.#listeners.dispose();
|
|
153
|
+
this.#pass.cancel();
|
|
154
|
+
this.#element = null;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
159
|
+
|
|
160
|
+
var NUMBER_BOUNDS = {
|
|
161
|
+
timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
|
|
162
|
+
function matchesNumberBounds(value, bounds) {
|
|
163
|
+
if (!Number.isFinite(value)) {
|
|
164
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
165
|
+
if (direction === null) return false;
|
|
166
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
167
|
+
}
|
|
168
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
169
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
170
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
171
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
172
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readNumber(raw, fallback, bounds) {
|
|
177
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
var NumberValueReader = class {
|
|
181
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
182
|
+
read(owner, name, raw, fallback, bounds) {
|
|
183
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
184
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
185
|
+
this.#lastRejected.delete(name);
|
|
186
|
+
return resolved;
|
|
187
|
+
}
|
|
188
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
189
|
+
const literal = owner.element.getAttribute(attribute);
|
|
190
|
+
if (literal === null) {
|
|
191
|
+
this.#lastRejected.delete(name);
|
|
192
|
+
return resolved;
|
|
193
|
+
}
|
|
194
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
195
|
+
this.#lastRejected.set(name, literal);
|
|
196
|
+
console.warn(
|
|
197
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return resolved;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
144
204
|
var TimerRegistry = class {
|
|
145
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
146
205
|
ids = /* @__PURE__ */ new Set();
|
|
147
|
-
/**
|
|
148
|
-
* Cancels a single tracked timer.
|
|
149
|
-
*
|
|
150
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
151
|
-
* registry), so callers can clear defensively without guarding.
|
|
152
|
-
*/
|
|
153
206
|
clear(id) {
|
|
154
207
|
if (this.ids.delete(id)) {
|
|
155
208
|
this.cancel(id);
|
|
156
209
|
}
|
|
157
210
|
}
|
|
158
|
-
/**
|
|
159
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
160
|
-
* to guarantee no timer outlives the element.
|
|
161
|
-
*/
|
|
162
211
|
clearAll() {
|
|
163
212
|
for (const id of this.ids) {
|
|
164
213
|
this.cancel(id);
|
|
165
214
|
}
|
|
166
215
|
this.ids.clear();
|
|
167
216
|
}
|
|
168
|
-
/** Number of timers currently tracked (pending). */
|
|
169
217
|
get size() {
|
|
170
218
|
return this.ids.size;
|
|
171
219
|
}
|
|
172
220
|
};
|
|
173
221
|
var SafeTimeout = class extends TimerRegistry {
|
|
174
|
-
/**
|
|
175
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
176
|
-
*
|
|
177
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
178
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
179
|
-
*/
|
|
180
222
|
set(callback, delay) {
|
|
181
223
|
const id = this.schedule(() => {
|
|
182
224
|
this.ids.delete(id);
|
|
@@ -193,38 +235,121 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
193
235
|
}
|
|
194
236
|
};
|
|
195
237
|
|
|
196
|
-
|
|
197
|
-
|
|
238
|
+
function queryOpenDescendants(root, selector) {
|
|
239
|
+
const matches = [];
|
|
240
|
+
const roots = [root];
|
|
241
|
+
for (const scope of roots) {
|
|
242
|
+
matches.push(...scope.querySelectorAll(selector));
|
|
243
|
+
if (scope.nodeType === Node.ELEMENT_NODE) {
|
|
244
|
+
const shadow = scope.shadowRoot;
|
|
245
|
+
if (shadow) roots.push(shadow);
|
|
246
|
+
}
|
|
247
|
+
for (const element of scope.querySelectorAll("*")) {
|
|
248
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return matches;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function sharedRegistry(key, create) {
|
|
255
|
+
const symbol = Symbol.for(key);
|
|
256
|
+
const scope = globalThis;
|
|
257
|
+
const existing = scope[symbol];
|
|
258
|
+
if (existing !== void 0) return existing;
|
|
259
|
+
const registry = create();
|
|
260
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
261
|
+
return registry;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
var TabindexLoan = class _TabindexLoan {
|
|
265
|
+
static #registry = sharedRegistry(
|
|
266
|
+
"stimeo-ui.tabindex-loan.registry.v1",
|
|
267
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
|
|
268
|
+
);
|
|
198
269
|
#value;
|
|
270
|
+
#record;
|
|
199
271
|
#lent = /* @__PURE__ */ new Set();
|
|
200
|
-
|
|
201
|
-
#beforeCache = new BeforeCacheReset(() => this.returnAll());
|
|
202
|
-
/**
|
|
203
|
-
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
204
|
-
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
205
|
-
* which a scroll region with no focusable content of its own needs.
|
|
206
|
-
*/
|
|
207
|
-
constructor(value = "-1") {
|
|
272
|
+
constructor(value, owner) {
|
|
208
273
|
this.#value = value;
|
|
274
|
+
this.#record = `data-${owner}-tabindex-loan`;
|
|
275
|
+
_TabindexLoan.#registry.names.add(this.#record);
|
|
209
276
|
}
|
|
210
|
-
/** Lends `element` the value; no-ops when it already carries a `tabindex`. */
|
|
211
277
|
lend(element) {
|
|
212
|
-
|
|
213
|
-
element.
|
|
278
|
+
const stack = this.#stack(element);
|
|
279
|
+
const current = element.getAttribute("tabindex");
|
|
280
|
+
const held = stack.find((hold) => hold.loan === this) ?? stack.find(
|
|
281
|
+
(hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
|
|
282
|
+
);
|
|
283
|
+
const linked = stack.at(-1)?.value === current;
|
|
284
|
+
if (!held && current !== null && !linked) return;
|
|
214
285
|
this.#lent.add(element);
|
|
215
|
-
|
|
286
|
+
if (held) held.loan = this;
|
|
287
|
+
else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
|
|
288
|
+
this.#mark(element, stack);
|
|
289
|
+
if (current === null || !held && current !== this.#value) {
|
|
290
|
+
element.setAttribute("tabindex", this.#value);
|
|
291
|
+
}
|
|
216
292
|
}
|
|
217
|
-
/** Takes back every loan whose value is still the one that was lent. */
|
|
218
293
|
returnAll() {
|
|
219
294
|
for (const element of this.#lent) {
|
|
220
|
-
|
|
295
|
+
const stack = this.#stack(element);
|
|
296
|
+
const hold = stack.find((held) => held.loan === this);
|
|
297
|
+
if (hold) this.#release(element, stack, hold);
|
|
221
298
|
}
|
|
222
299
|
this.#lent.clear();
|
|
223
|
-
|
|
300
|
+
}
|
|
301
|
+
reclaim(element) {
|
|
302
|
+
if (element.getAttribute(this.#record) !== this.#value) return;
|
|
303
|
+
const stack = this.#stack(element);
|
|
304
|
+
const hold = stack.find((held) => held.loan === null && held.record === this.#record);
|
|
305
|
+
if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
|
|
306
|
+
}
|
|
307
|
+
reclaimWithin(root) {
|
|
308
|
+
for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
|
|
309
|
+
this.reclaim(element);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
#stack(element) {
|
|
313
|
+
const stacks = _TabindexLoan.#registry.stacks;
|
|
314
|
+
const stack = stacks.get(element) ?? this.#seed(element);
|
|
315
|
+
stacks.set(element, stack);
|
|
316
|
+
return stack;
|
|
317
|
+
}
|
|
318
|
+
#seed(element) {
|
|
319
|
+
const current = element.getAttribute("tabindex");
|
|
320
|
+
const holds = [];
|
|
321
|
+
for (const record of _TabindexLoan.#registry.names) {
|
|
322
|
+
const value = element.getAttribute(record);
|
|
323
|
+
if (value !== null) holds.push({ loan: null, record, value, linked: true });
|
|
324
|
+
}
|
|
325
|
+
holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
|
|
326
|
+
for (const [index, hold] of holds.entries()) hold.linked = index > 0;
|
|
327
|
+
return holds;
|
|
328
|
+
}
|
|
329
|
+
#release(element, stack, hold) {
|
|
330
|
+
const current = element.getAttribute("tabindex");
|
|
331
|
+
const index = stack.indexOf(hold);
|
|
332
|
+
const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
|
|
333
|
+
stack.splice(index, 1);
|
|
334
|
+
const above = stack[index];
|
|
335
|
+
if (above) above.linked &&= hold.linked;
|
|
336
|
+
this.#mark(element, stack);
|
|
337
|
+
if (!owns) return;
|
|
338
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
339
|
+
if (!below) element.removeAttribute("tabindex");
|
|
340
|
+
else if (below.value !== current) element.setAttribute("tabindex", below.value);
|
|
341
|
+
}
|
|
342
|
+
#mark(element, stack) {
|
|
343
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
344
|
+
for (const hold of stack) records.set(hold.record, hold.value);
|
|
345
|
+
for (const [name, recorded] of records) {
|
|
346
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
347
|
+
if (recorded === null) element.removeAttribute(name);
|
|
348
|
+
else element.setAttribute(name, recorded);
|
|
349
|
+
}
|
|
224
350
|
}
|
|
225
351
|
};
|
|
226
352
|
|
|
227
|
-
// src/controllers/overflow_menu_controller.ts
|
|
228
353
|
var BANKED = "banked";
|
|
229
354
|
var INDEX = "index";
|
|
230
355
|
var BOUNDARY = "boundary";
|
|
@@ -234,8 +359,8 @@ var SAVED_MENU_TARGET = "saved-menu";
|
|
|
234
359
|
var MENU_TARGET = "data-stimeo--menu-target";
|
|
235
360
|
var BOOKKEEPING = [BANKED, INDEX, SAVED_ROLE, SAVED_TABINDEX, SAVED_MENU_TARGET];
|
|
236
361
|
var OWNED_LABEL = "owns-label";
|
|
237
|
-
var OverflowMenuController = class extends Controller {
|
|
238
|
-
|
|
362
|
+
var OverflowMenuController = class _OverflowMenuController extends Controller {
|
|
363
|
+
#numbers = new NumberValueReader();
|
|
239
364
|
#attr(name) {
|
|
240
365
|
return `data-${this.identifier}-${name}`;
|
|
241
366
|
}
|
|
@@ -244,66 +369,84 @@ var OverflowMenuController = class extends Controller {
|
|
|
244
369
|
moreLabel: { type: String, default: "More" },
|
|
245
370
|
debounce: { type: Number, default: 100 }
|
|
246
371
|
};
|
|
372
|
+
static valueConstraints = {
|
|
373
|
+
debounce: NUMBER_BOUNDS.timer
|
|
374
|
+
};
|
|
247
375
|
static actions = ["update"];
|
|
248
|
-
static events = ["change"];
|
|
376
|
+
static events = ["change", "reconcile"];
|
|
249
377
|
#layout = new LayoutObserver(() => this.#scheduleUpdate());
|
|
250
378
|
#timers = new SafeTimeout();
|
|
251
|
-
/** The managed items in canonical (authored) order, rebuilt from the DOM each pass. */
|
|
252
379
|
#items = [];
|
|
253
380
|
#index = /* @__PURE__ */ new WeakMap();
|
|
254
|
-
/** Cached bar-context width per item; see the class remarks for the staleness caveat. */
|
|
255
381
|
#widths = /* @__PURE__ */ new WeakMap();
|
|
256
|
-
/** Last reported overflow count, so `change` fires only on transitions. */
|
|
257
382
|
#lastHidden = null;
|
|
258
|
-
|
|
259
|
-
#
|
|
260
|
-
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
* Whether `connect()` has run for this connection. Stimulus delivers Value
|
|
264
|
-
* callbacks ahead of it, and the first pass writes the label anyway.
|
|
265
|
-
*/
|
|
383
|
+
#pass = 0;
|
|
384
|
+
#restoring = false;
|
|
385
|
+
#tabindex = new TabindexLoan("-1", this.identifier);
|
|
386
|
+
#bar = null;
|
|
387
|
+
#wrapper = null;
|
|
266
388
|
#connected = false;
|
|
389
|
+
#morphRender = new MorphRenderWatcher(() => this.#reconcile());
|
|
267
390
|
connect() {
|
|
391
|
+
this.#tabindex.reclaim(this.element);
|
|
392
|
+
this.#morphRender.observe(this.element);
|
|
268
393
|
this.#connected = true;
|
|
269
394
|
if (!this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
270
|
-
this.#beforeCache.activate();
|
|
271
395
|
this.#layout.observe(this.element);
|
|
272
396
|
this.#layout.observeViewport();
|
|
273
397
|
this.update();
|
|
274
398
|
}
|
|
275
399
|
disconnect() {
|
|
400
|
+
this.#morphRender.disconnect();
|
|
276
401
|
this.#connected = false;
|
|
277
402
|
this.#layout.disconnect();
|
|
278
403
|
this.#timers.clearAll();
|
|
279
|
-
this.#beforeCache.deactivate();
|
|
280
404
|
this.#restoreAll();
|
|
281
405
|
this.#lastHidden = null;
|
|
282
406
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
407
|
+
itemsTargetConnected() {
|
|
408
|
+
this.#followTargets();
|
|
409
|
+
}
|
|
410
|
+
itemsTargetDisconnected(bar) {
|
|
411
|
+
if (bar === this.#bar && !this.itemsTargets.includes(bar)) this.#release();
|
|
412
|
+
this.#followTargets();
|
|
413
|
+
}
|
|
414
|
+
moreTargetConnected() {
|
|
415
|
+
this.#followTargets();
|
|
416
|
+
}
|
|
417
|
+
moreTargetDisconnected(more) {
|
|
418
|
+
if (more === this.#wrapper && !this.moreTargets.includes(more)) this.#release();
|
|
419
|
+
this.#followTargets();
|
|
420
|
+
}
|
|
288
421
|
moreLabelValueChanged() {
|
|
289
422
|
if (!this.#connected || !this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
290
423
|
this.#syncLabel();
|
|
291
424
|
}
|
|
292
|
-
/** Re-measures and rebalances items between the bar and the More menu. */
|
|
293
425
|
update() {
|
|
294
|
-
|
|
426
|
+
const detail = this.#render();
|
|
427
|
+
if (detail) this.dispatch("change", { detail });
|
|
428
|
+
}
|
|
429
|
+
#reconcile() {
|
|
430
|
+
const detail = this.#render();
|
|
431
|
+
if (detail) this.dispatch("reconcile", { detail });
|
|
432
|
+
}
|
|
433
|
+
#render() {
|
|
434
|
+
if (this.#restoring || !this.hasItemsTarget || !this.hasMoreTarget) return null;
|
|
435
|
+
const pass = ++this.#pass;
|
|
436
|
+
this.#bar = this.itemsTarget;
|
|
437
|
+
this.#wrapper = this.moreTarget;
|
|
295
438
|
this.#syncLabel();
|
|
296
|
-
this.#syncItems();
|
|
439
|
+
this.#syncItems(this.itemsTarget, this.moreTarget);
|
|
297
440
|
this.moreTarget.hidden = false;
|
|
298
441
|
if (this.#items.some((item) => item.parentElement !== this.itemsTarget && !this.#widths.has(item))) {
|
|
299
|
-
this.#removeBoundary();
|
|
442
|
+
this.#removeBoundary(this.itemsTarget);
|
|
300
443
|
for (const item of this.#items) this.#unbank(item);
|
|
301
444
|
this.#reorder(this.itemsTarget, this.#items);
|
|
302
445
|
}
|
|
303
446
|
for (const item of this.#items) {
|
|
304
447
|
if (item.parentElement === this.itemsTarget) this.#widths.set(item, item.offsetWidth);
|
|
305
448
|
}
|
|
306
|
-
const moreWidth = (this.#trigger() ?? this.moreTarget).offsetWidth;
|
|
449
|
+
const moreWidth = (this.#trigger(this.moreTarget) ?? this.moreTarget).offsetWidth;
|
|
307
450
|
const itemGap = this.#gap(this.itemsTarget);
|
|
308
451
|
const barGap = this.#gap(this.element);
|
|
309
452
|
const widthOf = (el) => this.#widths.get(el) ?? el.offsetWidth;
|
|
@@ -325,36 +468,39 @@ var OverflowMenuController = class extends Controller {
|
|
|
325
468
|
}
|
|
326
469
|
const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
327
470
|
const focused = active === null ? null : this.#ownerOf(active);
|
|
328
|
-
const retreating = focused !== null && hidden.has(focused) && focused.parentElement === this.itemsTarget &&
|
|
329
|
-
!this.#menuExpanded();
|
|
330
|
-
const returning = focused !== null && !hidden.has(focused) && focused.parentElement !== this.itemsTarget &&
|
|
331
|
-
this.#menuExpanded();
|
|
332
|
-
if (hidden.size === 0 || hidden.size !== this.#items.length)
|
|
471
|
+
const retreating = focused !== null && hidden.has(focused) && focused.parentElement === this.itemsTarget &&
|
|
472
|
+
!this.#menuExpanded(this.moreTarget);
|
|
473
|
+
const returning = focused !== null && !hidden.has(focused) && focused.parentElement !== this.itemsTarget &&
|
|
474
|
+
this.#menuExpanded(this.moreTarget);
|
|
475
|
+
if (hidden.size === 0 || hidden.size !== this.#items.length) {
|
|
476
|
+
this.#removeBoundary(this.itemsTarget);
|
|
477
|
+
}
|
|
333
478
|
for (const item of this.#items) {
|
|
334
479
|
if (hidden.has(item)) this.#bank(item);
|
|
335
480
|
else this.#unbank(item);
|
|
336
481
|
}
|
|
337
482
|
this.#reorder(
|
|
338
|
-
this.#menuList(),
|
|
483
|
+
this.#menuList(this.moreTarget),
|
|
339
484
|
this.#items.filter((item) => hidden.has(item))
|
|
340
485
|
);
|
|
341
486
|
this.#reorder(
|
|
342
487
|
this.itemsTarget,
|
|
343
488
|
this.#items.filter((item) => !hidden.has(item))
|
|
344
489
|
);
|
|
345
|
-
if (retreating) (this.#trigger() ?? this.moreTarget).focus();
|
|
490
|
+
if (retreating) (this.#trigger(this.moreTarget) ?? this.moreTarget).focus();
|
|
346
491
|
else if (active !== null && this.#lostFocus(active)) active.focus();
|
|
492
|
+
if (pass !== this.#pass) return null;
|
|
347
493
|
const count = hidden.size;
|
|
348
494
|
for (const item of this.#items) {
|
|
349
495
|
if (count > 0) item.setAttribute(this.#attr(INDEX), String(this.#indexOf(item)));
|
|
350
496
|
else item.removeAttribute(this.#attr(INDEX));
|
|
351
497
|
}
|
|
352
498
|
if (count > 0 && count === this.#items.length) this.#ensureBoundary();
|
|
353
|
-
else this.#removeBoundary();
|
|
354
499
|
const inMore = document.activeElement instanceof HTMLElement && this.moreTarget.contains(document.activeElement);
|
|
355
500
|
if (count === 0 || returning) {
|
|
356
|
-
this.#closeMenu();
|
|
357
|
-
if (inMore) this.#rescueFocus();
|
|
501
|
+
this.#closeMenu(this.moreTarget);
|
|
502
|
+
if (inMore && pass === this.#pass) this.#rescueFocus();
|
|
503
|
+
if (pass !== this.#pass) return null;
|
|
358
504
|
}
|
|
359
505
|
this.moreTarget.hidden = count === 0;
|
|
360
506
|
if (count > 0) this.element.setAttribute("data-overflowing", "true");
|
|
@@ -362,15 +508,14 @@ var OverflowMenuController = class extends Controller {
|
|
|
362
508
|
this.element.setAttribute("data-overflow-count", String(count));
|
|
363
509
|
if (this.#lastHidden !== count) {
|
|
364
510
|
this.#lastHidden = count;
|
|
365
|
-
|
|
511
|
+
return { overflowCount: count, total: this.#items.length };
|
|
366
512
|
}
|
|
513
|
+
return null;
|
|
367
514
|
}
|
|
368
|
-
/** The flex `column-gap` on `el` in px (0 when none / unsupported). */
|
|
369
515
|
#gap(el) {
|
|
370
516
|
const style = this.#styleOf(el);
|
|
371
517
|
return style === null ? 0 : this.#px(style.columnGap || style.gap);
|
|
372
518
|
}
|
|
373
|
-
/** Horizontal padding on `el` in px — `clientWidth` counts it, the content box cannot. */
|
|
374
519
|
#paddingX(el) {
|
|
375
520
|
const style = this.#styleOf(el);
|
|
376
521
|
return style === null ? 0 : this.#px(style.paddingLeft) + this.#px(style.paddingRight);
|
|
@@ -382,16 +527,11 @@ var OverflowMenuController = class extends Controller {
|
|
|
382
527
|
const parsed = Number.parseFloat(value ?? "");
|
|
383
528
|
return Number.isNaN(parsed) ? 0 : parsed;
|
|
384
529
|
}
|
|
385
|
-
|
|
386
|
-
* Rebuilds the managed set and the canonical order from the DOM. The bar's children
|
|
387
|
-
* give the order of everything inline (so a leading insert is not silently appended)
|
|
388
|
-
* and {@link #merge} weaves the banked items back among them.
|
|
389
|
-
*/
|
|
390
|
-
#syncItems() {
|
|
530
|
+
#syncItems(row, more) {
|
|
391
531
|
const previous = this.#items;
|
|
392
532
|
const bar = [];
|
|
393
533
|
let boundaryAt;
|
|
394
|
-
for (const el of
|
|
534
|
+
for (const el of row.children) {
|
|
395
535
|
if (el instanceof HTMLTemplateElement && el.hasAttribute(this.#attr(BOUNDARY))) {
|
|
396
536
|
boundaryAt ??= bar.length;
|
|
397
537
|
continue;
|
|
@@ -399,7 +539,7 @@ var OverflowMenuController = class extends Controller {
|
|
|
399
539
|
if (el instanceof HTMLElement) bar.push(el);
|
|
400
540
|
}
|
|
401
541
|
const banked = [];
|
|
402
|
-
for (const el of this.#menuList().children) {
|
|
542
|
+
for (const el of this.#menuList(more).children) {
|
|
403
543
|
if (el instanceof HTMLElement && el.hasAttribute(this.#attr(BANKED))) banked.push(el);
|
|
404
544
|
}
|
|
405
545
|
banked.sort((a, b) => this.#bankedIndex(a) - this.#bankedIndex(b));
|
|
@@ -416,29 +556,6 @@ var OverflowMenuController = class extends Controller {
|
|
|
416
556
|
this.#index.set(el, i);
|
|
417
557
|
});
|
|
418
558
|
}
|
|
419
|
-
/**
|
|
420
|
-
* Interleaves the banked items back among the bar's children.
|
|
421
|
-
*
|
|
422
|
-
* A saved index is a position in the *canonical* order, so it can only be compared
|
|
423
|
-
* against another canonical position — never used as an offset into the current bar,
|
|
424
|
-
* which is a shorter and differently-indexed list. (Doing so survives an untouched
|
|
425
|
-
* round trip and silently scrambles the order as soon as the consumer inserts into a
|
|
426
|
-
* bar that is already short, and `#bank()` then burns the scrambled index back into
|
|
427
|
-
* the attribute, so widening never heals it.) Each bar child the controller already
|
|
428
|
-
* knows carries its canonical index from the previous pass; a banked item goes in
|
|
429
|
-
* front of the first known child whose index is higher.
|
|
430
|
-
*
|
|
431
|
-
* Children never seen before — inserted by the consumer since the last pass — have no
|
|
432
|
-
* canonical position yet, so they keep their DOM neighbours. The exception is a
|
|
433
|
-
* *trailing* run of them: appending to the bar means appending to the toolbar, so
|
|
434
|
-
* everything banked is emitted before it.
|
|
435
|
-
*
|
|
436
|
-
* With no known child at all (a fresh instance connecting to markup that already
|
|
437
|
-
* holds banked items), a fully-banked snapshot's inert boundary preserves whether an
|
|
438
|
-
* unindexed run was prepended or appended before connect. Markup that holds banked items but no boundary —
|
|
439
|
-
* server-rendered or hand-authored — uses the saved index as the
|
|
440
|
-
* offset, the inverse of the move that banked it.
|
|
441
|
-
*/
|
|
442
559
|
#merge(bar, banked, boundaryAt) {
|
|
443
560
|
const canonical = bar.map((el) => this.#index.get(el) ?? this.#savedIndex(el));
|
|
444
561
|
let lastKnown = -1;
|
|
@@ -474,56 +591,50 @@ var OverflowMenuController = class extends Controller {
|
|
|
474
591
|
drainWhile(() => true);
|
|
475
592
|
return items;
|
|
476
593
|
}
|
|
477
|
-
/** The canonical index a banked item saved, or ∞ (append) when it carries none. */
|
|
478
594
|
#bankedIndex(item) {
|
|
479
595
|
return this.#savedIndex(item) ?? Number.POSITIVE_INFINITY;
|
|
480
596
|
}
|
|
481
|
-
/** A finite canonical index stored on either side of the overflow split. */
|
|
482
597
|
#savedIndex(item) {
|
|
483
598
|
const raw = item.getAttribute(this.#attr(INDEX));
|
|
484
599
|
if (raw === null) return void 0;
|
|
485
600
|
const value = Number(raw);
|
|
486
601
|
return Number.isFinite(value) ? value : void 0;
|
|
487
602
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
for (const el of this.itemsTarget.children) {
|
|
603
|
+
#boundary(bar) {
|
|
604
|
+
for (const el of bar.children) {
|
|
491
605
|
if (el instanceof HTMLTemplateElement && el.hasAttribute(this.#attr(BOUNDARY))) return el;
|
|
492
606
|
}
|
|
493
607
|
return null;
|
|
494
608
|
}
|
|
495
609
|
#ensureBoundary() {
|
|
496
|
-
if (this.#boundary() !== null) return;
|
|
610
|
+
if (this.#boundary(this.itemsTarget) !== null) return;
|
|
497
611
|
const boundary = document.createElement("template");
|
|
498
612
|
boundary.setAttribute(this.#attr(BOUNDARY), "");
|
|
499
613
|
this.itemsTarget.appendChild(boundary);
|
|
500
614
|
}
|
|
501
|
-
#removeBoundary() {
|
|
502
|
-
this.#boundary()?.remove();
|
|
615
|
+
#removeBoundary(bar) {
|
|
616
|
+
this.#boundary(bar)?.remove();
|
|
503
617
|
}
|
|
504
|
-
/** Debounced re-measure for resize-driven churn. */
|
|
505
618
|
#scheduleUpdate() {
|
|
506
619
|
this.#timers.clearAll();
|
|
507
|
-
this.#timers.set(() => this.update(), this
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
* @stimeoRenderRoot
|
|
524
|
-
*/
|
|
620
|
+
this.#timers.set(() => this.update(), this.#safeDebounce);
|
|
621
|
+
}
|
|
622
|
+
#followTargets() {
|
|
623
|
+
if (this.#restoring || this.#lastHidden === null) return;
|
|
624
|
+
const bar = this.hasItemsTarget ? this.itemsTarget : null;
|
|
625
|
+
const more = this.hasMoreTarget ? this.moreTarget : null;
|
|
626
|
+
if (bar === this.#bar && more === this.#wrapper) return;
|
|
627
|
+
this.#release();
|
|
628
|
+
this.#morphRender.schedule();
|
|
629
|
+
}
|
|
630
|
+
#menuList(more) {
|
|
631
|
+
return more.querySelector('[data-stimeo--menu-target="menu"]') ?? more;
|
|
632
|
+
}
|
|
633
|
+
#trigger(more) {
|
|
634
|
+
return more.querySelector('[data-stimeo--menu-target="trigger"]');
|
|
635
|
+
}
|
|
525
636
|
#syncLabel() {
|
|
526
|
-
const trigger = this.#trigger();
|
|
637
|
+
const trigger = this.#trigger(this.moreTarget);
|
|
527
638
|
if (trigger === null) return;
|
|
528
639
|
const owned = this.#ownsLabel(trigger);
|
|
529
640
|
if (!owned && !this.#isBareTrigger(trigger)) return;
|
|
@@ -536,89 +647,43 @@ var OverflowMenuController = class extends Controller {
|
|
|
536
647
|
trigger.textContent = label;
|
|
537
648
|
trigger.setAttribute(this.#attr(OWNED_LABEL), label);
|
|
538
649
|
}
|
|
539
|
-
/**
|
|
540
|
-
* Whether the trigger holds the label this controller wrote there and nothing
|
|
541
|
-
* else: the text still matches the marker, and the trigger carries nothing but
|
|
542
|
-
* text. Ownership is read afresh on every call, so a trigger the consumer adds to
|
|
543
|
-
* is theirs for as long as the addition is there.
|
|
544
|
-
*/
|
|
545
650
|
#ownsLabel(trigger) {
|
|
546
651
|
return trigger.getAttribute(this.#attr(OWNED_LABEL)) === trigger.textContent && this.#carriesOnlyText(trigger);
|
|
547
652
|
}
|
|
548
|
-
/**
|
|
549
|
-
* Takes the label this controller wrote back out of the trigger, leaving it bare.
|
|
550
|
-
* A trigger that is not this controller's is left exactly as it is, marker
|
|
551
|
-
* included: taking the marker off would itself write to it.
|
|
552
|
-
*/
|
|
553
653
|
#releaseLabel(trigger) {
|
|
554
654
|
if (!this.#ownsLabel(trigger)) return;
|
|
555
655
|
trigger.textContent = "";
|
|
556
656
|
trigger.removeAttribute(this.#attr(OWNED_LABEL));
|
|
557
657
|
}
|
|
558
|
-
/**
|
|
559
|
-
* Whether the More trigger is bare, so the label is safe to write: no text, no
|
|
560
|
-
* element child, no `aria-label` and no `aria-labelledby`. A trigger with neither
|
|
561
|
-
* text, nor children, nor a name has nothing to lose and gets the fallback; one
|
|
562
|
-
* that is empty *and* unnamed is the only case the label rescues.
|
|
563
|
-
*/
|
|
564
658
|
#isBareTrigger(trigger) {
|
|
565
659
|
return (trigger.textContent ?? "").trim() === "" && this.#carriesOnlyText(trigger);
|
|
566
660
|
}
|
|
567
|
-
/**
|
|
568
|
-
* Whether the trigger carries nothing but text: no element child, no
|
|
569
|
-
* `aria-label` and no `aria-labelledby`. Assigning `textContent` replaces
|
|
570
|
-
* *every* child node, so a trigger holding an icon would silently lose it — and
|
|
571
|
-
* the loss is permanent, since only the label this controller wrote is ever
|
|
572
|
-
* taken back. A trigger that carries an accessible name is left alone too:
|
|
573
|
-
* visible text written under a different `aria-label` would make the label and
|
|
574
|
-
* the name disagree. Only a trigger that carries nothing but text is ever
|
|
575
|
-
* written or emptied.
|
|
576
|
-
*/
|
|
577
661
|
#carriesOnlyText(trigger) {
|
|
578
662
|
return trigger.firstElementChild === null && !trigger.hasAttribute("aria-label") && !trigger.hasAttribute("aria-labelledby");
|
|
579
663
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
#menuExpanded() {
|
|
586
|
-
return this.#trigger()?.getAttribute("aria-expanded") === "true";
|
|
587
|
-
}
|
|
588
|
-
/**
|
|
589
|
-
* Collapses the More menu before its wrapper is hidden. Menu owns the state (and the
|
|
590
|
-
* Escape-stack membership that goes with it), so ask it; the DOM fallback covers markup
|
|
591
|
-
* that reached an expanded state with no Menu mounted.
|
|
592
|
-
*/
|
|
593
|
-
#closeMenu() {
|
|
594
|
-
if (!this.#menuExpanded()) return;
|
|
664
|
+
#menuExpanded(more) {
|
|
665
|
+
return this.#trigger(more)?.getAttribute("aria-expanded") === "true";
|
|
666
|
+
}
|
|
667
|
+
#closeMenu(more) {
|
|
668
|
+
if (!this.#menuExpanded(more)) return;
|
|
595
669
|
const menu = this.application.getControllerForElementAndIdentifier(
|
|
596
|
-
|
|
670
|
+
more,
|
|
597
671
|
"stimeo--menu"
|
|
598
672
|
);
|
|
599
673
|
if (menu) {
|
|
600
674
|
menu.close();
|
|
601
675
|
return;
|
|
602
676
|
}
|
|
603
|
-
this.#trigger()?.setAttribute("aria-expanded", "false");
|
|
604
|
-
const list = this.#menuList();
|
|
605
|
-
if (list !==
|
|
677
|
+
this.#trigger(more)?.setAttribute("aria-expanded", "false");
|
|
678
|
+
const list = this.#menuList(more);
|
|
679
|
+
if (list !== more) list.hidden = true;
|
|
606
680
|
}
|
|
607
|
-
/** The managed item that is, or contains, `el` — focus rescue works on either. */
|
|
608
681
|
#ownerOf(el) {
|
|
609
682
|
return ownerOf(this.#items, el);
|
|
610
683
|
}
|
|
611
|
-
/** Whether the item takes up room in the bar; an authored `hidden` one does not. */
|
|
612
684
|
#rendered(item) {
|
|
613
685
|
return !item.hasAttribute("hidden");
|
|
614
686
|
}
|
|
615
|
-
/**
|
|
616
|
-
* Moves focus out of the More wrapper just before it is hidden, to the last
|
|
617
|
-
* item in canonical order that can hold it. When every item refuses, the root
|
|
618
|
-
* takes focus instead of letting the browser drop it to `<body>`, borrowing a
|
|
619
|
-
* `tabindex="-1"` just-in-time — not a Tab stop, and handed back on restore
|
|
620
|
-
* when this controller was the one that added it.
|
|
621
|
-
*/
|
|
622
687
|
#rescueFocus() {
|
|
623
688
|
for (let i = this.#items.length - 1; i >= 0; i--) {
|
|
624
689
|
const item = this.#items[i];
|
|
@@ -630,38 +695,38 @@ var OverflowMenuController = class extends Controller {
|
|
|
630
695
|
this.#tabindex.lend(this.element);
|
|
631
696
|
this.element.focus();
|
|
632
697
|
}
|
|
633
|
-
/**
|
|
634
|
-
* Whether `el` held focus, is still in a visible part of this controller, and lost it —
|
|
635
|
-
* what a re-insert does in a real browser. An item that retreated into a collapsed menu
|
|
636
|
-
* is excluded: it sits in a `hidden` subtree and must not be focused back.
|
|
637
|
-
*/
|
|
638
698
|
#lostFocus(el) {
|
|
639
699
|
return el.isConnected && document.activeElement !== el && this.element.contains(el) && el.closest("[hidden]") === null;
|
|
640
700
|
}
|
|
641
|
-
/** Returns every item to the bar and removes all traces of this controller. */
|
|
642
701
|
#restoreAll() {
|
|
643
702
|
if (!this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
644
|
-
this.#
|
|
645
|
-
|
|
646
|
-
|
|
703
|
+
this.#giveBack(this.itemsTarget, this.moreTarget);
|
|
704
|
+
}
|
|
705
|
+
#release() {
|
|
706
|
+
if (this.#bar && this.#wrapper) this.#giveBack(this.#bar, this.#wrapper);
|
|
707
|
+
}
|
|
708
|
+
#giveBack(bar, more) {
|
|
709
|
+
this.#bar = null;
|
|
710
|
+
this.#wrapper = null;
|
|
711
|
+
const restoring = this.#restoring;
|
|
712
|
+
this.#restoring = true;
|
|
713
|
+
this.#pass++;
|
|
714
|
+
this.#syncItems(bar, more);
|
|
715
|
+
this.#removeBoundary(bar);
|
|
716
|
+
this.#reorder(bar, this.#items);
|
|
647
717
|
for (const item of this.#items) {
|
|
648
718
|
this.#unbank(item);
|
|
649
719
|
item.removeAttribute(this.#attr(INDEX));
|
|
650
720
|
}
|
|
651
|
-
this.#closeMenu();
|
|
652
|
-
const trigger = this.#trigger();
|
|
721
|
+
this.#closeMenu(more);
|
|
722
|
+
const trigger = this.#trigger(more);
|
|
653
723
|
if (trigger !== null) this.#releaseLabel(trigger);
|
|
654
|
-
|
|
724
|
+
more.hidden = true;
|
|
655
725
|
this.element.removeAttribute("data-overflowing");
|
|
656
726
|
this.element.removeAttribute("data-overflow-count");
|
|
657
727
|
this.#tabindex.returnAll();
|
|
728
|
+
this.#restoring = restoring;
|
|
658
729
|
}
|
|
659
|
-
/**
|
|
660
|
-
* Makes `parent`'s managed children match `sequence`, moving only what is out of place.
|
|
661
|
-
* Walking backwards lets each item be compared against the element that should follow
|
|
662
|
-
* it, so an unchanged run costs zero DOM mutations — the point of the exercise, since
|
|
663
|
-
* every re-insert blurs the focused node.
|
|
664
|
-
*/
|
|
665
730
|
#reorder(parent, sequence) {
|
|
666
731
|
let next = null;
|
|
667
732
|
for (let i = sequence.length - 1; i >= 0; i--) {
|
|
@@ -673,7 +738,6 @@ var OverflowMenuController = class extends Controller {
|
|
|
673
738
|
next = item;
|
|
674
739
|
}
|
|
675
740
|
}
|
|
676
|
-
/** Relocates `item`, preferring `moveBefore` (keeps focus and element state). */
|
|
677
741
|
#insert(parent, item, before) {
|
|
678
742
|
const move = parent.moveBefore;
|
|
679
743
|
if (typeof move === "function") {
|
|
@@ -685,10 +749,6 @@ var OverflowMenuController = class extends Controller {
|
|
|
685
749
|
}
|
|
686
750
|
parent.insertBefore(item, before);
|
|
687
751
|
}
|
|
688
|
-
/**
|
|
689
|
-
* Gives an item menuitem semantics for Menu to drive and records its canonical index,
|
|
690
|
-
* saving every authored value it overwrites ("" means the attribute was absent).
|
|
691
|
-
*/
|
|
692
752
|
#bank(item) {
|
|
693
753
|
if (!item.hasAttribute(this.#attr(BANKED))) {
|
|
694
754
|
item.setAttribute(this.#attr(BANKED), "true");
|
|
@@ -701,7 +761,6 @@ var OverflowMenuController = class extends Controller {
|
|
|
701
761
|
}
|
|
702
762
|
item.setAttribute(this.#attr(INDEX), String(this.#indexOf(item)));
|
|
703
763
|
}
|
|
704
|
-
/** Undoes the banking: restores every authored value and drops the bookkeeping. */
|
|
705
764
|
#unbank(item) {
|
|
706
765
|
if (!item.hasAttribute(this.#attr(BANKED))) return;
|
|
707
766
|
this.#restoreAttr(item, "role", item.getAttribute(this.#attr(SAVED_ROLE)));
|
|
@@ -709,7 +768,6 @@ var OverflowMenuController = class extends Controller {
|
|
|
709
768
|
this.#restoreAttr(item, MENU_TARGET, item.getAttribute(this.#attr(SAVED_MENU_TARGET)));
|
|
710
769
|
for (const name of BOOKKEEPING) item.removeAttribute(this.#attr(name));
|
|
711
770
|
}
|
|
712
|
-
/** Re-applies a saved attribute value, or removes the attribute when it was absent. */
|
|
713
771
|
#restoreAttr(item, name, original) {
|
|
714
772
|
if (original === null || original === "") item.removeAttribute(name);
|
|
715
773
|
else item.setAttribute(name, original);
|
|
@@ -717,20 +775,22 @@ var OverflowMenuController = class extends Controller {
|
|
|
717
775
|
#indexOf(item) {
|
|
718
776
|
return this.#index.get(item) ?? 0;
|
|
719
777
|
}
|
|
720
|
-
/**
|
|
721
|
-
* Retention rank: lower keeps longer. An absent, empty, or non-numeric `data-priority`
|
|
722
|
-
* ranks lowest (drops first) — empty is the common ERB accident
|
|
723
|
-
* (`data-priority="<%= maybe_nil %>"`), and `Number("")` is 0, which would otherwise
|
|
724
|
-
* read as the *highest* retention.
|
|
725
|
-
*/
|
|
726
778
|
#rank(item) {
|
|
727
779
|
const raw = item.getAttribute("data-priority");
|
|
728
780
|
if (raw === null || raw.trim() === "") return Number.POSITIVE_INFINITY;
|
|
729
781
|
const value = Number(raw);
|
|
730
782
|
return Number.isNaN(value) ? Number.POSITIVE_INFINITY : value;
|
|
731
783
|
}
|
|
784
|
+
get #safeDebounce() {
|
|
785
|
+
return this.#numbers.read(
|
|
786
|
+
this,
|
|
787
|
+
"debounce",
|
|
788
|
+
this.debounceValue,
|
|
789
|
+
_OverflowMenuController.values.debounce.default,
|
|
790
|
+
_OverflowMenuController.valueConstraints.debounce
|
|
791
|
+
);
|
|
792
|
+
}
|
|
732
793
|
};
|
|
733
794
|
|
|
734
795
|
export { OverflowMenuController };
|
|
735
|
-
//# sourceMappingURL=overflow_menu_controller.js.map
|
|
736
796
|
//# sourceMappingURL=overflow_menu_controller.js.map
|