馃悕馃悕馃悕
at main 273 lines 6.8 kB view raw
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