viewer.ts (2381B)
1 /** 2 * A small image viewer for sheet images, so the cursor's "View image" 3 * pill does what it says. A native <dialog>: Escape, a click anywhere or 4 * the close button dismisses it, focus returns to the image, and the 5 * page behind is inert while it is open. 6 * 7 * Images inside a link keep the link's behaviour; tiny inline images 8 * (badges, icons) are left alone. 9 */ 10 let dialog: HTMLDialogElement | null = null; 11 let shown: HTMLImageElement | null = null; 12 13 function ensureDialog(): HTMLDialogElement { 14 if (dialog && dialog.isConnected) return dialog; 15 dialog = document.createElement('dialog'); 16 dialog.className = 'dmn-viewer'; 17 dialog.setAttribute('aria-label', 'Image viewer'); 18 const img = document.createElement('img'); 19 img.className = 'dmn-viewer__img'; 20 const cap = document.createElement('p'); 21 cap.className = 'dmn-viewer__cap'; 22 const close = document.createElement('button'); 23 close.type = 'button'; 24 close.className = 'dmn-viewer__close'; 25 close.textContent = 'Close'; 26 dialog.append(img, cap, close); 27 dialog.addEventListener('click', () => dialog?.close()); 28 dialog.addEventListener('close', () => shown?.focus({ preventScroll: true })); 29 document.body.appendChild(dialog); 30 return dialog; 31 } 32 33 function open(from: HTMLImageElement): void { 34 const d = ensureDialog(); 35 const img = d.querySelector<HTMLImageElement>('.dmn-viewer__img')!; 36 const cap = d.querySelector<HTMLElement>('.dmn-viewer__cap')!; 37 img.src = from.currentSrc || from.src; 38 img.alt = from.alt; 39 cap.textContent = from.alt || ''; 40 cap.hidden = !from.alt; 41 shown = from; 42 d.showModal(); 43 } 44 45 export function initViewer(): void { 46 document.querySelectorAll<HTMLImageElement>('.prose img:not([data-zoomable])').forEach((img) => { 47 if (img.closest('a')) return; 48 const mark = () => { 49 if (img.naturalWidth && img.naturalWidth < 120) return; 50 img.dataset.zoomable = ''; 51 img.tabIndex = 0; 52 img.setAttribute('role', 'button'); 53 img.setAttribute('aria-label', img.alt ? `Enlarge image: ${img.alt}` : 'Enlarge image'); 54 img.addEventListener('click', () => open(img)); 55 img.addEventListener('keydown', (e) => { 56 if (e.key === 'Enter' || e.key === ' ') { 57 e.preventDefault(); 58 open(img); 59 } 60 }); 61 }; 62 if (img.complete) mark(); 63 else img.addEventListener('load', mark, { once: true }); 64 }); 65 }