/** * Copy-to-clipboard button — click writes a value to the clipboard and swaps * the button icon to a check for ~1.2s. Self-contained: markup is fully * rendered server-side, this component only wires the click handler. * * Static value (read from the button itself): * * * Live input value (resolved at click-time from the target element's .value): * */ import { resolveHost } from '../core/app-dom.js'; const BUTTON_SELECTOR = '[data-copy-button]'; const DONE_CLASS = 'is-copied'; const DONE_TIMEOUT_MS = 1200; const copyText = async (value) => { if (!value) {return false;} if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(value); return true; } catch { return false; } } const textarea = document.createElement('textarea'); textarea.value = value; textarea.setAttribute('readonly', 'true'); textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); let ok = false; try { ok = document.execCommand('copy'); } catch { ok = false; } document.body.removeChild(textarea); return ok; }; export function initCopyField(root = document, options = {}) { const selector = String(options.selector || BUTTON_SELECTOR).trim() || BUTTON_SELECTOR; const host = resolveHost(root); const buttons = Array.from(host.querySelectorAll(selector)); if (!buttons.length) { return { destroy: () => {} }; } const cleanupFns = []; const timers = new Map(); buttons.forEach((button) => { if (!(button instanceof HTMLElement) || button.dataset.copyButtonBound === '1') { return; } button.dataset.copyButtonBound = '1'; // Clipboard API is unavailable in insecure contexts (http without localhost) // and the execCommand fallback is deprecated. If neither is usable, hide // the button entirely rather than show a non-functional control. const clipboardUsable = typeof navigator.clipboard?.writeText === 'function' || typeof document.execCommand === 'function'; if (!clipboardUsable) { button.hidden = true; return; } const onClick = async (event) => { event.preventDefault(); const targetId = (button.dataset.copyTarget || '').trim(); let value = button.dataset.copyValue || ''; if (targetId) { const target = document.getElementById(targetId); if (target && 'value' in target) { value = /** @type {HTMLInputElement} */ (target).value; } } const ok = await copyText(value); if (!ok) {return;} button.classList.add(DONE_CLASS); const prevTimer = timers.get(button); if (prevTimer) {window.clearTimeout(prevTimer);} const timer = window.setTimeout(() => { button.classList.remove(DONE_CLASS); timers.delete(button); }, DONE_TIMEOUT_MS); timers.set(button, timer); }; button.addEventListener('click', onClick); cleanupFns.push(() => { button.removeEventListener('click', onClick); delete button.dataset.copyButtonBound; }); }); const destroy = () => { cleanupFns.forEach((fn) => fn()); timers.forEach((timer) => window.clearTimeout(timer)); timers.clear(); }; return { destroy }; }