fix(web): copy button actually copies on iOS
The execCommand fallback returned true but copied nothing because the textarea was readOnly=false. iOS only copies from a readOnly + contentEditable field with a real Range selection + setSelectionRange — fixed that. Also skip the async Clipboard API unless window.isSecureContext (on the plain-HTTP LAN PWA it could resolve without copying, showing a false checkmark). If programmatic copy still fails, fall back to a prompt() with the text so it can be copied by hand, and only show the ✓ on real success. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user