mirror of
https://github.com/mdn/webextensions-examples.git
synced 2026-04-16 06:18:35 +02:00
Add example for menus.getTargetElement API (#369)
This commit is contained in:
36
menu-remove-element/README.md
Normal file
36
menu-remove-element/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# menu-remove-element
|
||||
|
||||
## What it does
|
||||
|
||||
This extension adds a menu item that's shown when the context menu is opened in a document.
|
||||
Upon click, a panel is shown, where the clicked element and its ancestor elements are displayed in a list.
|
||||
Upon hovering over these elements, the corresponding elements in the document are visually highlighted.
|
||||
When one of the list items are clicked, the element is removed from the page.
|
||||
|
||||
## What it shows
|
||||
|
||||
This extension demonstrates the following APIs:
|
||||
|
||||
- [`menus.create`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/menus/create)
|
||||
- [`menus.onClicked`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/menus/onClicked) and in particular its [`info.targetElementId`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/OnClickData) property.
|
||||
- [`menus.getTargetElement`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/getTargetElement)
|
||||
- [`pageAction.openPopup`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/openPopup)
|
||||
- [`pageAction.show`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/show) and [`pageAction.hide`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/hide)
|
||||
- [`tabs.executeScript`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs.executeScript)
|
||||
|
||||
The `pageAction.openPopup` method requires user interaction, which is satisfied by calling the method from the `menus.onClicked` event.
|
||||
|
||||
The `activeTab` permission is used to unlock access to the page upon clicking the menu item,
|
||||
so that the `info.targetElementId` becomes available and `tabs.executeScript` can be used to run a content script in a specific frame in the tab.
|
||||
|
||||
The `menus.getTargetElement` API and `info.targetElementId` were introduced in Firefox 63.
|
||||
|
||||
## Polyfill
|
||||
|
||||
In browsers that do not support this API, an extension has to register a content script that listens to the "contextmenu" event, as shown in `menusGetTargetElementPolyfill.js`.
|
||||
|
||||
This example includes the polyfill to demonstrate how one can create an extension that is compatible with browsers that do not support a new API.
|
||||
If you are not interested in supporting browsers that lack the `browser.menus.getTargetElement` API, modify the example as follows:
|
||||
|
||||
- Remove `menusGetTargetElementPolyfill.js`
|
||||
- Modify popup.js and remove the entire if-block that starts with `if (!browser.menus.getTargetElement) {`.
|
||||
35
menu-remove-element/background.js
Normal file
35
menu-remove-element/background.js
Normal file
@@ -0,0 +1,35 @@
|
||||
"use strict";
|
||||
|
||||
var popupParameters;
|
||||
|
||||
browser.menus.create({
|
||||
id: "remove_element",
|
||||
title: "Remove element",
|
||||
documentUrlPatterns: ["https://*/*", "http://*/*"],
|
||||
contexts: ["audio", "editable", "frame", "image", "link", "page", "password", "video"],
|
||||
});
|
||||
|
||||
browser.menus.onClicked.addListener(async (info, tab) => {
|
||||
popupParameters = {
|
||||
tabId: tab.id,
|
||||
frameId: info.frameId,
|
||||
targetElementId: info.targetElementId,
|
||||
};
|
||||
// Show our extension panel (popup.html) using pageAction.openPopup.
|
||||
// This panel can only be shown when the pageAction button is enabled,
|
||||
// so we temporarily enable it via pageAction.show().
|
||||
//
|
||||
// Even though pageAction.show is an asynchronous API, we do not use "await"
|
||||
// before continuing with the execution of the code, because the
|
||||
// pageAction.openPopup method requires a user gesture, and user gestures are
|
||||
// lost when a function returns or waits on a promise.
|
||||
browser.pageAction.show(tab.id);
|
||||
await browser.pageAction.openPopup();
|
||||
await browser.pageAction.hide(tab.id);
|
||||
});
|
||||
|
||||
browser.runtime.onMessage.addListener(async (msg) => {
|
||||
if (msg === "getPopupParameters") {
|
||||
return popupParameters;
|
||||
}
|
||||
});
|
||||
82
menu-remove-element/contentscript.js
Normal file
82
menu-remove-element/contentscript.js
Normal file
@@ -0,0 +1,82 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
if (window.hasRunContentScriptOnce === true) return;
|
||||
window.hasRunContentScriptOnce = true;
|
||||
|
||||
browser.runtime.onConnect.addListener(port => {
|
||||
if (port.name !== "portFromPopup") return;
|
||||
let targetElements;
|
||||
|
||||
port.onMessage.addListener(msg => {
|
||||
if (msg.action === "getElementDescriptions") {
|
||||
let elem = browser.menus.getTargetElement(msg.targetElementId);
|
||||
setTargetElement(elem);
|
||||
} else if (msg.action === "highlightElement") {
|
||||
let element = targetElements[msg.elementIndex];
|
||||
if (element) highlightElement(element);
|
||||
else removeHighlights();
|
||||
} else if (msg.action === "removeElement") {
|
||||
let element = targetElements[msg.elementIndex];
|
||||
if (element) {
|
||||
// When an element is removed, all of its descendants are removed too.
|
||||
// Update the UI, to show all nodes starting from the parent element.
|
||||
let parentElement = element.parentElement;
|
||||
element.remove();
|
||||
setTargetElement(parentElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
port.onDisconnect.addListener(() => {
|
||||
// Clean up when the port is disconnected (e.g. popup was closed).
|
||||
removeHighlights();
|
||||
});
|
||||
|
||||
function setTargetElement(elem) {
|
||||
targetElements = [];
|
||||
while (elem) {
|
||||
targetElements.unshift(elem);
|
||||
elem = elem.parentElement;
|
||||
}
|
||||
|
||||
// Reply with some description of the elements, so that the available
|
||||
// elements can be shown in the popup's UI.
|
||||
let descriptions = targetElements.map(elem => {
|
||||
// For example, take the first 100 characters of the HTML element.
|
||||
return elem.cloneNode().outerHTML.slice(0, 100);
|
||||
});
|
||||
port.postMessage({
|
||||
action: "elementDescriptions",
|
||||
descriptions,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var highlightedBox;
|
||||
function highlightElement(element) {
|
||||
removeHighlights();
|
||||
let boundingRect = element.getBoundingClientRect();
|
||||
highlightedBox = document.createElement("div");
|
||||
highlightedBox.style.outline = "2px dotted red";
|
||||
highlightedBox.style.margin = "0";
|
||||
highlightedBox.style.border = "0";
|
||||
highlightedBox.style.padding = "0";
|
||||
highlightedBox.style.backgroundColor = "rgba(100, 0, 0, 0.3)";
|
||||
highlightedBox.style.pointerEvents = "none";
|
||||
highlightedBox.style.zIndex = "2147483647";
|
||||
highlightedBox.style.position = "fixed";
|
||||
highlightedBox.style.top = boundingRect.top + "px";
|
||||
highlightedBox.style.left = boundingRect.left + "px";
|
||||
highlightedBox.style.width = boundingRect.width + "px";
|
||||
highlightedBox.style.height = boundingRect.height + "px";
|
||||
|
||||
(document.body || document.documentElement).appendChild(highlightedBox);
|
||||
}
|
||||
|
||||
function removeHighlights() {
|
||||
if (highlightedBox) {
|
||||
highlightedBox.remove();
|
||||
highlightedBox = null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
16
menu-remove-element/manifest.json
Normal file
16
menu-remove-element/manifest.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Remove element on click",
|
||||
"description": "Adds a context menu item that shows a panel upon click, from where you can choose to remove the clicked element.",
|
||||
"version": "1",
|
||||
"manifest_version": 2,
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"page_action": {
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"permissions": [
|
||||
"menus",
|
||||
"activeTab"
|
||||
]
|
||||
}
|
||||
35
menu-remove-element/menusGetTargetElementPolyfill.js
Normal file
35
menu-remove-element/menusGetTargetElementPolyfill.js
Normal file
@@ -0,0 +1,35 @@
|
||||
"use strict";
|
||||
|
||||
// The browser.menus.getTargetElement API allows extensions to identify the
|
||||
// clicked element that matches a menus.onClicked or menus.onShown event, as of
|
||||
// Firefox 63.
|
||||
//
|
||||
// This polyfill script approximates the behavior, by returning the target of
|
||||
// the most recently observed contextmenu event in this document.
|
||||
//
|
||||
// Limitations:
|
||||
// - Cannot return the menu target before this script has run.
|
||||
// In contrast, the real menus.getTargetElement API will return the element
|
||||
// even if the extension did not run any scripts at the time of the click.
|
||||
//
|
||||
// - Does not offer the guarantee that the element matches the menus.onClicked
|
||||
// or menus.onShown event.
|
||||
// In contrast, the real menus.getTargetElement API only returns an element
|
||||
// if the given first parameter matches the info.targetElementId integer from
|
||||
// the menus.onClicked or menus.onShown events.
|
||||
if (!browser.menus || !browser.menus.getTargetElement) {
|
||||
var menuTarget = null;
|
||||
let cleanupIfNeeded = () => {
|
||||
if (menuTarget && !document.contains(menuTarget)) menuTarget = null;
|
||||
};
|
||||
document.addEventListener("contextmenu", (event) => {
|
||||
menuTarget = event.target;
|
||||
}, true);
|
||||
document.addEventListener("visibilitychange", cleanupIfNeeded, true);
|
||||
browser.menus = browser.menus || {};
|
||||
browser.menus.getTargetElement = () => {
|
||||
cleanupIfNeeded();
|
||||
return menuTarget;
|
||||
};
|
||||
true; // Used by popup.js, as variable didUsePolyfill.
|
||||
}
|
||||
7
menu-remove-element/popup.html
Normal file
7
menu-remove-element/popup.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<body>
|
||||
<div id="outputStatus">
|
||||
Open a menu in the current tab and right-click on the "Remove element" option.
|
||||
</div>
|
||||
<script src="popup.js"></script>
|
||||
93
menu-remove-element/popup.js
Normal file
93
menu-remove-element/popup.js
Normal file
@@ -0,0 +1,93 @@
|
||||
"use strict";
|
||||
|
||||
(async () => {
|
||||
const popupParameters = await browser.runtime.sendMessage("getPopupParameters");
|
||||
let {tabId, frameId, targetElementId} = popupParameters;
|
||||
|
||||
// Ensure that the popup is opened for the currently active tab,
|
||||
// to prevent users from interacting with hidden tabs.
|
||||
await assertIsCurrentTab(tabId);
|
||||
|
||||
// The browser.menus.getTargetElement API is only available in Firefox 63+.
|
||||
if (!browser.menus.getTargetElement) {
|
||||
let [didUsePolyfill] = await browser.tabs.executeScript(tabId, {
|
||||
runAt: "document_start",
|
||||
frameId,
|
||||
file: "menusGetTargetElementPolyfill.js",
|
||||
});
|
||||
if (didUsePolyfill === true) {
|
||||
console.log("Registered a polyfill for browser.menus.getTargetElement - re-open the menu to see it in action.");
|
||||
let outputStatus = document.getElementById("outputStatus")
|
||||
outputStatus.textContent = `
|
||||
This extension requires the browser.menus.getTargetElement API,
|
||||
which is only available as of Firefox 63.
|
||||
To see the expected behavior, please re-open the menu.
|
||||
`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Inject script in page (requires activeTab permission).
|
||||
await browser.tabs.executeScript(tabId, {
|
||||
runAt: "document_start",
|
||||
frameId,
|
||||
file: "contentscript.js",
|
||||
});
|
||||
|
||||
let port = browser.tabs.connect(tabId, {
|
||||
name: "portFromPopup",
|
||||
frameId,
|
||||
});
|
||||
port.onMessage.addListener(msg => {
|
||||
if (msg.action === "elementDescriptions") {
|
||||
renderElementDescriptions(port, msg.descriptions);
|
||||
}
|
||||
});
|
||||
port.onDisconnect.addListener(() => {
|
||||
let outputStatus = document.getElementById("outputStatus")
|
||||
outputStatus.textContent = `The port to the page was closed.${port.error ? "Reason: " + port.error.message : ""}`;
|
||||
});
|
||||
port.postMessage({
|
||||
action: "getElementDescriptions",
|
||||
targetElementId,
|
||||
});
|
||||
})();
|
||||
|
||||
async function assertIsCurrentTab(tabId) {
|
||||
let [currentTab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
if (currentTab.id !== tabId) {
|
||||
throw new Error("The given tab ID is not the currently active tab");
|
||||
}
|
||||
}
|
||||
|
||||
function renderElementDescriptions(port, descriptions) {
|
||||
let outputStatus = document.getElementById("outputStatus")
|
||||
if (!descriptions.length) {
|
||||
outputStatus.textContent = "Cannot find the target element. Please re-open the menu to try again.";
|
||||
return;
|
||||
}
|
||||
let list = document.createElement("ul");
|
||||
descriptions.forEach((description, elementIndex) => {
|
||||
let item = document.createElement("li");
|
||||
item.textContent = description;
|
||||
item.tabIndex = 1;
|
||||
item.onclick = () => {
|
||||
port.postMessage({
|
||||
action: "removeElement",
|
||||
elementIndex: elementIndex,
|
||||
});
|
||||
};
|
||||
item.onmouseenter = () => {
|
||||
port.postMessage({
|
||||
action: "highlightElement",
|
||||
elementIndex: elementIndex,
|
||||
});
|
||||
};
|
||||
list.appendChild(item);
|
||||
});
|
||||
outputStatus.textContent = "Click on any item to remove the element and its descendants:";
|
||||
outputStatus.appendChild(list);
|
||||
}
|
||||
Reference in New Issue
Block a user