diff --git a/lyra/web/static/index.html b/lyra/web/static/index.html index 06f58d1..a77a5d6 100644 --- a/lyra/web/static/index.html +++ b/lyra/web/static/index.html @@ -499,7 +499,10 @@ // iOS over plain-HTTP LAN (where navigator.clipboard is undefined). function copyToClipboard(text) { text = text == null ? "" : String(text); - if (navigator.clipboard && navigator.clipboard.writeText) { + // Only trust the async Clipboard API in a secure context; on the LAN PWA + // (plain HTTP) it's either absent or resolves without actually copying, so + // we go straight to the iOS-tuned execCommand path there. + if (window.isSecureContext && navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(text).catch(() => legacyCopy(text)); } return legacyCopy(text); @@ -508,19 +511,24 @@ return new Promise((resolve, reject) => { const ta = document.createElement("textarea"); ta.value = text; + // iOS will only copy from a readOnly + contentEditable field with a real + // Range selection; readOnly also stops the keyboard from popping. + ta.readOnly = true; ta.contentEditable = "true"; - ta.readOnly = false; ta.style.position = "fixed"; ta.style.top = "0"; ta.style.left = "0"; - ta.style.opacity = "0"; + ta.style.width = "1px"; + ta.style.height = "1px"; + ta.style.fontSize = "16px"; // avoid iOS zoom side-effects document.body.appendChild(ta); + ta.focus(); const range = document.createRange(); range.selectNodeContents(ta); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); - ta.setSelectionRange(0, text.length); // iOS needs an explicit range + ta.setSelectionRange(0, text.length); // the bit iOS actually needs let ok = false; try { ok = document.execCommand("copy"); } catch (e) { ok = false; } sel.removeAllRanges(); @@ -537,14 +545,17 @@ b.title = "Copy message"; b.addEventListener("click", (e) => { e.stopPropagation(); - copyToClipboard(typeof getText === "function" ? getText() : getText) + const text = typeof getText === "function" ? getText() : getText; + copyToClipboard(text) .then(() => { b.textContent = "✓"; b.classList.add("copied"); setTimeout(() => { b.textContent = "⧉"; b.classList.remove("copied"); }, 1200); }) .catch(() => { - b.textContent = "✗"; - setTimeout(() => { b.textContent = "⧉"; }, 1200); + // Last resort (some iOS configs block programmatic copy): surface the + // text in a prompt so it can be selected + copied by hand. + window.prompt("Copy this message:", text); + b.textContent = "⧉"; }); }); return b;