@waggylabs/yumekit 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/components/y-appbar/y-appbar.d.ts +22 -15
- package/dist/components/y-appbar.d.ts +22 -15
- package/dist/components/y-appbar.js +659 -454
- package/dist/components/y-badge/y-badge.d.ts +1 -1
- package/dist/components/y-badge.d.ts +1 -1
- package/dist/components/y-badge.js +31 -15
- package/dist/components/y-menu/y-menu.d.ts +57 -1
- package/dist/components/y-menu.d.ts +57 -1
- package/dist/components/y-menu.js +477 -158
- package/dist/index.js +693 -462
- package/dist/yumekit.min.js +1 -1
- package/llm.txt +43 -12
- package/package.json +1 -1
|
@@ -1,6 +1,221 @@
|
|
|
1
|
-
import { resolveAnchor } from '../../modules/helpers.js';
|
|
1
|
+
import { createElement, resolveAnchor } from '../../modules/helpers.js';
|
|
2
|
+
import { getIcon } from '../../icons/registry.js';
|
|
3
|
+
|
|
4
|
+
// Allowlist-based SVG sanitizer — only known-safe elements and attributes are kept.
|
|
5
|
+
const ALLOWED_ELEMENTS = new Set([
|
|
6
|
+
"svg", "g", "path", "circle", "ellipse", "rect", "line", "polyline",
|
|
7
|
+
"polygon", "text", "tspan", "defs", "clippath", "mask", "lineargradient",
|
|
8
|
+
"radialgradient", "stop", "symbol", "title", "desc", "metadata",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const ALLOWED_ATTRS = new Set([
|
|
12
|
+
"viewbox", "xmlns", "fill", "stroke", "stroke-width", "stroke-linecap",
|
|
13
|
+
"stroke-linejoin", "stroke-dasharray", "stroke-dashoffset", "stroke-miterlimit",
|
|
14
|
+
"stroke-opacity", "fill-opacity", "fill-rule", "clip-rule", "opacity",
|
|
15
|
+
"d", "cx", "cy", "r", "rx", "ry", "x", "x1", "x2", "y", "y1", "y2",
|
|
16
|
+
"width", "height", "points", "transform", "id", "class", "clip-path", "mask",
|
|
17
|
+
"offset", "stop-color", "stop-opacity", "gradient-units", "gradienttransform",
|
|
18
|
+
"gradientunits", "spreadmethod", "patternunits", "patterntransform",
|
|
19
|
+
"font-size", "font-family", "font-weight", "text-anchor", "dominant-baseline",
|
|
20
|
+
"alignment-baseline", "dx", "dy", "rotate", "textlength", "lengthadjust",
|
|
21
|
+
"display", "visibility", "color", "vector-effect",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function sanitizeSvg(raw) {
|
|
25
|
+
if (!raw) return "";
|
|
26
|
+
const doc = new DOMParser().parseFromString(raw, "image/svg+xml");
|
|
27
|
+
const svg = doc.querySelector("svg");
|
|
28
|
+
if (!svg) return "";
|
|
29
|
+
|
|
30
|
+
const walk = (el) => {
|
|
31
|
+
for (const child of [...el.children]) {
|
|
32
|
+
if (!ALLOWED_ELEMENTS.has(child.tagName.toLowerCase())) {
|
|
33
|
+
child.remove();
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
for (const attr of [...child.attributes]) {
|
|
37
|
+
if (!ALLOWED_ATTRS.has(attr.name.toLowerCase())) {
|
|
38
|
+
child.removeAttribute(attr.name);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
walk(child);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (const attr of [...svg.attributes]) {
|
|
46
|
+
if (!ALLOWED_ATTRS.has(attr.name.toLowerCase())) {
|
|
47
|
+
svg.removeAttribute(attr.name);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
walk(svg);
|
|
51
|
+
return svg.outerHTML;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Cache sanitized SVG markup per icon name to avoid repeated DOMParser + DOM-walk
|
|
55
|
+
// on every render. The cache is naturally bounded by the number of registered icons.
|
|
56
|
+
const sanitizedSvgCache = new Map();
|
|
57
|
+
|
|
58
|
+
function getCachedSvg(name) {
|
|
59
|
+
if (sanitizedSvgCache.has(name)) return sanitizedSvgCache.get(name);
|
|
60
|
+
const result = sanitizeSvg(getIcon(name));
|
|
61
|
+
sanitizedSvgCache.set(name, result);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class YumeIcon extends HTMLElement {
|
|
66
|
+
static get observedAttributes() {
|
|
67
|
+
return ["name", "size", "color", "label", "weight"];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// -------------------------------------------------------------------------
|
|
71
|
+
// Lifecycle
|
|
72
|
+
// -------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
constructor() {
|
|
75
|
+
super();
|
|
76
|
+
this.attachShadow({ mode: "open" });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
connectedCallback() {
|
|
80
|
+
this.render();
|
|
81
|
+
}
|
|
2
82
|
|
|
3
|
-
|
|
83
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
84
|
+
if (oldVal === newVal) return;
|
|
85
|
+
this.render();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// -------------------------------------------------------------------------
|
|
89
|
+
// Getters / Setters
|
|
90
|
+
// -------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/** Color theme: "base" | "primary" | "secondary" | "success" | "warning" | "error" | "help". */
|
|
93
|
+
get color() { return this.getAttribute("color") || ""; }
|
|
94
|
+
set color(val) {
|
|
95
|
+
if (val) this.setAttribute("color", val);
|
|
96
|
+
else this.removeAttribute("color");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Accessible label for the icon. When set, the icon gets role="img". */
|
|
100
|
+
get label() { return this.getAttribute("label") || ""; }
|
|
101
|
+
set label(val) {
|
|
102
|
+
if (val) this.setAttribute("label", val);
|
|
103
|
+
else this.removeAttribute("label");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The registered icon name to display. */
|
|
107
|
+
get name() { return this.getAttribute("name") || ""; }
|
|
108
|
+
set name(val) { this.setAttribute("name", val); }
|
|
109
|
+
|
|
110
|
+
/** Icon size: "x-small" | "small" | "medium" | "large" | "x-large" (default "medium"). */
|
|
111
|
+
get size() { return this.getAttribute("size") || "medium"; }
|
|
112
|
+
set size(val) { this.setAttribute("size", val); }
|
|
113
|
+
|
|
114
|
+
/** Stroke weight: "thin" | "regular" | "thick". */
|
|
115
|
+
get weight() { return this.getAttribute("weight") || "regular"; }
|
|
116
|
+
set weight(val) {
|
|
117
|
+
if (val) this.setAttribute("weight", val);
|
|
118
|
+
else this.removeAttribute("weight");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// -------------------------------------------------------------------------
|
|
122
|
+
// Public
|
|
123
|
+
// -------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
render() {
|
|
126
|
+
const svg = getCachedSvg(this.name);
|
|
127
|
+
const sizeVal = this._getSize(this.size);
|
|
128
|
+
const colorVal = this.color ? this._getColor(this.color) : "inherit";
|
|
129
|
+
const weightVal = this._getWeight(this.weight);
|
|
130
|
+
|
|
131
|
+
this._updateAria();
|
|
132
|
+
|
|
133
|
+
this.shadowRoot.innerHTML = `
|
|
134
|
+
<style>
|
|
135
|
+
:host {
|
|
136
|
+
display: inline-flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: center;
|
|
139
|
+
width: ${sizeVal};
|
|
140
|
+
height: ${sizeVal};
|
|
141
|
+
color: ${colorVal};
|
|
142
|
+
line-height: 0;
|
|
143
|
+
}
|
|
144
|
+
.icon-wrapper svg {
|
|
145
|
+
width: 100%;
|
|
146
|
+
height: 100%;
|
|
147
|
+
}
|
|
148
|
+
${this._getWeightCSS(weightVal)}
|
|
149
|
+
</style>
|
|
150
|
+
<span class="icon-wrapper" part="icon">${svg}</span>
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// -------------------------------------------------------------------------
|
|
155
|
+
// Private
|
|
156
|
+
// -------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
_getColor(color) {
|
|
159
|
+
const map = {
|
|
160
|
+
base: "var(--base-content--, #f7f7fa)",
|
|
161
|
+
primary: "var(--primary-content--, #0576ff)",
|
|
162
|
+
secondary: "var(--secondary-content--, #04b8b8)",
|
|
163
|
+
success: "var(--success-content--, #2dba73)",
|
|
164
|
+
warning: "var(--warning-content--, #d17f04)",
|
|
165
|
+
error: "var(--error-content--, #b80421)",
|
|
166
|
+
help: "var(--help-content--, #5405ff)",
|
|
167
|
+
};
|
|
168
|
+
if (map[color]) return map[color];
|
|
169
|
+
if (color && (color.startsWith("#") || color.startsWith("rgb") || color.startsWith("hsl"))) {
|
|
170
|
+
return color;
|
|
171
|
+
}
|
|
172
|
+
return map.base;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_getSize(size) {
|
|
176
|
+
const map = {
|
|
177
|
+
"x-small": "var(--component-icon-size-x-small, 10px)",
|
|
178
|
+
small: "var(--component-icon-size-small, 14px)",
|
|
179
|
+
medium: "var(--component-icon-size-medium, 18px)",
|
|
180
|
+
large: "var(--component-icon-size-large, 22px)",
|
|
181
|
+
"x-large": "var(--component-icon-size-x-large, 28px)",
|
|
182
|
+
};
|
|
183
|
+
return map[size] || map.medium;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_getWeight(weight) {
|
|
187
|
+
const map = {
|
|
188
|
+
"x-thin": "1",
|
|
189
|
+
thin: "1.5",
|
|
190
|
+
regular: "2",
|
|
191
|
+
thick: "2.5",
|
|
192
|
+
"x-thick": "3",
|
|
193
|
+
};
|
|
194
|
+
return map[weight] || "";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_getWeightCSS(weightVal) {
|
|
198
|
+
if (!weightVal) return "";
|
|
199
|
+
return `.icon-wrapper svg,
|
|
200
|
+
.icon-wrapper svg * { stroke-width: ${weightVal} !important; }`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_updateAria() {
|
|
204
|
+
if (this.label) {
|
|
205
|
+
this.setAttribute("role", "img");
|
|
206
|
+
this.setAttribute("aria-label", this.label);
|
|
207
|
+
this.removeAttribute("aria-hidden");
|
|
208
|
+
} else {
|
|
209
|
+
this.setAttribute("aria-hidden", "true");
|
|
210
|
+
this.removeAttribute("role");
|
|
211
|
+
this.removeAttribute("aria-label");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!customElements.get("y-icon")) {
|
|
217
|
+
customElements.define("y-icon", YumeIcon);
|
|
218
|
+
}
|
|
4
219
|
|
|
5
220
|
class YumeMenu extends HTMLElement {
|
|
6
221
|
static get observedAttributes() {
|
|
@@ -17,6 +232,8 @@ class YumeMenu extends HTMLElement {
|
|
|
17
232
|
this._onAnchorClick = this._onAnchorClick.bind(this);
|
|
18
233
|
this._onDocumentClick = this._onDocumentClick.bind(this);
|
|
19
234
|
this._onScrollOrResize = this._onScrollOrResize.bind(this);
|
|
235
|
+
this._isReady = false;
|
|
236
|
+
this._slottedHandlers = new Map();
|
|
20
237
|
}
|
|
21
238
|
|
|
22
239
|
connectedCallback() {
|
|
@@ -33,10 +250,13 @@ class YumeMenu extends HTMLElement {
|
|
|
33
250
|
this.style.zIndex = "1000";
|
|
34
251
|
this.style.display = "none";
|
|
35
252
|
if (this.visible) this._updatePosition();
|
|
253
|
+
|
|
254
|
+
this._isReady = true;
|
|
36
255
|
}
|
|
37
256
|
|
|
38
257
|
disconnectedCallback() {
|
|
39
258
|
this._teardownAnchor();
|
|
259
|
+
this._teardownSlottedItems();
|
|
40
260
|
|
|
41
261
|
document.removeEventListener("click", this._onDocumentClick);
|
|
42
262
|
window.removeEventListener("scroll", this._onScrollOrResize, true);
|
|
@@ -56,6 +276,13 @@ class YumeMenu extends HTMLElement {
|
|
|
56
276
|
if (name === "visible" || name === "direction") {
|
|
57
277
|
this._updatePosition();
|
|
58
278
|
}
|
|
279
|
+
|
|
280
|
+
if (name === "visible" && this._isReady) {
|
|
281
|
+
this.dispatchEvent(new CustomEvent(this.visible ? "open" : "close", {
|
|
282
|
+
bubbles: true,
|
|
283
|
+
composed: true,
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
59
286
|
}
|
|
60
287
|
|
|
61
288
|
// -------------------------------------------------------------------------
|
|
@@ -70,6 +297,16 @@ class YumeMenu extends HTMLElement {
|
|
|
70
297
|
get direction() { return this.getAttribute("direction") || "down"; }
|
|
71
298
|
set direction(val) { this.setAttribute("direction", val); }
|
|
72
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Navigation mode: omit for pushState (SPA-friendly), set to "false" for full-page navigation.
|
|
302
|
+
* Regardless of this setting, a cancelable "navigate" event is always dispatched first.
|
|
303
|
+
*/
|
|
304
|
+
get history() { return this.getAttribute("history"); }
|
|
305
|
+
set history(val) {
|
|
306
|
+
if (val != null) this.setAttribute("history", val);
|
|
307
|
+
else this.removeAttribute("history");
|
|
308
|
+
}
|
|
309
|
+
|
|
73
310
|
/** Menu items array (JSON attribute). */
|
|
74
311
|
get items() {
|
|
75
312
|
try {
|
|
@@ -82,16 +319,6 @@ class YumeMenu extends HTMLElement {
|
|
|
82
319
|
this.setAttribute("items", Array.isArray(val) ? JSON.stringify(val) : (val ?? "[]"));
|
|
83
320
|
}
|
|
84
321
|
|
|
85
|
-
/**
|
|
86
|
-
* Navigation mode: omit for pushState (SPA-friendly), set to "false" for full-page navigation.
|
|
87
|
-
* Regardless of this setting, a cancelable "navigate" event is always dispatched first.
|
|
88
|
-
*/
|
|
89
|
-
get history() { return this.getAttribute("history"); }
|
|
90
|
-
set history(val) {
|
|
91
|
-
if (val != null) this.setAttribute("history", val);
|
|
92
|
-
else this.removeAttribute("history");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
322
|
/** Size: "small" | "medium" | "large" (default "medium"). */
|
|
96
323
|
get size() {
|
|
97
324
|
const sz = this.getAttribute("size");
|
|
@@ -115,63 +342,94 @@ class YumeMenu extends HTMLElement {
|
|
|
115
342
|
render() {
|
|
116
343
|
this.shadowRoot.innerHTML = "";
|
|
117
344
|
|
|
118
|
-
const style =
|
|
119
|
-
|
|
120
|
-
this.
|
|
345
|
+
const style = createElement("style", {}, [this._buildStyles()]);
|
|
346
|
+
|
|
347
|
+
const root = this._createMenuList(this.items);
|
|
348
|
+
root.classList.add("menu");
|
|
349
|
+
root.setAttribute("role", "menu");
|
|
350
|
+
root.setAttribute("part", "menu");
|
|
121
351
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
rootUl.setAttribute("part", "menu");
|
|
352
|
+
const childSlot = createElement("slot");
|
|
353
|
+
childSlot.addEventListener("slotchange", () => this._processSlottedItems());
|
|
354
|
+
root.appendChild(childSlot);
|
|
126
355
|
|
|
127
|
-
this.shadowRoot.appendChild(
|
|
356
|
+
this.shadowRoot.appendChild(style);
|
|
357
|
+
this.shadowRoot.appendChild(root);
|
|
358
|
+
this._processSlottedItems();
|
|
128
359
|
}
|
|
129
360
|
|
|
130
361
|
// -------------------------------------------------------------------------
|
|
131
362
|
// Private
|
|
132
363
|
// -------------------------------------------------------------------------
|
|
133
364
|
|
|
365
|
+
_activateItem(item) {
|
|
366
|
+
if (item.children?.length > 0) return;
|
|
367
|
+
|
|
368
|
+
this._dispatchSelect({
|
|
369
|
+
value: item.value ?? item.text,
|
|
370
|
+
item,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const href = item.href ?? item.url;
|
|
374
|
+
if (href) this._navigateTo(href);
|
|
375
|
+
|
|
376
|
+
this.visible = false;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
_activateSlottedItem(el) {
|
|
380
|
+
this._dispatchSelect({
|
|
381
|
+
value: el.dataset.value ?? el.textContent.trim(),
|
|
382
|
+
element: el,
|
|
383
|
+
});
|
|
384
|
+
this.visible = false;
|
|
385
|
+
}
|
|
386
|
+
|
|
134
387
|
_buildStyles() {
|
|
135
388
|
const paddingVar = `var(--component-button-padding-${this.size}, 0.5rem)`;
|
|
136
389
|
return `
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
list-style: none;
|
|
140
|
-
margin: 0;
|
|
141
|
-
padding: 0;
|
|
390
|
+
.menu,
|
|
391
|
+
.submenu {
|
|
142
392
|
background: var(--component-menu-background, #0c0c0d);
|
|
143
393
|
border: var(--component-menu-border-width, 1px) solid var(--component-menu-border-color, #37383a);
|
|
144
394
|
border-radius: var(--component-menu-border-radius, 4px);
|
|
145
395
|
box-shadow: var(--component-menu-shadow, 0 2px 8px rgba(0, 0, 0, 0.15));
|
|
146
396
|
min-width: 150px;
|
|
397
|
+
display: flex;
|
|
398
|
+
flex-direction: column;
|
|
147
399
|
}
|
|
148
400
|
|
|
149
|
-
|
|
401
|
+
.menuitem,
|
|
402
|
+
::slotted(:not([slot])) {
|
|
150
403
|
cursor: pointer;
|
|
151
404
|
padding: ${paddingVar};
|
|
152
|
-
display: flex;
|
|
153
|
-
align-items: center;
|
|
154
|
-
justify-content: space-between;
|
|
155
405
|
white-space: nowrap;
|
|
156
406
|
color: var(--component-menu-color, #f7f7fa);
|
|
157
407
|
font-size: var(--font-size-button, 1em);
|
|
408
|
+
box-sizing: border-box;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.menuitem {
|
|
412
|
+
display: flex;
|
|
413
|
+
align-items: center;
|
|
414
|
+
justify-content: space-between;
|
|
158
415
|
position: relative;
|
|
159
416
|
}
|
|
160
417
|
|
|
161
|
-
|
|
418
|
+
.menuitem:hover,
|
|
419
|
+
::slotted(:not([slot]):hover) {
|
|
162
420
|
background: var(--component-menu-hover-background, #292a2b);
|
|
163
421
|
}
|
|
164
422
|
|
|
165
|
-
|
|
423
|
+
.menuitem.selected {
|
|
166
424
|
background: var(--component-menu-selected-background);
|
|
167
425
|
color: var(--component-menu-selected-color);
|
|
168
426
|
}
|
|
169
427
|
|
|
170
|
-
|
|
428
|
+
.menuitem.selected:hover {
|
|
171
429
|
background: var(--component-menu-selected-background);
|
|
172
430
|
}
|
|
173
431
|
|
|
174
|
-
|
|
432
|
+
.submenu {
|
|
175
433
|
position: absolute;
|
|
176
434
|
top: 0;
|
|
177
435
|
left: 100%;
|
|
@@ -179,8 +437,8 @@ class YumeMenu extends HTMLElement {
|
|
|
179
437
|
z-index: var(--component-menu-z-index, 1001);
|
|
180
438
|
}
|
|
181
439
|
|
|
182
|
-
|
|
183
|
-
display:
|
|
440
|
+
.menuitem:hover > .submenu {
|
|
441
|
+
display: flex;
|
|
184
442
|
}
|
|
185
443
|
|
|
186
444
|
.submenu-indicator {
|
|
@@ -190,13 +448,11 @@ class YumeMenu extends HTMLElement {
|
|
|
190
448
|
opacity: 0.6;
|
|
191
449
|
}
|
|
192
450
|
|
|
193
|
-
.submenu-indicator svg {
|
|
194
|
-
width: 16px;
|
|
195
|
-
height: 16px;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
451
|
.item-content {
|
|
199
452
|
flex: 1;
|
|
453
|
+
display: inline-flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
gap: 0.5rem;
|
|
200
456
|
}
|
|
201
457
|
`;
|
|
202
458
|
}
|
|
@@ -209,91 +465,140 @@ class YumeMenu extends HTMLElement {
|
|
|
209
465
|
});
|
|
210
466
|
}
|
|
211
467
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
li.setAttribute("aria-current", item.selected ? "true" : "false");
|
|
221
|
-
li.tabIndex = 0;
|
|
222
|
-
|
|
223
|
-
const contentWrapper = document.createElement("span");
|
|
224
|
-
contentWrapper.className = "item-content";
|
|
225
|
-
|
|
226
|
-
if (item["icon-template"]) {
|
|
227
|
-
const iconTpl = this._findTemplate(item["icon-template"]);
|
|
228
|
-
if (iconTpl)
|
|
229
|
-
contentWrapper.appendChild(iconTpl.content.cloneNode(true));
|
|
230
|
-
}
|
|
468
|
+
_computeMenuOffset(direction, anchorRect, menuRect, vw, vh) {
|
|
469
|
+
if (direction === "right") {
|
|
470
|
+
let top = anchorRect.top;
|
|
471
|
+
let left = anchorRect.right;
|
|
472
|
+
if (left + menuRect.width > vw) left = anchorRect.left - menuRect.width;
|
|
473
|
+
if (top + menuRect.height > vh) top = anchorRect.top - menuRect.height;
|
|
474
|
+
return { top, left };
|
|
475
|
+
}
|
|
231
476
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
} else {
|
|
240
|
-
contentWrapper.append(item.text);
|
|
241
|
-
}
|
|
477
|
+
if (direction === "up") {
|
|
478
|
+
let top = anchorRect.top - menuRect.height;
|
|
479
|
+
let left = anchorRect.left;
|
|
480
|
+
if (top < 0) top = anchorRect.bottom;
|
|
481
|
+
if (left + menuRect.width > vw) left = vw - menuRect.width - 10;
|
|
482
|
+
return { top, left };
|
|
483
|
+
}
|
|
242
484
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
cancelable: true,
|
|
251
|
-
detail: { href: item.url },
|
|
252
|
-
});
|
|
253
|
-
const cancelled = !this.dispatchEvent(event);
|
|
254
|
-
if (cancelled) return;
|
|
255
|
-
if (this.getAttribute("history") !== "false") {
|
|
256
|
-
history.pushState({}, "", item.url);
|
|
257
|
-
window.dispatchEvent(new PopStateEvent("popstate", { state: {} }));
|
|
258
|
-
} else {
|
|
259
|
-
window.location.href = item.url;
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
}
|
|
485
|
+
if (direction === "left") {
|
|
486
|
+
let top = anchorRect.top;
|
|
487
|
+
let left = anchorRect.left - menuRect.width;
|
|
488
|
+
if (left < 0) left = anchorRect.right;
|
|
489
|
+
if (top + menuRect.height > vh) top = anchorRect.top - menuRect.height;
|
|
490
|
+
return { top, left };
|
|
491
|
+
}
|
|
263
492
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
493
|
+
// "down" (default)
|
|
494
|
+
let top = anchorRect.bottom;
|
|
495
|
+
let left = anchorRect.left;
|
|
496
|
+
if (top + menuRect.height > vh) top = anchorRect.top - menuRect.height;
|
|
497
|
+
if (left + menuRect.width > vw) left = vw - menuRect.width - 10;
|
|
498
|
+
return { top, left };
|
|
499
|
+
}
|
|
269
500
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
indicator.className = "submenu-indicator";
|
|
273
|
-
indicator.innerHTML = chevronRight;
|
|
274
|
-
li.appendChild(indicator);
|
|
501
|
+
_createItemContent(item) {
|
|
502
|
+
const wrapper = createElement("span", { class: "item-content" });
|
|
275
503
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
504
|
+
if (item.icon) {
|
|
505
|
+
wrapper.appendChild(createElement("y-icon", { name: item.icon, size: this.size }));
|
|
506
|
+
} else if (item["icon-template"]) {
|
|
507
|
+
YumeMenu._warnTemplateFieldDeprecated();
|
|
508
|
+
const tpl = this._findTemplate(item["icon-template"]);
|
|
509
|
+
if (tpl) wrapper.appendChild(tpl.content.cloneNode(true));
|
|
510
|
+
}
|
|
281
511
|
|
|
282
|
-
|
|
512
|
+
if (item.template) {
|
|
513
|
+
YumeMenu._warnTemplateFieldDeprecated();
|
|
514
|
+
const tpl = this._findTemplate(item.template);
|
|
515
|
+
if (tpl) wrapper.appendChild(tpl.content.cloneNode(true));
|
|
516
|
+
else wrapper.append(item.text ?? "");
|
|
517
|
+
} else {
|
|
518
|
+
wrapper.append(item.text ?? "");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (!item.slot) return wrapper;
|
|
522
|
+
|
|
523
|
+
const slotEl = createElement("slot", { name: item.slot });
|
|
524
|
+
slotEl.appendChild(wrapper);
|
|
525
|
+
return slotEl;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
_createMenuItem(item) {
|
|
529
|
+
const isSelected = !!item.selected;
|
|
530
|
+
const partValue = isSelected ? "menuitem selected" : "menuitem";
|
|
531
|
+
|
|
532
|
+
const itemEl = createElement("div", {
|
|
533
|
+
class: partValue,
|
|
534
|
+
role: "menuitem",
|
|
535
|
+
part: partValue,
|
|
536
|
+
"aria-current": isSelected ? "true" : "false",
|
|
537
|
+
tabindex: "0",
|
|
283
538
|
});
|
|
284
539
|
|
|
285
|
-
|
|
540
|
+
itemEl.appendChild(this._createItemContent(item));
|
|
541
|
+
|
|
542
|
+
if (item.url && !item.href) YumeMenu._warnUrlDeprecated();
|
|
543
|
+
|
|
544
|
+
itemEl.addEventListener("click", () => this._activateItem(item));
|
|
545
|
+
|
|
546
|
+
if (item.children?.length > 0) {
|
|
547
|
+
itemEl.appendChild(this._createSubmenuIndicator());
|
|
548
|
+
|
|
549
|
+
const submenu = this._createMenuList(item.children);
|
|
550
|
+
submenu.classList.add("submenu");
|
|
551
|
+
submenu.setAttribute("role", "menu");
|
|
552
|
+
itemEl.appendChild(submenu);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return itemEl;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
_createMenuList(items) {
|
|
559
|
+
const container = createElement("div");
|
|
560
|
+
items.forEach((item) => container.appendChild(this._createMenuItem(item)));
|
|
561
|
+
return container;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
_createSubmenuIndicator() {
|
|
565
|
+
return createElement("span", { class: "submenu-indicator" }, [
|
|
566
|
+
createElement("y-icon", { name: "chevron-right", size: this.size }),
|
|
567
|
+
]);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
_dispatchSelect(detail) {
|
|
571
|
+
this.dispatchEvent(new CustomEvent("select", {
|
|
572
|
+
detail,
|
|
573
|
+
bubbles: true,
|
|
574
|
+
composed: true,
|
|
575
|
+
}));
|
|
286
576
|
}
|
|
287
577
|
|
|
288
578
|
_findTemplate(name) {
|
|
289
579
|
return this.querySelector(`template[slot="${name}"]`);
|
|
290
580
|
}
|
|
291
581
|
|
|
582
|
+
_navigateTo(href) {
|
|
583
|
+
const event = new CustomEvent("navigate", {
|
|
584
|
+
bubbles: true,
|
|
585
|
+
composed: true,
|
|
586
|
+
cancelable: true,
|
|
587
|
+
detail: { href },
|
|
588
|
+
});
|
|
589
|
+
if (!this.dispatchEvent(event)) return;
|
|
590
|
+
|
|
591
|
+
if (this.getAttribute("history") === "false") {
|
|
592
|
+
window.location.href = href;
|
|
593
|
+
} else {
|
|
594
|
+
history.pushState({}, "", href);
|
|
595
|
+
window.dispatchEvent(new PopStateEvent("popstate", { state: {} }));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
292
599
|
_onAnchorClick(e) {
|
|
293
600
|
e.stopPropagation();
|
|
294
|
-
if (!this.visible)
|
|
295
|
-
YumeMenu._closeAll(this);
|
|
296
|
-
}
|
|
601
|
+
if (!this.visible) YumeMenu._closeAll(this);
|
|
297
602
|
this.visible = !this.visible;
|
|
298
603
|
}
|
|
299
604
|
|
|
@@ -308,9 +613,34 @@ class YumeMenu extends HTMLElement {
|
|
|
308
613
|
if (this.visible) this._updatePosition();
|
|
309
614
|
}
|
|
310
615
|
|
|
616
|
+
_processSlottedItems() {
|
|
617
|
+
const slot = this.shadowRoot.querySelector(".menu > slot");
|
|
618
|
+
if (!slot) return;
|
|
619
|
+
|
|
620
|
+
const assigned = new Set(slot.assignedElements());
|
|
621
|
+
|
|
622
|
+
for (const [el, handler] of this._slottedHandlers) {
|
|
623
|
+
if (assigned.has(el)) continue;
|
|
624
|
+
el.removeEventListener("click", handler);
|
|
625
|
+
this._slottedHandlers.delete(el);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
for (const el of assigned) {
|
|
629
|
+
if (this._slottedHandlers.has(el)) continue;
|
|
630
|
+
|
|
631
|
+
if (!el.hasAttribute("role")) el.setAttribute("role", "menuitem");
|
|
632
|
+
if (el.tabIndex < 0) el.tabIndex = 0;
|
|
633
|
+
|
|
634
|
+
const handler = () => this._activateSlottedItem(el);
|
|
635
|
+
el.addEventListener("click", handler);
|
|
636
|
+
this._slottedHandlers.set(el, handler);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
311
640
|
_setupAnchor() {
|
|
312
641
|
const id = this.anchor;
|
|
313
642
|
if (!id) return;
|
|
643
|
+
|
|
314
644
|
const root = this.getRootNode();
|
|
315
645
|
this._anchorResolveDispose = resolveAnchor(
|
|
316
646
|
this,
|
|
@@ -334,6 +664,13 @@ class YumeMenu extends HTMLElement {
|
|
|
334
664
|
}
|
|
335
665
|
}
|
|
336
666
|
|
|
667
|
+
_teardownSlottedItems() {
|
|
668
|
+
for (const [el, handler] of this._slottedHandlers) {
|
|
669
|
+
el.removeEventListener("click", handler);
|
|
670
|
+
}
|
|
671
|
+
this._slottedHandlers.clear();
|
|
672
|
+
}
|
|
673
|
+
|
|
337
674
|
_updatePosition() {
|
|
338
675
|
if (!this.visible || !this._anchorEl) {
|
|
339
676
|
this.style.display = "none";
|
|
@@ -342,7 +679,7 @@ class YumeMenu extends HTMLElement {
|
|
|
342
679
|
|
|
343
680
|
const anchorRect = this._anchorEl.getBoundingClientRect();
|
|
344
681
|
|
|
345
|
-
//
|
|
682
|
+
// Measure menu off-screen so we know its size before placing it.
|
|
346
683
|
this.style.visibility = "hidden";
|
|
347
684
|
this.style.display = "block";
|
|
348
685
|
const menuRect = this.getBoundingClientRect();
|
|
@@ -350,61 +687,43 @@ class YumeMenu extends HTMLElement {
|
|
|
350
687
|
|
|
351
688
|
const vw = window.innerWidth;
|
|
352
689
|
const vh = window.innerHeight;
|
|
690
|
+
const { top, left } = this._computeMenuOffset(
|
|
691
|
+
this.direction,
|
|
692
|
+
anchorRect,
|
|
693
|
+
menuRect,
|
|
694
|
+
vw,
|
|
695
|
+
vh,
|
|
696
|
+
);
|
|
353
697
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (this.direction === "right") {
|
|
357
|
-
top = anchorRect.top;
|
|
358
|
-
left = anchorRect.right;
|
|
359
|
-
|
|
360
|
-
if (left + menuRect.width > vw) {
|
|
361
|
-
left = anchorRect.left - menuRect.width;
|
|
362
|
-
}
|
|
363
|
-
if (top + menuRect.height > vh) {
|
|
364
|
-
top = anchorRect.top - menuRect.height;
|
|
365
|
-
}
|
|
366
|
-
} else if (this.direction === "up") {
|
|
367
|
-
top = anchorRect.top - menuRect.height;
|
|
368
|
-
left = anchorRect.left;
|
|
369
|
-
|
|
370
|
-
if (top < 0) {
|
|
371
|
-
top = anchorRect.bottom;
|
|
372
|
-
}
|
|
373
|
-
if (left + menuRect.width > vw) {
|
|
374
|
-
left = vw - menuRect.width - 10;
|
|
375
|
-
}
|
|
376
|
-
} else if (this.direction === "left") {
|
|
377
|
-
top = anchorRect.top;
|
|
378
|
-
left = anchorRect.left - menuRect.width;
|
|
379
|
-
|
|
380
|
-
if (left < 0) {
|
|
381
|
-
left = anchorRect.right;
|
|
382
|
-
}
|
|
383
|
-
if (top + menuRect.height > vh) {
|
|
384
|
-
top = anchorRect.top - menuRect.height;
|
|
385
|
-
}
|
|
386
|
-
} else {
|
|
387
|
-
// "down" (default)
|
|
388
|
-
top = anchorRect.bottom;
|
|
389
|
-
left = anchorRect.left;
|
|
698
|
+
const clampedTop = Math.max(0, Math.min(top, vh - menuRect.height));
|
|
699
|
+
const clampedLeft = Math.max(0, Math.min(left, vw - menuRect.width));
|
|
390
700
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
left = vw - menuRect.width - 10;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
701
|
+
this.style.top = `${clampedTop}px`;
|
|
702
|
+
this.style.left = `${clampedLeft}px`;
|
|
703
|
+
this.style.display = "block";
|
|
704
|
+
}
|
|
398
705
|
|
|
399
|
-
|
|
400
|
-
|
|
706
|
+
static _warnTemplateFieldDeprecated() {
|
|
707
|
+
if (YumeMenu._templateFieldDeprecationWarned) return;
|
|
708
|
+
YumeMenu._templateFieldDeprecationWarned = true;
|
|
709
|
+
// eslint-disable-next-line no-console
|
|
710
|
+
console.warn(
|
|
711
|
+
"[y-menu] item.template and item['icon-template'] are deprecated; use item.icon (icon name) and item.slot (named slot) instead. Support will be removed in a future release.",
|
|
712
|
+
);
|
|
713
|
+
}
|
|
401
714
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
715
|
+
static _warnUrlDeprecated() {
|
|
716
|
+
if (YumeMenu._urlDeprecationWarned) return;
|
|
717
|
+
YumeMenu._urlDeprecationWarned = true;
|
|
718
|
+
// eslint-disable-next-line no-console
|
|
719
|
+
console.warn(
|
|
720
|
+
"[y-menu] item.url is deprecated; use item.href instead. Support for item.url will be removed in a future release.",
|
|
721
|
+
);
|
|
405
722
|
}
|
|
406
723
|
}
|
|
407
724
|
|
|
408
725
|
if (!customElements.get("y-menu")) {
|
|
409
726
|
customElements.define("y-menu", YumeMenu);
|
|
410
727
|
}
|
|
728
|
+
|
|
729
|
+
export { YumeMenu };
|