From de37809eec254d897dcaffe0b65b24d6810ea6b2 Mon Sep 17 00:00:00 2001 From: Drew Malzahn Date: Thu, 18 Jun 2026 01:30:36 +0000 Subject: [PATCH] fix: Copy share URL over HTTP via execCommand fallback navigator.clipboard is only available in a secure context (HTTPS or localhost), so the Copy URL button threw over plain HTTP. Fall back to the deprecated document.execCommand("copy") when the Clipboard API is unavailable. Co-Authored-By: Claude Opus 4.8 --- src/CharacterSheet.tsx | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/CharacterSheet.tsx b/src/CharacterSheet.tsx index bdc7944..37b4a9e 100644 --- a/src/CharacterSheet.tsx +++ b/src/CharacterSheet.tsx @@ -192,6 +192,29 @@ function apiURL(path: string) { return new URL(path, window.location.href).toString(); } +// Copy text to the clipboard. navigator.clipboard is only available in a +// secure context (HTTPS or localhost); over plain HTTP it's undefined, so we +// fall back to the deprecated execCommand("copy"), which still works there. +// Must be called from within a user gesture (e.g. a click handler). +async function copyToClipboard(text: string) { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + return; + } + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.focus(); + ta.select(); + try { + document.execCommand("copy"); + } finally { + document.body.removeChild(ta); + } +} + export default function CharacterSheet() { const [activeTab, setActiveTab] = useState("main"); const [urlMode, setUrlMode] = useState(false); @@ -516,7 +539,7 @@ export default function CharacterSheet() { // Backend unreachable: fall back to a self-contained inline ?c= link. shareURL = base + "?c=" + (await compressToBase64(json)); } - await navigator.clipboard.writeText(shareURL); + await copyToClipboard(shareURL); setCopyStatus(true); setTimeout(() => setCopyStatus(false), 2000); }, [collectData]);