a simple web player for subsonic tinysub.devins.page
subsonic navidrome javascript
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: contextmenu

+58 -42
+58 -42
src/lib/ContextMenu.svelte
··· 1 1 <script lang="ts"> 2 2 import { onMount } from "svelte"; 3 3 4 + type Item = { 5 + label: string; 6 + action?: () => void; 7 + items?: Item[]; 8 + disabled?: boolean; 9 + }; 10 + 4 11 let { x, y, items, onclose } = $props<{ 5 12 x: number; 6 13 y: number; 7 - items: { label: string; action?: () => void; items?: any[] }[]; 14 + items: Item[]; 8 15 onclose: () => void; 9 16 }>(); 10 17 ··· 17 24 onMount(() => { 18 25 const prevFocus = document.activeElement as HTMLElement | null; 19 26 el?.focus(); 20 - return () => { 21 - if (prevFocus && typeof prevFocus.focus === "function") { 22 - prevFocus.focus(); 23 - } 24 - }; 27 + return () => prevFocus?.focus(); 25 28 }); 26 29 27 30 const exec = (a?: () => void) => { ··· 31 34 } 32 35 }; 33 36 34 - let left = $derived(x + w > window.innerWidth ? Math.max(0, x - w) : x); 35 - let top = $derived(y + h > window.innerHeight ? Math.max(0, y - h) : y); 37 + const left = $derived(x + w > window.innerWidth ? Math.max(0, x - w) : x); 38 + const top = $derived(y + h > window.innerHeight ? Math.max(0, y - h) : y); 36 39 37 40 function onKey(e: KeyboardEvent) { 38 41 const k = e.key; ··· 40 43 e.stopPropagation(); 41 44 onclose(); 42 45 } else if (k === "ArrowDown") { 43 - e.stopPropagation(); 44 46 e.preventDefault(); 45 47 if (subActive !== null && active !== null && items[active].items) { 46 48 const s = items[active].items!; 47 - subActive = subActive >= s.length - 1 ? 0 : subActive + 1; 49 + subActive = (subActive + 1) % s.length; 48 50 } else { 49 - active = active === null || active >= items.length - 1 ? 0 : active + 1; 51 + active = active === null ? 0 : (active + 1) % items.length; 50 52 subActive = null; 51 53 } 52 54 } else if (k === "ArrowUp") { 53 - e.stopPropagation(); 54 55 e.preventDefault(); 55 56 if (subActive !== null && active !== null && items[active].items) { 56 57 const s = items[active].items!; 57 - subActive = subActive <= 0 ? s.length - 1 : subActive - 1; 58 + subActive = (subActive - 1 + s.length) % s.length; 58 59 } else { 59 - active = active === null || active <= 0 ? items.length - 1 : active - 1; 60 + active = 61 + active === null 62 + ? items.length - 1 63 + : (active - 1 + items.length) % items.length; 60 64 subActive = null; 61 65 } 62 66 } else if (k === "ArrowRight") { 63 67 if (active !== null && items[active].items) { 64 - e.stopPropagation(); 65 68 e.preventDefault(); 66 69 subActive = 0; 67 70 } 68 71 } else if (k === "ArrowLeft") { 69 72 if (subActive !== null) { 70 - e.stopPropagation(); 71 73 e.preventDefault(); 72 74 subActive = null; 73 75 } 74 76 } else if (k === "Enter") { 75 - e.stopPropagation(); 76 77 e.preventDefault(); 77 78 if (active !== null) { 78 79 const item = items[active]; ··· 85 86 } 86 87 </script> 87 88 88 - <!-- svelte-ignore a11y_click_events_have_key_events --> 89 + {#snippet menuButton(item: Item, isActive: boolean, isSub: boolean)} 90 + <button 91 + class:active={isActive} 92 + disabled={item.disabled} 93 + onmouseenter={() => { 94 + if (isSub) { 95 + if (active !== null && items[active].items) { 96 + subActive = items[active].items.indexOf(item); 97 + } 98 + } else { 99 + active = items.indexOf(item); 100 + subActive = null; 101 + } 102 + }} 103 + onclick={(e) => { 104 + e.stopPropagation(); 105 + if (item.items) { 106 + active = items.indexOf(item); 107 + subActive = null; 108 + } else { 109 + exec(item.action); 110 + } 111 + }} 112 + > 113 + {item.label} 114 + {#if item.items}<span class="arrow">▶</span>{/if} 115 + </button> 116 + {/snippet} 117 + 89 118 <!-- svelte-ignore a11y_no_static_element_interactions --> 90 119 <!-- svelte-ignore a11y_no_noninteractive_tabindex --> 120 + <!-- svelte-ignore a11y_click_events_have_key_events --> 91 121 <div 92 122 class="overlay" 93 123 onclick={onclose} ··· 109 139 {#each items as item, i} 110 140 <div 111 141 class="row" 112 - onmouseenter={() => { 113 - active = i; 114 - subActive = null; 115 - }} 116 142 onmouseleave={() => { 117 143 if (subActive === null) active = null; 118 144 }} 119 145 > 120 - <button 121 - class:active={active === i && subActive === null} 122 - onclick={() => 123 - item.items ? (active = active === i ? null : i) : exec(item.action)} 124 - > 125 - {item.label} 126 - {#if item.items}<span class="arrow">▶</span>{/if} 127 - </button> 146 + {@render menuButton(item, active === i && subActive === null, false)} 128 147 {#if item.items && active === i} 129 148 <div 130 149 class="submenu" 131 150 class:flip-x={left + w + 160 > window.innerWidth} 132 - class:flip-y={y + i * 32 + item.items.length * 32 > 151 + class:flip-y={top + i * 32 + item.items.length * 32 > 133 152 window.innerHeight} 134 153 > 135 154 {#each item.items as sub, si} 136 - <button 137 - class:active={subActive === si} 138 - onmouseenter={() => (subActive = si)} 139 - onclick={() => exec(sub.action)} 140 - > 141 - {sub.label} 142 - </button> 155 + {@render menuButton(sub, subActive === si, true)} 143 156 {/each} 144 157 </div> 145 158 {/if} ··· 160 173 color: CanvasText; 161 174 border: 1px solid var(--border); 162 175 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); 163 - min-inline-size: 150px; 176 + min-inline-size: 160px; 164 177 display: flex; 165 178 flex-direction: column; 166 179 outline: none; ··· 185 198 button.active { 186 199 background: Highlight; 187 200 color: HighlightText; 201 + } 202 + button:disabled { 203 + opacity: 0.5; 188 204 } 189 205 .arrow { 190 206 font-size: 0.7em; ··· 193 209 .submenu { 194 210 position: absolute; 195 211 inset-inline-start: 100%; 196 - inset-block-start: -1px; 212 + inset-block-start: 0; 197 213 background: Canvas; 198 214 border: 1px solid var(--border); 199 215 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); 200 - min-inline-size: 150px; 216 + min-inline-size: 160px; 201 217 display: flex; 202 218 flex-direction: column; 203 219 } ··· 207 223 } 208 224 .submenu.flip-y { 209 225 inset-block-start: auto; 210 - inset-block-end: -1px; 226 + inset-block-end: 0; 211 227 } 212 228 </style>