atagen

malice

public
Star0
Watch0
2021154f9e16d427af2b4379df1017f1c9e10072
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/ImageLightbox.svelteSvelte107 lines3.2 KB
1
<script lang="ts">
2
  import { tick } from 'svelte';
3
  import { lightbox } from './lightbox';
4
5
  let closeBtn: HTMLButtonElement | null = null;
6
  let copied = false;
7
  let copiedTimer: number | null = null;
8
9
  async function copyMxc(mxc: string): Promise<void> {
10
    try {
11
      await navigator.clipboard.writeText(mxc);
12
    } catch {
13
      return;
14
    }
15
    copied = true;
16
    if (copiedTimer !== null) window.clearTimeout(copiedTimer);
17
    copiedTimer = window.setTimeout(() => (copied = false), 1500);
18
  }
19
  let returnFocus: HTMLElement | null = null;
20
21
  $: if ($lightbox) void focusClose();
22
23
  async function focusClose(): Promise<void> {
24
    if (!returnFocus) returnFocus = document.activeElement as HTMLElement | null;
25
    await tick();
26
    closeBtn?.focus();
27
  }
28
29
  function close(): void {
30
    lightbox.set(null);
31
    returnFocus?.focus();
32
    returnFocus = null;
33
  }
34
35
  function onKey(ev: KeyboardEvent): void {
36
    if ($lightbox && ev.key === 'Escape') {
37
      ev.stopPropagation();
38
      close();
39
    }
40
  }
41
</script>
42
43
<svelte:window on:keydown|capture={onKey} />
44
45
{#if $lightbox}
46
  <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
47
  <div class="lightbox" role="dialog" tabindex="-1" aria-modal="true" aria-label={$lightbox.filename} on:click|self={close}>
48
    <div class="lightbox-bar">
49
      <span class="lightbox-name">{$lightbox.filename}</span>
50
      <button
51
        type="button"
52
        class="ghost-btn"
53
        aria-label="Copy MXC URI"
54
        title={$lightbox.mxc ?? 'MXC URI unavailable for this attachment'}
55
        disabled={!$lightbox.mxc}
56
        on:click={() => $lightbox.mxc && void copyMxc($lightbox.mxc)}
57
      >
58
        {copied ? 'Copied' : 'Copy MXC'}
59
      </button>
60
      <a class="ghost-btn" href={$lightbox.src} target="_blank" rel="noopener noreferrer">Open original</a>
61
      <a class="ghost-btn" href={$lightbox.src} download={$lightbox.filename}>Download</a>
62
      <button type="button" class="icon-btn" aria-label="Close image" title="Close (Esc)" bind:this={closeBtn} on:click={close}>
63
        <span aria-hidden="true">✕</span>
64
      </button>
65
    </div>
66
    <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
67
    <img class="lightbox-image" src={$lightbox.src} alt={$lightbox.alt} on:click={close} />
68
  </div>
69
{/if}
70
71
<style>
72
  .lightbox {
73
    animation: fade-in var(--pop);
74
    position: fixed;
75
    inset: 0;
76
    z-index: 100;
77
    display: flex;
78
    flex-direction: column;
79
    align-items: center;
80
    justify-content: center;
81
    gap: 12px;
82
    padding: 56px 24px 24px;
83
    background: rgb(0 0 0 / 0.82);
84
    backdrop-filter: blur(4px);
85
  }
86
  .lightbox-bar {
87
    position: absolute;
88
    top: 0;
89
    left: 0;
90
    right: 0;
91
    display: flex;
92
    align-items: center;
93
    gap: 8px;
94
    padding: 10px 16px;
95
    color: #fff;
96
  }
97
  .lightbox-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
98
  .lightbox-bar a { text-decoration: none; }
99
  .lightbox-image {
100
    max-width: 100%;
101
    max-height: 100%;
102
    object-fit: contain;
103
    border-radius: var(--r-sm);
104
    box-shadow: 0 12px 40px rgb(0 0 0 / 0.5);
105
    cursor: zoom-out;
106
  }
107
</style>