diff options
Diffstat (limited to 'application/basilisk/components/extensions/ext-contextMenus.js')
-rw-r--r-- | application/basilisk/components/extensions/ext-contextMenus.js | 640 |
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(), - }, - }; -}); |