| 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 | {#if $lightbox.mxc} |
| 51 | {@const mxc = $lightbox.mxc} |
| 52 | <button type="button" class="ghost-btn" aria-label="Copy MXC URI" title={mxc} on:click={() => void copyMxc(mxc)}> |
| 53 | {copied ? 'Copied' : 'Copy MXC'} |
| 54 | </button> |
| 55 | {/if} |
| 56 | <a class="ghost-btn" href={$lightbox.src} target="_blank" rel="noopener noreferrer">Open original</a> |
| 57 | <a class="ghost-btn" href={$lightbox.src} download={$lightbox.filename}>Download</a> |
| 58 | <button type="button" class="icon-btn" aria-label="Close image" title="Close (Esc)" bind:this={closeBtn} on:click={close}> |
| 59 | <span aria-hidden="true">✕</span> |
| 60 | </button> |
| 61 | </div> |
| 62 | <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions --> |
| 63 | <img class="lightbox-image" src={$lightbox.src} alt={$lightbox.alt} on:click={close} /> |
| 64 | </div> |
| 65 | {/if} |
| 66 | |
| 67 | <style> |
| 68 | .lightbox { |
| 69 | animation: fade-in var(--pop); |
| 70 | position: fixed; |
| 71 | inset: 0; |
| 72 | z-index: 100; |
| 73 | display: flex; |
| 74 | flex-direction: column; |
| 75 | align-items: center; |
| 76 | justify-content: center; |
| 77 | gap: 12px; |
| 78 | padding: 56px 24px 24px; |
| 79 | background: rgb(0 0 0 / 0.82); |
| 80 | backdrop-filter: blur(4px); |
| 81 | } |
| 82 | .lightbox-bar { |
| 83 | position: absolute; |
| 84 | top: 0; |
| 85 | left: 0; |
| 86 | right: 0; |
| 87 | display: flex; |
| 88 | align-items: center; |
| 89 | gap: 8px; |
| 90 | padding: 10px 16px; |
| 91 | color: #fff; |
| 92 | } |
| 93 | .lightbox-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; } |
| 94 | .lightbox-bar a { text-decoration: none; } |
| 95 | .lightbox-image { |
| 96 | max-width: 100%; |
| 97 | max-height: 100%; |
| 98 | object-fit: contain; |
| 99 | border-radius: var(--r-sm); |
| 100 | box-shadow: 0 12px 40px rgb(0 0 0 / 0.5); |
| 101 | cursor: zoom-out; |
| 102 | } |
| 103 | </style> |