atagen

malice

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