diff options
Diffstat (limited to 'tools/addon-sdk-1.12/lib/sdk/context-menu.js')
-rw-r--r-- | tools/addon-sdk-1.12/lib/sdk/context-menu.js | 1494 |
1 files changed, 0 insertions, 1494 deletions
diff --git a/tools/addon-sdk-1.12/lib/sdk/context-menu.js b/tools/addon-sdk-1.12/lib/sdk/context-menu.js deleted file mode 100644 index 9d20f0b..0000000 --- a/tools/addon-sdk-1.12/lib/sdk/context-menu.js +++ /dev/null @@ -1,1494 +0,0 @@ -/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim:set ts=2 sw=2 sts=2 et: */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -module.metadata = { - "stability": "stable" -}; - -const {Ci} = require("chrome"); - -if (!require("./system/xul-app").is("Firefox")) { - throw new Error([ - "The context-menu module currently supports only Firefox. In the future ", - "we would like it to support other applications, however. Please see ", - "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." - ].join("")); -} - -const apiUtils = require("./deprecated/api-utils"); -const collection = require("./util/collection"); -const { Worker } = require("./content/worker"); -const { URL } = require("./url"); -const { MatchPattern } = require("./page-mod/match-pattern"); -const { EventEmitterTrait: EventEmitter } = require("./deprecated/events"); -const observerServ = require("./deprecated/observer-service"); -const jpSelf = require("./self"); -const { WindowTracker } = require("./deprecated/window-utils"); -const { getInnerId, isBrowser } = require("./window/utils"); -const { Trait } = require("./deprecated/light-traits"); -const { Cortex } = require("./deprecated/cortex"); -const timer = require("./timers"); - -// All user items we add have this class name. -const ITEM_CLASS = "jetpack-context-menu-item"; - -// Items in the top-level context menu also have this class. -const TOPLEVEL_ITEM_CLASS = "jetpack-context-menu-item-toplevel"; - -// Items in the overflow submenu also have this class. -const OVERFLOW_ITEM_CLASS = "jetpack-context-menu-item-overflow"; - -// The ID of the menu separator that separates standard context menu items from -// our user items. -const SEPARATOR_ID = "jetpack-context-menu-separator"; - -// If more than this number of items are added to the context menu, all items -// overflow into a "Jetpack" submenu. -const OVERFLOW_THRESH_DEFAULT = 10; -const OVERFLOW_THRESH_PREF = - "extensions.addon-sdk.context-menu.overflowThreshold"; - -// The label of the overflow sub-xul:menu. -// -// TODO: Localize this. -const OVERFLOW_MENU_LABEL = "Add-ons"; - -// The ID of the overflow sub-xul:menu. -const OVERFLOW_MENU_ID = "jetpack-content-menu-overflow-menu"; - -// The ID of the overflow submenu's xul:menupopup. -const OVERFLOW_POPUP_ID = "jetpack-content-menu-overflow-popup"; - -// These are used by PageContext.isCurrent below. If the popupNode or any of -// its ancestors is one of these, Firefox uses a tailored context menu, and so -// the page context doesn't apply. -const NON_PAGE_CONTEXT_ELTS = [ - Ci.nsIDOMHTMLAnchorElement, - Ci.nsIDOMHTMLAppletElement, - Ci.nsIDOMHTMLAreaElement, - Ci.nsIDOMHTMLButtonElement, - Ci.nsIDOMHTMLCanvasElement, - Ci.nsIDOMHTMLEmbedElement, - Ci.nsIDOMHTMLImageElement, - Ci.nsIDOMHTMLInputElement, - Ci.nsIDOMHTMLMapElement, - Ci.nsIDOMHTMLMediaElement, - Ci.nsIDOMHTMLMenuElement, - Ci.nsIDOMHTMLObjectElement, - Ci.nsIDOMHTMLOptionElement, - Ci.nsIDOMHTMLSelectElement, - Ci.nsIDOMHTMLTextAreaElement, -]; - -// This is used to access private properties of Item and Menu instances. -const PRIVATE_PROPS_KEY = { - valueOf: function valueOf() "private properties key" -}; - -// Used as an internal ID for items and as part of a public ID for item DOM -// elements. Careful: This number is not necessarily unique to any one instance -// of the module. For each module instance, when the first item is created this -// number will be 0, when the second is created it will be 1, and so on. -let nextItemID = 0; - -// The number of items that haven't finished initializing yet. See -// AIT__finishActiveItemInit(). -let numItemsWithUnfinishedInit = 0; - -exports.Item = Item; -exports.Menu = Menu; -exports.Separator = Separator; - - -// A word about traits and privates. `this` inside of traits methods is an -// object private to the implementation. It should never be publicly leaked. -// We use Cortex in the exported menu item constructors to create public -// reflections of the private objects that hide private properties -- those -// prefixed with an underscore. Public reflections are attached to the private -// objects via the `_public` property. -// -// All item objects passed into the implementation by the client will be public -// reflections, not private objects. Likewise, all item objects passed out of -// the implementation to the client must be public, not private. Mixing up -// public and private is bad and easy to do, so not only are private objects -// restricted to the implementation, but as much as possible we try to restrict -// them to the Item, Menu, and Separator traits and constructors. Everybody -// else in the implementation should expect to be passed public reflections, and -// they must specifically request private objects via privateItem(). - -// Item, Menu, and Separator are composed of this trait. -const ItemBaseTrait = Trait({ - - _initBase: function IBT__initBase(opts, optRules, optsToNotSet) { - this._optRules = optRules; - for (let optName in optRules) - if (optsToNotSet.indexOf(optName) < 0) - this[optName] = opts[optName]; - optsToNotSet.forEach(function (opt) validateOpt(opts[opt], optRules[opt])); - this._isInited = true; - - this._id = nextItemID++; - this._parentMenu = null; - - // This makes the private properties accessible to anyone with access to - // PRIVATE_PROPS_KEY. Barring loader tricks, only this file has has access - // to it, so only this file has access to the private properties. - const self = this; - this.valueOf = function IBT_valueOf(key) { - return key === PRIVATE_PROPS_KEY ? self : self._public; - }; - }, - - destroy: function IBT_destroy() { - if (this._wasDestroyed) - return; - if (this.parentMenu) - this.parentMenu.removeItem(this._public); - else if (!(this instanceof Separator) && this._hasFinishedInit) - browserManager.removeTopLevelItem(this._public); - browserManager.unregisterItem(this._public); - this._wasDestroyed = true; - }, - - get parentMenu() { - return this._parentMenu; - }, - - set parentMenu(val) { - throw new Error("The 'parentMenu' property is not intended to be set. " + - "Use menu.addItem(item) instead."); - }, - - set _isTopLevel(val) { - if (val) - this._workerReg = new WorkerRegistry(this._public); - else { - this._workerReg.destroy(); - delete this._workerReg; - } - }, - - get _topLevelItem() { - let topLevelItem = this._public; - let parentMenu = this.parentMenu; - while (parentMenu) { - topLevelItem = parentMenu; - parentMenu = parentMenu.parentMenu; - } - return topLevelItem; - } -}); - -// Item and Menu are composed of this trait. -const ActiveItemTrait = Trait.compose(ItemBaseTrait, EventEmitter, Trait({ - - _initActiveItem: function AIT__initActiveItem(opts, optRules, optsToNotSet) { - this._initBase(opts, optRules, - optsToNotSet.concat(["onMessage", "context"])); - - if ("onMessage" in opts) - this.on("message", opts.onMessage); - - // When a URL context is removed (by calling context.remove(urlContext)), we - // may need to create workers for windows containing pages that the item now - // matches. Likewise, when a URL context is added, we need to destroy - // workers for windows containing pages that the item now does not match. - // - // collection doesn't provide a way to listen for removals. utils/registry - // does, but it doesn't allow its elements to be enumerated. So as a hack, - // use a collection for item.context and replace its add and remove methods. - collection.addCollectionProperty(this, "context"); - if (opts.context) - this.context.add(opts.context); - - const self = this; - - let add = this.context.add; - this.context.add = function itemContextAdd() { - let args = Array.slice(arguments); - add.apply(self.context, args); - if (self._workerReg && args.some(function (a) a instanceof URLContext)) - self._workerReg.destroyUnneededWorkers(); - }; - - let remove = this.context.remove; - this.context.remove = function itemContextRemove() { - let args = Array.slice(arguments); - remove.apply(self.context, args); - if (self._workerReg && args.some(function (a) a instanceof URLContext)) - self._workerReg.createNeededWorkers(); - }; - }, - - // Workers are only created for top-level menu items. When a top-level item - // is later added to a Menu, its workers are destroyed. Well, all items start - // out as top-level because there is, unfortunately, no contextMenu.add(). So - // when an item is created and immediately added to a Menu, workers for it are - // needlessly created and destroyed. The point of this timeout is to avoid - // that. Items that are created and added to Menus in the same turn of the - // event loop won't have workers created for them. - _finishActiveItemInit: function AIT__finishActiveItemInit() { - numItemsWithUnfinishedInit++; - const self = this; - timer.setTimeout(function AIT__finishActiveItemInitTimeout() { - if (!self.parentMenu && !self._wasDestroyed) - browserManager.addTopLevelItem(self._public); - self._hasFinishedInit = true; - numItemsWithUnfinishedInit--; - }, 0); - }, - - get label() { - return this._label; - }, - - set label(val) { - this._label = validateOpt(val, this._optRules.label); - if (this._isInited) - browserManager.setItemLabel(this, this._label); - return this._label; - }, - - get image() { - return this._image; - }, - - set image(val) { - this._image = validateOpt(val, this._optRules.image); - if (this._isInited) - browserManager.setItemImage(this, this._image); - return this._image; - }, - - get contentScript() { - return this._contentScript; - }, - - set contentScript(val) { - this._contentScript = validateOpt(val, this._optRules.contentScript); - return this._contentScript; - }, - - get contentScriptFile() { - return this._contentScriptFile; - }, - - set contentScriptFile(val) { - this._contentScriptFile = - validateOpt(val, this._optRules.contentScriptFile); - return this._contentScriptFile; - } -})); - -// Item is composed of this trait. -const ItemTrait = Trait.compose(ActiveItemTrait, Trait({ - - _initItem: function IT__initItem(opts, optRules) { - this._initActiveItem(opts, optRules, []); - }, - - get data() { - return this._data; - }, - - set data(val) { - this._data = validateOpt(val, this._optRules.data); - if (this._isInited) - browserManager.setItemData(this, this._data); - return this._data; - }, - - toString: function IT_toString() { - return '[object Item "' + this.label + '"]'; - } -})); - -// The exported Item constructor. -function Item(options) { - let optRules = optionsRules(); - optRules.data = { - map: function (v) v.toString(), - is: ["string", "undefined"] - }; - - let item = ItemTrait.create(Item.prototype); - item._initItem(options, optRules); - - item._public = Cortex(item); - browserManager.registerItem(item._public); - item._finishActiveItemInit(); - - return item._public; -} - -// Menu is composed of this trait. -const MenuTrait = Trait.compose( - ActiveItemTrait.resolve({ destroy: "_destroyThisItem" }), - Trait({ - - _initMenu: function MT__initMenu(opts, optRules, optsToNotSet) { - this._items = []; - this._initActiveItem(opts, optRules, optsToNotSet); - }, - - destroy: function MT_destroy() { - while (this.items.length) - this.items[0].destroy(); - this._destroyThisItem(); - }, - - get items() { - return this._items; - }, - - set items(val) { - let newItems = validateOpt(val, this._optRules.items); - while (this.items.length) - this.items[0].destroy(); - newItems.forEach(function (i) this.addItem(i), this); - return newItems; - }, - - addItem: function MT_addItem(item) { - // First, remove the item from its current parent. - let privates = privateItem(item); - if (item.parentMenu) - item.parentMenu.removeItem(item); - else if (!(item instanceof Separator) && privates._hasFinishedInit) - browserManager.removeTopLevelItem(item); - - // Now add the item to this menu. - this._items.push(item); - privates._parentMenu = this._public; - browserManager.addItemToMenu(item, this._public); - }, - - removeItem: function MT_removeItem(item) { - let idx = this._items.indexOf(item); - if (idx < 0) - return; - this._items.splice(idx, 1); - privateItem(item)._parentMenu = null; - browserManager.removeItemFromMenu(item, this._public); - }, - - toString: function MT_toString() { - return '[object Menu "' + this.label + '"]'; - } -})); - -// The exported Menu constructor. -function Menu(options) { - let optRules = optionsRules(); - optRules.items = { - is: ["array"], - ok: function (v) { - return v.every(function (item) { - return (item instanceof Item) || - (item instanceof Menu) || - (item instanceof Separator); - }); - }, - msg: "items must be an array, and each element in the array must be an " + - "Item, Menu, or Separator." - }; - - let menu = MenuTrait.create(Menu.prototype); - - // We can't rely on _initBase to set the `items` property, because the menu - // needs to be registered with and added to the browserManager before any - // child items are added to it. - menu._initMenu(options, optRules, ["items"]); - - menu._public = Cortex(menu); - browserManager.registerItem(menu._public); - menu.items = options.items; - menu._finishActiveItemInit(); - - return menu._public; -} - -// The exported Separator constructor. -function Separator() { - let sep = ItemBaseTrait.create(Separator.prototype); - sep._initBase({}, {}, []); - - sep._public = Cortex(sep); - browserManager.registerItem(sep._public); - sep._hasFinishedInit = true; - return sep._public; -} - - -function Context() {} - -function PageContext() { - this.isCurrent = function PageContext_isCurrent(popupNode) { - let win = popupNode.ownerDocument.defaultView; - if (win && !win.getSelection().isCollapsed) - return false; - - let cursor = popupNode; - while (cursor && !(cursor instanceof Ci.nsIDOMHTMLHtmlElement)) { - if (NON_PAGE_CONTEXT_ELTS.some(function (iface) cursor instanceof iface)) - return false; - cursor = cursor.parentNode; - } - return true; - }; -} - -PageContext.prototype = new Context(); - -function SelectorContext(selector) { - let opts = apiUtils.validateOptions({ selector: selector }, { - selector: { - is: ["string"], - msg: "selector must be a string." - } - }); - - this.adjustPopupNode = function SelectorContext_adjustPopupNode(node) { - return closestMatchingAncestor(node); - }; - - this.isCurrent = function SelectorContext_isCurrent(popupNode) { - return !!closestMatchingAncestor(popupNode); - }; - - // Returns node if it matches selector, or the closest ancestor of node that - // matches, or null if node and none of its ancestors matches. - function closestMatchingAncestor(node) { - let cursor = node; - while (cursor) { - if (cursor.mozMatchesSelector(selector)) - return cursor; - if (cursor instanceof Ci.nsIDOMHTMLHtmlElement) - break; - cursor = cursor.parentNode; - } - return null; - } -} - -SelectorContext.prototype = new Context(); - -function SelectionContext() { - this.isCurrent = function SelectionContext_isCurrent(popupNode) { - let win = popupNode.ownerDocument.defaultView; - if (!win) - return false; - - let hasSelection = !win.getSelection().isCollapsed; - if (!hasSelection) { - // window.getSelection doesn't return a selection for text selected in a - // form field (see bug 85686), so before returning false we want to check - // if the popupNode is a text field. - let { selectionStart, selectionEnd } = popupNode; - hasSelection = !isNaN(selectionStart) && - !isNaN(selectionEnd) && - selectionStart !== selectionEnd; - } - return hasSelection; - }; -} - -SelectionContext.prototype = new Context(); - -function URLContext(patterns) { - let opts = apiUtils.validateOptions({ patterns: patterns }, { - patterns: { - map: function (v) apiUtils.getTypeOf(v) === "array" ? v : [v], - ok: function (v) v.every(function (p) typeof(p) === "string"), - msg: "patterns must be a string or an array of strings." - } - }); - try { - patterns = opts.patterns.map(function (p) new MatchPattern(p)); - } - catch (err) { - console.error("Error creating URLContext match pattern:"); - throw err; - } - - const self = this; - - this.isCurrent = function URLContext_isCurrent(popupNode) { - return self.isCurrentForURL(popupNode.ownerDocument.URL); - }; - - this.isCurrentForURL = function URLContext_isCurrentForURL(url) { - return patterns.some(function (p) p.test(url)); - }; -} - -URLContext.prototype = new Context(); - -exports.PageContext = apiUtils.publicConstructor(PageContext); -exports.SelectorContext = apiUtils.publicConstructor(SelectorContext); -exports.SelectionContext = apiUtils.publicConstructor(SelectionContext); -exports.URLContext = apiUtils.publicConstructor(URLContext); - - -// Returns a version of opt validated against the given rule. -function validateOpt(opt, rule) { - return apiUtils.validateOptions({ opt: opt }, { opt: rule }).opt; -} - -// Returns rules for apiUtils.validateOptions() common to Item and Menu. -function optionsRules() { - return { - context: { - is: ["undefined", "object", "array"], - ok: function (v) { - if (!v) - return true; - let arr = apiUtils.getTypeOf(v) === "array" ? v : [v]; - return arr.every(function (o) o instanceof Context); - }, - msg: "The 'context' option must be a Context object or an array of " + - "Context objects." - }, - label: { - map: function (v) v.toString(), - is: ["string"], - ok: function (v) !!v, - msg: "The item must have a non-empty string label." - }, - image: { - map: function (v) v.toString(), - is: ["string", "undefined", "null"] - }, - contentScript: { - is: ["string", "array", "undefined"], - ok: function (v) { - return apiUtils.getTypeOf(v) !== "array" || - v.every(function (s) typeof(s) === "string"); - } - }, - contentScriptFile: { - is: ["string", "array", "undefined"], - ok: function (v) { - if (!v) - return true; - let arr = apiUtils.getTypeOf(v) === "array" ? v : [v]; - try { - return arr.every(function (s) { - return apiUtils.getTypeOf(s) === "string" && - URL(s).scheme === 'resource'; - }); - } - catch (err) {} - return false; - }, - msg: "The 'contentScriptFile' option must be a local file URL or " + - "an array of local file URLs." - }, - onMessage: { - is: ["function", "undefined"] - } - }; -} - -// Does a binary search on elts, a NodeList, and returns the DOM element -// before which an item with targetLabel should be inserted. null is returned -// if the new item should be inserted at the end. -function insertionPoint(targetLabel, elts) { - let from = 0; - let to = elts.length - 1; - - while (from <= to) { - let i = Math.floor((from + to) / 2); - let comp = targetLabel.localeCompare(elts[i].getAttribute("label")); - if (comp < 0) - to = i - 1; - else if (comp > 0) - from = i + 1; - else - return elts[i]; - } - return elts[from] || null; -} - -// Builds an ID suitable for a DOM element from the given item ID. -// isInOverflowSubtree should be true if the returned element will be inserted -// into the DOM subtree rooted at the overflow menu. -function domEltIDFromItemID(itemID, isInOverflowSubtree) { - let suffix = isInOverflowSubtree ? "-overflow" : ""; - return jpSelf.id + "-context-menu-item-" + itemID + suffix; -} - -// Parses the item ID out of the given DOM element ID and returns it. If the -// element's ID is malformed or it indicates that the element was not created by -// the instance of the module calling this function, returns -1. -function itemIDFromDOMEltID(domEltID) { - let match = /^(.+?)-context-menu-item-([0-9]+)[-a-z]*$/.exec(domEltID); - return !match || match[1] !== jpSelf.id ? -1 : match[2]; -} - -// Returns the private version of the given public reflection. -function privateItem(publicItem) { - return publicItem.valueOf(PRIVATE_PROPS_KEY); -} - - -// A type of Worker tailored to our uses. -const ContextMenuWorker = Worker.compose({ - destroy: Worker.required, - - // Returns true if any context listeners are defined in the worker's port. - anyContextListeners: function CMW_anyContextListeners() { - return this._contentWorker.hasListenerFor("context"); - }, - - // Returns the first string or truthy value returned by a context listener in - // the worker's port. If none return a string or truthy value or if there are - // no context listeners, returns false. popupNode is the node that was - // context-clicked. - isAnyContextCurrent: function CMW_isAnyContextCurrent(popupNode) { - let results = this._contentWorker.emitSync("context", popupNode); - for (let i = 0; i < results.length; i++) { - let val = results[i]; - if (typeof val === "string" || val) - return val; - } - return false; - }, - - // Emits a click event in the worker's port. popupNode is the node that was - // context-clicked, and clickedItemData is the data of the item that was - // clicked. - fireClick: function CMW_fireClick(popupNode, clickedItemData) { - this._contentWorker.emitSync("click", popupNode, clickedItemData); - } -}); - - -// This class creates and stores content workers for pairs of menu items and -// content windows. Use one instance for each item. Not all pairs need a -// worker: if an item has a URL context that does not match a window's page, -// then no worker is created for the pair. -function WorkerRegistry(item) { - this.item = item; - - // inner window ID => { win, worker } - this.winWorkers = {}; - - // inner window ID => content window - this.winsWithoutWorkers = {}; -} - -WorkerRegistry.prototype = { - - // Registers a content window, creating a worker for it if it needs one. - registerContentWin: function WR_registerContentWin(win) { - let innerWinID = getInnerId(win); - if ((innerWinID in this.winWorkers) || - (innerWinID in this.winsWithoutWorkers)) - return; - if (this._doesURLNeedWorker(win.document.URL)) - this.winWorkers[innerWinID] = { win: win, worker: this._makeWorker(win) }; - else - this.winsWithoutWorkers[innerWinID] = win; - }, - - // Unregisters a content window, destroying its related worker if it has one. - unregisterContentWin: function WR_unregisterContentWin(innerWinID) { - if (innerWinID in this.winWorkers) { - let winWorker = this.winWorkers[innerWinID]; - winWorker.worker.destroy(); - delete winWorker.worker; - delete winWorker.win; - delete this.winWorkers[innerWinID]; - } - else - delete this.winsWithoutWorkers[innerWinID]; - }, - - // Creates a worker for each window that needs a worker but doesn't have one. - createNeededWorkers: function WR_createNeededWorkers() { - for (let innerWinID in this.winsWithoutWorkers) { - let win = this.winsWithoutWorkers[innerWinID] - delete this.winsWithoutWorkers[innerWinID]; - this.registerContentWin(win); - } - }, - - // Destroys the worker for each window that has a worker but doesn't need it. - destroyUnneededWorkers: function WR_destroyUnneededWorkers() { - for (let innerWinID in this.winWorkers) { - let winWorker = this.winWorkers[innerWinID]; - if (!this._doesURLNeedWorker(winWorker.win.document.URL)) { - this.unregisterContentWin(innerWinID); - this.winsWithoutWorkers[innerWinID] = winWorker.win; - } - } - }, - - // Returns the worker for the item-window pair or null if none exists. - find: function WR_find(contentWin) { - let innerWinID = getInnerId(contentWin); - return (innerWinID in this.winWorkers) ? - this.winWorkers[innerWinID].worker : - null; - }, - - // Unregisters all content windows from the registry, which destroys all - // workers. - destroy: function WR_destroy() { - for (let innerWinID in this.winWorkers) - this.unregisterContentWin(innerWinID); - for (let innerWinID in this.winsWithoutWorkers) - this.unregisterContentWin(innerWinID); - }, - - // Returns false if the item has a URL context that does not match the given - // URL. - _doesURLNeedWorker: function WR__doesURLNeedWorker(url) { - for (let ctxt in this.item.context) - if ((ctxt instanceof URLContext) && !ctxt.isCurrentForURL(url)) - return false; - return true; - }, - - _makeWorker: function WR__makeWorker(win) { - let worker = ContextMenuWorker({ - window: win, - contentScript: this.item.contentScript, - contentScriptFile: this.item.contentScriptFile, - onError: function (err) console.exception(err) - }); - let item = this.item; - worker.on("message", function workerOnMessage(msg) { - try { - privateItem(item)._emitOnObject(item, "message", msg); - } - catch (err) { - console.exception(err); - } - }); - return worker; - } -}; - - -// Mirrors state across all browser windows. Also responsible for detecting -// all content window loads and unloads. -let browserManager = { - topLevelItems: [], - browserWins: [], - - // inner window ID => content window - contentWins: {}, - - // Call this when a new item is created, top-level or not. - registerItem: function BM_registerItem(item) { - this.browserWins.forEach(function (w) w.registerItem(item)); - }, - - // Call this when an item is destroyed and won't be used again, top-level or - // not. - unregisterItem: function BM_unregisterItem(item) { - this.browserWins.forEach(function (w) w.unregisterItem(item)); - }, - - addTopLevelItem: function BM_addTopLevelItem(item) { - this.topLevelItems.push(item); - this.browserWins.forEach(function (w) w.addTopLevelItem(item)); - - // Create the item's worker registry and register all currently loaded - // content windows with it. - let privates = privateItem(item); - privates._isTopLevel = true; - for each (let win in this.contentWins) - privates._workerReg.registerContentWin(win); - }, - - removeTopLevelItem: function BM_removeTopLevelItem(item) { - let idx = this.topLevelItems.indexOf(item); - if (idx < 0) - throw new Error("Internal error: item not in top-level menu: " + item); - this.topLevelItems.splice(idx, 1); - this.browserWins.forEach(function (w) w.removeTopLevelItem(item)); - privateItem(item)._isTopLevel = false; - }, - - addItemToMenu: function BM_addItemToMenu(item, parentMenu) { - this.browserWins.forEach(function (w) w.addItemToMenu(item, parentMenu)); - }, - - removeItemFromMenu: function BM_removeItemFromMenu(item, parentMenu) { - this.browserWins.forEach(function (w) w.removeItemFromMenu(item, - parentMenu)); - }, - - setItemLabel: function BM_setItemLabel(item, label) { - this.browserWins.forEach(function (w) w.setItemLabel(item, label)); - }, - - setItemImage: function BM_setItemImage(item, imageURL) { - this.browserWins.forEach(function (w) w.setItemImage(item, imageURL)); - }, - - setItemData: function BM_setItemData(item, data) { - this.browserWins.forEach(function (w) w.setItemData(item, data)); - }, - - // Note that calling this method will cause onTrack to be called immediately - // for each currently open browser window. - init: function BM_init() { - require("./system/unload").ensure(this); - let windowTracker = WindowTracker(this); - - // Register content windows on content-document-global-created and - // unregister them on inner-window-destroyed. For rationale, see bug 667957 - // for the former and bug 642004 for the latter. - observerServ.add("content-document-global-created", - this._onDocGlobalCreated, this); - observerServ.add("inner-window-destroyed", - this._onInnerWinDestroyed, this); - }, - - _onDocGlobalCreated: function BM__onDocGlobalCreated(contentWin) { - let doc = contentWin.document; - if (doc.readyState == "loading") { - const self = this; - doc.addEventListener("readystatechange", function onReadyStateChange(e) { - if (e.target != doc || doc.readyState != "complete") - return; - doc.removeEventListener("readystatechange", onReadyStateChange, false); - self._registerContentWin(contentWin); - }, false); - } - else if (doc.readyState == "complete") - this._registerContentWin(contentWin); - }, - - _onInnerWinDestroyed: function BM__onInnerWinDestroyed(subj) { - this._unregisterContentWin( - subj.QueryInterface(Ci.nsISupportsPRUint64).data); - }, - - // Stores the given content window with the manager and registers it with each - // top-level item's worker registry. - _registerContentWin: function BM__registerContentWin(win) { - let innerID = getInnerId(win); - - // It's an error to call this method for the same window more than once, but - // we allow it in one case: when onTrack races _onDocGlobalCreated. (See - // the comment in onTrack.) Make sure the window is registered only once. - if (innerID in this.contentWins) - return; - - this.contentWins[innerID] = win; - this.topLevelItems.forEach(function (item) { - privateItem(item)._workerReg.registerContentWin(win); - }); - }, - - // Removes the given content window from the manager and unregisters it from - // each top-level item's worker registry. - _unregisterContentWin: function BM__unregisterContentWin(innerID) { - delete this.contentWins[innerID]; - this.topLevelItems.forEach(function (item) { - privateItem(item)._workerReg.unregisterContentWin(innerID); - }); - }, - - unload: function BM_unload() { - // The window tracker is unloaded at the same time this method is called, - // which causes onUntrack to be called for each open browser window, so - // there's no need to clean up browser windows here. - - while (this.topLevelItems.length) { - let item = this.topLevelItems[0]; - this.removeTopLevelItem(item); - this.unregisterItem(item); - } - delete this.contentWins; - }, - - // Registers a browser window with the manager. This is a WindowTracker - // callback. Note that this is called in two cases: for each newly opened - // chrome window, and for each chrome window that is open when the loader - // loads this module. - onTrack: function BM_onTrack(window) { - if (!isBrowser(window)) - return; - - let browserWin = new BrowserWindow(window); - this.browserWins.push(browserWin); - - // Register all loaded content windows in the browser window. Be sure to - // include frames and iframes. If onTrack is called as a result of a new - // browser window being opened, as opposed to the module being loaded, then - // this will race the content-document-global-created notification. That's - // OK, since _registerContentWin will not register the same content window - // more than once. - window.gBrowser.browsers.forEach(function (browser) { - let topContentWin = browser.contentWindow; - let allContentWins = Array.slice(topContentWin.frames); - allContentWins.push(topContentWin); - allContentWins.forEach(function (contentWin) { - if (contentWin.document.readyState == "complete") - this._registerContentWin(contentWin); - }, this); - }, this); - - // Add all top-level items and, recursively, their child items to the new - // browser window. - function addItemTree(item, parentMenu) { - browserWin.registerItem(item); - if (parentMenu) - browserWin.addItemToMenu(item, parentMenu); - else - browserWin.addTopLevelItem(item); - if (item instanceof Menu) - item.items.forEach(function (subitem) addItemTree(subitem, item)); - } - this.topLevelItems.forEach(function (item) addItemTree(item, null)); - }, - - // Unregisters a browser window from the manager. This is a WindowTracker - // callback. Note that this is called in two cases: for each newly closed - // chrome window, and for each chrome window that is open when this module is - // unloaded. - onUntrack: function BM_onUntrack(window) { - if (!isBrowser(window)) - return; - - // Remove the window from the window list. - let idx = 0; - for (; idx < this.browserWins.length; idx++) - if (this.browserWins[idx].window == window) - break; - if (idx == this.browserWins.length) - throw new Error("Internal error: browser window not found"); - let browserWin = this.browserWins.splice(idx, 1)[0]; - - // Remove all top-level items from the window. - this.topLevelItems.forEach(function (i) browserWin.removeTopLevelItem(i)); - browserWin.destroy(); - } -}; - - -// Responsible for creating and managing context menu item DOM elements for a -// browser window. Also responsible for providing a description of the window's -// current context and determining whether an item matches the current context. -// -// TODO: If other apps besides Firefox want to support the context menu in -// whatever way is appropriate for them, plugging in a substitute for or an -// adapter to this class should be the way to do it. Make it easy for them. -// See bug 560716. -function BrowserWindow(window) { - this.window = window; - this.doc = window.document; - - let popupDOMElt = this.doc.getElementById("contentAreaContextMenu"); - if (!popupDOMElt) - throw new Error("Internal error: Context menu popup not found."); - this.contextMenuPopup = new ContextMenuPopup(popupDOMElt, this); - - // item ID => { item, domElt, overflowDOMElt, popup, overflowPopup } - // item may or may not be top-level. domElt is the item's DOM element - // contained in the subtree rooted in the top-level context menu. - // overflowDOMElt is the item's DOM element contained in the subtree rooted in - // the overflow submenu. popup and overflowPopup are only defined if the item - // is a Menu; they're the Popup instances containing the Menu's child items, - // with the aforementioned distinction between top-level and overflow - // subtrees. - this.items = {}; -} - -BrowserWindow.prototype = { - - // Creates and stores DOM elements for the given item, top-level or not. - registerItem: function BW_registerItem(item) { - // this.items[id] is referenced by _makeMenu, so it needs to be defined - // before _makeDOMElt is called. - let props = { item: item }; - this.items[privateItem(item)._id] = props; - props.domElt = this._makeDOMElt(item, false); - props.overflowDOMElt = this._makeDOMElt(item, true); - }, - - // Removes the given item's DOM elements from the store. - unregisterItem: function BW_unregisterItem(item) { - delete this.items[privateItem(item)._id]; - }, - - addTopLevelItem: function BW_addTopLevelItem(item) { - this.contextMenuPopup.addItem(item); - }, - - removeTopLevelItem: function BW_removeTopLevelItem(item) { - this.contextMenuPopup.removeItem(item); - }, - - addItemToMenu: function BW_addItemToMenu(item, parentMenu) { - let { popup, overflowPopup } = this.items[privateItem(parentMenu)._id]; - popup.addItem(item); - overflowPopup.addItem(item); - }, - - removeItemFromMenu: function BW_removeItemFromMenu(item, parentMenu) { - let { popup, overflowPopup } = this.items[privateItem(parentMenu)._id]; - popup.removeItem(item); - overflowPopup.removeItem(item); - }, - - setItemLabel: function BW_setItemLabel(item, label) { - let privates = privateItem(item); - let { domElt, overflowDOMElt } = this.items[privates._id]; - this._setDOMEltLabel(domElt, label); - this._setDOMEltLabel(overflowDOMElt, label); - if (!item.parentMenu && privates._hasFinishedInit) - this.contextMenuPopup.itemLabelDidChange(item); - }, - - _setDOMEltLabel: function BW__setDOMEltLabel(domElt, label) { - domElt.setAttribute("label", label); - }, - - setItemImage: function BW_setItemImage(item, imageURL) { - let { domElt, overflowDOMElt } = this.items[privateItem(item)._id]; - let isMenu = item instanceof Menu; - this._setDOMEltImage(domElt, imageURL, isMenu); - this._setDOMEltImage(overflowDOMElt, imageURL, isMenu); - }, - - _setDOMEltImage: function BW__setDOMEltImage(domElt, imageURL, isMenu) { - if (!imageURL) { - domElt.removeAttribute("image"); - domElt.classList.remove("menu-iconic"); - domElt.classList.remove("menuitem-iconic"); - } - else { - domElt.setAttribute("image", imageURL); - domElt.classList.add(isMenu ? "menu-iconic" : "menuitem-iconic"); - } - }, - - setItemData: function BW_setItemData(item, data) { - let { domElt, overflowDOMElt } = this.items[privateItem(item)._id]; - this._setDOMEltData(domElt, data); - this._setDOMEltData(overflowDOMElt, data); - }, - - _setDOMEltData: function BW__setDOMEltData(domElt, data) { - domElt.setAttribute("value", data); - }, - - // The context specified for a top-level item may not match exactly the real - // context that triggers it. For example, if the user context-clicks a span - // inside an anchor, we want items that specify an anchor context to be - // triggered, but the real context will indicate that the span was clicked, - // not the anchor. Where the real context and an item's context conflict, - // clients should be given the item's context, and this method can be used to - // make such adjustments. Returns an adjusted popupNode. - adjustPopupNode: function BW_adjustPopupNode(popupNode, topLevelItem) { - for (let ctxt in topLevelItem.context) { - if (typeof(ctxt.adjustPopupNode) === "function") { - let ctxtNode = ctxt.adjustPopupNode(popupNode); - if (ctxtNode) { - popupNode = ctxtNode; - break; - } - } - } - return popupNode; - }, - - // Returns true if all of item's contexts are current in the window. - areAllContextsCurrent: function BW_areAllContextsCurrent(item, popupNode) { - let win = popupNode.ownerDocument.defaultView; - let worker = privateItem(item)._workerReg.find(win); - - // If the worker for the item-window pair doesn't exist (e.g., because the - // page hasn't loaded yet), we can't really make a good decision since the - // content script may have a context listener. So just don't show the item - // at all. - if (!worker) - return false; - - // If there are no contexts given at all, the page context applies. - let hasContentContext = worker.anyContextListeners(); - if (!hasContentContext && !item.context.length) - return new PageContext().isCurrent(popupNode); - - // Otherwise, determine if all given contexts are current. Evaluate the - // declarative contexts first and the worker's context listeners last. That - // way the listener might be able to avoid some work. - let curr = true; - for (let ctxt in item.context) { - curr = curr && ctxt.isCurrent(popupNode); - if (!curr) - return false; - } - return !hasContentContext || worker.isAnyContextCurrent(popupNode); - }, - - // Returns the node the user context-clicked to invoke the context menu. - // For Gecko 2.0 and later, triggerNode is this node; - // if it's falsey, document.popupNode is used. - capturePopupNode: function BW_capturePopupNode(triggerNode) { - var popupNode = triggerNode || this.doc.popupNode; - if (!popupNode) - console.warn("popupNode is null."); - return popupNode; - }, - - destroy: function BW_destroy() { - this.contextMenuPopup.destroy(); - delete this.window; - delete this.doc; - delete this.items; - }, - - // Emits a click event in the port of the content worker related to given top- - // level item and popupNode's content window. Listeners will be passed - // popupNode and clickedItemData. - fireClick: function BW_fireClick(topLevelItem, popupNode, clickedItemData) { - let win = popupNode.ownerDocument.defaultView; - let worker = privateItem(topLevelItem)._workerReg.find(win); - if (worker) - worker.fireClick(popupNode, clickedItemData); - }, - - _makeDOMElt: function BW__makeDOMElt(item, isInOverflowSubtree) { - let elt = item instanceof Item ? this._makeMenuitem(item) : - item instanceof Menu ? this._makeMenu(item, isInOverflowSubtree) : - item instanceof Separator ? this._makeSeparator() : - null; - if (!elt) - throw new Error("Internal error: can't make element, unknown item type"); - - elt.id = domEltIDFromItemID(privateItem(item)._id, isInOverflowSubtree); - elt.classList.add(ITEM_CLASS); - return elt; - }, - - // Returns a new xul:menu representing the menu. - _makeMenu: function BW__makeMenu(menu, isInOverflowSubtree) { - let menuElt = this.doc.createElement("menu"); - this._setDOMEltLabel(menuElt, menu.label); - if (menu.image) - this._setDOMEltImage(menuElt, menu.image, true); - let popupElt = this.doc.createElement("menupopup"); - menuElt.appendChild(popupElt); - - let popup = new Popup(popupElt, this, isInOverflowSubtree); - let props = this.items[privateItem(menu)._id]; - if (isInOverflowSubtree) - props.overflowPopup = popup; - else - props.popup = popup; - - return menuElt; - }, - - // Returns a new xul:menuitem representing the item. - _makeMenuitem: function BW__makeMenuitem(item) { - let elt = this.doc.createElement("menuitem"); - this._setDOMEltLabel(elt, item.label); - if (item.image) - this._setDOMEltImage(elt, item.image, false); - if (item.data) - this._setDOMEltData(elt, item.data); - return elt; - }, - - // Returns a new xul:menuseparator. - _makeSeparator: function BW__makeSeparator() { - return this.doc.createElement("menuseparator"); - } -}; - - -// Responsible for adding DOM elements to and removing them from poupDOMElt. -function Popup(popupDOMElt, browserWin, isInOverflowSubtree) { - this.popupDOMElt = popupDOMElt; - this.browserWin = browserWin; - this.isInOverflowSubtree = isInOverflowSubtree; -} - -Popup.prototype = { - - addItem: function Popup_addItem(item) { - let props = this.browserWin.items[privateItem(item)._id]; - let elt = this.isInOverflowSubtree ? props.overflowDOMElt : props.domElt; - this.popupDOMElt.appendChild(elt); - }, - - removeItem: function Popup_removeItem(item) { - let props = this.browserWin.items[privateItem(item)._id]; - let elt = this.isInOverflowSubtree ? props.overflowDOMElt : props.domElt; - this.popupDOMElt.removeChild(elt); - } -}; - - -// Represents a browser window's context menu popup. Responsible for hiding and -// showing items according to the browser window's current context and for -// handling item clicks. -function ContextMenuPopup(popupDOMElt, browserWin) { - this.popupDOMElt = popupDOMElt; - this.browserWin = browserWin; - this.doc = popupDOMElt.ownerDocument; - - // item ID => item - // Calling this variable "topLevelItems" is redundant, since Popup and - // ContextMenuPopup are only responsible for their child items, not all their - // descendant items. But calling it "items" might encourage one to believe - // otherwise, so topLevelItems it is. - this.topLevelItems = {}; - - popupDOMElt.addEventListener("popupshowing", this, false); - popupDOMElt.addEventListener("command", this, false); -} - -ContextMenuPopup.prototype = { - - addItem: function CMP_addItem(item) { - this._ensureStaticEltsExist(); - let itemID = privateItem(item)._id; - this.topLevelItems[itemID] = item; - let props = this.browserWin.items[itemID]; - props.domElt.classList.add(TOPLEVEL_ITEM_CLASS); - props.overflowDOMElt.classList.add(OVERFLOW_ITEM_CLASS); - this._insertItemInSortedOrder(item); - }, - - removeItem: function CMP_removeItem(item) { - let itemID = privateItem(item)._id; - delete this.topLevelItems[itemID]; - let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; - domElt.classList.remove(TOPLEVEL_ITEM_CLASS); - overflowDOMElt.classList.remove(OVERFLOW_ITEM_CLASS); - this.popupDOMElt.removeChild(domElt); - this._overflowPopup.removeChild(overflowDOMElt); - }, - - // Call this after the item's label changes. This re-inserts the item into - // the popup so that it remains in sorted order. - itemLabelDidChange: function CMP_itemLabelDidChange(item) { - let itemID = privateItem(item)._id; - let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; - this.popupDOMElt.removeChild(domElt); - this._overflowPopup.removeChild(overflowDOMElt); - this._insertItemInSortedOrder(item); - }, - - destroy: function CMP_destroy() { - // If there are no more items from any instance of the module, remove the - // separator and overflow submenu, if they exist. - let elts = this._topLevelElts; - if (!elts.length) { - let submenu = this._overflowMenu; - if (submenu) - this.popupDOMElt.removeChild(submenu); - - let sep = this._separator; - if (sep) - this.popupDOMElt.removeChild(sep); - } - - this.popupDOMElt.removeEventListener("popupshowing", this, false); - this.popupDOMElt.removeEventListener("command", this, false); - }, - - handleEvent: function CMP_handleEvent(event) { - try { - if (event.type === "command") - this._handleClick(event.target); - else if (event.type === "popupshowing" && - event.target === this.popupDOMElt) - this._handlePopupShowing(); - } - catch (err) { - console.exception(err); - } - }, - - // command events bubble to the context menu's top-level xul:menupopup and are - // caught here. - _handleClick: function CMP__handleClick(clickedDOMElt) { - if (!clickedDOMElt.classList.contains(ITEM_CLASS)) - return; - let itemID = itemIDFromDOMEltID(clickedDOMElt.id); - if (itemID < 0) - return; - let { item, domElt, overflowDOMElt } = this.browserWin.items[itemID]; - - // Bail if the DOM element was not created by this module instance. In - // real-world add-ons, the itemID < 0 check above is sufficient, but for the - // unit test the JID never changes, making this necessary. - if (clickedDOMElt != domElt && clickedDOMElt != overflowDOMElt) - return; - - let topLevelItem = privateItem(item)._topLevelItem; - let popupNode = this.browserWin.adjustPopupNode(this._getPopupNode(), - topLevelItem); - this.browserWin.fireClick(topLevelItem, popupNode, item.data); - }, - - _getPopupNode: function CMP__getPopupNode() { - // popupDOMElt.triggerNode was added in Gecko 2.0 by bug 383930. The || is - // to avoid a Spidermonkey strict warning on earlier versions. - let triggerNode = this.popupDOMElt.triggerNode || undefined; - return this.browserWin.capturePopupNode(triggerNode); - }, - - // popupshowing is used to show top-level items that match the browser - // window's current context and hide items that don't. Each module instance - // is responsible for showing and hiding the items it owns. - _handlePopupShowing: function CMP__handlePopupShowing() { - - // If there are items queued up to finish initializing, let them go first. - // Otherwise the overflow submenu and menu separator may end up in an - // inappropriate state when those items are later added to the menu. - if (numItemsWithUnfinishedInit) { - const self = this; - timer.setTimeout(function popupShowingTryAgain() { - self._handlePopupShowing(); - }, 0); - return; - } - - let popupNode = this._getPopupNode(); - // Show and hide items. Set a "jetpackContextCurrent" property on the - // DOM elements to signal which of our items match the current context. - for (let itemID in this.topLevelItems) { - let item = this.topLevelItems[itemID] - let areContextsCurr = - this.browserWin.areAllContextsCurrent(item, popupNode); - - // Change the item's label if the return value was a string. - if (typeof(areContextsCurr) === "string") { - item.label = areContextsCurr; - areContextsCurr = true; - } - - let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; - domElt.jetpackContextCurrent = areContextsCurr; - domElt.hidden = !areContextsCurr; - overflowDOMElt.jetpackContextCurrent = areContextsCurr; - overflowDOMElt.hidden = !areContextsCurr; - } - - // Get the total number of items that match the current context. It's a - // little tricky: There may be other instances of this module loaded, - // each hiding and showing their items. So we can't base this number on - // only our items, or on the hidden state of items. That's why we set - // the jetpackContextCurrent property above. The last instance to run - // will leave the menupopup in the correct state. - let elts = this._topLevelElts; - let numShown = Array.reduce(elts, function (total, elt) { - return total + (elt.jetpackContextCurrent ? 1 : 0); - }, 0); - - // If too many items are shown, show the submenu and hide the top-level - // items. Otherwise, hide the submenu and show the top-level items. - let overflow = numShown > this._overflowThreshold; - if (overflow) - Array.forEach(elts, function (e) e.hidden = true); - - let submenu = this._overflowMenu; - if (submenu) - submenu.hidden = !overflow; - - // If no items are shown, hide the menu separator. - let sep = this._separator; - if (sep) - sep.hidden = numShown === 0; - }, - - // Adds the menu separator and overflow submenu if they don't exist. - _ensureStaticEltsExist: function CMP__ensureStaticEltsExist() { - let sep = this._separator; - if (!sep) { - sep = this._makeSeparator(); - this.popupDOMElt.appendChild(sep); - } - - let submenu = this._overflowMenu; - if (!submenu) { - submenu = this._makeOverflowMenu(); - submenu.hidden = true; - this.popupDOMElt.insertBefore(submenu, sep.nextSibling); - } - }, - - // Inserts the given item's DOM element into the popup in sorted order. - _insertItemInSortedOrder: function CMP__insertItemInSortedOrder(item) { - let props = this.browserWin.items[privateItem(item)._id]; - this.popupDOMElt.insertBefore( - props.domElt, insertionPoint(item.label, this._topLevelElts)); - this._overflowPopup.insertBefore( - props.overflowDOMElt, insertionPoint(item.label, this._overflowElts)); - }, - - // Creates and returns the xul:menu that's shown when too many items are added - // to the popup. - _makeOverflowMenu: function CMP__makeOverflowMenu() { - let submenu = this.doc.createElement("menu"); - submenu.id = OVERFLOW_MENU_ID; - submenu.setAttribute("label", OVERFLOW_MENU_LABEL); - let popup = this.doc.createElement("menupopup"); - popup.id = OVERFLOW_POPUP_ID; - submenu.appendChild(popup); - return submenu; - }, - - // Creates and returns the xul:menuseparator that separates the standard - // context menu items from our items. - _makeSeparator: function CMP__makeSeparator() { - let elt = this.doc.createElement("menuseparator"); - elt.id = SEPARATOR_ID; - return elt; - }, - - // Returns the item elements contained in the overflow menu, a NodeList. - get _overflowElts() { - return this._overflowPopup.getElementsByClassName(OVERFLOW_ITEM_CLASS); - }, - - // Returns the overflow xul:menu. - get _overflowMenu() { - return this.doc.getElementById(OVERFLOW_MENU_ID); - }, - - // Returns the overflow xul:menupopup. - get _overflowPopup() { - return this.doc.getElementById(OVERFLOW_POPUP_ID); - }, - - // Returns the OVERFLOW_THRESH_PREF pref value if it exists or - // OVERFLOW_THRESH_DEFAULT if it doesn't. - get _overflowThreshold() { - let prefs = require("./preferences/service"); - return prefs.get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); - }, - - // Returns the xul:menuseparator. - get _separator() { - return this.doc.getElementById(SEPARATOR_ID); - }, - - // Returns the item elements contained in the top-level menu, a NodeList. - get _topLevelElts() { - return this.popupDOMElt.getElementsByClassName(TOPLEVEL_ITEM_CLASS); - } -}; - - -// Init the browserManager only after setting prototypes and such above, because -// it will cause browserManager.onTrack to be called immediately if there are -// open windows. -browserManager.init(); |