aboutsummaryrefslogtreecommitdiff
path: root/tools/addon-sdk-1.5/packages/addon-kit/lib/context-menu.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/addon-sdk-1.5/packages/addon-kit/lib/context-menu.js')
-rw-r--r--tools/addon-sdk-1.5/packages/addon-kit/lib/context-menu.js1492
1 files changed, 0 insertions, 1492 deletions
diff --git a/tools/addon-sdk-1.5/packages/addon-kit/lib/context-menu.js b/tools/addon-sdk-1.5/packages/addon-kit/lib/context-menu.js
deleted file mode 100644
index d57ebe0..0000000
--- a/tools/addon-sdk-1.5/packages/addon-kit/lib/context-menu.js
+++ /dev/null
@@ -1,1492 +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";
-
-const {Ci} = require("chrome");
-
-if (!require("api-utils/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("api-utils/api-utils");
-const collection = require("api-utils/collection");
-const { Worker } = require("api-utils/content");
-const { URL } = require("api-utils/url");
-const { MatchPattern } = require("api-utils/match-pattern");
-const { EventEmitterTrait: EventEmitter } = require("api-utils/events");
-const observerServ = require("api-utils/observer-service");
-const jpSelf = require("self");
-const winUtils = require("api-utils/window-utils");
-const { Trait } = require("api-utils/light-traits");
-const { Cortex } = require("api-utils/cortex");
-const timer = require("timer");
-
-// 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._listeners("context").length > 0;
- },
-
- // 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 listeners = this._contentWorker._listeners("context");
- for (let i = 0; i < listeners.length; i++) {
- try {
- let val = listeners[i].call(this._contentWorker._sandbox, popupNode);
- if (typeof(val) === "string" || val)
- return val;
- }
- catch (err) {
- console.exception(err);
- }
- }
- 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._asyncEmit("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 = winUtils.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, win] in Iterator(this.winsWithoutWorkers)) {
- 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, winWorker] in Iterator(this.winWorkers)) {
- 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 = winUtils.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("api-utils/unload").ensure(this);
- let windowTracker = winUtils.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 = winUtils.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 (!this._isBrowserWindow(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 (!this._isBrowserWindow(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();
- },
-
- _isBrowserWindow: function BM__isBrowserWindow(win) {
- let winType = win.document.documentElement.getAttribute("windowtype");
- return winType === "navigator:browser";
- }
-};
-
-
-// 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);
- },
-
- // Sets this.popupNode to 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. Returns the popupNode.
- capturePopupNode: function BW_capturePopupNode(triggerNode) {
- this.popupNode = triggerNode || this.doc.popupNode;
- if (!this.popupNode)
- console.warn("popupNode is null.");
- return this.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.browserWin.popupNode,
- topLevelItem);
- this.browserWin.fireClick(topLevelItem, popupNode, item.data);
- },
-
- // 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;
- }
-
- // 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;
- let popupNode = this.browserWin.capturePopupNode(triggerNode);
-
- // 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, item] in Iterator(this.topLevelItems)) {
- 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("api-utils/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();