// Callback reads runtime.lastError to prevent an unchecked error from being // logged when the extension attempt to register the already-registered menu // again. Menu registrations in event pages persist across extension restarts. browser.contextMenus.create({ id: "copy-link-to-clipboard", title: "Copy link to clipboard", contexts: ["link"], }, // See https://extensionworkshop.com/documentation/develop/manifest-v3-migration-guide/#event-pages-and-backward-compatibility // for information on the purpose of this error capture. () => void browser.runtime.lastError, ); browser.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === "copy-link-to-clipboard") { // Examples: text and HTML to be copied. const text = "This is text: " + info.linkUrl; // Always HTML-escape external input to avoid XSS. const safeUrl = escapeHTML(info.linkUrl); const html = `This is HTML: ${safeUrl}`; // The example will show how data can be copied, but since background // pages cannot directly write to the clipboard, we will run a content // script that copies the actual content. // clipboard-helper.js defines function copyToClipboard. const code = "copyToClipboard(" + JSON.stringify(text) + "," + JSON.stringify(html) + ");"; browser.tabs.executeScript({ code: "typeof copyToClipboard === 'function';", }).then((results) => { // The content script's last expression will be true if the function // has been defined. If this is not the case, then we need to run // clipboard-helper.js to define function copyToClipboard. if (!results || results[0] !== true) { return browser.tabs.executeScript(tab.id, { file: "clipboard-helper.js", }); } }).then(() => { return browser.tabs.executeScript(tab.id, { code, }); }).catch((error) => { // This could happen if the extension is not allowed to run code in // the page, for example if the tab is a privileged page. console.error("Failed to copy text: " + error); }); } }); // https://gist.github.com/Rob--W/ec23b9d6db9e56b7e4563f1544e0d546 function escapeHTML(str) { // Note: string cast using String; may throw if `str` is non-serializable, e.g. a Symbol. // Most often this is not the case though. return String(str) .replace(/&/g, "&") .replace(/"/g, """).replace(/'/g, "'") .replace(//g, ">"); }