Add example for menus.getTargetElement API (#369)

This commit is contained in:
Rob Wu
2018-09-05 20:02:53 +02:00
committed by wbamberg
parent 7d587095ac
commit 4c3d87e214
7 changed files with 304 additions and 0 deletions

View 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) {`.

View 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;
}
});

View 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;
}
}
})();

View 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"
]
}

View 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.
}

View 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>

View 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);
}