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