armcord/docs/examples/clipboard-fallback-plugin/custom-bundle.js

216 lines
9.6 KiB
JavaScript

(() => {
const module = { exports: {} };
const api = {
logger: {
log: (...args) => console.log("[ClipboardFallback]", ...args),
warn: (...args) => console.warn("[ClipboardFallback]", ...args),
error: (...args) => console.error("[ClipboardFallback]", ...args),
},
};
/**
* Legcord Clipboard Fallback
*
* Discord's web UI uses navigator.clipboard.writeText for actions such as
* "Copy User ID" and "Copy Message Link", and navigator.clipboard.write for
* richer clipboard payloads such as images. In some Legcord/Electron/macOS
* combinations Chromium rejects those calls because the document is not focused
* or the clipboard permission is not granted, leaving the clipboard unchanged.
*
* This renderer plugin replaces those APIs with selection-based copy fallbacks
* that run inside the original click gesture.
*/
module.exports.activate = (api) => {
const PATCH_KEY = Symbol.for("legcord.clipboardFallback.installed");
function install() {
try {
if (!navigator.clipboard) {
api.logger.warn("navigator.clipboard is unavailable");
return;
}
if (navigator.clipboard[PATCH_KEY]) return;
const originalWriteText = navigator.clipboard.writeText?.bind(navigator.clipboard);
const originalWrite = navigator.clipboard.write?.bind(navigator.clipboard);
async function blobToDataUrl(blob) {
if (typeof FileReader !== "undefined") {
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result));
reader.onerror = () => reject(reader.error ?? new Error("Failed to read clipboard image"));
reader.readAsDataURL(blob);
});
}
// Test/runtime fallback for environments with Blob but no FileReader.
const bytes = new Uint8Array(await blob.arrayBuffer());
let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte);
const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(bytes).toString("base64");
return `data:${blob.type || "application/octet-stream"};base64,${base64}`;
}
function selectElementForCopy(element) {
const previousActiveElement = document.activeElement;
const selection = window.getSelection?.() ?? globalThis.getSelection?.();
const range = document.createRange();
element.focus?.();
range.selectNodeContents(element);
selection?.removeAllRanges();
selection?.addRange(range);
const copied = document.execCommand("copy");
selection?.removeAllRanges();
if (previousActiveElement && typeof previousActiveElement.focus === "function") {
try {
previousActiveElement.focus();
} catch {}
}
if (!copied) throw new Error("document.execCommand('copy') returned false");
}
async function fallbackCopyText(text) {
const value = String(text);
const parent = document.body || document.documentElement;
if (!parent) throw new Error("No document body available for clipboard fallback");
const textarea = document.createElement("textarea");
textarea.value = value;
textarea.setAttribute("readonly", "");
textarea.setAttribute("aria-hidden", "true");
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
textarea.style.top = "0";
textarea.style.width = "1px";
textarea.style.height = "1px";
textarea.style.opacity = "0";
textarea.style.pointerEvents = "none";
parent.appendChild(textarea);
const previousActiveElement = document.activeElement;
textarea.focus();
textarea.select();
textarea.setSelectionRange(0, value.length);
const copied = document.execCommand("copy");
textarea.remove();
if (previousActiveElement && typeof previousActiveElement.focus === "function") {
try {
previousActiveElement.focus();
} catch {}
}
if (!copied) throw new Error("document.execCommand('copy') returned false");
}
async function getClipboardItemType(item, type) {
if (!item?.types?.includes(type) || typeof item.getType !== "function") return null;
return await item.getType(type);
}
async function fallbackCopyItems(items) {
const parent = document.body || document.documentElement;
if (!parent) throw new Error("No document body available for clipboard fallback");
const container = document.createElement("div");
container.contentEditable = "true";
container.setAttribute("aria-hidden", "true");
container.style.position = "fixed";
container.style.left = "-9999px";
container.style.top = "0";
container.style.width = "1px";
container.style.height = "1px";
container.style.overflow = "hidden";
for (const item of items) {
const htmlBlob = await getClipboardItemType(item, "text/html");
if (htmlBlob) {
container.innerHTML += await htmlBlob.text();
continue;
}
const textBlob = await getClipboardItemType(item, "text/plain");
if (textBlob) {
const span = document.createElement("span");
span.textContent = await textBlob.text();
container.appendChild(span);
continue;
}
const imageType = item?.types?.find((type) => type.startsWith("image/"));
if (imageType && typeof item.getType === "function") {
const imageBlob = await item.getType(imageType);
const image = document.createElement("img");
image.src = await blobToDataUrl(imageBlob);
image.alt = "";
container.appendChild(image);
}
}
if (!container.innerHTML && !container.textContent && !container.children?.length) {
throw new Error("No supported clipboard item types found");
}
parent.appendChild(container);
selectElementForCopy(container);
container.remove();
}
if (originalWriteText) {
Object.defineProperty(navigator.clipboard, "writeText", {
configurable: true,
value: async (text) => {
try {
await fallbackCopyText(text);
api.logger.log("copied text via fallback", text);
} catch (fallbackError) {
api.logger.warn("text fallback failed; trying original writeText", fallbackError);
return originalWriteText(text);
}
},
});
}
if (originalWrite) {
Object.defineProperty(navigator.clipboard, "write", {
configurable: true,
value: async (items) => {
try {
await fallbackCopyItems(items);
api.logger.log("copied rich clipboard payload via fallback");
} catch (fallbackError) {
api.logger.warn("rich clipboard fallback failed; trying original write", fallbackError);
return originalWrite(items);
}
},
});
}
Object.defineProperty(navigator.clipboard, PATCH_KEY, {
configurable: false,
enumerable: false,
value: true,
});
api.logger.log("installed");
} catch (error) {
api.logger.error("install failed", error);
}
}
install();
window.addEventListener("DOMContentLoaded", install, { once: true });
};
if (typeof module.exports.activate === "function") {
module.exports.activate(api);
}
})();