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,14 +1,179 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
4
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
5
|
+
function stateReasonFor(event) {
|
|
6
|
+
if (!event) return "api";
|
|
7
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
8
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
9
|
+
return "user";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function actionSource(source) {
|
|
13
|
+
if (source instanceof HTMLElement) {
|
|
14
|
+
return { event: null, host: source, origin: source, reason: "api" };
|
|
15
|
+
}
|
|
16
|
+
const event = source ?? null;
|
|
17
|
+
return {
|
|
18
|
+
event,
|
|
19
|
+
host: event?.currentTarget instanceof Element ? event.currentTarget : null,
|
|
20
|
+
origin: event?.target instanceof Element ? event.target : null,
|
|
21
|
+
reason: stateReasonFor(event)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
4
24
|
|
|
5
|
-
// src/utils/arrow_step.ts
|
|
6
25
|
function isReservedArrowChord(event, allow = []) {
|
|
7
26
|
if (!event.key.startsWith("Arrow")) return false;
|
|
8
27
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
28
|
}
|
|
10
29
|
|
|
11
|
-
|
|
30
|
+
function sharedRegistry(key, create) {
|
|
31
|
+
const symbol = Symbol.for(key);
|
|
32
|
+
const scope = globalThis;
|
|
33
|
+
const existing = scope[symbol];
|
|
34
|
+
if (existing !== void 0) return existing;
|
|
35
|
+
const registry = create();
|
|
36
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
37
|
+
return registry;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isAttributeValue(value) {
|
|
41
|
+
return value === null || typeof value === "string";
|
|
42
|
+
}
|
|
43
|
+
function readLeaseRecord(element, record) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
46
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
47
|
+
const [original, written, ...rest] = parsed;
|
|
48
|
+
const linked = rest[0] !== false;
|
|
49
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
50
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
51
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
52
|
+
} catch {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
var AttributeLease = class _AttributeLease {
|
|
57
|
+
static #registry = sharedRegistry(
|
|
58
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
59
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
60
|
+
);
|
|
61
|
+
#attribute;
|
|
62
|
+
#record;
|
|
63
|
+
#base;
|
|
64
|
+
#held = /* @__PURE__ */ new Set();
|
|
65
|
+
constructor(attribute, owner, options = {}) {
|
|
66
|
+
this.#attribute = attribute;
|
|
67
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
68
|
+
this.#base = options.base === true;
|
|
69
|
+
const names = _AttributeLease.#registry.names;
|
|
70
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
71
|
+
}
|
|
72
|
+
write(element, value) {
|
|
73
|
+
const stack = this.#stack(element);
|
|
74
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
75
|
+
this.#held.add(element);
|
|
76
|
+
const shows = !this.#covered(element, stack, hold);
|
|
77
|
+
hold.written = value;
|
|
78
|
+
this.#mark(element, stack);
|
|
79
|
+
if (shows) this.#reflect(element, value);
|
|
80
|
+
}
|
|
81
|
+
return(element) {
|
|
82
|
+
const held = this.#held.delete(element);
|
|
83
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
84
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
85
|
+
const hold = this.#take(stack);
|
|
86
|
+
if (!hold) return false;
|
|
87
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
88
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
89
|
+
const index = stack.indexOf(hold);
|
|
90
|
+
stack.splice(index, 1);
|
|
91
|
+
const above = stack[index];
|
|
92
|
+
if (above) above.linked &&= hold.linked;
|
|
93
|
+
this.#mark(element, stack);
|
|
94
|
+
if (!owns) return true;
|
|
95
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
96
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
returnAll() {
|
|
100
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
101
|
+
}
|
|
102
|
+
#stack(element) {
|
|
103
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
104
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
105
|
+
stacks.set(element, byAttribute);
|
|
106
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
107
|
+
byAttribute.set(this.#attribute, stack);
|
|
108
|
+
return stack;
|
|
109
|
+
}
|
|
110
|
+
#seed(element) {
|
|
111
|
+
const found = [];
|
|
112
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
113
|
+
const read = readLeaseRecord(element, record);
|
|
114
|
+
if (read) found.push({ ...read, record });
|
|
115
|
+
}
|
|
116
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
117
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
118
|
+
lease: null,
|
|
119
|
+
record,
|
|
120
|
+
written,
|
|
121
|
+
original,
|
|
122
|
+
linked: linked && index > 0
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
#take(stack) {
|
|
126
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
127
|
+
if (hold) hold.lease = this;
|
|
128
|
+
return hold;
|
|
129
|
+
}
|
|
130
|
+
#acquire(element, stack) {
|
|
131
|
+
const current = element.getAttribute(this.#attribute);
|
|
132
|
+
const top = stack.at(-1);
|
|
133
|
+
const bottom = stack[0];
|
|
134
|
+
const over = top?.written === current ? top : void 0;
|
|
135
|
+
const from = this.#base ? bottom : over;
|
|
136
|
+
const hold = {
|
|
137
|
+
lease: this,
|
|
138
|
+
record: this.#record,
|
|
139
|
+
written: current,
|
|
140
|
+
original: from ? from.original : current,
|
|
141
|
+
linked: !this.#base && over !== void 0
|
|
142
|
+
};
|
|
143
|
+
if (this.#base && bottom) {
|
|
144
|
+
bottom.linked = true;
|
|
145
|
+
stack.unshift(hold);
|
|
146
|
+
} else stack.push(hold);
|
|
147
|
+
return hold;
|
|
148
|
+
}
|
|
149
|
+
#covered(element, stack, hold) {
|
|
150
|
+
const current = element.getAttribute(this.#attribute);
|
|
151
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
152
|
+
}
|
|
153
|
+
#mark(element, stack) {
|
|
154
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
155
|
+
stack.forEach((hold, index) => {
|
|
156
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
157
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
158
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
159
|
+
records.set(
|
|
160
|
+
hold.record,
|
|
161
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
for (const [name, recorded] of records) {
|
|
165
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
166
|
+
if (recorded === null) element.removeAttribute(name);
|
|
167
|
+
else element.setAttribute(name, recorded);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
#reflect(element, value) {
|
|
171
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
172
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
173
|
+
else element.setAttribute(this.#attribute, value);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
12
177
|
function claimsWhileFocusWithin(element) {
|
|
13
178
|
return () => {
|
|
14
179
|
const active = element.ownerDocument.activeElement;
|
|
@@ -16,17 +181,11 @@ function claimsWhileFocusWithin(element) {
|
|
|
16
181
|
};
|
|
17
182
|
}
|
|
18
183
|
var EscapeLayer = class _EscapeLayer {
|
|
19
|
-
static #registries =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#claims = null;
|
|
25
|
-
/**
|
|
26
|
-
* Activates this layer at the top of its document's Escape stack, installing
|
|
27
|
-
* the document's shared resolver listener if this is its first layer.
|
|
28
|
-
* Re-activating an already-active layer moves it to the top.
|
|
29
|
-
*/
|
|
184
|
+
static #registries = sharedRegistry(
|
|
185
|
+
"stimeo-ui.escape-layer.registry.v1",
|
|
186
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
187
|
+
);
|
|
188
|
+
#active = null;
|
|
30
189
|
activate(ownerDocument = document, options) {
|
|
31
190
|
this.deactivate();
|
|
32
191
|
let registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
@@ -35,44 +194,31 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
35
194
|
_EscapeLayer.#registries.set(ownerDocument, registry);
|
|
36
195
|
ownerDocument.addEventListener("keydown", registry.onKeydown);
|
|
37
196
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this.#
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Removes this layer from its document's Escape stack, uninstalling the
|
|
45
|
-
* shared listener when the stack empties. Safe to call when inactive.
|
|
46
|
-
*/
|
|
197
|
+
const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
|
|
198
|
+
registry.stack.push(entry);
|
|
199
|
+
this.#active = { document: ownerDocument, entry };
|
|
200
|
+
}
|
|
47
201
|
deactivate() {
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
50
|
-
const registry = _EscapeLayer.#registries.get(
|
|
202
|
+
const active = this.#active;
|
|
203
|
+
if (!active) return;
|
|
204
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
51
205
|
if (registry) {
|
|
52
|
-
const index = registry.stack.lastIndexOf(
|
|
206
|
+
const index = registry.stack.lastIndexOf(active.entry);
|
|
53
207
|
if (index >= 0) registry.stack.splice(index, 1);
|
|
54
208
|
if (registry.stack.length === 0) {
|
|
55
|
-
|
|
56
|
-
_EscapeLayer.#registries.delete(
|
|
209
|
+
active.document.removeEventListener("keydown", registry.onKeydown);
|
|
210
|
+
_EscapeLayer.#registries.delete(active.document);
|
|
57
211
|
}
|
|
58
212
|
}
|
|
59
|
-
this.#
|
|
60
|
-
|
|
61
|
-
this.#claims = null;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Whether this active layer would own a press right now: it is the topmost
|
|
65
|
-
* layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
|
|
66
|
-
* and diagnostics — production dismissal goes through the shared listener.
|
|
67
|
-
*/
|
|
213
|
+
this.#active = null;
|
|
214
|
+
}
|
|
68
215
|
get ownsEscape() {
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
71
|
-
const registry = _EscapeLayer.#registries.get(
|
|
216
|
+
const active = this.#active;
|
|
217
|
+
if (!active) return false;
|
|
218
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
72
219
|
if (!registry) return false;
|
|
73
|
-
return _EscapeLayer.#resolveOwner(registry.stack) ===
|
|
220
|
+
return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
|
|
74
221
|
}
|
|
75
|
-
/** Builds a document's registry with its shared resolver listener. */
|
|
76
222
|
static #createRegistry() {
|
|
77
223
|
const registry = {
|
|
78
224
|
stack: [],
|
|
@@ -81,60 +227,40 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
81
227
|
const owner = _EscapeLayer.#resolveOwner(registry.stack);
|
|
82
228
|
if (!owner) return;
|
|
83
229
|
event.preventDefault();
|
|
84
|
-
owner
|
|
230
|
+
owner.onDismiss();
|
|
85
231
|
}
|
|
86
232
|
};
|
|
87
233
|
return registry;
|
|
88
234
|
}
|
|
89
|
-
/** The topmost stack layer whose claims predicate passes, or `null`. */
|
|
90
235
|
static #resolveOwner(stack) {
|
|
91
236
|
for (let index = stack.length - 1; index >= 0; index--) {
|
|
92
237
|
const layer = stack[index];
|
|
93
238
|
if (!layer) continue;
|
|
94
|
-
if (layer
|
|
239
|
+
if (layer.claims && !layer.claims()) continue;
|
|
95
240
|
return layer;
|
|
96
241
|
}
|
|
97
242
|
return null;
|
|
98
243
|
}
|
|
99
244
|
};
|
|
100
245
|
|
|
101
|
-
// src/utils/safe_timeout.ts
|
|
102
246
|
var TimerRegistry = class {
|
|
103
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
104
247
|
ids = /* @__PURE__ */ new Set();
|
|
105
|
-
/**
|
|
106
|
-
* Cancels a single tracked timer.
|
|
107
|
-
*
|
|
108
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
109
|
-
* registry), so callers can clear defensively without guarding.
|
|
110
|
-
*/
|
|
111
248
|
clear(id) {
|
|
112
249
|
if (this.ids.delete(id)) {
|
|
113
250
|
this.cancel(id);
|
|
114
251
|
}
|
|
115
252
|
}
|
|
116
|
-
/**
|
|
117
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
118
|
-
* to guarantee no timer outlives the element.
|
|
119
|
-
*/
|
|
120
253
|
clearAll() {
|
|
121
254
|
for (const id of this.ids) {
|
|
122
255
|
this.cancel(id);
|
|
123
256
|
}
|
|
124
257
|
this.ids.clear();
|
|
125
258
|
}
|
|
126
|
-
/** Number of timers currently tracked (pending). */
|
|
127
259
|
get size() {
|
|
128
260
|
return this.ids.size;
|
|
129
261
|
}
|
|
130
262
|
};
|
|
131
263
|
var SafeTimeout = class extends TimerRegistry {
|
|
132
|
-
/**
|
|
133
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
134
|
-
*
|
|
135
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
136
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
137
|
-
*/
|
|
138
264
|
set(callback, delay) {
|
|
139
265
|
const id = this.schedule(() => {
|
|
140
266
|
this.ids.delete(id);
|
|
@@ -151,36 +277,28 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
151
277
|
}
|
|
152
278
|
};
|
|
153
279
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
157
|
-
function stateReasonFor(event) {
|
|
158
|
-
if (!event) return "api";
|
|
159
|
-
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
160
|
-
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
161
|
-
return "user";
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// src/controllers/context_menu_controller.ts
|
|
165
|
-
var ContextMenuController = class extends Controller {
|
|
280
|
+
var ContextMenuController = class _ContextMenuController extends Controller {
|
|
281
|
+
static #COORDINATE = ["--stimeo--context-menu-x", "--stimeo--context-menu-y"];
|
|
166
282
|
static targets = ["region", "menu", "item"];
|
|
167
283
|
static actions = ["activate", "onItemKeydown", "onRegionKeydown", "open"];
|
|
168
284
|
static events = ["close", "open"];
|
|
169
285
|
#timers = new SafeTimeout();
|
|
170
|
-
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
171
286
|
#escapeLayer = new EscapeLayer();
|
|
172
|
-
|
|
287
|
+
#menuHidden = new AttributeLease("hidden", this.identifier);
|
|
288
|
+
#regionState = new AttributeLease("data-state", this.identifier);
|
|
289
|
+
#menu = null;
|
|
290
|
+
#connected = false;
|
|
173
291
|
#reporting = false;
|
|
174
|
-
/** Starts closed and registers delegated activation and outside-pointer listeners. */
|
|
175
292
|
connect() {
|
|
176
293
|
this.#closeMenu("api");
|
|
177
294
|
this.element.addEventListener("click", this.#onItemClickCapture, true);
|
|
178
295
|
document.addEventListener("click", this.#onOutsidePointer, true);
|
|
179
296
|
document.addEventListener("contextmenu", this.#onOutsidePointer, true);
|
|
297
|
+
this.#connected = true;
|
|
180
298
|
this.#reporting = true;
|
|
181
299
|
}
|
|
182
|
-
/** Releases the listeners, stack membership, and pending Tab-close task. */
|
|
183
300
|
disconnect() {
|
|
301
|
+
this.#connected = false;
|
|
184
302
|
this.#reporting = false;
|
|
185
303
|
this.#timers.clearAll();
|
|
186
304
|
this.#escapeLayer.deactivate();
|
|
@@ -188,15 +306,31 @@ var ContextMenuController = class extends Controller {
|
|
|
188
306
|
document.removeEventListener("click", this.#onOutsidePointer, true);
|
|
189
307
|
document.removeEventListener("contextmenu", this.#onOutsidePointer, true);
|
|
190
308
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
309
|
+
regionTargetConnected() {
|
|
310
|
+
if (this.#connected) this.#reflectRegion();
|
|
311
|
+
}
|
|
312
|
+
regionTargetDisconnected(region) {
|
|
313
|
+
if (!this.regionTargets.includes(region)) this.#regionState.return(region);
|
|
314
|
+
if (this.#connected) this.#reflectRegion();
|
|
315
|
+
}
|
|
316
|
+
menuTargetConnected() {
|
|
317
|
+
this.#adoptMenu();
|
|
318
|
+
}
|
|
319
|
+
menuTargetDisconnected(menu) {
|
|
320
|
+
this.#adoptMenu();
|
|
321
|
+
if (!this.menuTargets.includes(menu)) this.#menuHidden.return(menu);
|
|
322
|
+
}
|
|
323
|
+
open(input) {
|
|
324
|
+
const source = actionSource(input);
|
|
325
|
+
source.event?.preventDefault();
|
|
326
|
+
if (source.event instanceof MouseEvent) {
|
|
327
|
+
this.#openAt(source.event.clientX, source.event.clientY, source.reason);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const anchor = source.host ?? (this.hasRegionTarget ? this.regionTarget : null);
|
|
331
|
+
const rect = anchor?.getBoundingClientRect() ?? { left: 0, top: 0, width: 0, height: 0 };
|
|
332
|
+
this.#openAt(rect.left + rect.width / 2, rect.top + rect.height / 2, source.reason);
|
|
198
333
|
}
|
|
199
|
-
/** Keyboard entry on the region: `Shift+F10` / `ContextMenu` open at center. */
|
|
200
334
|
onRegionKeydown(event) {
|
|
201
335
|
if (event.defaultPrevented) return;
|
|
202
336
|
const isContextKey = event.key === "ContextMenu" || event.key === "F10" && event.shiftKey;
|
|
@@ -205,7 +339,6 @@ var ContextMenuController = class extends Controller {
|
|
|
205
339
|
const rect = this.hasRegionTarget ? this.regionTarget.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 };
|
|
206
340
|
this.#openAt(rect.left + rect.width / 2, rect.top + rect.height / 2, stateReasonFor(event));
|
|
207
341
|
}
|
|
208
|
-
/** Roving focus and closing keys inside the menu. */
|
|
209
342
|
onItemKeydown(event) {
|
|
210
343
|
if (event.defaultPrevented) return;
|
|
211
344
|
if (isReservedArrowChord(event)) return;
|
|
@@ -240,56 +373,67 @@ var ContextMenuController = class extends Controller {
|
|
|
240
373
|
break;
|
|
241
374
|
}
|
|
242
375
|
}
|
|
243
|
-
/** Closes after an item is activated and restores focus to the region. */
|
|
244
376
|
activate() {
|
|
245
377
|
this.#closeAndRestore("select");
|
|
246
378
|
}
|
|
247
|
-
/** Opens the menu at viewport coordinates `(x, y)` and focuses the first item. */
|
|
248
379
|
#openAt(x, y, reason) {
|
|
249
380
|
if (!this.hasMenuTarget) return;
|
|
250
381
|
this.#timers.clearAll();
|
|
382
|
+
const menu = this.menuTarget;
|
|
251
383
|
const was = this.#isOpen;
|
|
252
384
|
this.#escapeLayer.activate(document, {
|
|
253
385
|
onDismiss: () => this.#closeAndRestore("escape"),
|
|
254
386
|
claims: claimsWhileFocusWithin(this.element)
|
|
255
387
|
});
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
this.
|
|
259
|
-
|
|
388
|
+
menu.style.setProperty(_ContextMenuController.#COORDINATE[0], `${x}px`);
|
|
389
|
+
menu.style.setProperty(_ContextMenuController.#COORDINATE[1], `${y}px`);
|
|
390
|
+
this.#menuHidden.write(menu, null);
|
|
391
|
+
this.#menu = menu;
|
|
392
|
+
if (this.hasRegionTarget) this.#regionState.write(this.regionTarget, "open");
|
|
260
393
|
if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
261
394
|
if (!this.#isOpen) return;
|
|
262
395
|
this.#navigableItems[0]?.focus();
|
|
263
396
|
}
|
|
264
|
-
/** Hides the menu, reflects the collapsed state on the region, and reports a move. */
|
|
265
397
|
#closeMenu(reason) {
|
|
266
398
|
this.#timers.clearAll();
|
|
267
399
|
this.#escapeLayer.deactivate();
|
|
268
|
-
|
|
400
|
+
const menu = this.hasMenuTarget ? this.menuTarget : null;
|
|
401
|
+
this.#menu = menu;
|
|
402
|
+
if (!menu) return;
|
|
269
403
|
const was = this.#isOpen;
|
|
270
|
-
this.
|
|
271
|
-
if (this.hasRegionTarget) this.regionTarget
|
|
404
|
+
this.#menuHidden.write(menu, "");
|
|
405
|
+
if (this.hasRegionTarget) this.#regionState.write(this.regionTarget, "closed");
|
|
272
406
|
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
273
407
|
}
|
|
274
|
-
|
|
408
|
+
#adoptMenu() {
|
|
409
|
+
if (!this.#connected) return;
|
|
410
|
+
const menu = this.hasMenuTarget ? this.menuTarget : null;
|
|
411
|
+
const previous = this.#menu;
|
|
412
|
+
this.#menu = menu;
|
|
413
|
+
if (!menu) {
|
|
414
|
+
this.#escapeLayer.deactivate();
|
|
415
|
+
} else if (menu !== previous) {
|
|
416
|
+
const open = previous !== null && !previous.hidden;
|
|
417
|
+
if (open) {
|
|
418
|
+
for (const property of _ContextMenuController.#COORDINATE) {
|
|
419
|
+
menu.style.setProperty(property, previous.style.getPropertyValue(property));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
this.#menuHidden.write(menu, open ? null : "");
|
|
423
|
+
}
|
|
424
|
+
this.#reflectRegion();
|
|
425
|
+
}
|
|
426
|
+
#reflectRegion() {
|
|
427
|
+
if (!this.hasRegionTarget) return;
|
|
428
|
+
this.#regionState.write(this.regionTarget, this.#isOpen ? "open" : "closed");
|
|
429
|
+
}
|
|
275
430
|
#closeAndRestore(reason) {
|
|
276
431
|
this.#closeMenu(reason);
|
|
277
432
|
if (this.hasRegionTarget) this.regionTarget.focus();
|
|
278
433
|
}
|
|
279
|
-
/**
|
|
280
|
-
* Closes when a click or context-menu invocation lands outside this instance.
|
|
281
|
-
* Both subscriptions observe in the capture phase, so the target is judged
|
|
282
|
-
* against the tree the user actually pressed even when a consumer handler
|
|
283
|
-
* detaches it, and application code that stops bubbling cannot leave the menu
|
|
284
|
-
* open.
|
|
285
|
-
*/
|
|
286
434
|
#onOutsidePointer = (event) => {
|
|
287
435
|
if (this.#isOpen && !this.element.contains(event.target)) this.#closeMenu("outside");
|
|
288
436
|
};
|
|
289
|
-
/**
|
|
290
|
-
* Captures clicks so `aria-disabled` commands cannot reach consumer handlers.
|
|
291
|
-
* Native Enter/Space activation also synthesizes a click and is blocked here.
|
|
292
|
-
*/
|
|
293
437
|
#onItemClickCapture = (event) => {
|
|
294
438
|
const target = event.target;
|
|
295
439
|
if (!(target instanceof Node)) return;
|
|
@@ -300,33 +444,17 @@ var ContextMenuController = class extends Controller {
|
|
|
300
444
|
event.preventDefault();
|
|
301
445
|
event.stopImmediatePropagation();
|
|
302
446
|
};
|
|
303
|
-
/** Menu items eligible for roving focus (excludes hidden / natively disabled). */
|
|
304
447
|
get #navigableItems() {
|
|
305
448
|
return this.itemTargets.filter((item) => this.#isNavigable(item));
|
|
306
449
|
}
|
|
307
|
-
/**
|
|
308
|
-
* An item can take roving focus unless it is `hidden` or a natively `disabled`
|
|
309
|
-
* form control. CSS-only visibility is not detectable here and is the
|
|
310
|
-
* consumer's responsibility.
|
|
311
|
-
*
|
|
312
|
-
* **`aria-disabled="true"` stays reachable.** APG separates the two attributes
|
|
313
|
-
* by intent: `disabled` is for controls whose existence can be inferred from a
|
|
314
|
-
* neighbour (a greyed Next next to a Prev), while `aria-disabled` marks a
|
|
315
|
-
* control that must stay *discoverable* — and it names menu items as the
|
|
316
|
-
* example. Skipping it would hide the command's existence from a keyboard user
|
|
317
|
-
* entirely. Activation is suppressed separately, so the item announces itself
|
|
318
|
-
* and does nothing.
|
|
319
|
-
*/
|
|
320
450
|
#isNavigable(item) {
|
|
321
451
|
if (item.hasAttribute("hidden")) return false;
|
|
322
452
|
return !item.disabled;
|
|
323
453
|
}
|
|
324
|
-
/** Whether the menu is currently visible. */
|
|
325
454
|
get #isOpen() {
|
|
326
455
|
return this.hasMenuTarget && !this.menuTarget.hidden;
|
|
327
456
|
}
|
|
328
457
|
};
|
|
329
458
|
|
|
330
459
|
export { ContextMenuController };
|
|
331
|
-
//# sourceMappingURL=context_menu_controller.js.map
|
|
332
460
|
//# sourceMappingURL=context_menu_controller.js.map
|