馃悕馃悕馃悕
1
2$css(`
3 .context-backdrop {
4 position: absolute;
5 margin: 0;
6 padding: 0;
7 top: 0;
8 left: 0;
9 border: none;
10 width: 100%;
11 height: 100%;
12 display: none;
13 pointer-events: none;
14 }
15
16 .context-menu {
17 position: fixed;
18 background-color: var(--main-background);
19 border: 1px solid var(--main-faded);
20 border-radius: 2px;
21 min-width: 8rem;
22 font-size: 0.875rem;
23 user-select: none;
24 z-index: 10;
25 pointer-events: auto;
26 }
27
28 .context-menu[data-centered] {
29 position: absolute;
30 left: 50%;
31 top: 50%;
32 transform: translate(-50%, -50%);
33 }
34
35 .context-menu-item {
36 padding: 0.2rem 0.5rem;
37 cursor: pointer;
38 white-space: nowrap;
39 color: var(--main-solid);
40 background-color: var(--main-background);
41 display: block;
42 width: 100%;
43 border-radius: 0;
44 text-align: left;
45 height: auto;
46 }
47
48 .context-menu-item:focus {
49 outline: none;
50 background-color: var(--main-faded);
51 }
52
53 .context-menu-item:hover {
54 background-color: var(--main-faded);
55 }
56
57 .context-menu-item.disabled {
58 opacity: 0.5;
59 cursor: default;
60 }
61
62 .context-menu-item.disabled:hover {
63 background-color: transparent;
64 }
65
66 .context-menu-separator {
67 height: 1px;
68 background-color: var(--main-faded);
69 margin: 0.25rem 0;
70 }
71`);
72
73function collectItems(element) {
74 const items = [];
75
76 for (let node = element; node; node = node.parentNode) {
77 if (node.$contextMenu !== null && node.$contextMenu !== undefined) {
78 const nodeItems = $actualize(node.$contextMenu.items);
79 for (const item of nodeItems.map($actualize)) {
80 if (Array.isArray(item) && item[0] && Array.isArray(item[0])) {
81 items.push(...item);
82 }
83 else {
84 items.push(item);
85 }
86 }
87
88 if (node.$contextMenu.override) {
89 break;
90 }
91 }
92 }
93
94 return items;
95}
96
97var menuTarget = document.body;
98
99const backdrop = document.createElement("div");
100backdrop.className = "context-backdrop";
101
102const menu = document.createElement("div");
103menu.$ = {};
104menu.className = "context-menu";
105menu.setAttribute("role", "menu");
106menu.setAttribute("aria-orientation", "vertical");
107
108menu.addEventListener("focusout", (e) => {
109 if (menu.contains(e.relatedTarget)) return;
110
111 backdrop.style.display = "none";
112 menu.$.previousFocus?.focus();
113});
114
115menu.addEventListener("keydown", (e) => {
116 if (!["ArrowDown", "ArrowUp", "j", "k", "Escape"].includes(e.key)) return;
117
118 e.preventDefault();
119 e.stopPropagation();
120
121 if (e.key === "Escape") {
122 backdrop.style.display = "none";
123 menu.$.previousFocus?.focus();
124 return;
125 }
126
127 const currentItem = document.activeElement;
128 if (!menu.contains(currentItem)) {
129 menu.firstElementChild?.focus();
130 return;
131 }
132
133 let nextItem;
134 if (e.key === "ArrowDown" || e.key === "j") {
135 nextItem = currentItem.nextElementSibling || menu.firstElementChild;
136 } else {
137 nextItem = currentItem.previousElementSibling || menu.lastElementChild;
138 }
139
140 nextItem.focus();
141});
142
143backdrop.appendChild(menu);
144
145const showMenu = (target, position = null) => {
146 const body = document.fullscreenElement || document.body;
147 body.appendChild(backdrop);
148 backdrop.style.display = "block";
149
150 menu.$.previousFocus = document.activeElement;
151 menu.firstChild?.focus();
152
153 const bounds = target.getBoundingClientRect();
154
155 if (!position) {
156 menu.dataset.centered = "";
157 menu.style.left = "";
158 menu.style.top = "";
159 return;
160 }
161
162 const {x,y} = position;
163
164 delete menu.dataset.centered;
165 menu.style.left = x + "px";
166 menu.style.top = y + "px";
167
168 const rect = menu.getBoundingClientRect();
169
170 if (rect.right > bounds.right) {
171 menu.style.left = (x - rect.width) + "px";
172 }
173 if (rect.left < bounds.left) {
174 menu.style.left = bounds.left + "px";
175 }
176 if (rect.bottom > bounds.bottom) {
177 menu.style.top = (y - rect.height) + "px";
178 }
179 if (rect.top < bounds.top) {
180 menu.style.top = bounds.top + "px";
181 }
182};
183
184document.addEventListener("contextmenu", (e) => {
185 menu.replaceChildren();
186
187 const items = collectItems(e.target);
188
189 if (items.length === 0) return;
190
191 items.forEach(item => {
192 if (!item) return;
193 if (item === "separator") { // TODO improve this
194 const separator = document.createElement("div");
195 separator.className = "context-menu-separator";
196 menu.appendChild(separator);
197 return;
198 }
199
200 const menuItem = document.createElement("button");
201 menuItem.className = "context-menu-item";
202 menu.setAttribute("role", "menuItem");
203 menu.setAttribute("tabIndex", "-1");
204
205 menuItem.textContent = item[0];
206
207 const select = async () => {
208 backdrop.style.display = "none";
209 menu.$.previousFocus?.focus();
210
211 await item[1]();
212 };
213
214 menuItem.onclick = select;
215 menuItem.addEventListener("keydown", (e) => {
216 if (e.key === "o" || e.key === "Enter") {
217 select();
218 e.stopPropagation();
219 }
220 });
221
222 menu.appendChild(menuItem);
223 });
224
225 e.preventDefault();
226
227 showMenu(e.target, {x: e.clientX, y: e.clientY});
228});
229
230document.$showMenu = (target) => {
231 menu.replaceChildren();
232
233 const items = collectItems(target);
234
235 if (items.length === 0) return;
236
237 items.forEach(item => {
238 if (!item) return;
239 if (item === "separator") { // TODO improve this
240 const separator = document.createElement("div");
241 separator.className = "context-menu-separator";
242 menu.appendChild(separator);
243 return;
244 }
245
246 const menuItem = document.createElement("button");
247 menuItem.className = "context-menu-item";
248 menu.setAttribute("role", "menuItem");
249 menu.setAttribute("tabIndex", "-1");
250
251 menuItem.textContent = item[0];
252
253 const select = async () => {
254 backdrop.style.display = "none";
255 menu.$.previousFocus?.focus();
256
257 await item[1]();
258 };
259
260 menuItem.onclick = select;
261 menuItem.addEventListener("keydown", (e) => {
262 if (e.key === "o" || e.key === "Enter") {
263 select();
264 e.stopPropagation();
265 }
266 });
267
268 menu.appendChild(menuItem);
269 });
270
271 showMenu(target);
272};
273