@jsswift/ui 1.0.44 → 1.0.46

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.
@@ -1,361 +1,361 @@
1
- UI.Menu = (...args) => {
2
- const { props, children } = JSswift.uiNormalizeArgs(args);
3
- const slots = props.slots || {};
4
- const stateList = ["primary", "secondary", "warning", "danger", "success", "info", "light", "dark"];
5
- const sizeList = ["xs", "sm", "md", "lg", "xl"];
6
- let currentProps = { ...props };
7
- let entry = null;
8
- let boundEl = null;
9
- let lastActive = null;
10
- let openTimer = null;
11
- let closeTimer = null;
12
-
13
- const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
14
- const resolveRender = (value, ctx) => typeof value === "function" ? value(ctx) : value;
15
- const splitClasses = (value) => String(value || "").split(/\s+/).filter(Boolean);
16
- const isPlainObject = (value) => value && typeof value === "object" && !value.nodeType && !Array.isArray(value) && !(value instanceof Function);
17
- const isAnchorLike = (value) => !!value && typeof value === "object" && (value.nodeType === 1 || typeof value.getBoundingClientRect === "function");
18
- const clearTimers = () => {
19
- clearTimeout(openTimer);
20
- clearTimeout(closeTimer);
21
- openTimer = null;
22
- closeTimer = null;
23
- };
24
- const setTrackedClasses = (target, key, classes) => {
25
- if (!target) return;
26
- const prev = target[key] || [];
27
- if (prev.length) target.classList.remove(...prev);
28
- const next = (classes || []).filter(Boolean);
29
- if (next.length) target.classList.add(...next);
30
- target[key] = next;
31
- };
32
- const setStyleValue = (target, name, value, formatter = (v) => v) => {
33
- if (!target) return;
34
- if (value == null || value === "") {
35
- target.style.removeProperty(name);
36
- return;
37
- }
38
- target.style.setProperty(name, formatter(value));
39
- };
40
- const getOptions = () => currentProps || {};
41
- const getStateClass = (opts) => {
42
- const value = String(uiUnwrap(opts.state ?? opts.color) || "").toLowerCase();
43
- return stateList.includes(value) ? value : "";
44
- };
45
- const getSizeClass = (opts) => {
46
- const value = String(uiUnwrap(opts.size) || "").toLowerCase();
47
- return sizeList.includes(value) ? value : "";
48
- };
49
- const getPlacement = (opts) => String(uiUnwrap(opts.placement ?? opts.position ?? "bottom-start"));
50
- const getDelay = () => {
51
- const raw = uiUnwrap(getOptions().delay ?? getOptions().showDelay);
52
- const value = Number(raw);
53
- return Number.isFinite(value) ? Math.max(0, value) : 0;
54
- };
55
- const getHideDelay = () => {
56
- const raw = uiUnwrap(getOptions().hideDelay);
57
- const value = Number(raw);
58
- return Number.isFinite(value) ? Math.max(0, value) : 80;
59
- };
60
- const getAnchor = (fallback = null) => {
61
- if (fallback) return fallback;
62
- const opts = getOptions();
63
- return boundEl || uiUnwrap(opts.anchorEl ?? opts.triggerEl ?? opts.target ?? opts.anchor) || null;
64
- };
65
- const parseTriggers = (value) => {
66
- if (value == null || value === true) return new Set(["click"]);
67
- if (value === false) return new Set(["manual"]);
68
- const raw = Array.isArray(value) ? value : String(value).split(/[\s,|/]+/);
69
- const out = new Set();
70
- raw.forEach((item) => {
71
- const key = String(item || "").trim().toLowerCase();
72
- if (!key) return;
73
- if (key === "mouseenter" || key === "mouseover") out.add("hover");
74
- else if (key === "blur" || key === "focusin") out.add("focus");
75
- else if (key === "tap") out.add("click");
76
- else out.add(key);
77
- });
78
- return out.size ? out : new Set(["manual"]);
79
- };
80
- const parseOpenArgs = (arg1, arg2) => {
81
- let anchorEl = null;
82
- let nextProps = null;
83
- if (isAnchorLike(arg1)) {
84
- anchorEl = arg1;
85
- if (isPlainObject(arg2)) nextProps = arg2;
86
- } else if (isPlainObject(arg1)) {
87
- nextProps = arg1;
88
- const maybeAnchor = arg1.anchorEl ?? arg1.triggerEl ?? arg1.target ?? arg1.anchor;
89
- if (isAnchorLike(maybeAnchor)) anchorEl = maybeAnchor;
90
- } else if (arg1 != null) {
91
- anchorEl = arg1;
92
- }
93
- return { anchorEl, nextProps };
94
- };
95
- const getFocusableItems = (root = entry?.panel) => Array.from(root?.querySelectorAll?.(
96
- "[data-menu-item]:not([disabled]):not([tabindex='-1']), .cms-menu-item:not([disabled]):not([tabindex='-1']), [role='menuitem']:not([disabled]):not([tabindex='-1']), a[href]:not([tabindex='-1']), button:not([disabled]):not([tabindex='-1'])"
97
- ) || []).filter((el) => el.offsetParent !== null && el.getAttribute("aria-hidden") !== "true");
98
- const focusInitialItem = () => {
99
- if (uiUnwrap(getOptions().autoFocus) === false) return;
100
- setTimeout(() => {
101
- const items = getFocusableItems();
102
- if (!items.length) return;
103
- const active = items.find((el) => el.classList.contains("is-active") || el.classList.contains("is-selected") || el.classList.contains("is-checked"));
104
- (active || items[0]).focus();
105
- }, 0);
106
- };
107
- const buildIconNode = (value, ctx, size = "sm") => {
108
- const raw = resolveRender(value, ctx);
109
- if (raw == null) return [];
110
- if (typeof raw === "string") return [UI.Icon({ name: raw, size })];
111
- return slotToArray(raw, { ...ctx, as: "icon" });
112
- };
113
- const renderItemEntries = (source, ctx) => {
114
- const raw = resolveRender(source, ctx);
115
- if (raw == null || raw === false) return [];
116
- if (Array.isArray(raw)) return raw.flatMap((entryValue, index) => renderItemEntries(entryValue, { ...ctx, index }));
117
- if (typeof raw === "function") return renderItemEntries(raw(ctx), ctx);
118
- if (raw?.nodeType) return [raw];
119
- if (typeof raw === "string" || typeof raw === "number") {
120
- return renderItemEntries({ label: raw }, ctx);
121
- }
122
- if (!isPlainObject(raw)) {
123
- return slotToArray(raw, { ...ctx, as: "item" });
124
- }
125
-
126
- const kind = String(raw.type ?? raw.kind ?? "").toLowerCase();
127
- if (kind === "separator" || kind === "divider" || raw.separator === true || raw.divider === true) {
128
- return [_.div({ class: "cms-menu-divider", role: "separator" })];
129
- }
130
-
131
- const groupItems = raw.items ?? raw.children;
132
- if ((kind === "group" || kind === "section" || Array.isArray(groupItems)) && groupItems != null) {
133
- const groupCtx = { ...ctx, item: raw };
134
- const labelNodes = renderSlotToArray(slots, "groupLabel", groupCtx, resolveRender(raw.label ?? raw.title, groupCtx));
135
- const contentNodes = renderItemEntries(groupItems, groupCtx);
136
- if (!contentNodes.length) return [];
137
- return [_.div(
138
- { class: uiClass(["cms-menu-group", raw.class]) },
139
- labelNodes.length ? _.div({ class: "cms-menu-group-label" }, ...labelNodes) : null,
140
- _.div({ class: "cms-menu-group-items" }, ...contentNodes)
141
- )];
142
- }
143
-
144
- const itemCtx = {
145
- ...ctx,
146
- item: raw,
147
- close,
148
- dismiss: close,
149
- open,
150
- toggle,
151
- update,
152
- isOpen,
153
- entry: () => entry,
154
- anchorEl: getAnchor(),
155
- props: getOptions()
156
- };
157
- const disabled = !!uiUnwrap(raw.disabled);
158
- const checked = raw.checked == null ? null : !!uiUnwrap(raw.checked);
159
- const active = !!uiUnwrap(raw.active ?? raw.selected);
160
- const itemState = getStateClass(raw);
161
- const href = uiUnwrap(raw.href);
162
- const to = uiUnwrap(raw.to);
163
- const closeOnSelect = hasOwn(raw, "closeOnSelect")
164
- ? !!uiUnwrap(raw.closeOnSelect)
165
- : (raw.keepOpen === true ? false : (getOptions().closeOnSelect !== false));
166
- const ariaRole = raw.role || (checked != null ? "menuitemcheckbox" : "menuitem");
167
- const iconNodes = buildIconNode(raw.icon, itemCtx);
168
- const iconRightNodes = buildIconNode(raw.iconRight ?? raw.endIcon, itemCtx);
169
- const titleNodes = renderSlotToArray(slots, "itemTitle", itemCtx, resolveRender(raw.title ?? raw.label, itemCtx));
170
- const subtitleNodes = renderSlotToArray(slots, "itemSubtitle", itemCtx, resolveRender(raw.subtitle ?? raw.description, itemCtx));
171
- const shortcutNodes = renderSlotToArray(slots, "itemShortcut", itemCtx, resolveRender(raw.shortcut, itemCtx));
172
- const badgeNodes = renderSlotToArray(slots, "itemBadge", itemCtx, resolveRender(raw.badge, itemCtx));
173
- const customBodyNodes = renderSlotToArray(slots, "item", itemCtx, resolveRender(raw.content ?? raw.body ?? raw.render, itemCtx));
174
- const asideNodes = [
175
- ...badgeNodes,
176
- ...shortcutNodes,
177
- ...(checked === true && !iconRightNodes.length && !badgeNodes.length
178
- ? [_.span({ class: "cms-menu-entry-check" }, UI.Icon({ name: raw.checkedIcon || "check", size: "sm" }))]
179
- : []),
180
- ...iconRightNodes
181
- ];
182
- const entryClass = uiClass([
183
- "cms-menu-entry",
184
- "cms-menu-item",
185
- itemState ? `cms-state-${itemState}` : "",
186
- uiWhen(active, "is-active"),
187
- uiWhen(checked === true, "is-checked"),
188
- uiWhen(disabled, "is-disabled"),
189
- raw.class
190
- ]);
191
- const entryProps = {
192
- class: entryClass,
193
- role: ariaRole,
194
- tabIndex: disabled ? -1 : (raw.tabIndex ?? -1),
195
- "data-menu-item": true,
196
- "data-menu-select": closeOnSelect ? "true" : "false",
197
- "data-menu-close": raw.close === true ? "true" : null,
198
- "data-menu-keep-open": closeOnSelect ? null : "true",
199
- "aria-disabled": disabled ? "true" : null,
200
- "aria-checked": checked == null ? null : String(checked),
201
- title: raw.titleAttr ?? raw.tooltip ?? null,
202
- type: href ? null : "button",
203
- href: href || null,
204
- target: raw.target || null,
205
- rel: raw.rel || (raw.target === "_blank" ? "noopener noreferrer" : null),
206
- onClick: (e) => {
207
- if (disabled) {
208
- e.preventDefault();
209
- e.stopPropagation();
210
- return;
211
- }
212
- getOptions().onItemClick?.(raw, itemCtx, e);
213
- const result = raw.onClick?.(itemCtx, e);
214
- if (e.defaultPrevented) return;
215
- if (to && JSswift.router?.navigate) {
216
- e.preventDefault();
217
- JSswift.router.navigate(to);
218
- }
219
- if (result === false) return;
220
- if (closeOnSelect) close();
221
- }
222
- };
223
- if (raw.attrs && typeof raw.attrs === "object") Object.assign(entryProps, raw.attrs);
224
-
225
- const Tag = href ? _.a : _.button;
226
- const bodyNodes = customBodyNodes.length
227
- ? customBodyNodes
228
- : [
229
- titleNodes.length ? _.div({ class: "cms-menu-entry-title" }, ...titleNodes) : null,
230
- subtitleNodes.length ? _.div({ class: "cms-menu-entry-subtitle" }, ...subtitleNodes) : null
231
- ].filter(Boolean);
232
-
233
- return [Tag(
234
- entryProps,
235
- iconNodes.length ? _.span({ class: "cms-menu-entry-icon" }, ...iconNodes) : null,
236
- _.span(
237
- { class: "cms-menu-entry-main" },
238
- ...bodyNodes
239
- ),
240
- asideNodes.length ? _.span({ class: "cms-menu-entry-aside" }, ...asideNodes) : null
241
- )];
242
- };
243
- const buildContent = () => {
244
- const opts = getOptions();
245
- const ctx = {
246
- close,
247
- dismiss: close,
248
- open,
249
- toggle,
250
- update,
251
- isOpen,
252
- entry: () => entry,
253
- anchorEl: getAnchor(),
254
- props: opts
255
- };
256
- const iconNodes = buildIconNode(opts.icon, ctx, opts.iconSize || "md");
257
- const eyebrowNodes = renderSlotToArray(slots, "eyebrow", ctx, resolveRender(opts.eyebrow, ctx));
258
- const titleNodes = renderSlotToArray(slots, "title", ctx, resolveRender(opts.title ?? opts.heading ?? opts.label, ctx));
259
- const subtitleNodes = renderSlotToArray(slots, "subtitle", ctx, resolveRender(opts.subtitle ?? opts.description, ctx));
260
- const beforeNodes = renderSlotToArray(slots, "before", ctx, resolveRender(opts.before, ctx));
261
- const headerNodes = renderSlotToArray(slots, "header", ctx, resolveRender(opts.header, ctx));
262
- const afterNodes = renderSlotToArray(slots, "after", ctx, resolveRender(opts.after, ctx));
263
- const footerNodes = renderSlotToArray(slots, "footer", ctx, resolveRender(opts.footer, ctx));
264
- const statusNodes = renderSlotToArray(slots, "status", ctx, resolveRender(opts.status, ctx));
265
-
266
- let contentRaw = opts.content ?? opts.body;
267
- if (contentRaw == null && children && children.length) contentRaw = children;
268
- let contentNodes = renderSlotToArray(slots, "content", ctx, resolveRender(contentRaw, ctx));
269
- if (!contentNodes.length) contentNodes = renderSlotToArray(slots, "body", ctx, resolveRender(contentRaw, ctx));
270
- if (!contentNodes.length) contentNodes = renderSlotToArray(slots, "default", ctx, resolveRender(contentRaw, ctx));
271
-
272
- const itemNodes = renderItemEntries(opts.items ?? opts.actionsList ?? opts.entries, ctx);
273
- const emptyNodes = !itemNodes.length
274
- ? renderSlotToArray(slots, "empty", ctx, resolveRender(opts.empty ?? opts.emptyText, ctx))
275
- : [];
276
-
277
- let headerEl = null;
278
- if (headerNodes.length) {
279
- headerEl = _.div({ class: uiClass(["cms-menu-head", "cms-menu-head-custom", opts.headerClass]) }, ...headerNodes);
280
- } else if (iconNodes.length || eyebrowNodes.length || titleNodes.length || subtitleNodes.length) {
281
- headerEl = _.div(
282
- { class: uiClass(["cms-menu-head", opts.headerClass]) },
283
- iconNodes.length ? _.div({ class: "cms-menu-head-icon" }, ...iconNodes) : null,
284
- _.div(
285
- { class: "cms-menu-head-main" },
286
- eyebrowNodes.length ? _.div({ class: "cms-menu-eyebrow" }, ...eyebrowNodes) : null,
287
- titleNodes.length ? _.div({ class: "cms-menu-title" }, ...titleNodes) : null,
288
- subtitleNodes.length ? _.div({ class: "cms-menu-subtitle" }, ...subtitleNodes) : null
289
- )
290
- );
291
- }
292
-
293
- return _.div(
294
- {
295
- class: uiClass([
296
- "cms-menu",
297
- "cms-menu-shell",
298
- uiWhen(opts.dense, "dense"),
299
- uiWhen(itemNodes.length > 0, "has-items"),
300
- uiWhen(footerNodes.length > 0 || statusNodes.length > 0, "has-footer"),
301
- opts.class
302
- ]),
303
- style: opts.style || {},
304
- role: opts.role || "menu",
305
- "aria-label": opts.ariaLabel || null
306
- },
307
- beforeNodes.length ? _.div({ class: "cms-menu-before" }, ...beforeNodes) : null,
308
- headerEl,
309
- contentNodes.length ? _.div({ class: uiClass(["cms-menu-content", opts.contentClass]) }, ...contentNodes) : null,
310
- itemNodes.length ? _.div({ class: uiClass(["cms-menu-items", opts.itemsClass]) }, ...itemNodes) : null,
311
- (!itemNodes.length && emptyNodes.length) ? _.div({ class: "cms-menu-empty" }, ...emptyNodes) : null,
312
- afterNodes.length ? _.div({ class: "cms-menu-after" }, ...afterNodes) : null,
313
- (statusNodes.length || footerNodes.length)
314
- ? _.div(
315
- { class: uiClass(["cms-menu-footer", opts.footerClass]) },
316
- statusNodes.length ? _.div({ class: "cms-menu-status" }, ...statusNodes) : null,
317
- footerNodes.length ? _.div({ class: "cms-menu-footer-actions" }, ...footerNodes) : null
318
- )
319
- : null
320
- );
321
- };
322
- const applyEntryOptions = (currentEntry) => {
323
- if (!currentEntry?.panel) return;
324
- const opts = getOptions();
325
- const stateClass = getStateClass(opts);
326
- const sizeClass = getSizeClass(opts);
327
- setTrackedClasses(currentEntry.panel, "_menuClassTokens", [
328
- "cms-menu-panel",
329
- "cms-clear-set",
330
- "cms-singularity",
331
- sizeClass ? `cms-menu-${sizeClass}` : "",
332
- stateClass ? `cms-state-${stateClass}` : "",
333
- uiUnwrap(opts.outline) ? "outline" : "",
334
- ...splitClasses(opts.panelClass)
335
- ]);
336
- setTrackedClasses(currentEntry.overlay, "_menuOverlayClassTokens", [
337
- ...splitClasses(opts.overlayClass)
338
- ]);
339
- currentEntry.panel.dataset.placement = getPlacement(opts);
340
- setStyleValue(currentEntry.panel, "--cms-menu-width", uiUnwrap(opts.width), toCssSize);
341
- setStyleValue(currentEntry.panel, "--cms-menu-min-width", uiUnwrap(opts.minWidth), toCssSize);
342
- setStyleValue(currentEntry.panel, "--cms-menu-max-width", uiUnwrap(opts.maxWidth), toCssSize);
343
- setStyleValue(currentEntry.panel, "--cms-menu-max-height", uiUnwrap(opts.maxHeight), toCssSize);
344
- setStyleValue(currentEntry.panel, "--cms-menu-body-max-height", uiUnwrap(opts.bodyMaxHeight ?? opts.contentMaxHeight), toCssSize);
345
- if (opts.panelStyle) Object.assign(currentEntry.panel.style, opts.panelStyle);
346
- setPropertyProps(currentEntry.panel, opts);
347
- };
348
- const renderOpenContent = () => {
349
- if (!entry?.panel) return;
350
- entry.panel.replaceChildren(buildContent());
351
- };
352
- const close = () => {
353
- clearTimers();
354
- if (!entry) return;
355
- const toClose = entry;
356
- entry = null;
357
- overlayLeave(toClose, () => JSswift.overlay.close(toClose.id));
358
- };
1
+ UI.Menu = (...args) => {
2
+ const { props, children } = JSswift.uiNormalizeArgs(args);
3
+ const slots = props.slots || {};
4
+ const stateList = ["primary", "secondary", "warning", "danger", "success", "info", "light", "dark"];
5
+ const sizeList = ["xs", "sm", "md", "lg", "xl"];
6
+ let currentProps = { ...props };
7
+ let entry = null;
8
+ let boundEl = null;
9
+ let lastActive = null;
10
+ let openTimer = null;
11
+ let closeTimer = null;
12
+
13
+ const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
14
+ const resolveRender = (value, ctx) => typeof value === "function" ? value(ctx) : value;
15
+ const splitClasses = (value) => String(value || "").split(/\s+/).filter(Boolean);
16
+ const isPlainObject = (value) => value && typeof value === "object" && !value.nodeType && !Array.isArray(value) && !(value instanceof Function);
17
+ const isAnchorLike = (value) => !!value && typeof value === "object" && (value.nodeType === 1 || typeof value.getBoundingClientRect === "function");
18
+ const clearTimers = () => {
19
+ clearTimeout(openTimer);
20
+ clearTimeout(closeTimer);
21
+ openTimer = null;
22
+ closeTimer = null;
23
+ };
24
+ const setTrackedClasses = (target, key, classes) => {
25
+ if (!target) return;
26
+ const prev = target[key] || [];
27
+ if (prev.length) target.classList.remove(...prev);
28
+ const next = (classes || []).filter(Boolean);
29
+ if (next.length) target.classList.add(...next);
30
+ target[key] = next;
31
+ };
32
+ const setStyleValue = (target, name, value, formatter = (v) => v) => {
33
+ if (!target) return;
34
+ if (value == null || value === "") {
35
+ target.style.removeProperty(name);
36
+ return;
37
+ }
38
+ target.style.setProperty(name, formatter(value));
39
+ };
40
+ const getOptions = () => currentProps || {};
41
+ const getStateClass = (opts) => {
42
+ const value = String(uiUnwrap(opts.state ?? opts.color) || "").toLowerCase();
43
+ return stateList.includes(value) ? value : "";
44
+ };
45
+ const getSizeClass = (opts) => {
46
+ const value = String(uiUnwrap(opts.size) || "").toLowerCase();
47
+ return sizeList.includes(value) ? value : "";
48
+ };
49
+ const getPlacement = (opts) => String(uiUnwrap(opts.placement ?? opts.position ?? "bottom-start"));
50
+ const getDelay = () => {
51
+ const raw = uiUnwrap(getOptions().delay ?? getOptions().showDelay);
52
+ const value = Number(raw);
53
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
54
+ };
55
+ const getHideDelay = () => {
56
+ const raw = uiUnwrap(getOptions().hideDelay);
57
+ const value = Number(raw);
58
+ return Number.isFinite(value) ? Math.max(0, value) : 80;
59
+ };
60
+ const getAnchor = (fallback = null) => {
61
+ if (fallback) return fallback;
62
+ const opts = getOptions();
63
+ return boundEl || uiUnwrap(opts.anchorEl ?? opts.triggerEl ?? opts.target ?? opts.anchor) || null;
64
+ };
65
+ const parseTriggers = (value) => {
66
+ if (value == null || value === true) return new Set(["click"]);
67
+ if (value === false) return new Set(["manual"]);
68
+ const raw = Array.isArray(value) ? value : String(value).split(/[\s,|/]+/);
69
+ const out = new Set();
70
+ raw.forEach((item) => {
71
+ const key = String(item || "").trim().toLowerCase();
72
+ if (!key) return;
73
+ if (key === "mouseenter" || key === "mouseover") out.add("hover");
74
+ else if (key === "blur" || key === "focusin") out.add("focus");
75
+ else if (key === "tap") out.add("click");
76
+ else out.add(key);
77
+ });
78
+ return out.size ? out : new Set(["manual"]);
79
+ };
80
+ const parseOpenArgs = (arg1, arg2) => {
81
+ let anchorEl = null;
82
+ let nextProps = null;
83
+ if (isAnchorLike(arg1)) {
84
+ anchorEl = arg1;
85
+ if (isPlainObject(arg2)) nextProps = arg2;
86
+ } else if (isPlainObject(arg1)) {
87
+ nextProps = arg1;
88
+ const maybeAnchor = arg1.anchorEl ?? arg1.triggerEl ?? arg1.target ?? arg1.anchor;
89
+ if (isAnchorLike(maybeAnchor)) anchorEl = maybeAnchor;
90
+ } else if (arg1 != null) {
91
+ anchorEl = arg1;
92
+ }
93
+ return { anchorEl, nextProps };
94
+ };
95
+ const getFocusableItems = (root = entry?.panel) => Array.from(root?.querySelectorAll?.(
96
+ "[data-menu-item]:not([disabled]):not([tabindex='-1']), .cms-menu-item:not([disabled]):not([tabindex='-1']), [role='menuitem']:not([disabled]):not([tabindex='-1']), a[href]:not([tabindex='-1']), button:not([disabled]):not([tabindex='-1'])"
97
+ ) || []).filter((el) => el.offsetParent !== null && el.getAttribute("aria-hidden") !== "true");
98
+ const focusInitialItem = () => {
99
+ if (uiUnwrap(getOptions().autoFocus) === false) return;
100
+ setTimeout(() => {
101
+ const items = getFocusableItems();
102
+ if (!items.length) return;
103
+ const active = items.find((el) => el.classList.contains("is-active") || el.classList.contains("is-selected") || el.classList.contains("is-checked"));
104
+ (active || items[0]).focus();
105
+ }, 0);
106
+ };
107
+ const buildIconNode = (value, ctx, size = "sm") => {
108
+ const raw = resolveRender(value, ctx);
109
+ if (raw == null) return [];
110
+ if (typeof raw === "string") return [UI.Icon({ name: raw, size })];
111
+ return slotToArray(raw, { ...ctx, as: "icon" });
112
+ };
113
+ const renderItemEntries = (source, ctx) => {
114
+ const raw = resolveRender(source, ctx);
115
+ if (raw == null || raw === false) return [];
116
+ if (Array.isArray(raw)) return raw.flatMap((entryValue, index) => renderItemEntries(entryValue, { ...ctx, index }));
117
+ if (typeof raw === "function") return renderItemEntries(raw(ctx), ctx);
118
+ if (raw?.nodeType) return [raw];
119
+ if (typeof raw === "string" || typeof raw === "number") {
120
+ return renderItemEntries({ label: raw }, ctx);
121
+ }
122
+ if (!isPlainObject(raw)) {
123
+ return slotToArray(raw, { ...ctx, as: "item" });
124
+ }
125
+
126
+ const kind = String(raw.type ?? raw.kind ?? "").toLowerCase();
127
+ if (kind === "separator" || kind === "divider" || raw.separator === true || raw.divider === true) {
128
+ return [_.div({ class: "cms-menu-divider", role: "separator" })];
129
+ }
130
+
131
+ const groupItems = raw.items ?? raw.children;
132
+ if ((kind === "group" || kind === "section" || Array.isArray(groupItems)) && groupItems != null) {
133
+ const groupCtx = { ...ctx, item: raw };
134
+ const labelNodes = renderSlotToArray(slots, "groupLabel", groupCtx, resolveRender(raw.label ?? raw.title, groupCtx));
135
+ const contentNodes = renderItemEntries(groupItems, groupCtx);
136
+ if (!contentNodes.length) return [];
137
+ return [_.div(
138
+ { class: uiClass(["cms-menu-group", raw.class]) },
139
+ labelNodes.length ? _.div({ class: "cms-menu-group-label" }, ...labelNodes) : null,
140
+ _.div({ class: "cms-menu-group-items" }, ...contentNodes)
141
+ )];
142
+ }
143
+
144
+ const itemCtx = {
145
+ ...ctx,
146
+ item: raw,
147
+ close,
148
+ dismiss: close,
149
+ open,
150
+ toggle,
151
+ update,
152
+ isOpen,
153
+ entry: () => entry,
154
+ anchorEl: getAnchor(),
155
+ props: getOptions()
156
+ };
157
+ const disabled = !!uiUnwrap(raw.disabled);
158
+ const checked = raw.checked == null ? null : !!uiUnwrap(raw.checked);
159
+ const active = !!uiUnwrap(raw.active ?? raw.selected);
160
+ const itemState = getStateClass(raw);
161
+ const href = uiUnwrap(raw.href);
162
+ const to = uiUnwrap(raw.to);
163
+ const closeOnSelect = hasOwn(raw, "closeOnSelect")
164
+ ? !!uiUnwrap(raw.closeOnSelect)
165
+ : (raw.keepOpen === true ? false : (getOptions().closeOnSelect !== false));
166
+ const ariaRole = raw.role || (checked != null ? "menuitemcheckbox" : "menuitem");
167
+ const iconNodes = buildIconNode(raw.icon, itemCtx);
168
+ const iconRightNodes = buildIconNode(raw.iconRight ?? raw.endIcon, itemCtx);
169
+ const titleNodes = renderSlotToArray(slots, "itemTitle", itemCtx, resolveRender(raw.title ?? raw.label, itemCtx));
170
+ const subtitleNodes = renderSlotToArray(slots, "itemSubtitle", itemCtx, resolveRender(raw.subtitle ?? raw.description, itemCtx));
171
+ const shortcutNodes = renderSlotToArray(slots, "itemShortcut", itemCtx, resolveRender(raw.shortcut, itemCtx));
172
+ const badgeNodes = renderSlotToArray(slots, "itemBadge", itemCtx, resolveRender(raw.badge, itemCtx));
173
+ const customBodyNodes = renderSlotToArray(slots, "item", itemCtx, resolveRender(raw.content ?? raw.body ?? raw.render, itemCtx));
174
+ const asideNodes = [
175
+ ...badgeNodes,
176
+ ...shortcutNodes,
177
+ ...(checked === true && !iconRightNodes.length && !badgeNodes.length
178
+ ? [_.span({ class: "cms-menu-entry-check" }, UI.Icon({ name: raw.checkedIcon || "check", size: "sm" }))]
179
+ : []),
180
+ ...iconRightNodes
181
+ ];
182
+ const entryClass = uiClass([
183
+ "cms-menu-entry",
184
+ "cms-menu-item",
185
+ itemState ? `cms-state-${itemState}` : "",
186
+ uiWhen(active, "is-active"),
187
+ uiWhen(checked === true, "is-checked"),
188
+ uiWhen(disabled, "is-disabled"),
189
+ raw.class
190
+ ]);
191
+ const entryProps = {
192
+ class: entryClass,
193
+ role: ariaRole,
194
+ tabIndex: disabled ? -1 : (raw.tabIndex ?? -1),
195
+ "data-menu-item": true,
196
+ "data-menu-select": closeOnSelect ? "true" : "false",
197
+ "data-menu-close": raw.close === true ? "true" : null,
198
+ "data-menu-keep-open": closeOnSelect ? null : "true",
199
+ "aria-disabled": disabled ? "true" : null,
200
+ "aria-checked": checked == null ? null : String(checked),
201
+ title: raw.titleAttr ?? raw.tooltip ?? null,
202
+ type: href ? null : "button",
203
+ href: href || null,
204
+ target: raw.target || null,
205
+ rel: raw.rel || (raw.target === "_blank" ? "noopener noreferrer" : null),
206
+ onClick: (e) => {
207
+ if (disabled) {
208
+ e.preventDefault();
209
+ e.stopPropagation();
210
+ return;
211
+ }
212
+ getOptions().onItemClick?.(raw, itemCtx, e);
213
+ const result = raw.onClick?.(itemCtx, e);
214
+ if (e.defaultPrevented) return;
215
+ if (to && JSswift.router?.navigate) {
216
+ e.preventDefault();
217
+ JSswift.router.navigate(to);
218
+ }
219
+ if (result === false) return;
220
+ if (closeOnSelect) close();
221
+ }
222
+ };
223
+ if (raw.attrs && typeof raw.attrs === "object") Object.assign(entryProps, raw.attrs);
224
+
225
+ const Tag = href ? _.a : _.button;
226
+ const bodyNodes = customBodyNodes.length
227
+ ? customBodyNodes
228
+ : [
229
+ titleNodes.length ? _.div({ class: "cms-menu-entry-title" }, ...titleNodes) : null,
230
+ subtitleNodes.length ? _.div({ class: "cms-menu-entry-subtitle" }, ...subtitleNodes) : null
231
+ ].filter(Boolean);
232
+
233
+ return [Tag(
234
+ entryProps,
235
+ iconNodes.length ? _.span({ class: "cms-menu-entry-icon" }, ...iconNodes) : null,
236
+ _.span(
237
+ { class: "cms-menu-entry-main" },
238
+ ...bodyNodes
239
+ ),
240
+ asideNodes.length ? _.span({ class: "cms-menu-entry-aside" }, ...asideNodes) : null
241
+ )];
242
+ };
243
+ const buildContent = () => {
244
+ const opts = getOptions();
245
+ const ctx = {
246
+ close,
247
+ dismiss: close,
248
+ open,
249
+ toggle,
250
+ update,
251
+ isOpen,
252
+ entry: () => entry,
253
+ anchorEl: getAnchor(),
254
+ props: opts
255
+ };
256
+ const iconNodes = buildIconNode(opts.icon, ctx, opts.iconSize || "md");
257
+ const eyebrowNodes = renderSlotToArray(slots, "eyebrow", ctx, resolveRender(opts.eyebrow, ctx));
258
+ const titleNodes = renderSlotToArray(slots, "title", ctx, resolveRender(opts.title ?? opts.heading ?? opts.label, ctx));
259
+ const subtitleNodes = renderSlotToArray(slots, "subtitle", ctx, resolveRender(opts.subtitle ?? opts.description, ctx));
260
+ const beforeNodes = renderSlotToArray(slots, "before", ctx, resolveRender(opts.before, ctx));
261
+ const headerNodes = renderSlotToArray(slots, "header", ctx, resolveRender(opts.header, ctx));
262
+ const afterNodes = renderSlotToArray(slots, "after", ctx, resolveRender(opts.after, ctx));
263
+ const footerNodes = renderSlotToArray(slots, "footer", ctx, resolveRender(opts.footer, ctx));
264
+ const statusNodes = renderSlotToArray(slots, "status", ctx, resolveRender(opts.status, ctx));
265
+
266
+ let contentRaw = opts.content ?? opts.body;
267
+ if (contentRaw == null && children && children.length) contentRaw = children;
268
+ let contentNodes = renderSlotToArray(slots, "content", ctx, resolveRender(contentRaw, ctx));
269
+ if (!contentNodes.length) contentNodes = renderSlotToArray(slots, "body", ctx, resolveRender(contentRaw, ctx));
270
+ if (!contentNodes.length) contentNodes = renderSlotToArray(slots, "default", ctx, resolveRender(contentRaw, ctx));
271
+
272
+ const itemNodes = renderItemEntries(opts.items ?? opts.actionsList ?? opts.entries, ctx);
273
+ const emptyNodes = !itemNodes.length
274
+ ? renderSlotToArray(slots, "empty", ctx, resolveRender(opts.empty ?? opts.emptyText, ctx))
275
+ : [];
276
+
277
+ let headerEl = null;
278
+ if (headerNodes.length) {
279
+ headerEl = _.div({ class: uiClass(["cms-menu-head", "cms-menu-head-custom", opts.headerClass]) }, ...headerNodes);
280
+ } else if (iconNodes.length || eyebrowNodes.length || titleNodes.length || subtitleNodes.length) {
281
+ headerEl = _.div(
282
+ { class: uiClass(["cms-menu-head", opts.headerClass]) },
283
+ iconNodes.length ? _.div({ class: "cms-menu-head-icon" }, ...iconNodes) : null,
284
+ _.div(
285
+ { class: "cms-menu-head-main" },
286
+ eyebrowNodes.length ? _.div({ class: "cms-menu-eyebrow" }, ...eyebrowNodes) : null,
287
+ titleNodes.length ? _.div({ class: "cms-menu-title" }, ...titleNodes) : null,
288
+ subtitleNodes.length ? _.div({ class: "cms-menu-subtitle" }, ...subtitleNodes) : null
289
+ )
290
+ );
291
+ }
292
+
293
+ return _.div(
294
+ {
295
+ class: uiClass([
296
+ "cms-menu",
297
+ "cms-menu-shell",
298
+ uiWhen(opts.dense, "dense"),
299
+ uiWhen(itemNodes.length > 0, "has-items"),
300
+ uiWhen(footerNodes.length > 0 || statusNodes.length > 0, "has-footer"),
301
+ opts.class
302
+ ]),
303
+ style: opts.style || {},
304
+ role: opts.role || "menu",
305
+ "aria-label": opts.ariaLabel || null
306
+ },
307
+ beforeNodes.length ? _.div({ class: "cms-menu-before" }, ...beforeNodes) : null,
308
+ headerEl,
309
+ contentNodes.length ? _.div({ class: uiClass(["cms-menu-content", opts.contentClass]) }, ...contentNodes) : null,
310
+ itemNodes.length ? _.div({ class: uiClass(["cms-menu-items", opts.itemsClass]) }, ...itemNodes) : null,
311
+ (!itemNodes.length && emptyNodes.length) ? _.div({ class: "cms-menu-empty" }, ...emptyNodes) : null,
312
+ afterNodes.length ? _.div({ class: "cms-menu-after" }, ...afterNodes) : null,
313
+ (statusNodes.length || footerNodes.length)
314
+ ? _.div(
315
+ { class: uiClass(["cms-menu-footer", opts.footerClass]) },
316
+ statusNodes.length ? _.div({ class: "cms-menu-status" }, ...statusNodes) : null,
317
+ footerNodes.length ? _.div({ class: "cms-menu-footer-actions" }, ...footerNodes) : null
318
+ )
319
+ : null
320
+ );
321
+ };
322
+ const applyEntryOptions = (currentEntry) => {
323
+ if (!currentEntry?.panel) return;
324
+ const opts = getOptions();
325
+ const stateClass = getStateClass(opts);
326
+ const sizeClass = getSizeClass(opts);
327
+ setTrackedClasses(currentEntry.panel, "_menuClassTokens", [
328
+ "cms-menu-panel",
329
+ "cms-clear-set",
330
+ "cms-singularity",
331
+ sizeClass ? `cms-menu-${sizeClass}` : "",
332
+ stateClass ? `cms-state-${stateClass}` : "",
333
+ uiUnwrap(opts.outline) ? "outline" : "",
334
+ ...splitClasses(opts.panelClass)
335
+ ]);
336
+ setTrackedClasses(currentEntry.overlay, "_menuOverlayClassTokens", [
337
+ ...splitClasses(opts.overlayClass)
338
+ ]);
339
+ currentEntry.panel.dataset.placement = getPlacement(opts);
340
+ setStyleValue(currentEntry.panel, "--cms-menu-width", uiUnwrap(opts.width), toCssSize);
341
+ setStyleValue(currentEntry.panel, "--cms-menu-min-width", uiUnwrap(opts.minWidth), toCssSize);
342
+ setStyleValue(currentEntry.panel, "--cms-menu-max-width", uiUnwrap(opts.maxWidth), toCssSize);
343
+ setStyleValue(currentEntry.panel, "--cms-menu-max-height", uiUnwrap(opts.maxHeight), toCssSize);
344
+ setStyleValue(currentEntry.panel, "--cms-menu-body-max-height", uiUnwrap(opts.bodyMaxHeight ?? opts.contentMaxHeight), toCssSize);
345
+ if (opts.panelStyle) Object.assign(currentEntry.panel.style, opts.panelStyle);
346
+ setPropertyProps(currentEntry.panel, opts);
347
+ };
348
+ const renderOpenContent = () => {
349
+ if (!entry?.panel) return;
350
+ entry.panel.replaceChildren(buildContent());
351
+ };
352
+ const close = () => {
353
+ clearTimers();
354
+ if (!entry) return;
355
+ const toClose = entry;
356
+ entry = null;
357
+ overlayLeave(toClose, () => JSswift.overlay.close(toClose.id));
358
+ };
359
359
  const update = (nextProps = {}) => {
360
360
  if (nextProps && typeof nextProps === "object") currentProps = { ...currentProps, ...nextProps };
361
361
  if (entry) {
@@ -371,14 +371,14 @@
371
371
  focusInitialItem();
372
372
  }
373
373
  return api;
374
- };
375
- const open = (arg1 = null, arg2 = null) => {
376
- clearTimers();
377
- const { anchorEl, nextProps } = parseOpenArgs(arg1, arg2);
378
- if (nextProps) currentProps = { ...currentProps, ...nextProps };
379
- const opts = getOptions();
380
- const anchor = getAnchor(anchorEl);
381
- if (!anchor || uiUnwrap(opts.disabled)) return null;
374
+ };
375
+ const open = (arg1 = null, arg2 = null) => {
376
+ clearTimers();
377
+ const { anchorEl, nextProps } = parseOpenArgs(arg1, arg2);
378
+ if (nextProps) currentProps = { ...currentProps, ...nextProps };
379
+ const opts = getOptions();
380
+ const anchor = getAnchor(anchorEl);
381
+ if (!anchor || uiUnwrap(opts.disabled)) return null;
382
382
  if (entry && entry._anchorEl === anchor) {
383
383
  const nextOpts = getOptions();
384
384
  applyEntryOptions(entry);
@@ -392,99 +392,99 @@
392
392
  focusInitialItem();
393
393
  return entry;
394
394
  }
395
- if (entry) close();
396
- lastActive = document.activeElement;
397
- let currentRef = null;
398
- entry = JSswift.overlay.open(() => buildContent(), {
399
- type: "menu",
400
- anchorEl: anchor,
401
- placement: getPlacement(opts),
402
- offsetX: opts.offsetX ?? 0,
403
- offsetY: opts.offsetY ?? opts.offset ?? 8,
404
- backdrop: opts.backdrop === true,
405
- lockScroll: opts.lockScroll === true,
406
- trapFocus: opts.trapFocus === true,
407
- autoFocus: false,
408
- closeOnOutside: opts.closeOnOutside !== false,
409
- closeOnBackdrop: opts.closeOnBackdrop ?? opts.backdrop === true,
410
- closeOnEsc: opts.closeOnEsc !== false,
411
- className: uiClassStatic(["cms-menu-panel"]),
412
- onClose: () => {
413
- currentRef?._menuCleanup?.();
414
- getOptions().onClose?.(currentRef);
415
- if (getOptions().returnFocus !== false && lastActive && typeof lastActive.focus === "function") {
416
- setTimeout(() => lastActive.focus(), 0);
417
- }
418
- }
419
- });
420
- if (!entry?.panel) return entry;
421
- const current = entry;
422
- currentRef = current;
423
- current._anchorEl = anchor;
424
- const cancelHide = () => {
425
- clearTimeout(closeTimer);
426
- closeTimer = null;
427
- };
428
- const scheduleHide = () => {
429
- hide();
430
- };
431
- const onPanelClick = (e) => {
432
- const target = e.target;
433
- if (!target || !target.closest) return;
434
- if (target.closest("[data-menu-close]")) {
435
- close();
436
- return;
437
- }
438
- if (getOptions().closeOnSelect === false) return;
439
- const selected = target.closest("[data-menu-item], [data-menu-select], .cms-menu-item, [role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio'], a[href], button:not([disabled])");
440
- if (!selected) return;
441
- if (selected.closest("[data-menu-keep-open]")) return;
442
- if (selected.getAttribute("data-menu-select") === "false") return;
443
- close();
444
- };
445
- const onPanelKeydown = (e) => {
446
- if (e.key === "Escape") {
447
- e.preventDefault();
448
- close();
449
- return;
450
- }
451
- if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(e.key)) return;
452
- const items = getFocusableItems(current.panel);
453
- if (!items.length) return;
454
- e.preventDefault();
455
- const activeIndex = items.indexOf(document.activeElement);
456
- if (e.key === "Home") {
457
- items[0].focus();
458
- return;
459
- }
460
- if (e.key === "End") {
461
- items[items.length - 1].focus();
462
- return;
463
- }
464
- const dir = e.key === "ArrowDown" ? 1 : -1;
465
- const next = activeIndex < 0
466
- ? (dir > 0 ? 0 : items.length - 1)
467
- : (activeIndex + dir + items.length) % items.length;
468
- items[next].focus();
469
- };
470
- current.panel.addEventListener("click", onPanelClick);
471
- current.panel.addEventListener("keydown", onPanelKeydown);
472
- const activeTriggers = parseTriggers(opts.trigger ?? opts.triggers ?? (hasOwn(opts, "open") ? "manual" : null));
473
- const allowHover = activeTriggers.has("hover");
474
- const allowFocus = activeTriggers.has("focus");
475
- if (allowHover || allowFocus) {
476
- current.panel.addEventListener("mouseenter", cancelHide);
477
- current.panel.addEventListener("mouseleave", scheduleHide);
478
- current.panel.addEventListener("focusin", cancelHide);
479
- current.panel.addEventListener("focusout", scheduleHide);
480
- }
395
+ if (entry) close();
396
+ lastActive = document.activeElement;
397
+ let currentRef = null;
398
+ entry = JSswift.overlay.open(() => buildContent(), {
399
+ type: "menu",
400
+ anchorEl: anchor,
401
+ placement: getPlacement(opts),
402
+ offsetX: opts.offsetX ?? 0,
403
+ offsetY: opts.offsetY ?? opts.offset ?? 8,
404
+ backdrop: opts.backdrop === true,
405
+ lockScroll: opts.lockScroll === true,
406
+ trapFocus: opts.trapFocus === true,
407
+ autoFocus: false,
408
+ closeOnOutside: opts.closeOnOutside !== false,
409
+ closeOnBackdrop: opts.closeOnBackdrop ?? opts.backdrop === true,
410
+ closeOnEsc: opts.closeOnEsc !== false,
411
+ className: uiClassStatic(["cms-menu-panel"]),
412
+ onClose: () => {
413
+ currentRef?._menuCleanup?.();
414
+ getOptions().onClose?.(currentRef);
415
+ if (getOptions().returnFocus !== false && lastActive && typeof lastActive.focus === "function") {
416
+ setTimeout(() => lastActive.focus(), 0);
417
+ }
418
+ }
419
+ });
420
+ if (!entry?.panel) return entry;
421
+ const current = entry;
422
+ currentRef = current;
423
+ current._anchorEl = anchor;
424
+ const cancelHide = () => {
425
+ clearTimeout(closeTimer);
426
+ closeTimer = null;
427
+ };
428
+ const scheduleHide = () => {
429
+ hide();
430
+ };
431
+ const onPanelClick = (e) => {
432
+ const target = e.target;
433
+ if (!target || !target.closest) return;
434
+ if (target.closest("[data-menu-close]")) {
435
+ close();
436
+ return;
437
+ }
438
+ if (getOptions().closeOnSelect === false) return;
439
+ const selected = target.closest("[data-menu-item], [data-menu-select], .cms-menu-item, [role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio'], a[href], button:not([disabled])");
440
+ if (!selected) return;
441
+ if (selected.closest("[data-menu-keep-open]")) return;
442
+ if (selected.getAttribute("data-menu-select") === "false") return;
443
+ close();
444
+ };
445
+ const onPanelKeydown = (e) => {
446
+ if (e.key === "Escape") {
447
+ e.preventDefault();
448
+ close();
449
+ return;
450
+ }
451
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(e.key)) return;
452
+ const items = getFocusableItems(current.panel);
453
+ if (!items.length) return;
454
+ e.preventDefault();
455
+ const activeIndex = items.indexOf(document.activeElement);
456
+ if (e.key === "Home") {
457
+ items[0].focus();
458
+ return;
459
+ }
460
+ if (e.key === "End") {
461
+ items[items.length - 1].focus();
462
+ return;
463
+ }
464
+ const dir = e.key === "ArrowDown" ? 1 : -1;
465
+ const next = activeIndex < 0
466
+ ? (dir > 0 ? 0 : items.length - 1)
467
+ : (activeIndex + dir + items.length) % items.length;
468
+ items[next].focus();
469
+ };
470
+ current.panel.addEventListener("click", onPanelClick);
471
+ current.panel.addEventListener("keydown", onPanelKeydown);
472
+ const activeTriggers = parseTriggers(opts.trigger ?? opts.triggers ?? (hasOwn(opts, "open") ? "manual" : null));
473
+ const allowHover = activeTriggers.has("hover");
474
+ const allowFocus = activeTriggers.has("focus");
475
+ if (allowHover || allowFocus) {
476
+ current.panel.addEventListener("mouseenter", cancelHide);
477
+ current.panel.addEventListener("mouseleave", scheduleHide);
478
+ current.panel.addEventListener("focusin", cancelHide);
479
+ current.panel.addEventListener("focusout", scheduleHide);
480
+ }
481
481
  current._menuCleanup = () => {
482
482
  current.panel?.removeEventListener("click", onPanelClick);
483
483
  current.panel?.removeEventListener("keydown", onPanelKeydown);
484
- current.panel?.removeEventListener("mouseenter", cancelHide);
485
- current.panel?.removeEventListener("mouseleave", scheduleHide);
486
- current.panel?.removeEventListener("focusin", cancelHide);
487
- current.panel?.removeEventListener("focusout", scheduleHide);
484
+ current.panel?.removeEventListener("mouseenter", cancelHide);
485
+ current.panel?.removeEventListener("mouseleave", scheduleHide);
486
+ current.panel?.removeEventListener("focusin", cancelHide);
487
+ current.panel?.removeEventListener("focusout", scheduleHide);
488
488
  };
489
489
  applyEntryOptions(current);
490
490
  current.reposition?.({
@@ -496,947 +496,722 @@
496
496
  overlayEnter(current);
497
497
  getOptions().onOpen?.(current);
498
498
  focusInitialItem();
499
- return current;
500
- };
501
- const show = (anchorEl, nextProps = null) => {
502
- clearTimeout(closeTimer);
503
- closeTimer = null;
504
- clearTimeout(openTimer);
505
- openTimer = setTimeout(() => open(anchorEl, nextProps), getDelay());
506
- };
507
- const hide = (immediate = false) => {
508
- clearTimeout(openTimer);
509
- openTimer = null;
510
- if (!entry) return;
511
- clearTimeout(closeTimer);
512
- if (immediate) {
513
- close();
514
- return;
515
- }
516
- closeTimer = setTimeout(() => close(), getHideDelay());
517
- };
518
- const toggle = (arg1 = null, arg2 = null) => isOpen() ? (close(), null) : open(arg1, arg2);
519
- const isOpen = () => !!entry;
520
- const bind = (el) => {
521
- if (!el) return () => { };
522
- boundEl = el;
523
- const opts = getOptions();
524
- const triggers = parseTriggers(opts.trigger ?? opts.triggers ?? (hasOwn(opts, "open") ? "manual" : null));
525
- const allowHover = triggers.has("hover");
526
- const allowFocus = triggers.has("focus");
527
- const allowClick = triggers.has("click");
528
- const isManual = triggers.has("manual") || (!allowHover && !allowFocus && !allowClick);
529
- const cleanups = [];
530
- const cleanup = () => {
531
- clearTimers();
532
- cleanups.forEach((fn) => fn());
533
- if (boundEl === el) boundEl = null;
534
- };
535
- if (hasOwn(opts, "open")) {
536
- if (uiIsReactive(opts.open)) {
537
- JSswift.reactive.effect(() => {
538
- if (!boundEl) return;
539
- if (uiUnwrap(getOptions().open)) open(boundEl);
540
- else hide(true);
541
- }, "UI.Menu:open");
542
- } else if (opts.open === true) {
543
- open(el);
544
- } else if (opts.open === false) {
545
- hide(true);
546
- }
547
- return cleanup;
548
- }
549
- if (isManual || uiUnwrap(opts.disabled)) return cleanup;
550
- if (allowHover) {
551
- const onEnter = () => show(el);
552
- const onLeave = () => hide();
553
- el.addEventListener("mouseenter", onEnter);
554
- el.addEventListener("mouseleave", onLeave);
555
- cleanups.push(() => {
556
- el.removeEventListener("mouseenter", onEnter);
557
- el.removeEventListener("mouseleave", onLeave);
558
- });
559
- }
560
- if (allowFocus) {
561
- const onFocus = () => show(el);
562
- const onBlur = () => hide();
563
- el.addEventListener("focus", onFocus);
564
- el.addEventListener("blur", onBlur);
565
- cleanups.push(() => {
566
- el.removeEventListener("focus", onFocus);
567
- el.removeEventListener("blur", onBlur);
568
- });
569
- }
570
- if (allowClick) {
571
- const onClick = (e) => {
572
- getOptions().onTriggerClick?.(e);
573
- if (e.defaultPrevented) return;
574
- toggle(el);
575
- };
576
- el.addEventListener("click", onClick);
577
- cleanups.push(() => el.removeEventListener("click", onClick));
578
- }
579
- return cleanup;
580
- };
581
- const api = {
582
- open,
583
- close,
584
- show,
585
- hide,
586
- toggle,
587
- update,
588
- bind,
589
- isOpen,
590
- entry: () => entry,
591
- props: () => ({ ...currentProps })
592
- };
593
-
594
- return api;
595
- };
596
- if (JSswift.isDev?.()) {
597
- UI.meta = UI.meta || {};
598
- UI.meta.Menu = {
599
- signature: "UI.Menu(props) | UI.Menu(props, ...children) -> { open, close, show, hide, toggle, update, bind, isOpen }",
600
- props: {
601
- title: "String|Node|Function|Array|({ close })=>Node",
602
- subtitle: "String|Node|Function|Array|({ close })=>Node",
603
- eyebrow: "String|Node|Function|Array|({ close })=>Node",
604
- icon: "String|Node|Function|Array",
605
- content: "Node|Function|Array|({ close })=>Node",
606
- body: "Alias of content",
607
- items: "Array<string|object>|Function|Array",
608
- before: "Node|Function|Array",
609
- after: "Node|Function|Array",
610
- footer: "Node|Function|Array",
611
- status: "Node|Function|Array",
612
- empty: "Node|Function|Array",
613
- size: "xs|sm|md|lg|xl",
614
- state: "primary|secondary|warning|danger|success|info|light|dark",
615
- color: "Alias of state",
616
- trigger: "click|hover|focus|manual|Array",
617
- placement: "string",
618
- offsetX: "number",
619
- offsetY: "number",
620
- offset: "Alias of offsetY",
621
- width: "string|number",
622
- minWidth: "string|number",
623
- maxWidth: "string|number",
624
- maxHeight: "string|number",
625
- bodyMaxHeight: "string|number",
626
- contentMaxHeight: "Alias of bodyMaxHeight",
627
- closeOnSelect: "boolean",
628
- closeOnOutside: "boolean",
629
- closeOnEsc: "boolean",
630
- autoFocus: "boolean",
631
- anchorEl: "HTMLElement|VirtualAnchor",
632
- triggerEl: "Alias of anchorEl",
633
- target: "Alias of anchorEl",
634
- slots: "{ before?, icon?, eyebrow?, title?, subtitle?, header?, content?, body?, item?, itemTitle?, itemSubtitle?, itemBadge?, itemShortcut?, groupLabel?, empty?, status?, footer?, after?, default? }",
635
- class: "string",
636
- panelClass: "string",
637
- overlayClass: "string",
638
- style: "object",
639
- panelStyle: "object",
640
- onOpen: "function",
641
- onClose: "function",
642
- onItemClick: "(item, ctx, event) => void",
643
- onTriggerClick: "function"
644
- },
645
- slots: {
646
- before: "Region above header/body ({ close })",
647
- icon: "Menu icon",
648
- eyebrow: "Menu eyebrow ({ close })",
649
- title: "Menu title ({ close })",
650
- subtitle: "Menu subtitle ({ close })",
651
- header: "Header custom ({ close })",
652
- content: "Custom content ({ close })",
653
- body: "Alias of content ({ close })",
654
- item: "Custom item body ({ item, close })",
655
- itemTitle: "Item title ({ item, close })",
656
- itemSubtitle: "Item subtitle ({ item, close })",
657
- itemBadge: "Item badge ({ item, close })",
658
- itemShortcut: "Item shortcut ({ item, close })",
659
- groupLabel: "Group label ({ item, close })",
660
- empty: "Empty state ({ close })",
661
- status: "Status row ({ close })",
662
- footer: "Footer actions ({ close })",
663
- after: "Region below body/footer ({ close })",
664
- default: "Fallback content ({ close })"
665
- },
666
- events: {
667
- onOpen: "void",
668
- onClose: "void",
669
- onItemClick: "item click"
670
- },
671
- returns: "Object { open(), close(), show(), hide(), toggle(), update(), bind(), isOpen() }",
672
- description: "Standardized menu overlay with item model, rich slots, bindable triggers, header/footer, and imperative API."
673
- };
674
- }
675
-
676
- UI.Popover = (...args) => {
677
- const { props, children } = JSswift.uiNormalizeArgs(args);
678
- const slots = props.slots || {};
679
- const stateList = ["primary", "secondary", "warning", "danger", "success", "info", "light", "dark"];
680
- const sizeList = ["xs", "sm", "md", "lg", "xl"];
681
- let currentProps = { ...props };
682
- let entry = null;
683
- let boundEl = null;
684
- let lastActive = null;
685
- let openTimer = null;
686
- let closeTimer = null;
687
-
688
- const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
689
- const resolveRender = (value, ctx) => typeof value === "function" ? value(ctx) : value;
690
- const splitClasses = (value) => String(value || "").split(/\s+/).filter(Boolean);
691
- const isPlainObject = (value) => value && typeof value === "object" && !value.nodeType && !Array.isArray(value) && !(value instanceof Function);
692
- const isAnchorLike = (value) => !!value && typeof value === "object" && (value.nodeType === 1 || typeof value.getBoundingClientRect === "function");
693
- const clearTimers = () => {
694
- clearTimeout(openTimer);
695
- clearTimeout(closeTimer);
696
- openTimer = null;
697
- closeTimer = null;
698
- };
699
- const setTrackedClasses = (target, key, classes) => {
700
- if (!target) return;
701
- const prev = target[key] || [];
702
- if (prev.length) target.classList.remove(...prev);
703
- const next = (classes || []).filter(Boolean);
704
- if (next.length) target.classList.add(...next);
705
- target[key] = next;
706
- };
707
- const setStyleValue = (target, name, value, formatter = (v) => v) => {
708
- if (!target) return;
709
- if (value == null || value === "") {
710
- target.style.removeProperty(name);
711
- return;
712
- }
713
- target.style.setProperty(name, formatter(value));
714
- };
715
- const getOptions = () => currentProps;
716
- const getStateClass = (opts) => {
717
- const value = uiUnwrap(opts.state ?? opts.color);
718
- if (!value) return "";
719
- return stateList.includes(value) ? value : String(value);
720
- };
721
- const getSizeClass = (opts) => {
722
- const value = uiUnwrap(opts.size);
723
- return (typeof value === "string" && sizeList.includes(value)) ? value : "";
724
- };
725
- const getAlignClass = (opts) => {
726
- const raw = String(uiUnwrap(opts.actionsAlign ?? opts.footerAlign ?? opts.alignActions ?? "end")).toLowerCase();
727
- if (["start", "left"].includes(raw)) return "start";
728
- if (["center", "middle"].includes(raw)) return "center";
729
- if (["between", "space-between"].includes(raw)) return "between";
730
- if (["stretch", "full"].includes(raw)) return "stretch";
731
- return "end";
732
- };
733
- const getPlacement = (opts) => String(uiUnwrap(opts.placement ?? opts.position ?? "bottom-start"));
734
- const getNumber = (value, fallback) => {
735
- const raw = uiUnwrap(value);
736
- if (raw == null || raw === "") return fallback;
737
- const n = Number(raw);
738
- return Number.isFinite(n) ? n : fallback;
739
- };
740
- const getDelay = () => getNumber(getOptions().delay, 0);
741
- const getHideDelay = () => getNumber(getOptions().hideDelay, 120);
742
- const isClosable = (opts) => {
743
- const value = opts.closable ?? opts.dismissible ?? opts.closeButton;
744
- return value === true;
745
- };
746
- const getAnchor = (anchorOverride = null) => {
747
- if (anchorOverride) return anchorOverride;
748
- const opts = getOptions();
749
- return boundEl || uiUnwrap(opts.anchorEl ?? opts.triggerEl ?? opts.target ?? opts.anchor) || null;
750
- };
751
- const parseTriggers = (value) => {
752
- if (value == null || value === true) return new Set(["click"]);
753
- if (value === false) return new Set(["manual"]);
754
- const raw = Array.isArray(value) ? value : String(value).split(/[\s,|/]+/);
755
- const out = new Set();
756
- raw.forEach((item) => {
757
- const key = String(item || "").trim().toLowerCase();
758
- if (!key) return;
759
- if (key === "mouseenter" || key === "mouseover") out.add("hover");
760
- else if (key === "blur" || key === "focusin") out.add("focus");
761
- else if (key === "tap") out.add("click");
762
- else out.add(key);
763
- });
764
- return out.size ? out : new Set(["manual"]);
765
- };
766
- const parseOpenArgs = (arg1, arg2) => {
767
- let anchorEl = null;
768
- let nextProps = null;
769
- if (isAnchorLike(arg1)) {
770
- anchorEl = arg1;
771
- if (isPlainObject(arg2)) nextProps = arg2;
772
- } else if (isPlainObject(arg1)) {
773
- nextProps = arg1;
774
- const maybeAnchor = arg1.anchorEl ?? arg1.triggerEl ?? arg1.target ?? arg1.anchor;
775
- if (isAnchorLike(maybeAnchor)) anchorEl = maybeAnchor;
776
- } else if (arg1 != null) {
777
- anchorEl = arg1;
778
- }
779
- return { anchorEl, nextProps };
780
- };
781
- const buildContent = () => {
782
- const opts = getOptions();
783
- const ctx = {
784
- close,
785
- dismiss: close,
786
- open,
787
- toggle,
788
- update,
789
- isOpen,
790
- entry: () => entry,
791
- anchorEl: getAnchor(),
792
- props: opts
793
- };
794
- const iconFallback = opts.icon != null
795
- ? (typeof opts.icon === "string"
796
- ? UI.Icon({ name: opts.icon, size: opts.iconSize || "md" })
797
- : JSswift.ui.slot(opts.icon, { as: "icon" }))
798
- : null;
799
- const eyebrowNodes = renderSlotToArray(slots, "eyebrow", ctx, resolveRender(opts.eyebrow, ctx));
800
- const titleNodes = renderSlotToArray(slots, "title", ctx, resolveRender(opts.title ?? opts.heading ?? opts.label, ctx));
801
- const subtitleNodes = renderSlotToArray(slots, "subtitle", ctx, resolveRender(opts.subtitle ?? opts.description, ctx));
802
- const iconNodes = renderSlotToArray(slots, "icon", ctx, iconFallback);
803
- const headerNodes = renderSlotToArray(slots, "header", ctx, resolveRender(opts.headerContent ?? opts.head ?? opts.header, ctx));
804
- const closeSlotNodes = isClosable(opts)
805
- ? renderSlotToArray(slots, "close", ctx, UI.Btn({
806
- class: "cms-dialog-close",
807
- size: "sm",
808
- outline: true,
809
- "aria-label": opts.closeLabel || "Chiudi popover",
810
- "data-popover-close": true
811
- }, UI.Icon({ name: opts.closeIcon || "close", size: "sm" })))
812
- : [];
813
-
814
- let bodyRaw = opts.content ?? opts.body ?? opts.message ?? opts.text;
815
- if (bodyRaw == null && children && children.length) bodyRaw = children;
816
- let contentNodes = renderSlotToArray(slots, "content", ctx, resolveRender(bodyRaw, ctx));
817
- if (!contentNodes.length) contentNodes = renderSlotToArray(slots, "body", ctx, resolveRender(bodyRaw, ctx));
818
- if (!contentNodes.length) contentNodes = renderSlotToArray(slots, "default", ctx, resolveRender(bodyRaw, ctx));
819
-
820
- const footerRaw = resolveRender(opts.footer ?? opts.actions, ctx);
821
- let footerNodes = renderSlotToArray(slots, "footer", ctx, footerRaw);
822
- if (!footerNodes.length) footerNodes = renderSlotToArray(slots, "actions", ctx, footerRaw);
823
-
824
- let headerEl = null;
825
- if (headerNodes.length) {
826
- headerEl = _.div({ class: "cms-dialog-head cms-dialog-head-custom" }, ...headerNodes);
827
- } else if (eyebrowNodes.length || titleNodes.length || subtitleNodes.length || iconNodes.length || closeSlotNodes.length) {
828
- headerEl = _.div(
829
- { class: "cms-dialog-head" },
830
- iconNodes.length ? _.div({ class: "cms-dialog-icon" }, ...iconNodes) : null,
831
- _.div(
832
- { class: "cms-dialog-head-main" },
833
- eyebrowNodes.length ? _.div({ class: "cms-dialog-eyebrow" }, ...eyebrowNodes) : null,
834
- titleNodes.length ? _.div({ class: "cms-dialog-title" }, ...titleNodes) : null,
835
- subtitleNodes.length ? _.div({ class: "cms-dialog-subtitle" }, ...subtitleNodes) : null
836
- ),
837
- closeSlotNodes.length ? _.div({ class: "cms-dialog-close-wrap" }, ...closeSlotNodes) : null
838
- );
839
- }
840
-
841
- const sections = [];
842
- if (headerEl) sections.push(headerEl);
843
- if (contentNodes.length) {
844
- sections.push(_.div({ class: uiClass(["cms-dialog-body", opts.bodyClass]) }, ...contentNodes));
845
- }
846
- if (footerNodes.length) {
847
- sections.push(_.div({
848
- class: uiClass([
849
- "cms-dialog-actions",
850
- `is-${getAlignClass(opts)}`,
851
- uiWhen(opts.stackActions, "is-stacked"),
852
- opts.actionsClass,
853
- opts.footerClass
854
- ])
855
- }, ...footerNodes));
856
- }
857
- if (!sections.length) {
858
- sections.push(_.div({ class: uiClass(["cms-dialog-body", opts.bodyClass]) }));
859
- }
860
-
861
- return _.div({
862
- class: uiClass([
863
- "cms-dialog-shell",
864
- "cms-popover-shell",
865
- uiWhen(!!headerEl, "has-head"),
866
- uiWhen(footerNodes.length > 0, "has-footer"),
867
- uiWhen(opts.divider !== false, "with-divider")
868
- ])
869
- }, ...sections);
870
- };
871
- const applyEntryOptions = (currentEntry) => {
872
- if (!currentEntry?.panel) return;
873
- const opts = getOptions();
874
- const stateClass = getStateClass(opts);
875
- const sizeClass = getSizeClass(opts);
876
- setTrackedClasses(currentEntry.panel, "_popoverClassTokens", [
877
- "cms-dialog",
878
- "cms-popover",
879
- "cms-singularity",
880
- "cms-clear-set",
881
- sizeClass ? `cms-dialog-${sizeClass}` : "",
882
- stateClass,
883
- stateClass ? `cms-state-${stateClass}` : "",
884
- uiUnwrap(opts.scrollable) ? "scrollable" : "",
885
- uiUnwrap(opts.stickyHeader) ? "sticky-head" : "",
886
- uiUnwrap(opts.stickyActions) ? "sticky-actions" : "",
887
- uiUnwrap(opts.borderless) ? "borderless" : "",
888
- ...splitClasses(opts.class),
889
- ...splitClasses(opts.panelClass)
890
- ]);
891
- setTrackedClasses(currentEntry.overlay, "_popoverOverlayClassTokens", [
892
- ...splitClasses(opts.overlayClass)
893
- ]);
894
- currentEntry.panel.dataset.placement = getPlacement(opts);
895
- setStyleValue(currentEntry.panel, "--cms-dialog-width", uiUnwrap(opts.width), toCssSize);
896
- setStyleValue(currentEntry.panel, "--cms-dialog-min-width", uiUnwrap(opts.minWidth), toCssSize);
897
- setStyleValue(currentEntry.panel, "--cms-dialog-max-width", uiUnwrap(opts.maxWidth), toCssSize);
898
- setStyleValue(currentEntry.panel, "--cms-dialog-max-height", uiUnwrap(opts.maxHeight), toCssSize);
899
- setStyleValue(currentEntry.panel, "--cms-dialog-body-max-height", uiUnwrap(opts.bodyMaxHeight ?? opts.contentMaxHeight), toCssSize);
900
- if (opts.style) Object.assign(currentEntry.panel.style, opts.style);
901
- setPropertyProps(currentEntry.panel, opts);
902
- currentEntry.panel.setAttribute("role", opts.role || "dialog");
903
- currentEntry.panel.setAttribute("aria-modal", opts.modal === true ? "true" : "false");
904
- if (opts.ariaLabel) currentEntry.panel.setAttribute("aria-label", opts.ariaLabel);
905
- else currentEntry.panel.removeAttribute("aria-label");
906
- };
907
- const renderOpenContent = () => {
908
- if (!entry?.panel) return;
909
- entry.panel.replaceChildren(buildContent());
910
- };
911
- const close = () => {
912
- clearTimers();
913
- if (!entry) return;
914
- const toClose = entry;
915
- entry = null;
916
- overlayLeave(toClose, () => JSswift.overlay.close(toClose.id));
917
- };
918
- const update = (nextProps = {}) => {
919
- if (nextProps && typeof nextProps === "object") currentProps = { ...currentProps, ...nextProps };
920
- if (entry) {
921
- applyEntryOptions(entry);
922
- renderOpenContent();
923
- }
924
- return api;
925
- };
926
- const open = (arg1 = null, arg2 = null) => {
927
- clearTimers();
928
- const { anchorEl, nextProps } = parseOpenArgs(arg1, arg2);
929
- if (nextProps) currentProps = { ...currentProps, ...nextProps };
930
- const opts = getOptions();
931
- const anchor = getAnchor(anchorEl);
932
- if (!anchor || uiUnwrap(opts.disabled)) return null;
933
- if (entry && entry._anchorEl === anchor) {
934
- applyEntryOptions(entry);
935
- renderOpenContent();
936
- return entry;
937
- }
938
- if (entry) close();
939
- lastActive = document.activeElement;
940
- const activeTriggers = parseTriggers(opts.trigger ?? opts.triggers ?? (hasOwn(opts, "open") ? "manual" : null));
941
- const allowHover = activeTriggers.has("hover");
942
- const allowFocus = activeTriggers.has("focus");
943
- let currentRef = null;
944
- entry = JSswift.overlay.open(() => buildContent(), {
945
- type: "popover",
946
- anchorEl: anchor,
947
- placement: getPlacement(opts),
948
- offsetX: opts.offsetX ?? 0,
949
- offsetY: opts.offsetY ?? opts.offset ?? 10,
950
- backdrop: opts.backdrop === true,
951
- lockScroll: opts.lockScroll === true,
952
- trapFocus: opts.trapFocus === true,
953
- autoFocus: opts.autoFocus === true,
954
- closeOnOutside: opts.closeOnOutside !== false,
955
- closeOnBackdrop: opts.closeOnBackdrop ?? opts.backdrop === true,
956
- closeOnEsc: opts.closeOnEsc !== false,
957
- className: uiClassStatic(["cms-dialog", "cms-popover"]),
958
- onClose: () => {
959
- currentRef?._popoverCleanup?.();
960
- getOptions().onClose?.(currentRef);
961
- if (getOptions().returnFocus !== false && lastActive && typeof lastActive.focus === "function") {
962
- setTimeout(() => lastActive.focus(), 0);
963
- }
964
- }
965
- });
966
- if (!entry?.panel) return entry;
967
- const current = entry;
968
- currentRef = current;
969
- current._anchorEl = anchor;
970
- const cancelHide = () => {
971
- clearTimeout(closeTimer);
972
- closeTimer = null;
973
- };
974
- const scheduleHide = () => {
975
- if (!allowHover && !allowFocus) return;
976
- hide();
977
- };
978
- const onPanelClick = (e) => {
979
- const target = e.target;
980
- if (!target || !target.closest) return;
981
- if (target.closest("[data-popover-close]") || target.closest("[data-dialog-close]")) {
982
- close();
983
- return;
984
- }
985
- if (getOptions().closeOnSelect === true && target.closest("[data-popover-select]")) {
986
- close();
987
- }
988
- };
989
- current.panel.addEventListener("click", onPanelClick);
990
- if (allowHover || allowFocus) {
991
- current.panel.addEventListener("mouseenter", cancelHide);
992
- current.panel.addEventListener("mouseleave", scheduleHide);
993
- current.panel.addEventListener("focusin", cancelHide);
994
- current.panel.addEventListener("focusout", scheduleHide);
995
- }
996
- current._popoverCleanup = () => {
997
- current.panel?.removeEventListener("click", onPanelClick);
998
- current.panel?.removeEventListener("mouseenter", cancelHide);
999
- current.panel?.removeEventListener("mouseleave", scheduleHide);
1000
- current.panel?.removeEventListener("focusin", cancelHide);
1001
- current.panel?.removeEventListener("focusout", scheduleHide);
1002
- };
1003
- applyEntryOptions(current);
1004
- overlayEnter(current);
1005
- getOptions().onOpen?.(current);
1006
- return current;
1007
- };
1008
- const show = (anchorEl, nextProps = null) => {
1009
- clearTimeout(closeTimer);
1010
- closeTimer = null;
1011
- clearTimeout(openTimer);
1012
- openTimer = setTimeout(() => open(anchorEl, nextProps), getDelay());
1013
- };
1014
- const hide = (immediate = false) => {
1015
- clearTimeout(openTimer);
1016
- openTimer = null;
1017
- if (!entry) return;
1018
- clearTimeout(closeTimer);
1019
- if (immediate) {
1020
- close();
1021
- return;
1022
- }
1023
- closeTimer = setTimeout(() => close(), getHideDelay());
1024
- };
1025
- const toggle = (arg1 = null, arg2 = null) => isOpen() ? (close(), null) : open(arg1, arg2);
1026
- const isOpen = () => !!entry;
1027
- const bind = (el) => {
1028
- if (!el) return () => { };
1029
- boundEl = el;
1030
- const opts = getOptions();
1031
- const triggers = parseTriggers(opts.trigger ?? opts.triggers ?? (hasOwn(opts, "open") ? "manual" : null));
1032
- const allowHover = triggers.has("hover");
1033
- const allowFocus = triggers.has("focus");
1034
- const allowClick = triggers.has("click");
1035
- const isManual = triggers.has("manual") || (!allowHover && !allowFocus && !allowClick);
1036
- const cleanups = [];
1037
- const cleanup = () => {
1038
- clearTimers();
1039
- cleanups.forEach((fn) => fn());
1040
- if (boundEl === el) boundEl = null;
1041
- };
1042
- if (hasOwn(opts, "open")) {
1043
- if (uiIsReactive(opts.open)) {
1044
- JSswift.reactive.effect(() => {
1045
- if (!boundEl) return;
1046
- if (uiUnwrap(getOptions().open)) open(boundEl);
1047
- else hide(true);
1048
- }, "UI.Popover:open");
1049
- } else if (opts.open === true) {
1050
- open(el);
1051
- } else if (opts.open === false) {
1052
- hide(true);
1053
- }
1054
- return cleanup;
1055
- }
1056
- if (isManual || uiUnwrap(opts.disabled)) return cleanup;
1057
- if (allowHover) {
1058
- const onEnter = () => show(el);
1059
- const onLeave = () => hide();
1060
- el.addEventListener("mouseenter", onEnter);
1061
- el.addEventListener("mouseleave", onLeave);
1062
- cleanups.push(() => {
1063
- el.removeEventListener("mouseenter", onEnter);
1064
- el.removeEventListener("mouseleave", onLeave);
1065
- });
1066
- }
1067
- if (allowFocus) {
1068
- const onFocus = () => show(el);
1069
- const onBlur = () => hide();
1070
- el.addEventListener("focus", onFocus);
1071
- el.addEventListener("blur", onBlur);
1072
- cleanups.push(() => {
1073
- el.removeEventListener("focus", onFocus);
1074
- el.removeEventListener("blur", onBlur);
1075
- });
1076
- }
1077
- if (allowClick) {
1078
- const onClick = (e) => {
1079
- getOptions().onTriggerClick?.(e);
1080
- if (e.defaultPrevented) return;
1081
- toggle(el);
1082
- };
1083
- el.addEventListener("click", onClick);
1084
- cleanups.push(() => el.removeEventListener("click", onClick));
1085
- }
1086
- return cleanup;
1087
- };
1088
- const api = {
1089
- open,
1090
- close,
1091
- show,
1092
- hide,
1093
- toggle,
1094
- update,
1095
- bind,
1096
- isOpen,
1097
- entry: () => entry,
1098
- props: () => ({ ...currentProps })
1099
- };
1100
-
1101
- return api;
1102
- };
1103
- if (JSswift.isDev?.()) {
1104
- UI.meta = UI.meta || {};
1105
- UI.meta.Popover = {
1106
- signature: "UI.Popover(props) | UI.Popover(props, ...children) -> { open, close, show, hide, toggle, update, bind, isOpen }",
1107
- props: {
1108
- title: "String|Node|Function|Array|({ close })=>Node",
1109
- subtitle: "String|Node|Function|Array|({ close })=>Node",
1110
- eyebrow: "String|Node|Function|Array|({ close })=>Node",
1111
- icon: "String|Node|Function|Array",
1112
- content: "Node|Function|Array|({ close })=>Node",
1113
- body: "Alias of content",
1114
- actions: "Node|Function|Array|({ close })=>Node",
1115
- footer: "Alias of actions",
1116
- size: "xs|sm|md|lg|xl",
1117
- state: "primary|secondary|warning|danger|success|info|light|dark",
1118
- color: "Alias of state",
1119
- trigger: "click|hover|focus|manual|Array",
1120
- placement: "string",
1121
- offsetX: "number",
1122
- offsetY: "number",
1123
- offset: "Alias of offsetY",
1124
- width: "string|number",
1125
- minWidth: "string|number",
1126
- maxWidth: "string|number",
1127
- maxHeight: "string|number",
1128
- bodyMaxHeight: "string|number",
1129
- contentMaxHeight: "Alias of bodyMaxHeight",
1130
- backdrop: "boolean",
1131
- lockScroll: "boolean",
1132
- trapFocus: "boolean",
1133
- autoFocus: "boolean",
1134
- closeButton: "boolean",
1135
- closable: "boolean",
1136
- closeOnSelect: "boolean",
1137
- closeOnOutside: "boolean",
1138
- closeOnBackdrop: "boolean",
1139
- closeOnEsc: "boolean",
1140
- open: "boolean|rod|signal",
1141
- anchorEl: "HTMLElement|VirtualAnchor",
1142
- triggerEl: "Alias of anchorEl",
1143
- target: "Alias of anchorEl",
1144
- slots: "{ icon?, eyebrow?, title?, subtitle?, header?, content?, body?, footer?, actions?, close?, default? }",
1145
- class: "string",
1146
- panelClass: "string",
1147
- overlayClass: "string",
1148
- style: "object",
1149
- onOpen: "function",
1150
- onClose: "function",
1151
- onTriggerClick: "function"
1152
- },
1153
- slots: {
1154
- icon: "Popover icon",
1155
- eyebrow: "Popover eyebrow ({ close })",
1156
- title: "Popover title ({ close })",
1157
- subtitle: "Popover subtitle ({ close })",
1158
- header: "Header custom ({ close })",
1159
- content: "Popover body ({ close })",
1160
- body: "Alias of content ({ close })",
1161
- footer: "Popover footer ({ close })",
1162
- actions: "Alias of footer ({ close })",
1163
- close: "Close button slot ({ close })",
1164
- default: "Fallback body ({ close })"
1165
- },
1166
- events: {
1167
- onOpen: "void",
1168
- onClose: "void"
1169
- },
1170
- returns: "Object { open(), close(), show(), hide(), toggle(), update(), bind(), isOpen() }",
1171
- description: "Standardized anchored popover with rich layout, complete slots, bindable triggers, and imperative API."
1172
- };
1173
- }
1174
-
1175
- UI.ContextMenu = (...args) => {
1176
- const { props, children } = JSswift.uiNormalizeArgs(args);
1177
- let currentProps = { ...props };
1178
- let lastPoint = null;
1179
- let lastAnchor = null;
1180
- let lastTriggerEl = null;
1181
-
1182
- const isPlainObject = (value) => value && typeof value === "object" && !value.nodeType && !Array.isArray(value) && !(value instanceof Function);
1183
- const isAnchorLike = (value) => !!value && typeof value === "object" && (value.nodeType === 1 || typeof value.getBoundingClientRect === "function");
1184
- const isEventLike = (value) => !!value && typeof value === "object" && ("clientX" in value || "type" in value || "target" in value || "currentTarget" in value);
1185
- const isPointLike = (value) => !!value && Number.isFinite(Number(value.x)) && Number.isFinite(Number(value.y));
1186
- const getOptions = () => currentProps;
1187
- const normalizePoint = (x, y) => {
1188
- const px = Number(x);
1189
- const py = Number(y);
1190
- if (!Number.isFinite(px) || !Number.isFinite(py)) return null;
1191
- return { x: px, y: py };
1192
- };
1193
- const createVirtualAnchor = (point) => point ? {
1194
- _cmsContextPoint: point,
1195
- getBoundingClientRect: () => ({
1196
- top: point.y,
1197
- bottom: point.y,
1198
- left: point.x,
1199
- right: point.x,
1200
- width: 0,
1201
- height: 0
1202
- })
1203
- } : null;
1204
- const getElementPoint = (el) => {
1205
- if (!isAnchorLike(el)) return null;
1206
- const rect = el.getBoundingClientRect();
1207
- return normalizePoint(rect.left + Math.min(rect.width / 2, 24), rect.top + Math.min(rect.height / 2, 24));
1208
- };
1209
- const extractAnchor = (value) => uiUnwrap(value?.anchorEl ?? value?.triggerEl ?? value?.target ?? value?.anchor ?? null);
1210
- const getEventPoint = (event, fallbackEl = null) => {
1211
- if (!event) return getElementPoint(fallbackEl || lastTriggerEl);
1212
- const point = normalizePoint(event.clientX, event.clientY);
1213
- if (point) return point;
1214
- const anchor = extractAnchor({ anchorEl: fallbackEl || event.currentTarget || event.target || lastTriggerEl });
1215
- return getElementPoint(anchor);
1216
- };
1217
- const stripContextKeys = (value = {}) => {
1218
- if (!isPlainObject(value)) return {};
1219
- const next = { ...value };
1220
- delete next.event;
1221
- delete next.point;
1222
- delete next.x;
1223
- delete next.y;
1224
- return next;
1225
- };
1226
- const buildMenuProps = () => {
1227
- const opts = getOptions();
1228
- return {
1229
- ...opts,
1230
- trigger: "manual",
1231
- placement: opts.placement || "bottom-start",
1232
- offsetX: opts.offsetX ?? 0,
1233
- offsetY: opts.offsetY ?? opts.offset ?? 4,
1234
- closeOnOutside: opts.closeOnOutside !== false,
1235
- closeOnEsc: opts.closeOnEsc !== false,
1236
- role: opts.role || "menu",
1237
- ariaLabel: opts.ariaLabel || opts.title || "Context menu",
1238
- class: uiClass(["cms-context-menu-shell", opts.class]),
1239
- panelClass: uiClass(["cms-context-menu-panel", opts.panelClass])
1240
- };
1241
- };
1242
- const menu = UI.Menu(buildMenuProps(), ...children);
1243
-
1244
- const resolveOpenInput = (arg1 = null, arg2 = null, arg3 = null) => {
1245
- let point = null;
1246
- let anchor = null;
1247
- let nextProps = null;
1248
-
1249
- if (isEventLike(arg1)) {
1250
- point = getEventPoint(arg1, lastTriggerEl);
1251
- nextProps = isPlainObject(arg2) ? stripContextKeys(arg2) : null;
1252
- } else if (typeof arg1 === "number" || typeof arg2 === "number") {
1253
- point = normalizePoint(arg1, arg2);
1254
- nextProps = isPlainObject(arg3) ? stripContextKeys(arg3) : null;
1255
- } else if (isPointLike(arg1)) {
1256
- point = normalizePoint(arg1.x, arg1.y);
1257
- nextProps = isPlainObject(arg2) ? stripContextKeys(arg2) : null;
1258
- } else if (isAnchorLike(arg1)) {
1259
- anchor = arg1;
1260
- nextProps = isPlainObject(arg2) ? stripContextKeys(arg2) : null;
1261
- } else if (isPlainObject(arg1)) {
1262
- const rawPoint = isPointLike(arg1.point) ? arg1.point : (isPointLike(arg1) ? arg1 : null);
1263
- const rawAnchor = extractAnchor(arg1);
1264
- const rawEvent = arg1.event;
1265
- point = rawPoint ? normalizePoint(rawPoint.x, rawPoint.y) : getEventPoint(rawEvent, rawAnchor || lastTriggerEl);
1266
- anchor = rawAnchor && isAnchorLike(rawAnchor) ? rawAnchor : null;
1267
- nextProps = {
1268
- ...stripContextKeys(arg1),
1269
- ...(isPlainObject(arg2) ? stripContextKeys(arg2) : {})
1270
- };
1271
- } else if (lastPoint) {
1272
- point = { ...lastPoint };
1273
- }
1274
-
1275
- return { point, anchor, nextProps };
1276
- };
1277
- const rememberAnchor = (anchor, point = null) => {
1278
- lastAnchor = anchor || lastAnchor;
1279
- lastPoint = point || (anchor && anchor._cmsContextPoint) || lastPoint;
1280
- };
1281
- const mergeProps = (nextProps = null) => {
1282
- if (isPlainObject(nextProps) && Object.keys(nextProps).length) {
1283
- currentProps = { ...currentProps, ...nextProps };
1284
- }
1285
- };
1286
- const getOpenAnchor = (inputAnchor = null, inputPoint = null) => {
1287
- if (inputPoint) return createVirtualAnchor(inputPoint);
1288
- if (inputAnchor && isAnchorLike(inputAnchor)) return inputAnchor;
1289
- const configAnchor = extractAnchor(getOptions());
1290
- if (isAnchorLike(configAnchor)) return configAnchor;
1291
- if (lastPoint) return createVirtualAnchor(lastPoint);
1292
- if (isAnchorLike(lastAnchor)) return lastAnchor;
1293
- if (isAnchorLike(lastTriggerEl)) return lastTriggerEl;
1294
- return null;
1295
- };
1296
- const open = (arg1 = null, arg2 = null, arg3 = null) => {
1297
- const { point, anchor, nextProps } = resolveOpenInput(arg1, arg2, arg3);
1298
- mergeProps(nextProps);
1299
- const openAnchor = getOpenAnchor(anchor, point);
1300
- if (!openAnchor) return null;
1301
- rememberAnchor(openAnchor, point);
1302
- return menu.open(openAnchor, buildMenuProps());
1303
- };
1304
- const openAt = (x, y, nextProps = null) => open(x, y, nextProps);
1305
- const openFromEvent = (event, nextProps = null) => open(event, nextProps);
1306
- const close = () => {
1307
- menu.close();
1308
- return api;
1309
- };
1310
- const hide = (immediate = true) => {
1311
- if (immediate === false) menu.hide(false);
1312
- else menu.close();
1313
- return api;
1314
- };
1315
- const show = (arg1 = null, arg2 = null, arg3 = null) => open(arg1, arg2, arg3);
1316
- const toggle = (arg1 = null, arg2 = null, arg3 = null) => menu.isOpen() ? (close(), null) : open(arg1, arg2, arg3);
1317
- const update = (nextProps = {}) => {
1318
- mergeProps(stripContextKeys(nextProps));
1319
- menu.update(buildMenuProps());
1320
- return api;
1321
- };
1322
- const bind = (el, bindProps = null) => {
1323
- if (!el) return () => { };
1324
- const scopedProps = isPlainObject(bindProps) ? stripContextKeys(bindProps) : null;
1325
- const onContextMenu = (e) => {
1326
- lastTriggerEl = el;
1327
- e.preventDefault();
1328
- const result = getOptions().onTrigger?.(e, { element: el, open, close, update });
1329
- if (result === false || uiUnwrap(getOptions().disabled)) return;
1330
- open({ event: e, anchorEl: el }, scopedProps);
1331
- };
1332
- const onKeyboardOpen = (e) => {
1333
- if (e.key !== "ContextMenu" && !(e.shiftKey && e.key === "F10")) return;
1334
- lastTriggerEl = el;
1335
- e.preventDefault();
1336
- const result = getOptions().onTrigger?.(e, { element: el, open, close, update });
1337
- if (result === false || uiUnwrap(getOptions().disabled)) return;
1338
- open({ event: e, anchorEl: el }, scopedProps);
1339
- };
1340
- el.addEventListener("contextmenu", onContextMenu);
1341
- el.addEventListener("keydown", onKeyboardOpen);
1342
- return () => {
1343
- el.removeEventListener("contextmenu", onContextMenu);
1344
- el.removeEventListener("keydown", onKeyboardOpen);
1345
- };
1346
- };
1347
- const api = {
1348
- open,
1349
- openAt,
1350
- openFromEvent,
1351
- show,
1352
- hide,
1353
- close,
1354
- toggle,
1355
- update,
1356
- bind,
1357
- isOpen: menu.isOpen,
1358
- entry: menu.entry,
1359
- props: () => ({ ...currentProps })
1360
- };
1361
-
1362
- return api;
1363
- };
1364
- if (JSswift.isDev?.()) {
1365
- UI.meta = UI.meta || {};
1366
- UI.meta.ContextMenu = {
1367
- signature: "UI.ContextMenu(props) | UI.ContextMenu(props, ...children) -> { open, openAt, openFromEvent, show, hide, close, toggle, update, bind, isOpen }",
1368
- props: {
1369
- title: "String|Node|Function|Array|({ close })=>Node",
1370
- subtitle: "String|Node|Function|Array|({ close })=>Node",
1371
- eyebrow: "String|Node|Function|Array|({ close })=>Node",
1372
- icon: "String|Node|Function|Array",
1373
- content: "Node|Function|Array|({ close })=>Node",
1374
- body: "Alias of content",
1375
- items: "Array<string|object>|Function|Array",
1376
- before: "Node|Function|Array",
1377
- after: "Node|Function|Array",
1378
- footer: "Node|Function|Array",
1379
- status: "Node|Function|Array",
1380
- empty: "Node|Function|Array",
1381
- size: "xs|sm|md|lg|xl",
1382
- state: "primary|secondary|warning|danger|success|info|light|dark",
1383
- color: "Alias of state",
1384
- placement: "string",
1385
- offsetX: "number",
1386
- offsetY: "number",
1387
- offset: "Alias of offsetY",
1388
- width: "string|number",
1389
- minWidth: "string|number",
1390
- maxWidth: "string|number",
1391
- maxHeight: "string|number",
1392
- bodyMaxHeight: "string|number",
1393
- contentMaxHeight: "Alias of bodyMaxHeight",
1394
- closeOnSelect: "boolean",
1395
- closeOnOutside: "boolean",
1396
- closeOnEsc: "boolean",
1397
- anchorEl: "HTMLElement|VirtualAnchor",
1398
- triggerEl: "Alias of anchorEl",
1399
- target: "Alias of anchorEl",
1400
- slots: "{ before?, icon?, eyebrow?, title?, subtitle?, header?, content?, body?, item?, itemTitle?, itemSubtitle?, itemBadge?, itemShortcut?, groupLabel?, empty?, status?, footer?, after?, default? }",
1401
- class: "string",
1402
- panelClass: "string",
1403
- overlayClass: "string",
1404
- style: "object",
1405
- panelStyle: "object",
1406
- onOpen: "function",
1407
- onClose: "function",
1408
- onItemClick: "(item, ctx, event) => void",
1409
- onTrigger: "(event, ctx) => boolean|void"
1410
- },
1411
- slots: {
1412
- before: "Region above header/body ({ close })",
1413
- icon: "Context icon",
1414
- eyebrow: "Context eyebrow ({ close })",
1415
- title: "Context title ({ close })",
1416
- subtitle: "Context subtitle ({ close })",
1417
- header: "Header custom ({ close })",
1418
- content: "Context menu content ({ close })",
1419
- body: "Alias of content ({ close })",
1420
- item: "Custom item body ({ item, close })",
1421
- itemTitle: "Item title ({ item, close })",
1422
- itemSubtitle: "Item subtitle ({ item, close })",
1423
- itemBadge: "Item badge ({ item, close })",
1424
- itemShortcut: "Item shortcut ({ item, close })",
1425
- groupLabel: "Group label ({ item, close })",
1426
- empty: "Empty state ({ close })",
1427
- status: "Status row ({ close })",
1428
- footer: "Footer actions ({ close })",
1429
- after: "Region below body/footer ({ close })",
1430
- default: "Fallback content ({ close })"
1431
- },
1432
- events: {
1433
- onOpen: "void",
1434
- onClose: "void",
1435
- onItemClick: "item click",
1436
- onTrigger: "contextmenu / keyboard trigger"
1437
- },
1438
- returns: "Object { open(), openAt(), openFromEvent(), show(), hide(), close(), toggle(), update(), bind(), isOpen() }",
1439
- description: "Menu specialization for right-click and context-menu key interactions, with items, rich slots, runtime overrides, and coordinate-based positioning."
1440
- };
1441
- }
1442
- })(JSswift);
499
+ return current;
500
+ };
501
+ const show = (anchorEl, nextProps = null) => {
502
+ clearTimeout(closeTimer);
503
+ closeTimer = null;
504
+ clearTimeout(openTimer);
505
+ openTimer = setTimeout(() => open(anchorEl, nextProps), getDelay());
506
+ };
507
+ const hide = (immediate = false) => {
508
+ clearTimeout(openTimer);
509
+ openTimer = null;
510
+ if (!entry) return;
511
+ clearTimeout(closeTimer);
512
+ if (immediate) {
513
+ close();
514
+ return;
515
+ }
516
+ closeTimer = setTimeout(() => close(), getHideDelay());
517
+ };
518
+ const toggle = (arg1 = null, arg2 = null) => isOpen() ? (close(), null) : open(arg1, arg2);
519
+ const isOpen = () => !!entry;
520
+ const bind = (el) => {
521
+ if (!el) return () => { };
522
+ boundEl = el;
523
+ const opts = getOptions();
524
+ const triggers = parseTriggers(opts.trigger ?? opts.triggers ?? (hasOwn(opts, "open") ? "manual" : null));
525
+ const allowHover = triggers.has("hover");
526
+ const allowFocus = triggers.has("focus");
527
+ const allowClick = triggers.has("click");
528
+ const isManual = triggers.has("manual") || (!allowHover && !allowFocus && !allowClick);
529
+ const cleanups = [];
530
+ const cleanup = () => {
531
+ clearTimers();
532
+ cleanups.forEach((fn) => fn());
533
+ if (boundEl === el) boundEl = null;
534
+ };
535
+ if (hasOwn(opts, "open")) {
536
+ if (uiIsReactive(opts.open)) {
537
+ JSswift.reactive.effect(() => {
538
+ if (!boundEl) return;
539
+ if (uiUnwrap(getOptions().open)) open(boundEl);
540
+ else hide(true);
541
+ }, "UI.Menu:open");
542
+ } else if (opts.open === true) {
543
+ open(el);
544
+ } else if (opts.open === false) {
545
+ hide(true);
546
+ }
547
+ return cleanup;
548
+ }
549
+ if (isManual || uiUnwrap(opts.disabled)) return cleanup;
550
+ if (allowHover) {
551
+ const onEnter = () => show(el);
552
+ const onLeave = () => hide();
553
+ el.addEventListener("mouseenter", onEnter);
554
+ el.addEventListener("mouseleave", onLeave);
555
+ cleanups.push(() => {
556
+ el.removeEventListener("mouseenter", onEnter);
557
+ el.removeEventListener("mouseleave", onLeave);
558
+ });
559
+ }
560
+ if (allowFocus) {
561
+ const onFocus = () => show(el);
562
+ const onBlur = () => hide();
563
+ el.addEventListener("focus", onFocus);
564
+ el.addEventListener("blur", onBlur);
565
+ cleanups.push(() => {
566
+ el.removeEventListener("focus", onFocus);
567
+ el.removeEventListener("blur", onBlur);
568
+ });
569
+ }
570
+ if (allowClick) {
571
+ const onClick = (e) => {
572
+ getOptions().onTriggerClick?.(e);
573
+ if (e.defaultPrevented) return;
574
+ toggle(el);
575
+ };
576
+ el.addEventListener("click", onClick);
577
+ cleanups.push(() => el.removeEventListener("click", onClick));
578
+ }
579
+ return cleanup;
580
+ };
581
+ const api = {
582
+ open,
583
+ close,
584
+ show,
585
+ hide,
586
+ toggle,
587
+ update,
588
+ bind,
589
+ isOpen,
590
+ entry: () => entry,
591
+ props: () => ({ ...currentProps })
592
+ };
593
+
594
+ return api;
595
+ };
596
+
597
+
598
+ UI.Popover = (...args) => {
599
+ const { props, children } = JSswift.uiNormalizeArgs(args);
600
+ const slots = props.slots || {};
601
+ const stateList = ["primary", "secondary", "warning", "danger", "success", "info", "light", "dark"];
602
+ const sizeList = ["xs", "sm", "md", "lg", "xl"];
603
+ let currentProps = { ...props };
604
+ let entry = null;
605
+ let boundEl = null;
606
+ let lastActive = null;
607
+ let openTimer = null;
608
+ let closeTimer = null;
609
+
610
+ const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
611
+ const resolveRender = (value, ctx) => typeof value === "function" ? value(ctx) : value;
612
+ const splitClasses = (value) => String(value || "").split(/\s+/).filter(Boolean);
613
+ const isPlainObject = (value) => value && typeof value === "object" && !value.nodeType && !Array.isArray(value) && !(value instanceof Function);
614
+ const isAnchorLike = (value) => !!value && typeof value === "object" && (value.nodeType === 1 || typeof value.getBoundingClientRect === "function");
615
+ const clearTimers = () => {
616
+ clearTimeout(openTimer);
617
+ clearTimeout(closeTimer);
618
+ openTimer = null;
619
+ closeTimer = null;
620
+ };
621
+ const setTrackedClasses = (target, key, classes) => {
622
+ if (!target) return;
623
+ const prev = target[key] || [];
624
+ if (prev.length) target.classList.remove(...prev);
625
+ const next = (classes || []).filter(Boolean);
626
+ if (next.length) target.classList.add(...next);
627
+ target[key] = next;
628
+ };
629
+ const setStyleValue = (target, name, value, formatter = (v) => v) => {
630
+ if (!target) return;
631
+ if (value == null || value === "") {
632
+ target.style.removeProperty(name);
633
+ return;
634
+ }
635
+ target.style.setProperty(name, formatter(value));
636
+ };
637
+ const getOptions = () => currentProps;
638
+ const getStateClass = (opts) => {
639
+ const value = uiUnwrap(opts.state ?? opts.color);
640
+ if (!value) return "";
641
+ return stateList.includes(value) ? value : String(value);
642
+ };
643
+ const getSizeClass = (opts) => {
644
+ const value = uiUnwrap(opts.size);
645
+ return (typeof value === "string" && sizeList.includes(value)) ? value : "";
646
+ };
647
+ const getAlignClass = (opts) => {
648
+ const raw = String(uiUnwrap(opts.actionsAlign ?? opts.footerAlign ?? opts.alignActions ?? "end")).toLowerCase();
649
+ if (["start", "left"].includes(raw)) return "start";
650
+ if (["center", "middle"].includes(raw)) return "center";
651
+ if (["between", "space-between"].includes(raw)) return "between";
652
+ if (["stretch", "full"].includes(raw)) return "stretch";
653
+ return "end";
654
+ };
655
+ const getPlacement = (opts) => String(uiUnwrap(opts.placement ?? opts.position ?? "bottom-start"));
656
+ const getNumber = (value, fallback) => {
657
+ const raw = uiUnwrap(value);
658
+ if (raw == null || raw === "") return fallback;
659
+ const n = Number(raw);
660
+ return Number.isFinite(n) ? n : fallback;
661
+ };
662
+ const getDelay = () => getNumber(getOptions().delay, 0);
663
+ const getHideDelay = () => getNumber(getOptions().hideDelay, 120);
664
+ const isClosable = (opts) => {
665
+ const value = opts.closable ?? opts.dismissible ?? opts.closeButton;
666
+ return value === true;
667
+ };
668
+ const getAnchor = (anchorOverride = null) => {
669
+ if (anchorOverride) return anchorOverride;
670
+ const opts = getOptions();
671
+ return boundEl || uiUnwrap(opts.anchorEl ?? opts.triggerEl ?? opts.target ?? opts.anchor) || null;
672
+ };
673
+ const parseTriggers = (value) => {
674
+ if (value == null || value === true) return new Set(["click"]);
675
+ if (value === false) return new Set(["manual"]);
676
+ const raw = Array.isArray(value) ? value : String(value).split(/[\s,|/]+/);
677
+ const out = new Set();
678
+ raw.forEach((item) => {
679
+ const key = String(item || "").trim().toLowerCase();
680
+ if (!key) return;
681
+ if (key === "mouseenter" || key === "mouseover") out.add("hover");
682
+ else if (key === "blur" || key === "focusin") out.add("focus");
683
+ else if (key === "tap") out.add("click");
684
+ else out.add(key);
685
+ });
686
+ return out.size ? out : new Set(["manual"]);
687
+ };
688
+ const parseOpenArgs = (arg1, arg2) => {
689
+ let anchorEl = null;
690
+ let nextProps = null;
691
+ if (isAnchorLike(arg1)) {
692
+ anchorEl = arg1;
693
+ if (isPlainObject(arg2)) nextProps = arg2;
694
+ } else if (isPlainObject(arg1)) {
695
+ nextProps = arg1;
696
+ const maybeAnchor = arg1.anchorEl ?? arg1.triggerEl ?? arg1.target ?? arg1.anchor;
697
+ if (isAnchorLike(maybeAnchor)) anchorEl = maybeAnchor;
698
+ } else if (arg1 != null) {
699
+ anchorEl = arg1;
700
+ }
701
+ return { anchorEl, nextProps };
702
+ };
703
+ const buildContent = () => {
704
+ const opts = getOptions();
705
+ const ctx = {
706
+ close,
707
+ dismiss: close,
708
+ open,
709
+ toggle,
710
+ update,
711
+ isOpen,
712
+ entry: () => entry,
713
+ anchorEl: getAnchor(),
714
+ props: opts
715
+ };
716
+ const iconFallback = opts.icon != null
717
+ ? (typeof opts.icon === "string"
718
+ ? UI.Icon({ name: opts.icon, size: opts.iconSize || "md" })
719
+ : JSswift.ui.slot(opts.icon, { as: "icon" }))
720
+ : null;
721
+ const eyebrowNodes = renderSlotToArray(slots, "eyebrow", ctx, resolveRender(opts.eyebrow, ctx));
722
+ const titleNodes = renderSlotToArray(slots, "title", ctx, resolveRender(opts.title ?? opts.heading ?? opts.label, ctx));
723
+ const subtitleNodes = renderSlotToArray(slots, "subtitle", ctx, resolveRender(opts.subtitle ?? opts.description, ctx));
724
+ const iconNodes = renderSlotToArray(slots, "icon", ctx, iconFallback);
725
+ const headerNodes = renderSlotToArray(slots, "header", ctx, resolveRender(opts.headerContent ?? opts.head ?? opts.header, ctx));
726
+ const closeSlotNodes = isClosable(opts)
727
+ ? renderSlotToArray(slots, "close", ctx, UI.Btn({
728
+ class: "cms-dialog-close",
729
+ size: "sm",
730
+ outline: true,
731
+ "aria-label": opts.closeLabel || "Chiudi popover",
732
+ "data-popover-close": true
733
+ }, UI.Icon({ name: opts.closeIcon || "close", size: "sm" })))
734
+ : [];
735
+
736
+ let bodyRaw = opts.content ?? opts.body ?? opts.message ?? opts.text;
737
+ if (bodyRaw == null && children && children.length) bodyRaw = children;
738
+ let contentNodes = renderSlotToArray(slots, "content", ctx, resolveRender(bodyRaw, ctx));
739
+ if (!contentNodes.length) contentNodes = renderSlotToArray(slots, "body", ctx, resolveRender(bodyRaw, ctx));
740
+ if (!contentNodes.length) contentNodes = renderSlotToArray(slots, "default", ctx, resolveRender(bodyRaw, ctx));
741
+
742
+ const footerRaw = resolveRender(opts.footer ?? opts.actions, ctx);
743
+ let footerNodes = renderSlotToArray(slots, "footer", ctx, footerRaw);
744
+ if (!footerNodes.length) footerNodes = renderSlotToArray(slots, "actions", ctx, footerRaw);
745
+
746
+ let headerEl = null;
747
+ if (headerNodes.length) {
748
+ headerEl = _.div({ class: "cms-dialog-head cms-dialog-head-custom" }, ...headerNodes);
749
+ } else if (eyebrowNodes.length || titleNodes.length || subtitleNodes.length || iconNodes.length || closeSlotNodes.length) {
750
+ headerEl = _.div(
751
+ { class: "cms-dialog-head" },
752
+ iconNodes.length ? _.div({ class: "cms-dialog-icon" }, ...iconNodes) : null,
753
+ _.div(
754
+ { class: "cms-dialog-head-main" },
755
+ eyebrowNodes.length ? _.div({ class: "cms-dialog-eyebrow" }, ...eyebrowNodes) : null,
756
+ titleNodes.length ? _.div({ class: "cms-dialog-title" }, ...titleNodes) : null,
757
+ subtitleNodes.length ? _.div({ class: "cms-dialog-subtitle" }, ...subtitleNodes) : null
758
+ ),
759
+ closeSlotNodes.length ? _.div({ class: "cms-dialog-close-wrap" }, ...closeSlotNodes) : null
760
+ );
761
+ }
762
+
763
+ const sections = [];
764
+ if (headerEl) sections.push(headerEl);
765
+ if (contentNodes.length) {
766
+ sections.push(_.div({ class: uiClass(["cms-dialog-body", opts.bodyClass]) }, ...contentNodes));
767
+ }
768
+ if (footerNodes.length) {
769
+ sections.push(_.div({
770
+ class: uiClass([
771
+ "cms-dialog-actions",
772
+ `is-${getAlignClass(opts)}`,
773
+ uiWhen(opts.stackActions, "is-stacked"),
774
+ opts.actionsClass,
775
+ opts.footerClass
776
+ ])
777
+ }, ...footerNodes));
778
+ }
779
+ if (!sections.length) {
780
+ sections.push(_.div({ class: uiClass(["cms-dialog-body", opts.bodyClass]) }));
781
+ }
782
+
783
+ return _.div({
784
+ class: uiClass([
785
+ "cms-dialog-shell",
786
+ "cms-popover-shell",
787
+ uiWhen(!!headerEl, "has-head"),
788
+ uiWhen(footerNodes.length > 0, "has-footer"),
789
+ uiWhen(opts.divider !== false, "with-divider")
790
+ ])
791
+ }, ...sections);
792
+ };
793
+ const applyEntryOptions = (currentEntry) => {
794
+ if (!currentEntry?.panel) return;
795
+ const opts = getOptions();
796
+ const stateClass = getStateClass(opts);
797
+ const sizeClass = getSizeClass(opts);
798
+ setTrackedClasses(currentEntry.panel, "_popoverClassTokens", [
799
+ "cms-dialog",
800
+ "cms-popover",
801
+ "cms-singularity",
802
+ "cms-clear-set",
803
+ sizeClass ? `cms-dialog-${sizeClass}` : "",
804
+ stateClass,
805
+ stateClass ? `cms-state-${stateClass}` : "",
806
+ uiUnwrap(opts.scrollable) ? "scrollable" : "",
807
+ uiUnwrap(opts.stickyHeader) ? "sticky-head" : "",
808
+ uiUnwrap(opts.stickyActions) ? "sticky-actions" : "",
809
+ uiUnwrap(opts.borderless) ? "borderless" : "",
810
+ ...splitClasses(opts.class),
811
+ ...splitClasses(opts.panelClass)
812
+ ]);
813
+ setTrackedClasses(currentEntry.overlay, "_popoverOverlayClassTokens", [
814
+ ...splitClasses(opts.overlayClass)
815
+ ]);
816
+ currentEntry.panel.dataset.placement = getPlacement(opts);
817
+ setStyleValue(currentEntry.panel, "--cms-dialog-width", uiUnwrap(opts.width), toCssSize);
818
+ setStyleValue(currentEntry.panel, "--cms-dialog-min-width", uiUnwrap(opts.minWidth), toCssSize);
819
+ setStyleValue(currentEntry.panel, "--cms-dialog-max-width", uiUnwrap(opts.maxWidth), toCssSize);
820
+ setStyleValue(currentEntry.panel, "--cms-dialog-max-height", uiUnwrap(opts.maxHeight), toCssSize);
821
+ setStyleValue(currentEntry.panel, "--cms-dialog-body-max-height", uiUnwrap(opts.bodyMaxHeight ?? opts.contentMaxHeight), toCssSize);
822
+ if (opts.style) Object.assign(currentEntry.panel.style, opts.style);
823
+ setPropertyProps(currentEntry.panel, opts);
824
+ currentEntry.panel.setAttribute("role", opts.role || "dialog");
825
+ currentEntry.panel.setAttribute("aria-modal", opts.modal === true ? "true" : "false");
826
+ if (opts.ariaLabel) currentEntry.panel.setAttribute("aria-label", opts.ariaLabel);
827
+ else currentEntry.panel.removeAttribute("aria-label");
828
+ };
829
+ const renderOpenContent = () => {
830
+ if (!entry?.panel) return;
831
+ entry.panel.replaceChildren(buildContent());
832
+ };
833
+ const close = () => {
834
+ clearTimers();
835
+ if (!entry) return;
836
+ const toClose = entry;
837
+ entry = null;
838
+ overlayLeave(toClose, () => JSswift.overlay.close(toClose.id));
839
+ };
840
+ const update = (nextProps = {}) => {
841
+ if (nextProps && typeof nextProps === "object") currentProps = { ...currentProps, ...nextProps };
842
+ if (entry) {
843
+ applyEntryOptions(entry);
844
+ renderOpenContent();
845
+ }
846
+ return api;
847
+ };
848
+ const open = (arg1 = null, arg2 = null) => {
849
+ clearTimers();
850
+ const { anchorEl, nextProps } = parseOpenArgs(arg1, arg2);
851
+ if (nextProps) currentProps = { ...currentProps, ...nextProps };
852
+ const opts = getOptions();
853
+ const anchor = getAnchor(anchorEl);
854
+ if (!anchor || uiUnwrap(opts.disabled)) return null;
855
+ if (entry && entry._anchorEl === anchor) {
856
+ applyEntryOptions(entry);
857
+ renderOpenContent();
858
+ return entry;
859
+ }
860
+ if (entry) close();
861
+ lastActive = document.activeElement;
862
+ const activeTriggers = parseTriggers(opts.trigger ?? opts.triggers ?? (hasOwn(opts, "open") ? "manual" : null));
863
+ const allowHover = activeTriggers.has("hover");
864
+ const allowFocus = activeTriggers.has("focus");
865
+ let currentRef = null;
866
+ entry = JSswift.overlay.open(() => buildContent(), {
867
+ type: "popover",
868
+ anchorEl: anchor,
869
+ placement: getPlacement(opts),
870
+ offsetX: opts.offsetX ?? 0,
871
+ offsetY: opts.offsetY ?? opts.offset ?? 10,
872
+ backdrop: opts.backdrop === true,
873
+ lockScroll: opts.lockScroll === true,
874
+ trapFocus: opts.trapFocus === true,
875
+ autoFocus: opts.autoFocus === true,
876
+ closeOnOutside: opts.closeOnOutside !== false,
877
+ closeOnBackdrop: opts.closeOnBackdrop ?? opts.backdrop === true,
878
+ closeOnEsc: opts.closeOnEsc !== false,
879
+ className: uiClassStatic(["cms-dialog", "cms-popover"]),
880
+ onClose: () => {
881
+ currentRef?._popoverCleanup?.();
882
+ getOptions().onClose?.(currentRef);
883
+ if (getOptions().returnFocus !== false && lastActive && typeof lastActive.focus === "function") {
884
+ setTimeout(() => lastActive.focus(), 0);
885
+ }
886
+ }
887
+ });
888
+ if (!entry?.panel) return entry;
889
+ const current = entry;
890
+ currentRef = current;
891
+ current._anchorEl = anchor;
892
+ const cancelHide = () => {
893
+ clearTimeout(closeTimer);
894
+ closeTimer = null;
895
+ };
896
+ const scheduleHide = () => {
897
+ if (!allowHover && !allowFocus) return;
898
+ hide();
899
+ };
900
+ const onPanelClick = (e) => {
901
+ const target = e.target;
902
+ if (!target || !target.closest) return;
903
+ if (target.closest("[data-popover-close]") || target.closest("[data-dialog-close]")) {
904
+ close();
905
+ return;
906
+ }
907
+ if (getOptions().closeOnSelect === true && target.closest("[data-popover-select]")) {
908
+ close();
909
+ }
910
+ };
911
+ current.panel.addEventListener("click", onPanelClick);
912
+ if (allowHover || allowFocus) {
913
+ current.panel.addEventListener("mouseenter", cancelHide);
914
+ current.panel.addEventListener("mouseleave", scheduleHide);
915
+ current.panel.addEventListener("focusin", cancelHide);
916
+ current.panel.addEventListener("focusout", scheduleHide);
917
+ }
918
+ current._popoverCleanup = () => {
919
+ current.panel?.removeEventListener("click", onPanelClick);
920
+ current.panel?.removeEventListener("mouseenter", cancelHide);
921
+ current.panel?.removeEventListener("mouseleave", scheduleHide);
922
+ current.panel?.removeEventListener("focusin", cancelHide);
923
+ current.panel?.removeEventListener("focusout", scheduleHide);
924
+ };
925
+ applyEntryOptions(current);
926
+ overlayEnter(current);
927
+ getOptions().onOpen?.(current);
928
+ return current;
929
+ };
930
+ const show = (anchorEl, nextProps = null) => {
931
+ clearTimeout(closeTimer);
932
+ closeTimer = null;
933
+ clearTimeout(openTimer);
934
+ openTimer = setTimeout(() => open(anchorEl, nextProps), getDelay());
935
+ };
936
+ const hide = (immediate = false) => {
937
+ clearTimeout(openTimer);
938
+ openTimer = null;
939
+ if (!entry) return;
940
+ clearTimeout(closeTimer);
941
+ if (immediate) {
942
+ close();
943
+ return;
944
+ }
945
+ closeTimer = setTimeout(() => close(), getHideDelay());
946
+ };
947
+ const toggle = (arg1 = null, arg2 = null) => isOpen() ? (close(), null) : open(arg1, arg2);
948
+ const isOpen = () => !!entry;
949
+ const bind = (el) => {
950
+ if (!el) return () => { };
951
+ boundEl = el;
952
+ const opts = getOptions();
953
+ const triggers = parseTriggers(opts.trigger ?? opts.triggers ?? (hasOwn(opts, "open") ? "manual" : null));
954
+ const allowHover = triggers.has("hover");
955
+ const allowFocus = triggers.has("focus");
956
+ const allowClick = triggers.has("click");
957
+ const isManual = triggers.has("manual") || (!allowHover && !allowFocus && !allowClick);
958
+ const cleanups = [];
959
+ const cleanup = () => {
960
+ clearTimers();
961
+ cleanups.forEach((fn) => fn());
962
+ if (boundEl === el) boundEl = null;
963
+ };
964
+ if (hasOwn(opts, "open")) {
965
+ if (uiIsReactive(opts.open)) {
966
+ JSswift.reactive.effect(() => {
967
+ if (!boundEl) return;
968
+ if (uiUnwrap(getOptions().open)) open(boundEl);
969
+ else hide(true);
970
+ }, "UI.Popover:open");
971
+ } else if (opts.open === true) {
972
+ open(el);
973
+ } else if (opts.open === false) {
974
+ hide(true);
975
+ }
976
+ return cleanup;
977
+ }
978
+ if (isManual || uiUnwrap(opts.disabled)) return cleanup;
979
+ if (allowHover) {
980
+ const onEnter = () => show(el);
981
+ const onLeave = () => hide();
982
+ el.addEventListener("mouseenter", onEnter);
983
+ el.addEventListener("mouseleave", onLeave);
984
+ cleanups.push(() => {
985
+ el.removeEventListener("mouseenter", onEnter);
986
+ el.removeEventListener("mouseleave", onLeave);
987
+ });
988
+ }
989
+ if (allowFocus) {
990
+ const onFocus = () => show(el);
991
+ const onBlur = () => hide();
992
+ el.addEventListener("focus", onFocus);
993
+ el.addEventListener("blur", onBlur);
994
+ cleanups.push(() => {
995
+ el.removeEventListener("focus", onFocus);
996
+ el.removeEventListener("blur", onBlur);
997
+ });
998
+ }
999
+ if (allowClick) {
1000
+ const onClick = (e) => {
1001
+ getOptions().onTriggerClick?.(e);
1002
+ if (e.defaultPrevented) return;
1003
+ toggle(el);
1004
+ };
1005
+ el.addEventListener("click", onClick);
1006
+ cleanups.push(() => el.removeEventListener("click", onClick));
1007
+ }
1008
+ return cleanup;
1009
+ };
1010
+ const api = {
1011
+ open,
1012
+ close,
1013
+ show,
1014
+ hide,
1015
+ toggle,
1016
+ update,
1017
+ bind,
1018
+ isOpen,
1019
+ entry: () => entry,
1020
+ props: () => ({ ...currentProps })
1021
+ };
1022
+
1023
+ return api;
1024
+ };
1025
+
1026
+
1027
+ UI.ContextMenu = (...args) => {
1028
+ const { props, children } = JSswift.uiNormalizeArgs(args);
1029
+ let currentProps = { ...props };
1030
+ let lastPoint = null;
1031
+ let lastAnchor = null;
1032
+ let lastTriggerEl = null;
1033
+
1034
+ const isPlainObject = (value) => value && typeof value === "object" && !value.nodeType && !Array.isArray(value) && !(value instanceof Function);
1035
+ const isAnchorLike = (value) => !!value && typeof value === "object" && (value.nodeType === 1 || typeof value.getBoundingClientRect === "function");
1036
+ const isEventLike = (value) => !!value && typeof value === "object" && ("clientX" in value || "type" in value || "target" in value || "currentTarget" in value);
1037
+ const isPointLike = (value) => !!value && Number.isFinite(Number(value.x)) && Number.isFinite(Number(value.y));
1038
+ const getOptions = () => currentProps;
1039
+ const normalizePoint = (x, y) => {
1040
+ const px = Number(x);
1041
+ const py = Number(y);
1042
+ if (!Number.isFinite(px) || !Number.isFinite(py)) return null;
1043
+ return { x: px, y: py };
1044
+ };
1045
+ const createVirtualAnchor = (point) => point ? {
1046
+ _cmsContextPoint: point,
1047
+ getBoundingClientRect: () => ({
1048
+ top: point.y,
1049
+ bottom: point.y,
1050
+ left: point.x,
1051
+ right: point.x,
1052
+ width: 0,
1053
+ height: 0
1054
+ })
1055
+ } : null;
1056
+ const getElementPoint = (el) => {
1057
+ if (!isAnchorLike(el)) return null;
1058
+ const rect = el.getBoundingClientRect();
1059
+ return normalizePoint(rect.left + Math.min(rect.width / 2, 24), rect.top + Math.min(rect.height / 2, 24));
1060
+ };
1061
+ const extractAnchor = (value) => uiUnwrap(value?.anchorEl ?? value?.triggerEl ?? value?.target ?? value?.anchor ?? null);
1062
+ const getEventPoint = (event, fallbackEl = null) => {
1063
+ if (!event) return getElementPoint(fallbackEl || lastTriggerEl);
1064
+ const point = normalizePoint(event.clientX, event.clientY);
1065
+ if (point) return point;
1066
+ const anchor = extractAnchor({ anchorEl: fallbackEl || event.currentTarget || event.target || lastTriggerEl });
1067
+ return getElementPoint(anchor);
1068
+ };
1069
+ const stripContextKeys = (value = {}) => {
1070
+ if (!isPlainObject(value)) return {};
1071
+ const next = { ...value };
1072
+ delete next.event;
1073
+ delete next.point;
1074
+ delete next.x;
1075
+ delete next.y;
1076
+ return next;
1077
+ };
1078
+ const buildMenuProps = () => {
1079
+ const opts = getOptions();
1080
+ return {
1081
+ ...opts,
1082
+ trigger: "manual",
1083
+ placement: opts.placement || "bottom-start",
1084
+ offsetX: opts.offsetX ?? 0,
1085
+ offsetY: opts.offsetY ?? opts.offset ?? 4,
1086
+ closeOnOutside: opts.closeOnOutside !== false,
1087
+ closeOnEsc: opts.closeOnEsc !== false,
1088
+ role: opts.role || "menu",
1089
+ ariaLabel: opts.ariaLabel || opts.title || "Context menu",
1090
+ class: uiClass(["cms-context-menu-shell", opts.class]),
1091
+ panelClass: uiClass(["cms-context-menu-panel", opts.panelClass])
1092
+ };
1093
+ };
1094
+ const menu = UI.Menu(buildMenuProps(), ...children);
1095
+
1096
+ const resolveOpenInput = (arg1 = null, arg2 = null, arg3 = null) => {
1097
+ let point = null;
1098
+ let anchor = null;
1099
+ let nextProps = null;
1100
+
1101
+ if (isEventLike(arg1)) {
1102
+ point = getEventPoint(arg1, lastTriggerEl);
1103
+ nextProps = isPlainObject(arg2) ? stripContextKeys(arg2) : null;
1104
+ } else if (typeof arg1 === "number" || typeof arg2 === "number") {
1105
+ point = normalizePoint(arg1, arg2);
1106
+ nextProps = isPlainObject(arg3) ? stripContextKeys(arg3) : null;
1107
+ } else if (isPointLike(arg1)) {
1108
+ point = normalizePoint(arg1.x, arg1.y);
1109
+ nextProps = isPlainObject(arg2) ? stripContextKeys(arg2) : null;
1110
+ } else if (isAnchorLike(arg1)) {
1111
+ anchor = arg1;
1112
+ nextProps = isPlainObject(arg2) ? stripContextKeys(arg2) : null;
1113
+ } else if (isPlainObject(arg1)) {
1114
+ const rawPoint = isPointLike(arg1.point) ? arg1.point : (isPointLike(arg1) ? arg1 : null);
1115
+ const rawAnchor = extractAnchor(arg1);
1116
+ const rawEvent = arg1.event;
1117
+ point = rawPoint ? normalizePoint(rawPoint.x, rawPoint.y) : getEventPoint(rawEvent, rawAnchor || lastTriggerEl);
1118
+ anchor = rawAnchor && isAnchorLike(rawAnchor) ? rawAnchor : null;
1119
+ nextProps = {
1120
+ ...stripContextKeys(arg1),
1121
+ ...(isPlainObject(arg2) ? stripContextKeys(arg2) : {})
1122
+ };
1123
+ } else if (lastPoint) {
1124
+ point = { ...lastPoint };
1125
+ }
1126
+
1127
+ return { point, anchor, nextProps };
1128
+ };
1129
+ const rememberAnchor = (anchor, point = null) => {
1130
+ lastAnchor = anchor || lastAnchor;
1131
+ lastPoint = point || (anchor && anchor._cmsContextPoint) || lastPoint;
1132
+ };
1133
+ const mergeProps = (nextProps = null) => {
1134
+ if (isPlainObject(nextProps) && Object.keys(nextProps).length) {
1135
+ currentProps = { ...currentProps, ...nextProps };
1136
+ }
1137
+ };
1138
+ const getOpenAnchor = (inputAnchor = null, inputPoint = null) => {
1139
+ if (inputPoint) return createVirtualAnchor(inputPoint);
1140
+ if (inputAnchor && isAnchorLike(inputAnchor)) return inputAnchor;
1141
+ const configAnchor = extractAnchor(getOptions());
1142
+ if (isAnchorLike(configAnchor)) return configAnchor;
1143
+ if (lastPoint) return createVirtualAnchor(lastPoint);
1144
+ if (isAnchorLike(lastAnchor)) return lastAnchor;
1145
+ if (isAnchorLike(lastTriggerEl)) return lastTriggerEl;
1146
+ return null;
1147
+ };
1148
+ const open = (arg1 = null, arg2 = null, arg3 = null) => {
1149
+ const { point, anchor, nextProps } = resolveOpenInput(arg1, arg2, arg3);
1150
+ mergeProps(nextProps);
1151
+ const openAnchor = getOpenAnchor(anchor, point);
1152
+ if (!openAnchor) return null;
1153
+ rememberAnchor(openAnchor, point);
1154
+ return menu.open(openAnchor, buildMenuProps());
1155
+ };
1156
+ const openAt = (x, y, nextProps = null) => open(x, y, nextProps);
1157
+ const openFromEvent = (event, nextProps = null) => open(event, nextProps);
1158
+ const close = () => {
1159
+ menu.close();
1160
+ return api;
1161
+ };
1162
+ const hide = (immediate = true) => {
1163
+ if (immediate === false) menu.hide(false);
1164
+ else menu.close();
1165
+ return api;
1166
+ };
1167
+ const show = (arg1 = null, arg2 = null, arg3 = null) => open(arg1, arg2, arg3);
1168
+ const toggle = (arg1 = null, arg2 = null, arg3 = null) => menu.isOpen() ? (close(), null) : open(arg1, arg2, arg3);
1169
+ const update = (nextProps = {}) => {
1170
+ mergeProps(stripContextKeys(nextProps));
1171
+ menu.update(buildMenuProps());
1172
+ return api;
1173
+ };
1174
+ const bind = (el, bindProps = null) => {
1175
+ if (!el) return () => { };
1176
+ const scopedProps = isPlainObject(bindProps) ? stripContextKeys(bindProps) : null;
1177
+ const onContextMenu = (e) => {
1178
+ lastTriggerEl = el;
1179
+ e.preventDefault();
1180
+ const result = getOptions().onTrigger?.(e, { element: el, open, close, update });
1181
+ if (result === false || uiUnwrap(getOptions().disabled)) return;
1182
+ open({ event: e, anchorEl: el }, scopedProps);
1183
+ };
1184
+ const onKeyboardOpen = (e) => {
1185
+ if (e.key !== "ContextMenu" && !(e.shiftKey && e.key === "F10")) return;
1186
+ lastTriggerEl = el;
1187
+ e.preventDefault();
1188
+ const result = getOptions().onTrigger?.(e, { element: el, open, close, update });
1189
+ if (result === false || uiUnwrap(getOptions().disabled)) return;
1190
+ open({ event: e, anchorEl: el }, scopedProps);
1191
+ };
1192
+ el.addEventListener("contextmenu", onContextMenu);
1193
+ el.addEventListener("keydown", onKeyboardOpen);
1194
+ return () => {
1195
+ el.removeEventListener("contextmenu", onContextMenu);
1196
+ el.removeEventListener("keydown", onKeyboardOpen);
1197
+ };
1198
+ };
1199
+ const api = {
1200
+ open,
1201
+ openAt,
1202
+ openFromEvent,
1203
+ show,
1204
+ hide,
1205
+ close,
1206
+ toggle,
1207
+ update,
1208
+ bind,
1209
+ isOpen: menu.isOpen,
1210
+ entry: menu.entry,
1211
+ props: () => ({ ...currentProps })
1212
+ };
1213
+
1214
+ return api;
1215
+ };
1216
+
1217
+ })(JSswift);