atagen

malice

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