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).
|
// iOS over plain-HTTP LAN (where navigator.clipboard is undefined).
|
||||||
function copyToClipboard(text) {
|
function copyToClipboard(text) {
|
||||||
text = text == null ? "" : String(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 navigator.clipboard.writeText(text).catch(() => legacyCopy(text));
|
||||||
}
|
}
|
||||||
return legacyCopy(text);
|
return legacyCopy(text);
|
||||||
@@ -508,19 +511,24 @@
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ta = document.createElement("textarea");
|
const ta = document.createElement("textarea");
|
||||||
ta.value = text;
|
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.contentEditable = "true";
|
||||||
ta.readOnly = false;
|
|
||||||
ta.style.position = "fixed";
|
ta.style.position = "fixed";
|
||||||
ta.style.top = "0";
|
ta.style.top = "0";
|
||||||
ta.style.left = "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);
|
document.body.appendChild(ta);
|
||||||
|
ta.focus();
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.selectNodeContents(ta);
|
range.selectNodeContents(ta);
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
sel.addRange(range);
|
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;
|
let ok = false;
|
||||||
try { ok = document.execCommand("copy"); } catch (e) { ok = false; }
|
try { ok = document.execCommand("copy"); } catch (e) { ok = false; }
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
@@ -537,14 +545,17 @@
|
|||||||
b.title = "Copy message";
|
b.title = "Copy message";
|
||||||
b.addEventListener("click", (e) => {
|
b.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
copyToClipboard(typeof getText === "function" ? getText() : getText)
|
const text = typeof getText === "function" ? getText() : getText;
|
||||||
|
copyToClipboard(text)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
b.textContent = "✓"; b.classList.add("copied");
|
b.textContent = "✓"; b.classList.add("copied");
|
||||||
setTimeout(() => { b.textContent = "⧉"; b.classList.remove("copied"); }, 1200);
|
setTimeout(() => { b.textContent = "⧉"; b.classList.remove("copied"); }, 1200);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
b.textContent = "✗";
|
// Last resort (some iOS configs block programmatic copy): surface the
|
||||||
setTimeout(() => { b.textContent = "⧉"; }, 1200);
|
// text in a prompt so it can be selected + copied by hand.
|
||||||
|
window.prompt("Copy this message:", text);
|
||||||
|
b.textContent = "⧉";
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return b;
|
return b;
|
||||||
|
|||||||
Reference in New Issue
Block a user