d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 1 | <script lang="ts" context="module"> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 2 | export interface MenuItem { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 3 | label: string; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 4 | action: () => void; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 5 | danger?: boolean; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 6 | disabled?: boolean; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 7 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 8 | </script> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 9 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 10 | <script lang="ts"> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 11 | import { onMount, tick } from 'svelte'; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 12 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 13 | export let items: MenuItem[]; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 14 | export let x: number; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 15 | export let y: number; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 16 | export let onClose: () => void; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 17 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 18 | const MARGIN = 8; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 19 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 20 | let menu: HTMLDivElement; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 21 | let left = x; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 22 | let top = y; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 23 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 24 | onMount(async () => { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 25 | await tick(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 26 | const rect = menu.getBoundingClientRect(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 27 | left = Math.max(MARGIN, Math.min(x, window.innerWidth - rect.width - MARGIN)); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 28 | top = Math.max(MARGIN, Math.min(y, window.innerHeight - rect.height - MARGIN)); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 29 | menu.querySelector<HTMLButtonElement>('button:not(:disabled)')?.focus(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 30 | }); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 31 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 32 | function pick(item: MenuItem): void { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 33 | onClose(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 34 | item.action(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 35 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 36 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 37 | function outside(ev: PointerEvent): void { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 38 | if (!menu.contains(ev.target as Node)) onClose(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 39 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 40 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 41 | function key(ev: KeyboardEvent): void { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 42 | if (ev.key === 'Escape') { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 43 | ev.stopPropagation(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 44 | onClose(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 45 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 46 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 47 | </script> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 48 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 49 | <svelte:window on:pointerdown|capture={outside} on:keydown|capture={key} on:resize={onClose} on:blur={onClose} /> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 50 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 51 | <div class="context-menu" role="menu" style={`left:${left}px;top:${top}px`} bind:this={menu}> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 52 | {#each items as item} |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 53 | <button type="button" role="menuitem" class:danger={item.danger} disabled={item.disabled} on:click={() => pick(item)}> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 54 | {item.label} |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 55 | </button> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 56 | {/each} |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 57 | </div> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 58 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 59 | <style> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 60 | .context-menu { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 61 | position: fixed; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 62 | z-index: 90; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 63 | min-width: 180px; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 64 | display: flex; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 65 | flex-direction: column; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 66 | padding: 4px; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 67 | background: var(--panel-2); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 68 | border: 1px solid var(--border); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 69 | border-radius: 8px; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 70 | box-shadow: 0 8px 24px rgb(0 0 0 / 0.35); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 71 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 72 | button { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 73 | background: transparent; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 74 | border: none; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 75 | border-radius: 6px; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 76 | color: var(--text); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 77 | font: inherit; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 78 | font-size: 13.5px; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 79 | text-align: left; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 80 | padding: 7px 10px; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 81 | cursor: pointer; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 82 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 83 | button:hover:not(:disabled), button:focus-visible { background: var(--hover); outline: none; } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 84 | button:disabled { opacity: 0.5; cursor: default; } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 85 | .danger { color: var(--danger); } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 86 | </style> |