summaryrefslogtreecommitdiffstats
path: root/application/basilisk/components/extensions/ext-contextMenus.js
diff options
context:
space:
mode:
Diffstat (limited to 'application/basilisk/components/extensions/ext-contextMenus.js')
-rw-r--r--application/basilisk/components/extensions/ext-contextMenus.js640
1 files changed, 0 insertions, 640 deletions
diff --git a/application/basilisk/components/extensions/ext-contextMenus.js b/application/basilisk/components/extensions/ext-contextMenus.js
deleted file mode 100644
index 34a828f13..000000000
--- a/application/basilisk/components/extensions/ext-contextMenus.js
+++ /dev/null
@@ -1,640 +0,0 @@
-/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set sts=2 sw=2 et tw=80: */
-"use strict";
-
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-Cu.import("resource://gre/modules/MatchPattern.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-var {
- EventManager,
- ExtensionError,
- IconDetails,
-} = ExtensionUtils;
-
-const ACTION_MENU_TOP_LEVEL_LIMIT = 6;
-
-// Map[Extension -> Map[ID -> MenuItem]]
-// Note: we want to enumerate all the menu items so
-// this cannot be a weak map.
-var gContextMenuMap = new Map();
-
-// Map[Extension -> MenuItem]
-var gRootItems = new Map();
-
-// If id is not specified for an item we use an integer.
-var gNextMenuItemID = 0;
-
-// Used to assign unique names to radio groups.
-var gNextRadioGroupID = 0;
-
-// The max length of a menu item's label.
-var gMaxLabelLength = 64;
-
-var gMenuBuilder = {
- // When a new contextMenu is opened, this function is called and
- // we populate the |xulMenu| with all the items from extensions
- // to be displayed. We always clear all the items again when
- // popuphidden fires.
- build(contextData) {
- let firstItem = true;
- let xulMenu = contextData.menu;
- xulMenu.addEventListener("popuphidden", this);
- this.xulMenu = xulMenu;
- for (let [, root] of gRootItems) {
- let rootElement = this.buildElementWithChildren(root, contextData);
- if (!rootElement.firstChild || !rootElement.firstChild.childNodes.length) {
- // If the root has no visible children, there is no reason to show
- // the root menu item itself either.
- continue;
- }
- rootElement.setAttribute("ext-type", "top-level-menu");
- rootElement = this.removeTopLevelMenuIfNeeded(rootElement);
-
- // Display the extension icon on the root element.
- if (root.extension.manifest.icons) {
- let parentWindow = contextData.menu.ownerGlobal;
- let extension = root.extension;
-
- let {icon} = IconDetails.getPreferredIcon(extension.manifest.icons, extension,
- 16 * parentWindow.devicePixelRatio);
-
- // The extension icons in the manifest are not pre-resolved, since
- // they're sometimes used by the add-on manager when the extension is
- // not enabled, and its URLs are not resolvable.
- let resolvedURL = root.extension.baseURI.resolve(icon);
-
- if (rootElement.localName == "menu") {
- rootElement.setAttribute("class", "menu-iconic");
- } else if (rootElement.localName == "menuitem") {
- rootElement.setAttribute("class", "menuitem-iconic");
- }
- rootElement.setAttribute("image", resolvedURL);
- }
-
- if (firstItem) {
- firstItem = false;
- const separator = xulMenu.ownerDocument.createElement("menuseparator");
- this.itemsToCleanUp.add(separator);
- xulMenu.append(separator);
- }
-
- xulMenu.appendChild(rootElement);
- this.itemsToCleanUp.add(rootElement);
- }
- },
-
- // Builds a context menu for browserAction and pageAction buttons.
- buildActionContextMenu(contextData) {
- const {menu} = contextData;
-
- contextData.tab = TabManager.activeTab;
- contextData.pageUrl = contextData.tab.linkedBrowser.currentURI.spec;
-
- const root = gRootItems.get(contextData.extension);
- const children = this.buildChildren(root, contextData);
- const visible = children.slice(0, ACTION_MENU_TOP_LEVEL_LIMIT);
-
- if (visible.length) {
- this.xulMenu = menu;
- menu.addEventListener("popuphidden", this);
-
- const separator = menu.ownerDocument.createElement("menuseparator");
- menu.insertBefore(separator, menu.firstChild);
- this.itemsToCleanUp.add(separator);
-
- for (const child of visible) {
- this.itemsToCleanUp.add(child);
- menu.insertBefore(child, separator);
- }
- }
- },
-
- buildElementWithChildren(item, contextData) {
- const element = this.buildSingleElement(item, contextData);
- const children = this.buildChildren(item, contextData);
- if (children.length) {
- element.firstChild.append(...children);
- }
- return element;
- },
-
- buildChildren(item, contextData) {
- let groupName;
- let children = [];
- for (let child of item.children) {
- if (child.type == "radio" && !child.groupName) {
- if (!groupName) {
- groupName = `webext-radio-group-${gNextRadioGroupID++}`;
- }
- child.groupName = groupName;
- } else {
- groupName = null;
- }
-
- if (child.enabledForContext(contextData)) {
- children.push(this.buildElementWithChildren(child, contextData));
- }
- }
- return children;
- },
-
- removeTopLevelMenuIfNeeded(element) {
- // If there is only one visible top level element we don't need the
- // root menu element for the extension.
- let menuPopup = element.firstChild;
- if (menuPopup && menuPopup.childNodes.length == 1) {
- let onlyChild = menuPopup.firstChild;
- onlyChild.remove();
- return onlyChild;
- }
-
- return element;
- },
-
- buildSingleElement(item, contextData) {
- let doc = contextData.menu.ownerDocument;
- let element;
- if (item.children.length > 0) {
- element = this.createMenuElement(doc, item);
- } else if (item.type == "separator") {
- element = doc.createElement("menuseparator");
- } else {
- element = doc.createElement("menuitem");
- }
-
- return this.customizeElement(element, item, contextData);
- },
-
- createMenuElement(doc, item) {
- let element = doc.createElement("menu");
- // Menu elements need to have a menupopup child for its menu items.
- let menupopup = doc.createElement("menupopup");
- element.appendChild(menupopup);
- return element;
- },
-
- customizeElement(element, item, contextData) {
- let label = item.title;
- if (label) {
- if (contextData.isTextSelected && label.indexOf("%s") > -1) {
- let selection = contextData.selectionText;
- // The rendering engine will truncate the title if it's longer than 64 characters.
- // But if it makes sense let's try truncate selection text only, to handle cases like
- // 'look up "%s" in MyDictionary' more elegantly.
- let maxSelectionLength = gMaxLabelLength - label.length + 2;
- if (maxSelectionLength > 4) {
- selection = selection.substring(0, maxSelectionLength - 3) + "...";
- }
- label = label.replace(/%s/g, selection);
- }
-
- element.setAttribute("label", label);
- }
-
- if (item.type == "checkbox") {
- element.setAttribute("type", "checkbox");
- if (item.checked) {
- element.setAttribute("checked", "true");
- }
- } else if (item.type == "radio") {
- element.setAttribute("type", "radio");
- element.setAttribute("name", item.groupName);
- if (item.checked) {
- element.setAttribute("checked", "true");
- }
- }
-
- if (!item.enabled) {
- element.setAttribute("disabled", "true");
- }
-
- element.addEventListener("command", event => { // eslint-disable-line mozilla/balanced-listeners
- if (event.target !== event.currentTarget) {
- return;
- }
- const wasChecked = item.checked;
- if (item.type == "checkbox") {
- item.checked = !item.checked;
- } else if (item.type == "radio") {
- // Deselect all radio items in the current radio group.
- for (let child of item.parent.children) {
- if (child.type == "radio" && child.groupName == item.groupName) {
- child.checked = false;
- }
- }
- // Select the clicked radio item.
- item.checked = true;
- }
-
- item.tabManager.addActiveTabPermission();
-
- let tab = item.tabManager.convert(contextData.tab);
- let info = item.getClickInfo(contextData, wasChecked);
- item.extension.emit("webext-contextmenu-menuitem-click", info, tab);
- });
-
- return element;
- },
-
- handleEvent(event) {
- if (this.xulMenu != event.target || event.type != "popuphidden") {
- return;
- }
-
- delete this.xulMenu;
- let target = event.target;
- target.removeEventListener("popuphidden", this);
- for (let item of this.itemsToCleanUp) {
- item.remove();
- }
- this.itemsToCleanUp.clear();
- },
-
- itemsToCleanUp: new Set(),
-};
-
-// Called from pageAction or browserAction popup.
-global.actionContextMenu = function(contextData) {
- gMenuBuilder.buildActionContextMenu(contextData);
-};
-
-function getContexts(contextData) {
- let contexts = new Set();
-
- if (contextData.inFrame) {
- contexts.add("frame");
- }
-
- if (contextData.isTextSelected) {
- contexts.add("selection");
- }
-
- if (contextData.onLink) {
- contexts.add("link");
- }
-
- if (contextData.onEditableArea) {
- contexts.add("editable");
- }
-
- if (contextData.onPassword) {
- contexts.add("password");
- }
-
- if (contextData.onImage) {
- contexts.add("image");
- }
-
- if (contextData.onVideo) {
- contexts.add("video");
- }
-
- if (contextData.onAudio) {
- contexts.add("audio");
- }
-
- if (contextData.onPageAction) {
- contexts.add("page_action");
- }
-
- if (contextData.onBrowserAction) {
- contexts.add("browser_action");
- }
-
- if (contexts.size === 0) {
- contexts.add("page");
- }
-
- if (contextData.onTab) {
- contexts.add("tab");
- } else {
- contexts.add("all");
- }
-
- return contexts;
-}
-
-function MenuItem(extension, createProperties, isRoot = false) {
- this.extension = extension;
- this.children = [];
- this.parent = null;
- this.tabManager = TabManager.for(extension);
-
- this.setDefaults();
- this.setProps(createProperties);
-
- if (!this.hasOwnProperty("_id")) {
- this.id = gNextMenuItemID++;
- }
- // If the item is not the root and has no parent
- // it must be a child of the root.
- if (!isRoot && !this.parent) {
- this.root.addChild(this);
- }
-}
-
-MenuItem.prototype = {
- setProps(createProperties) {
- for (let propName in createProperties) {
- if (createProperties[propName] === null) {
- // Omitted optional argument.
- continue;
- }
- this[propName] = createProperties[propName];
- }
-
- if (createProperties.documentUrlPatterns != null) {
- this.documentUrlMatchPattern = new MatchPattern(this.documentUrlPatterns);
- }
-
- if (createProperties.targetUrlPatterns != null) {
- this.targetUrlMatchPattern = new MatchPattern(this.targetUrlPatterns);
- }
-
- // If a child MenuItem does not specify any contexts, then it should
- // inherit the contexts specified from its parent.
- if (createProperties.parentId && !createProperties.contexts) {
- this.contexts = this.parent.contexts;
- }
- },
-
- setDefaults() {
- this.setProps({
- type: "normal",
- checked: false,
- contexts: ["all"],
- enabled: true,
- });
- },
-
- set id(id) {
- if (this.hasOwnProperty("_id")) {
- throw new Error("Id of a MenuItem cannot be changed");
- }
- let isIdUsed = gContextMenuMap.get(this.extension).has(id);
- if (isIdUsed) {
- throw new Error("Id already exists");
- }
- this._id = id;
- },
-
- get id() {
- return this._id;
- },
-
- ensureValidParentId(parentId) {
- if (parentId === undefined) {
- return;
- }
- let menuMap = gContextMenuMap.get(this.extension);
- if (!menuMap.has(parentId)) {
- throw new Error("Could not find any MenuItem with id: " + parentId);
- }
- for (let item = menuMap.get(parentId); item; item = item.parent) {
- if (item === this) {
- throw new ExtensionError("MenuItem cannot be an ancestor (or self) of its new parent.");
- }
- }
- },
-
- set parentId(parentId) {
- this.ensureValidParentId(parentId);
-
- if (this.parent) {
- this.parent.detachChild(this);
- }
-
- if (parentId === undefined) {
- this.root.addChild(this);
- } else {
- let menuMap = gContextMenuMap.get(this.extension);
- menuMap.get(parentId).addChild(this);
- }
- },
-
- get parentId() {
- return this.parent ? this.parent.id : undefined;
- },
-
- addChild(child) {
- if (child.parent) {
- throw new Error("Child MenuItem already has a parent.");
- }
- this.children.push(child);
- child.parent = this;
- },
-
- detachChild(child) {
- let idx = this.children.indexOf(child);
- if (idx < 0) {
- throw new Error("Child MenuItem not found, it cannot be removed.");
- }
- this.children.splice(idx, 1);
- child.parent = null;
- },
-
- get root() {
- let extension = this.extension;
- if (!gRootItems.has(extension)) {
- let root = new MenuItem(extension,
- {title: extension.name},
- /* isRoot = */ true);
- gRootItems.set(extension, root);
- }
-
- return gRootItems.get(extension);
- },
-
- remove() {
- if (this.parent) {
- this.parent.detachChild(this);
- }
- let children = this.children.slice(0);
- for (let child of children) {
- child.remove();
- }
-
- let menuMap = gContextMenuMap.get(this.extension);
- menuMap.delete(this.id);
- if (this.root == this) {
- gRootItems.delete(this.extension);
- }
- },
-
- getClickInfo(contextData, wasChecked) {
- let mediaType;
- if (contextData.onVideo) {
- mediaType = "video";
- }
- if (contextData.onAudio) {
- mediaType = "audio";
- }
- if (contextData.onImage) {
- mediaType = "image";
- }
-
- let info = {
- menuItemId: this.id,
- editable: contextData.onEditableArea || contextData.onPassword,
- };
-
- function setIfDefined(argName, value) {
- if (value !== undefined) {
- info[argName] = value;
- }
- }
-
- setIfDefined("parentMenuItemId", this.parentId);
- setIfDefined("mediaType", mediaType);
- setIfDefined("linkUrl", contextData.linkUrl);
- setIfDefined("srcUrl", contextData.srcUrl);
- setIfDefined("pageUrl", contextData.pageUrl);
- setIfDefined("frameUrl", contextData.frameUrl);
- setIfDefined("selectionText", contextData.selectionText);
-
- if ((this.type === "checkbox") || (this.type === "radio")) {
- info.checked = this.checked;
- info.wasChecked = wasChecked;
- }
-
- return info;
- },
-
- enabledForContext(contextData) {
- let contexts = getContexts(contextData);
- if (!this.contexts.some(n => contexts.has(n))) {
- return false;
- }
-
- let docPattern = this.documentUrlMatchPattern;
- let pageURI = Services.io.newURI(contextData.pageUrl);
- if (docPattern && !docPattern.matches(pageURI)) {
- return false;
- }
-
- let targetPattern = this.targetUrlMatchPattern;
- if (targetPattern) {
- let targetUrls = [];
- if (contextData.onImage || contextData.onAudio || contextData.onVideo) {
- // TODO: double check if srcUrl is always set when we need it
- targetUrls.push(contextData.srcUrl);
- }
- if (contextData.onLink) {
- targetUrls.push(contextData.linkUrl);
- }
- if (!targetUrls.some(targetUrl => targetPattern.matches(NetUtil.newURI(targetUrl)))) {
- return false;
- }
- }
-
- return true;
- },
-};
-
-// While any extensions are active, this Tracker registers to observe/listen
-// for contex-menu events from both content and chrome.
-const contextMenuTracker = {
- register() {
- Services.obs.addObserver(this, "on-build-contextmenu", false);
- for (const window of WindowListManager.browserWindows()) {
- this.onWindowOpen(window);
- }
- WindowListManager.addOpenListener(this.onWindowOpen);
- },
-
- unregister() {
- Services.obs.removeObserver(this, "on-build-contextmenu");
- for (const window of WindowListManager.browserWindows()) {
- const menu = window.document.getElementById("tabContextMenu");
- menu.removeEventListener("popupshowing", this);
- }
- WindowListManager.removeOpenListener(this.onWindowOpen);
- },
-
- observe(subject, topic, data) {
- subject = subject.wrappedJSObject;
- gMenuBuilder.build(subject);
- },
-
- onWindowOpen(window) {
- const menu = window.document.getElementById("tabContextMenu");
- menu.addEventListener("popupshowing", contextMenuTracker);
- },
-
- handleEvent(event) {
- const menu = event.target;
- if (menu.id === "tabContextMenu") {
- const trigger = menu.triggerNode;
- const tab = trigger.localName === "tab" ? trigger : TabManager.activeTab;
- const pageUrl = tab.linkedBrowser.currentURI.spec;
- gMenuBuilder.build({menu, tab, pageUrl, onTab: true});
- }
- },
-};
-
-var gExtensionCount = 0;
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("startup", (type, extension) => {
- gContextMenuMap.set(extension, new Map());
- if (++gExtensionCount == 1) {
- contextMenuTracker.register();
- }
-});
-
-extensions.on("shutdown", (type, extension) => {
- gContextMenuMap.delete(extension);
- gRootItems.delete(extension);
- if (--gExtensionCount == 0) {
- contextMenuTracker.unregister();
- }
-});
-/* eslint-enable mozilla/balanced-listeners */
-
-extensions.registerSchemaAPI("contextMenus", "addon_parent", context => {
- let {extension} = context;
- return {
- contextMenus: {
- createInternal: function(createProperties) {
- // Note that the id is required by the schema. If the addon did not set
- // it, the implementation of contextMenus.create in the child should
- // have added it.
- let menuItem = new MenuItem(extension, createProperties);
- gContextMenuMap.get(extension).set(menuItem.id, menuItem);
- },
-
- update: function(id, updateProperties) {
- let menuItem = gContextMenuMap.get(extension).get(id);
- if (menuItem) {
- menuItem.setProps(updateProperties);
- }
- },
-
- remove: function(id) {
- let menuItem = gContextMenuMap.get(extension).get(id);
- if (menuItem) {
- menuItem.remove();
- }
- },
-
- removeAll: function() {
- let root = gRootItems.get(extension);
- if (root) {
- root.remove();
- }
- },
-
- onClicked: new EventManager(context, "contextMenus.onClicked", fire => {
- let listener = (event, info, tab) => {
- fire(info, tab);
- };
-
- extension.on("webext-contextmenu-menuitem-click", listener);
- return () => {
- extension.off("webext-contextmenu-menuitem-click", listener);
- };
- }).api(),
- },
- };
-});