addon-sdk/source/lib/sdk/context-menu.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4 "use strict";
michael@0 5
michael@0 6 module.metadata = {
michael@0 7 "stability": "stable",
michael@0 8 "engines": {
michael@0 9 // TODO Fennec support Bug 788334
michael@0 10 "Firefox": "*"
michael@0 11 }
michael@0 12 };
michael@0 13
michael@0 14 const { Class, mix } = require("./core/heritage");
michael@0 15 const { addCollectionProperty } = require("./util/collection");
michael@0 16 const { ns } = require("./core/namespace");
michael@0 17 const { validateOptions, getTypeOf } = require("./deprecated/api-utils");
michael@0 18 const { URL, isValidURI } = require("./url");
michael@0 19 const { WindowTracker, browserWindowIterator } = require("./deprecated/window-utils");
michael@0 20 const { isBrowser, getInnerId } = require("./window/utils");
michael@0 21 const { Ci } = require("chrome");
michael@0 22 const { MatchPattern } = require("./util/match-pattern");
michael@0 23 const { Worker } = require("./content/worker");
michael@0 24 const { EventTarget } = require("./event/target");
michael@0 25 const { emit } = require('./event/core');
michael@0 26 const { when } = require('./system/unload');
michael@0 27 const selection = require('./selection');
michael@0 28
michael@0 29 // All user items we add have this class.
michael@0 30 const ITEM_CLASS = "addon-context-menu-item";
michael@0 31
michael@0 32 // Items in the top-level context menu also have this class.
michael@0 33 const TOPLEVEL_ITEM_CLASS = "addon-context-menu-item-toplevel";
michael@0 34
michael@0 35 // Items in the overflow submenu also have this class.
michael@0 36 const OVERFLOW_ITEM_CLASS = "addon-context-menu-item-overflow";
michael@0 37
michael@0 38 // The class of the menu separator that separates standard context menu items
michael@0 39 // from our user items.
michael@0 40 const SEPARATOR_CLASS = "addon-context-menu-separator";
michael@0 41
michael@0 42 // If more than this number of items are added to the context menu, all items
michael@0 43 // overflow into a "Jetpack" submenu.
michael@0 44 const OVERFLOW_THRESH_DEFAULT = 10;
michael@0 45 const OVERFLOW_THRESH_PREF =
michael@0 46 "extensions.addon-sdk.context-menu.overflowThreshold";
michael@0 47
michael@0 48 // The label of the overflow sub-xul:menu.
michael@0 49 //
michael@0 50 // TODO: Localize this.
michael@0 51 const OVERFLOW_MENU_LABEL = "Add-ons";
michael@0 52
michael@0 53 // The class of the overflow sub-xul:menu.
michael@0 54 const OVERFLOW_MENU_CLASS = "addon-content-menu-overflow-menu";
michael@0 55
michael@0 56 // The class of the overflow submenu's xul:menupopup.
michael@0 57 const OVERFLOW_POPUP_CLASS = "addon-content-menu-overflow-popup";
michael@0 58
michael@0 59 //These are used by PageContext.isCurrent below. If the popupNode or any of
michael@0 60 //its ancestors is one of these, Firefox uses a tailored context menu, and so
michael@0 61 //the page context doesn't apply.
michael@0 62 const NON_PAGE_CONTEXT_ELTS = [
michael@0 63 Ci.nsIDOMHTMLAnchorElement,
michael@0 64 Ci.nsIDOMHTMLAppletElement,
michael@0 65 Ci.nsIDOMHTMLAreaElement,
michael@0 66 Ci.nsIDOMHTMLButtonElement,
michael@0 67 Ci.nsIDOMHTMLCanvasElement,
michael@0 68 Ci.nsIDOMHTMLEmbedElement,
michael@0 69 Ci.nsIDOMHTMLImageElement,
michael@0 70 Ci.nsIDOMHTMLInputElement,
michael@0 71 Ci.nsIDOMHTMLMapElement,
michael@0 72 Ci.nsIDOMHTMLMediaElement,
michael@0 73 Ci.nsIDOMHTMLMenuElement,
michael@0 74 Ci.nsIDOMHTMLObjectElement,
michael@0 75 Ci.nsIDOMHTMLOptionElement,
michael@0 76 Ci.nsIDOMHTMLSelectElement,
michael@0 77 Ci.nsIDOMHTMLTextAreaElement,
michael@0 78 ];
michael@0 79
michael@0 80 // Holds private properties for API objects
michael@0 81 let internal = ns();
michael@0 82
michael@0 83 function getScheme(spec) {
michael@0 84 try {
michael@0 85 return URL(spec).scheme;
michael@0 86 }
michael@0 87 catch(e) {
michael@0 88 return null;
michael@0 89 }
michael@0 90 }
michael@0 91
michael@0 92 let Context = Class({
michael@0 93 // Returns the node that made this context current
michael@0 94 adjustPopupNode: function adjustPopupNode(popupNode) {
michael@0 95 return popupNode;
michael@0 96 },
michael@0 97
michael@0 98 // Returns whether this context is current for the current node
michael@0 99 isCurrent: function isCurrent(popupNode) {
michael@0 100 return false;
michael@0 101 }
michael@0 102 });
michael@0 103
michael@0 104 // Matches when the context-clicked node doesn't have any of
michael@0 105 // NON_PAGE_CONTEXT_ELTS in its ancestors
michael@0 106 let PageContext = Class({
michael@0 107 extends: Context,
michael@0 108
michael@0 109 isCurrent: function isCurrent(popupNode) {
michael@0 110 // If there is a selection in the window then this context does not match
michael@0 111 if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed)
michael@0 112 return false;
michael@0 113
michael@0 114 // If the clicked node or any of its ancestors is one of the blacklisted
michael@0 115 // NON_PAGE_CONTEXT_ELTS then this context does not match
michael@0 116 while (!(popupNode instanceof Ci.nsIDOMDocument)) {
michael@0 117 if (NON_PAGE_CONTEXT_ELTS.some(function(type) popupNode instanceof type))
michael@0 118 return false;
michael@0 119
michael@0 120 popupNode = popupNode.parentNode;
michael@0 121 }
michael@0 122
michael@0 123 return true;
michael@0 124 }
michael@0 125 });
michael@0 126 exports.PageContext = PageContext;
michael@0 127
michael@0 128 // Matches when there is an active selection in the window
michael@0 129 let SelectionContext = Class({
michael@0 130 extends: Context,
michael@0 131
michael@0 132 isCurrent: function isCurrent(popupNode) {
michael@0 133 if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed)
michael@0 134 return true;
michael@0 135
michael@0 136 try {
michael@0 137 // The node may be a text box which has selectionStart and selectionEnd
michael@0 138 // properties. If not this will throw.
michael@0 139 let { selectionStart, selectionEnd } = popupNode;
michael@0 140 return !isNaN(selectionStart) && !isNaN(selectionEnd) &&
michael@0 141 selectionStart !== selectionEnd;
michael@0 142 }
michael@0 143 catch (e) {
michael@0 144 return false;
michael@0 145 }
michael@0 146 }
michael@0 147 });
michael@0 148 exports.SelectionContext = SelectionContext;
michael@0 149
michael@0 150 // Matches when the context-clicked node or any of its ancestors matches the
michael@0 151 // selector given
michael@0 152 let SelectorContext = Class({
michael@0 153 extends: Context,
michael@0 154
michael@0 155 initialize: function initialize(selector) {
michael@0 156 let options = validateOptions({ selector: selector }, {
michael@0 157 selector: {
michael@0 158 is: ["string"],
michael@0 159 msg: "selector must be a string."
michael@0 160 }
michael@0 161 });
michael@0 162 internal(this).selector = options.selector;
michael@0 163 },
michael@0 164
michael@0 165 adjustPopupNode: function adjustPopupNode(popupNode) {
michael@0 166 let selector = internal(this).selector;
michael@0 167
michael@0 168 while (!(popupNode instanceof Ci.nsIDOMDocument)) {
michael@0 169 if (popupNode.mozMatchesSelector(selector))
michael@0 170 return popupNode;
michael@0 171
michael@0 172 popupNode = popupNode.parentNode;
michael@0 173 }
michael@0 174
michael@0 175 return null;
michael@0 176 },
michael@0 177
michael@0 178 isCurrent: function isCurrent(popupNode) {
michael@0 179 return !!this.adjustPopupNode(popupNode);
michael@0 180 }
michael@0 181 });
michael@0 182 exports.SelectorContext = SelectorContext;
michael@0 183
michael@0 184 // Matches when the page url matches any of the patterns given
michael@0 185 let URLContext = Class({
michael@0 186 extends: Context,
michael@0 187
michael@0 188 initialize: function initialize(patterns) {
michael@0 189 patterns = Array.isArray(patterns) ? patterns : [patterns];
michael@0 190
michael@0 191 try {
michael@0 192 internal(this).patterns = patterns.map(function (p) new MatchPattern(p));
michael@0 193 }
michael@0 194 catch (err) {
michael@0 195 throw new Error("Patterns must be a string, regexp or an array of " +
michael@0 196 "strings or regexps: " + err);
michael@0 197 }
michael@0 198
michael@0 199 },
michael@0 200
michael@0 201 isCurrent: function isCurrent(popupNode) {
michael@0 202 let url = popupNode.ownerDocument.URL;
michael@0 203 return internal(this).patterns.some(function (p) p.test(url));
michael@0 204 }
michael@0 205 });
michael@0 206 exports.URLContext = URLContext;
michael@0 207
michael@0 208 // Matches when the user-supplied predicate returns true
michael@0 209 let PredicateContext = Class({
michael@0 210 extends: Context,
michael@0 211
michael@0 212 initialize: function initialize(predicate) {
michael@0 213 let options = validateOptions({ predicate: predicate }, {
michael@0 214 predicate: {
michael@0 215 is: ["function"],
michael@0 216 msg: "predicate must be a function."
michael@0 217 }
michael@0 218 });
michael@0 219 internal(this).predicate = options.predicate;
michael@0 220 },
michael@0 221
michael@0 222 isCurrent: function isCurrent(popupNode) {
michael@0 223 return internal(this).predicate(populateCallbackNodeData(popupNode));
michael@0 224 }
michael@0 225 });
michael@0 226 exports.PredicateContext = PredicateContext;
michael@0 227
michael@0 228 // List all editable types of inputs. Or is it better to have a list
michael@0 229 // of non-editable inputs?
michael@0 230 let editableInputs = {
michael@0 231 email: true,
michael@0 232 number: true,
michael@0 233 password: true,
michael@0 234 search: true,
michael@0 235 tel: true,
michael@0 236 text: true,
michael@0 237 textarea: true,
michael@0 238 url: true
michael@0 239 };
michael@0 240
michael@0 241 function populateCallbackNodeData(node) {
michael@0 242 let window = node.ownerDocument.defaultView;
michael@0 243 let data = {};
michael@0 244
michael@0 245 data.documentType = node.ownerDocument.contentType;
michael@0 246
michael@0 247 data.documentURL = node.ownerDocument.location.href;
michael@0 248 data.targetName = node.nodeName.toLowerCase();
michael@0 249 data.targetID = node.id || null ;
michael@0 250
michael@0 251 if ((data.targetName === 'input' && editableInputs[node.type]) ||
michael@0 252 data.targetName === 'textarea') {
michael@0 253 data.isEditable = !node.readOnly && !node.disabled;
michael@0 254 }
michael@0 255 else {
michael@0 256 data.isEditable = node.isContentEditable;
michael@0 257 }
michael@0 258
michael@0 259 data.selectionText = selection.text;
michael@0 260
michael@0 261 data.srcURL = node.src || null;
michael@0 262 data.linkURL = node.href || null;
michael@0 263 data.value = node.value || null;
michael@0 264
michael@0 265 return data;
michael@0 266 }
michael@0 267
michael@0 268 function removeItemFromArray(array, item) {
michael@0 269 return array.filter(function(i) i !== item);
michael@0 270 }
michael@0 271
michael@0 272 // Converts anything that isn't false, null or undefined into a string
michael@0 273 function stringOrNull(val) val ? String(val) : val;
michael@0 274
michael@0 275 // Shared option validation rules for Item and Menu
michael@0 276 let baseItemRules = {
michael@0 277 parentMenu: {
michael@0 278 is: ["object", "undefined"],
michael@0 279 ok: function (v) {
michael@0 280 if (!v)
michael@0 281 return true;
michael@0 282 return (v instanceof ItemContainer) || (v instanceof Menu);
michael@0 283 },
michael@0 284 msg: "parentMenu must be a Menu or not specified."
michael@0 285 },
michael@0 286 context: {
michael@0 287 is: ["undefined", "object", "array"],
michael@0 288 ok: function (v) {
michael@0 289 if (!v)
michael@0 290 return true;
michael@0 291 let arr = Array.isArray(v) ? v : [v];
michael@0 292 return arr.every(function (o) o instanceof Context);
michael@0 293 },
michael@0 294 msg: "The 'context' option must be a Context object or an array of " +
michael@0 295 "Context objects."
michael@0 296 },
michael@0 297 contentScript: {
michael@0 298 is: ["string", "array", "undefined"],
michael@0 299 ok: function (v) {
michael@0 300 return !Array.isArray(v) ||
michael@0 301 v.every(function (s) typeof(s) === "string");
michael@0 302 }
michael@0 303 },
michael@0 304 contentScriptFile: {
michael@0 305 is: ["string", "array", "undefined"],
michael@0 306 ok: function (v) {
michael@0 307 if (!v)
michael@0 308 return true;
michael@0 309 let arr = Array.isArray(v) ? v : [v];
michael@0 310 return arr.every(function (s) {
michael@0 311 return getTypeOf(s) === "string" &&
michael@0 312 getScheme(s) === 'resource';
michael@0 313 });
michael@0 314 },
michael@0 315 msg: "The 'contentScriptFile' option must be a local file URL or " +
michael@0 316 "an array of local file URLs."
michael@0 317 },
michael@0 318 onMessage: {
michael@0 319 is: ["function", "undefined"]
michael@0 320 }
michael@0 321 };
michael@0 322
michael@0 323 let labelledItemRules = mix(baseItemRules, {
michael@0 324 label: {
michael@0 325 map: stringOrNull,
michael@0 326 is: ["string"],
michael@0 327 ok: function (v) !!v,
michael@0 328 msg: "The item must have a non-empty string label."
michael@0 329 },
michael@0 330 image: {
michael@0 331 map: stringOrNull,
michael@0 332 is: ["string", "undefined", "null"],
michael@0 333 ok: function (url) {
michael@0 334 if (!url)
michael@0 335 return true;
michael@0 336 return isValidURI(url);
michael@0 337 },
michael@0 338 msg: "Image URL validation failed"
michael@0 339 }
michael@0 340 });
michael@0 341
michael@0 342 // Additional validation rules for Item
michael@0 343 let itemRules = mix(labelledItemRules, {
michael@0 344 data: {
michael@0 345 map: stringOrNull,
michael@0 346 is: ["string", "undefined", "null"]
michael@0 347 }
michael@0 348 });
michael@0 349
michael@0 350 // Additional validation rules for Menu
michael@0 351 let menuRules = mix(labelledItemRules, {
michael@0 352 items: {
michael@0 353 is: ["array", "undefined"],
michael@0 354 ok: function (v) {
michael@0 355 if (!v)
michael@0 356 return true;
michael@0 357 return v.every(function (item) {
michael@0 358 return item instanceof BaseItem;
michael@0 359 });
michael@0 360 },
michael@0 361 msg: "items must be an array, and each element in the array must be an " +
michael@0 362 "Item, Menu, or Separator."
michael@0 363 }
michael@0 364 });
michael@0 365
michael@0 366 let ContextWorker = Class({
michael@0 367 implements: [ Worker ],
michael@0 368
michael@0 369 //Returns true if any context listeners are defined in the worker's port.
michael@0 370 anyContextListeners: function anyContextListeners() {
michael@0 371 return this.getSandbox().hasListenerFor("context");
michael@0 372 },
michael@0 373
michael@0 374 // Calls the context workers context listeners and returns the first result
michael@0 375 // that is either a string or a value that evaluates to true. If all of the
michael@0 376 // listeners returned false then returns false. If there are no listeners
michael@0 377 // then returns null.
michael@0 378 getMatchedContext: function getCurrentContexts(popupNode) {
michael@0 379 let results = this.getSandbox().emitSync("context", popupNode);
michael@0 380 return results.reduce(function(val, result) val || result, null);
michael@0 381 },
michael@0 382
michael@0 383 // Emits a click event in the worker's port. popupNode is the node that was
michael@0 384 // context-clicked, and clickedItemData is the data of the item that was
michael@0 385 // clicked.
michael@0 386 fireClick: function fireClick(popupNode, clickedItemData) {
michael@0 387 this.getSandbox().emitSync("click", popupNode, clickedItemData);
michael@0 388 }
michael@0 389 });
michael@0 390
michael@0 391 // Returns true if any contexts match. If there are no contexts then a
michael@0 392 // PageContext is tested instead
michael@0 393 function hasMatchingContext(contexts, popupNode) {
michael@0 394 for (let context in contexts) {
michael@0 395 if (!context.isCurrent(popupNode))
michael@0 396 return false;
michael@0 397 }
michael@0 398
michael@0 399 return true;
michael@0 400 }
michael@0 401
michael@0 402 // Gets the matched context from any worker for this item. If there is no worker
michael@0 403 // or no matched context then returns false.
michael@0 404 function getCurrentWorkerContext(item, popupNode) {
michael@0 405 let worker = getItemWorkerForWindow(item, popupNode.ownerDocument.defaultView);
michael@0 406 if (!worker || !worker.anyContextListeners())
michael@0 407 return true;
michael@0 408 return worker.getMatchedContext(popupNode);
michael@0 409 }
michael@0 410
michael@0 411 // Tests whether an item should be visible or not based on its contexts and
michael@0 412 // content scripts
michael@0 413 function isItemVisible(item, popupNode, defaultVisibility) {
michael@0 414 if (!item.context.length) {
michael@0 415 let worker = getItemWorkerForWindow(item, popupNode.ownerDocument.defaultView);
michael@0 416 if (!worker || !worker.anyContextListeners())
michael@0 417 return defaultVisibility;
michael@0 418 }
michael@0 419
michael@0 420 if (!hasMatchingContext(item.context, popupNode))
michael@0 421 return false;
michael@0 422
michael@0 423 let context = getCurrentWorkerContext(item, popupNode);
michael@0 424 if (typeof(context) === "string" && context != "")
michael@0 425 item.label = context;
michael@0 426
michael@0 427 return !!context;
michael@0 428 }
michael@0 429
michael@0 430 // Gets the item's content script worker for a window, creating one if necessary
michael@0 431 // Once created it will be automatically destroyed when the window unloads.
michael@0 432 // If there is not content scripts for the item then null will be returned.
michael@0 433 function getItemWorkerForWindow(item, window) {
michael@0 434 if (!item.contentScript && !item.contentScriptFile)
michael@0 435 return null;
michael@0 436
michael@0 437 let id = getInnerId(window);
michael@0 438 let worker = internal(item).workerMap.get(id);
michael@0 439
michael@0 440 if (worker)
michael@0 441 return worker;
michael@0 442
michael@0 443 worker = ContextWorker({
michael@0 444 window: window,
michael@0 445 contentScript: item.contentScript,
michael@0 446 contentScriptFile: item.contentScriptFile,
michael@0 447 onMessage: function(msg) {
michael@0 448 emit(item, "message", msg);
michael@0 449 },
michael@0 450 onDetach: function() {
michael@0 451 internal(item).workerMap.delete(id);
michael@0 452 }
michael@0 453 });
michael@0 454
michael@0 455 internal(item).workerMap.set(id, worker);
michael@0 456
michael@0 457 return worker;
michael@0 458 }
michael@0 459
michael@0 460 // Called when an item is clicked to send out click events to the content
michael@0 461 // scripts
michael@0 462 function itemClicked(item, clickedItem, popupNode) {
michael@0 463 let worker = getItemWorkerForWindow(item, popupNode.ownerDocument.defaultView);
michael@0 464
michael@0 465 if (worker) {
michael@0 466 let adjustedNode = popupNode;
michael@0 467 for (let context in item.context)
michael@0 468 adjustedNode = context.adjustPopupNode(adjustedNode);
michael@0 469 worker.fireClick(adjustedNode, clickedItem.data);
michael@0 470 }
michael@0 471
michael@0 472 if (item.parentMenu)
michael@0 473 itemClicked(item.parentMenu, clickedItem, popupNode);
michael@0 474 }
michael@0 475
michael@0 476 // All things that appear in the context menu extend this
michael@0 477 let BaseItem = Class({
michael@0 478 initialize: function initialize() {
michael@0 479 addCollectionProperty(this, "context");
michael@0 480
michael@0 481 // Used to cache content script workers and the windows they have been
michael@0 482 // created for
michael@0 483 internal(this).workerMap = new Map();
michael@0 484
michael@0 485 if ("context" in internal(this).options && internal(this).options.context) {
michael@0 486 let contexts = internal(this).options.context;
michael@0 487 if (Array.isArray(contexts)) {
michael@0 488 for (let context of contexts)
michael@0 489 this.context.add(context);
michael@0 490 }
michael@0 491 else {
michael@0 492 this.context.add(contexts);
michael@0 493 }
michael@0 494 }
michael@0 495
michael@0 496 let parentMenu = internal(this).options.parentMenu;
michael@0 497 if (!parentMenu)
michael@0 498 parentMenu = contentContextMenu;
michael@0 499
michael@0 500 parentMenu.addItem(this);
michael@0 501
michael@0 502 Object.defineProperty(this, "contentScript", {
michael@0 503 enumerable: true,
michael@0 504 value: internal(this).options.contentScript
michael@0 505 });
michael@0 506
michael@0 507 Object.defineProperty(this, "contentScriptFile", {
michael@0 508 enumerable: true,
michael@0 509 value: internal(this).options.contentScriptFile
michael@0 510 });
michael@0 511 },
michael@0 512
michael@0 513 destroy: function destroy() {
michael@0 514 if (this.parentMenu)
michael@0 515 this.parentMenu.removeItem(this);
michael@0 516 },
michael@0 517
michael@0 518 get parentMenu() {
michael@0 519 return internal(this).parentMenu;
michael@0 520 },
michael@0 521 });
michael@0 522
michael@0 523 // All things that have a label on the context menu extend this
michael@0 524 let LabelledItem = Class({
michael@0 525 extends: BaseItem,
michael@0 526 implements: [ EventTarget ],
michael@0 527
michael@0 528 initialize: function initialize(options) {
michael@0 529 BaseItem.prototype.initialize.call(this);
michael@0 530 EventTarget.prototype.initialize.call(this, options);
michael@0 531 },
michael@0 532
michael@0 533 destroy: function destroy() {
michael@0 534 for (let [,worker] of internal(this).workerMap)
michael@0 535 worker.destroy();
michael@0 536
michael@0 537 BaseItem.prototype.destroy.call(this);
michael@0 538 },
michael@0 539
michael@0 540 get label() {
michael@0 541 return internal(this).options.label;
michael@0 542 },
michael@0 543
michael@0 544 set label(val) {
michael@0 545 internal(this).options.label = val;
michael@0 546
michael@0 547 MenuManager.updateItem(this);
michael@0 548 },
michael@0 549
michael@0 550 get image() {
michael@0 551 return internal(this).options.image;
michael@0 552 },
michael@0 553
michael@0 554 set image(val) {
michael@0 555 internal(this).options.image = val;
michael@0 556
michael@0 557 MenuManager.updateItem(this);
michael@0 558 },
michael@0 559
michael@0 560 get data() {
michael@0 561 return internal(this).options.data;
michael@0 562 },
michael@0 563
michael@0 564 set data(val) {
michael@0 565 internal(this).options.data = val;
michael@0 566 }
michael@0 567 });
michael@0 568
michael@0 569 let Item = Class({
michael@0 570 extends: LabelledItem,
michael@0 571
michael@0 572 initialize: function initialize(options) {
michael@0 573 internal(this).options = validateOptions(options, itemRules);
michael@0 574
michael@0 575 LabelledItem.prototype.initialize.call(this, options);
michael@0 576 },
michael@0 577
michael@0 578 toString: function toString() {
michael@0 579 return "[object Item \"" + this.label + "\"]";
michael@0 580 },
michael@0 581
michael@0 582 get data() {
michael@0 583 return internal(this).options.data;
michael@0 584 },
michael@0 585
michael@0 586 set data(val) {
michael@0 587 internal(this).options.data = val;
michael@0 588
michael@0 589 MenuManager.updateItem(this);
michael@0 590 },
michael@0 591 });
michael@0 592 exports.Item = Item;
michael@0 593
michael@0 594 let ItemContainer = Class({
michael@0 595 initialize: function initialize() {
michael@0 596 internal(this).children = [];
michael@0 597 },
michael@0 598
michael@0 599 destroy: function destroy() {
michael@0 600 // Destroys the entire hierarchy
michael@0 601 for (let item of internal(this).children)
michael@0 602 item.destroy();
michael@0 603 },
michael@0 604
michael@0 605 addItem: function addItem(item) {
michael@0 606 let oldParent = item.parentMenu;
michael@0 607
michael@0 608 // Don't just call removeItem here as that would remove the corresponding
michael@0 609 // UI element which is more costly than just moving it to the right place
michael@0 610 if (oldParent)
michael@0 611 internal(oldParent).children = removeItemFromArray(internal(oldParent).children, item);
michael@0 612
michael@0 613 let after = null;
michael@0 614 let children = internal(this).children;
michael@0 615 if (children.length > 0)
michael@0 616 after = children[children.length - 1];
michael@0 617
michael@0 618 children.push(item);
michael@0 619 internal(item).parentMenu = this;
michael@0 620
michael@0 621 // If there was an old parent then we just have to move the item, otherwise
michael@0 622 // it needs to be created
michael@0 623 if (oldParent)
michael@0 624 MenuManager.moveItem(item, after);
michael@0 625 else
michael@0 626 MenuManager.createItem(item, after);
michael@0 627 },
michael@0 628
michael@0 629 removeItem: function removeItem(item) {
michael@0 630 // If the item isn't a child of this menu then ignore this call
michael@0 631 if (item.parentMenu !== this)
michael@0 632 return;
michael@0 633
michael@0 634 MenuManager.removeItem(item);
michael@0 635
michael@0 636 internal(this).children = removeItemFromArray(internal(this).children, item);
michael@0 637 internal(item).parentMenu = null;
michael@0 638 },
michael@0 639
michael@0 640 get items() {
michael@0 641 return internal(this).children.slice(0);
michael@0 642 },
michael@0 643
michael@0 644 set items(val) {
michael@0 645 // Validate the arguments before making any changes
michael@0 646 if (!Array.isArray(val))
michael@0 647 throw new Error(menuOptionRules.items.msg);
michael@0 648
michael@0 649 for (let item of val) {
michael@0 650 if (!(item instanceof BaseItem))
michael@0 651 throw new Error(menuOptionRules.items.msg);
michael@0 652 }
michael@0 653
michael@0 654 // Remove the old items and add the new ones
michael@0 655 for (let item of internal(this).children)
michael@0 656 this.removeItem(item);
michael@0 657
michael@0 658 for (let item of val)
michael@0 659 this.addItem(item);
michael@0 660 },
michael@0 661 });
michael@0 662
michael@0 663 let Menu = Class({
michael@0 664 extends: LabelledItem,
michael@0 665 implements: [ItemContainer],
michael@0 666
michael@0 667 initialize: function initialize(options) {
michael@0 668 internal(this).options = validateOptions(options, menuRules);
michael@0 669
michael@0 670 LabelledItem.prototype.initialize.call(this, options);
michael@0 671 ItemContainer.prototype.initialize.call(this);
michael@0 672
michael@0 673 if (internal(this).options.items) {
michael@0 674 for (let item of internal(this).options.items)
michael@0 675 this.addItem(item);
michael@0 676 }
michael@0 677 },
michael@0 678
michael@0 679 destroy: function destroy() {
michael@0 680 ItemContainer.prototype.destroy.call(this);
michael@0 681 LabelledItem.prototype.destroy.call(this);
michael@0 682 },
michael@0 683
michael@0 684 toString: function toString() {
michael@0 685 return "[object Menu \"" + this.label + "\"]";
michael@0 686 },
michael@0 687 });
michael@0 688 exports.Menu = Menu;
michael@0 689
michael@0 690 let Separator = Class({
michael@0 691 extends: BaseItem,
michael@0 692
michael@0 693 initialize: function initialize(options) {
michael@0 694 internal(this).options = validateOptions(options, baseItemRules);
michael@0 695
michael@0 696 BaseItem.prototype.initialize.call(this);
michael@0 697 },
michael@0 698
michael@0 699 toString: function toString() {
michael@0 700 return "[object Separator]";
michael@0 701 }
michael@0 702 });
michael@0 703 exports.Separator = Separator;
michael@0 704
michael@0 705 // Holds items for the content area context menu
michael@0 706 let contentContextMenu = ItemContainer();
michael@0 707 exports.contentContextMenu = contentContextMenu;
michael@0 708
michael@0 709 when(function() {
michael@0 710 contentContextMenu.destroy();
michael@0 711 });
michael@0 712
michael@0 713 // App specific UI code lives here, it should handle populating the context
michael@0 714 // menu and passing clicks etc. through to the items.
michael@0 715
michael@0 716 function countVisibleItems(nodes) {
michael@0 717 return Array.reduce(nodes, function(sum, node) {
michael@0 718 return node.hidden ? sum : sum + 1;
michael@0 719 }, 0);
michael@0 720 }
michael@0 721
michael@0 722 let MenuWrapper = Class({
michael@0 723 initialize: function initialize(winWrapper, items, contextMenu) {
michael@0 724 this.winWrapper = winWrapper;
michael@0 725 this.window = winWrapper.window;
michael@0 726 this.items = items;
michael@0 727 this.contextMenu = contextMenu;
michael@0 728 this.populated = false;
michael@0 729 this.menuMap = new Map();
michael@0 730
michael@0 731 // updateItemVisibilities will run first, updateOverflowState will run after
michael@0 732 // all other instances of this module have run updateItemVisibilities
michael@0 733 this._updateItemVisibilities = this.updateItemVisibilities.bind(this);
michael@0 734 this.contextMenu.addEventListener("popupshowing", this._updateItemVisibilities, true);
michael@0 735 this._updateOverflowState = this.updateOverflowState.bind(this);
michael@0 736 this.contextMenu.addEventListener("popupshowing", this._updateOverflowState, false);
michael@0 737 },
michael@0 738
michael@0 739 destroy: function destroy() {
michael@0 740 this.contextMenu.removeEventListener("popupshowing", this._updateOverflowState, false);
michael@0 741 this.contextMenu.removeEventListener("popupshowing", this._updateItemVisibilities, true);
michael@0 742
michael@0 743 if (!this.populated)
michael@0 744 return;
michael@0 745
michael@0 746 // If we're getting unloaded at runtime then we must remove all the
michael@0 747 // generated XUL nodes
michael@0 748 let oldParent = null;
michael@0 749 for (let item of internal(this.items).children) {
michael@0 750 let xulNode = this.getXULNodeForItem(item);
michael@0 751 oldParent = xulNode.parentNode;
michael@0 752 oldParent.removeChild(xulNode);
michael@0 753 }
michael@0 754
michael@0 755 if (oldParent)
michael@0 756 this.onXULRemoved(oldParent);
michael@0 757 },
michael@0 758
michael@0 759 get separator() {
michael@0 760 return this.contextMenu.querySelector("." + SEPARATOR_CLASS);
michael@0 761 },
michael@0 762
michael@0 763 get overflowMenu() {
michael@0 764 return this.contextMenu.querySelector("." + OVERFLOW_MENU_CLASS);
michael@0 765 },
michael@0 766
michael@0 767 get overflowPopup() {
michael@0 768 return this.contextMenu.querySelector("." + OVERFLOW_POPUP_CLASS);
michael@0 769 },
michael@0 770
michael@0 771 get topLevelItems() {
michael@0 772 return this.contextMenu.querySelectorAll("." + TOPLEVEL_ITEM_CLASS);
michael@0 773 },
michael@0 774
michael@0 775 get overflowItems() {
michael@0 776 return this.contextMenu.querySelectorAll("." + OVERFLOW_ITEM_CLASS);
michael@0 777 },
michael@0 778
michael@0 779 getXULNodeForItem: function getXULNodeForItem(item) {
michael@0 780 return this.menuMap.get(item);
michael@0 781 },
michael@0 782
michael@0 783 // Recurses through the item hierarchy creating XUL nodes for everything
michael@0 784 populate: function populate(menu) {
michael@0 785 for (let i = 0; i < internal(menu).children.length; i++) {
michael@0 786 let item = internal(menu).children[i];
michael@0 787 let after = i === 0 ? null : internal(menu).children[i - 1];
michael@0 788 this.createItem(item, after);
michael@0 789
michael@0 790 if (item instanceof Menu)
michael@0 791 this.populate(item);
michael@0 792 }
michael@0 793 },
michael@0 794
michael@0 795 // Recurses through the menu setting the visibility of items. Returns true
michael@0 796 // if any of the items in this menu were visible
michael@0 797 setVisibility: function setVisibility(menu, popupNode, defaultVisibility) {
michael@0 798 let anyVisible = false;
michael@0 799
michael@0 800 for (let item of internal(menu).children) {
michael@0 801 let visible = isItemVisible(item, popupNode, defaultVisibility);
michael@0 802
michael@0 803 // Recurse through Menus, if none of the sub-items were visible then the
michael@0 804 // menu is hidden too.
michael@0 805 if (visible && (item instanceof Menu))
michael@0 806 visible = this.setVisibility(item, popupNode, true);
michael@0 807
michael@0 808 let xulNode = this.getXULNodeForItem(item);
michael@0 809 xulNode.hidden = !visible;
michael@0 810
michael@0 811 anyVisible = anyVisible || visible;
michael@0 812 }
michael@0 813
michael@0 814 return anyVisible;
michael@0 815 },
michael@0 816
michael@0 817 // Works out where to insert a XUL node for an item in a browser window
michael@0 818 insertIntoXUL: function insertIntoXUL(item, node, after) {
michael@0 819 let menupopup = null;
michael@0 820 let before = null;
michael@0 821
michael@0 822 let menu = item.parentMenu;
michael@0 823 if (menu === this.items) {
michael@0 824 // Insert into the overflow popup if it exists, otherwise the normal
michael@0 825 // context menu
michael@0 826 menupopup = this.overflowPopup;
michael@0 827 if (!menupopup)
michael@0 828 menupopup = this.contextMenu;
michael@0 829 }
michael@0 830 else {
michael@0 831 let xulNode = this.getXULNodeForItem(menu);
michael@0 832 menupopup = xulNode.firstChild;
michael@0 833 }
michael@0 834
michael@0 835 if (after) {
michael@0 836 let afterNode = this.getXULNodeForItem(after);
michael@0 837 before = afterNode.nextSibling;
michael@0 838 }
michael@0 839 else if (menupopup === this.contextMenu) {
michael@0 840 let topLevel = this.topLevelItems;
michael@0 841 if (topLevel.length > 0)
michael@0 842 before = topLevel[topLevel.length - 1].nextSibling;
michael@0 843 else
michael@0 844 before = this.separator.nextSibling;
michael@0 845 }
michael@0 846
michael@0 847 menupopup.insertBefore(node, before);
michael@0 848 },
michael@0 849
michael@0 850 // Sets the right class for XUL nodes
michael@0 851 updateXULClass: function updateXULClass(xulNode) {
michael@0 852 if (xulNode.parentNode == this.contextMenu)
michael@0 853 xulNode.classList.add(TOPLEVEL_ITEM_CLASS);
michael@0 854 else
michael@0 855 xulNode.classList.remove(TOPLEVEL_ITEM_CLASS);
michael@0 856
michael@0 857 if (xulNode.parentNode == this.overflowPopup)
michael@0 858 xulNode.classList.add(OVERFLOW_ITEM_CLASS);
michael@0 859 else
michael@0 860 xulNode.classList.remove(OVERFLOW_ITEM_CLASS);
michael@0 861 },
michael@0 862
michael@0 863 // Creates a XUL node for an item
michael@0 864 createItem: function createItem(item, after) {
michael@0 865 if (!this.populated)
michael@0 866 return;
michael@0 867
michael@0 868 // Create the separator if it doesn't already exist
michael@0 869 if (!this.separator) {
michael@0 870 let separator = this.window.document.createElement("menuseparator");
michael@0 871 separator.setAttribute("class", SEPARATOR_CLASS);
michael@0 872
michael@0 873 // Insert before the separator created by the old context-menu if it
michael@0 874 // exists to avoid bug 832401
michael@0 875 let oldSeparator = this.window.document.getElementById("jetpack-context-menu-separator");
michael@0 876 if (oldSeparator && oldSeparator.parentNode != this.contextMenu)
michael@0 877 oldSeparator = null;
michael@0 878 this.contextMenu.insertBefore(separator, oldSeparator);
michael@0 879 }
michael@0 880
michael@0 881 let type = "menuitem";
michael@0 882 if (item instanceof Menu)
michael@0 883 type = "menu";
michael@0 884 else if (item instanceof Separator)
michael@0 885 type = "menuseparator";
michael@0 886
michael@0 887 let xulNode = this.window.document.createElement(type);
michael@0 888 xulNode.setAttribute("class", ITEM_CLASS);
michael@0 889 if (item instanceof LabelledItem) {
michael@0 890 xulNode.setAttribute("label", item.label);
michael@0 891 if (item.image) {
michael@0 892 xulNode.setAttribute("image", item.image);
michael@0 893 if (item instanceof Menu)
michael@0 894 xulNode.classList.add("menu-iconic");
michael@0 895 else
michael@0 896 xulNode.classList.add("menuitem-iconic");
michael@0 897 }
michael@0 898 if (item.data)
michael@0 899 xulNode.setAttribute("value", item.data);
michael@0 900
michael@0 901 let self = this;
michael@0 902 xulNode.addEventListener("command", function(event) {
michael@0 903 // Only care about clicks directly on this item
michael@0 904 if (event.target !== xulNode)
michael@0 905 return;
michael@0 906
michael@0 907 itemClicked(item, item, self.contextMenu.triggerNode);
michael@0 908 }, false);
michael@0 909 }
michael@0 910
michael@0 911 this.insertIntoXUL(item, xulNode, after);
michael@0 912 this.updateXULClass(xulNode);
michael@0 913 xulNode.data = item.data;
michael@0 914
michael@0 915 if (item instanceof Menu) {
michael@0 916 let menupopup = this.window.document.createElement("menupopup");
michael@0 917 xulNode.appendChild(menupopup);
michael@0 918 }
michael@0 919
michael@0 920 this.menuMap.set(item, xulNode);
michael@0 921 },
michael@0 922
michael@0 923 // Updates the XUL node for an item in this window
michael@0 924 updateItem: function updateItem(item) {
michael@0 925 if (!this.populated)
michael@0 926 return;
michael@0 927
michael@0 928 let xulNode = this.getXULNodeForItem(item);
michael@0 929
michael@0 930 // TODO figure out why this requires setAttribute
michael@0 931 xulNode.setAttribute("label", item.label);
michael@0 932
michael@0 933 if (item.image) {
michael@0 934 xulNode.setAttribute("image", item.image);
michael@0 935 if (item instanceof Menu)
michael@0 936 xulNode.classList.add("menu-iconic");
michael@0 937 else
michael@0 938 xulNode.classList.add("menuitem-iconic");
michael@0 939 }
michael@0 940 else {
michael@0 941 xulNode.removeAttribute("image");
michael@0 942 xulNode.classList.remove("menu-iconic");
michael@0 943 xulNode.classList.remove("menuitem-iconic");
michael@0 944 }
michael@0 945
michael@0 946 if (item.data)
michael@0 947 xulNode.setAttribute("value", item.data);
michael@0 948 else
michael@0 949 xulNode.removeAttribute("value");
michael@0 950 },
michael@0 951
michael@0 952 // Moves the XUL node for an item in this window to its new place in the
michael@0 953 // hierarchy
michael@0 954 moveItem: function moveItem(item, after) {
michael@0 955 if (!this.populated)
michael@0 956 return;
michael@0 957
michael@0 958 let xulNode = this.getXULNodeForItem(item);
michael@0 959 let oldParent = xulNode.parentNode;
michael@0 960
michael@0 961 this.insertIntoXUL(item, xulNode, after);
michael@0 962 this.updateXULClass(xulNode);
michael@0 963 this.onXULRemoved(oldParent);
michael@0 964 },
michael@0 965
michael@0 966 // Removes the XUL nodes for an item in every window we've ever populated.
michael@0 967 removeItem: function removeItem(item) {
michael@0 968 if (!this.populated)
michael@0 969 return;
michael@0 970
michael@0 971 let xulItem = this.getXULNodeForItem(item);
michael@0 972
michael@0 973 let oldParent = xulItem.parentNode;
michael@0 974
michael@0 975 oldParent.removeChild(xulItem);
michael@0 976 this.menuMap.delete(item);
michael@0 977
michael@0 978 this.onXULRemoved(oldParent);
michael@0 979 },
michael@0 980
michael@0 981 // Called when any XUL nodes have been removed from a menupopup. This handles
michael@0 982 // making sure the separator and overflow are correct
michael@0 983 onXULRemoved: function onXULRemoved(parent) {
michael@0 984 if (parent == this.contextMenu) {
michael@0 985 let toplevel = this.topLevelItems;
michael@0 986
michael@0 987 // If there are no more items then remove the separator
michael@0 988 if (toplevel.length == 0) {
michael@0 989 let separator = this.separator;
michael@0 990 if (separator)
michael@0 991 separator.parentNode.removeChild(separator);
michael@0 992 }
michael@0 993 }
michael@0 994 else if (parent == this.overflowPopup) {
michael@0 995 // If there are no more items then remove the overflow menu and separator
michael@0 996 if (parent.childNodes.length == 0) {
michael@0 997 let separator = this.separator;
michael@0 998 separator.parentNode.removeChild(separator);
michael@0 999 this.contextMenu.removeChild(parent.parentNode);
michael@0 1000 }
michael@0 1001 }
michael@0 1002 },
michael@0 1003
michael@0 1004 // Recurses through all the items owned by this module and sets their hidden
michael@0 1005 // state
michael@0 1006 updateItemVisibilities: function updateItemVisibilities(event) {
michael@0 1007 try {
michael@0 1008 if (event.type != "popupshowing")
michael@0 1009 return;
michael@0 1010 if (event.target != this.contextMenu)
michael@0 1011 return;
michael@0 1012
michael@0 1013 if (internal(this.items).children.length == 0)
michael@0 1014 return;
michael@0 1015
michael@0 1016 if (!this.populated) {
michael@0 1017 this.populated = true;
michael@0 1018 this.populate(this.items);
michael@0 1019 }
michael@0 1020
michael@0 1021 let popupNode = event.target.triggerNode;
michael@0 1022 this.setVisibility(this.items, popupNode, PageContext().isCurrent(popupNode));
michael@0 1023 }
michael@0 1024 catch (e) {
michael@0 1025 console.exception(e);
michael@0 1026 }
michael@0 1027 },
michael@0 1028
michael@0 1029 // Counts the number of visible items across all modules and makes sure they
michael@0 1030 // are in the right place between the top level context menu and the overflow
michael@0 1031 // menu
michael@0 1032 updateOverflowState: function updateOverflowState(event) {
michael@0 1033 try {
michael@0 1034 if (event.type != "popupshowing")
michael@0 1035 return;
michael@0 1036 if (event.target != this.contextMenu)
michael@0 1037 return;
michael@0 1038
michael@0 1039 // The main items will be in either the top level context menu or the
michael@0 1040 // overflow menu at this point. Count the visible ones and if they are in
michael@0 1041 // the wrong place move them
michael@0 1042 let toplevel = this.topLevelItems;
michael@0 1043 let overflow = this.overflowItems;
michael@0 1044 let visibleCount = countVisibleItems(toplevel) +
michael@0 1045 countVisibleItems(overflow);
michael@0 1046
michael@0 1047 if (visibleCount == 0) {
michael@0 1048 let separator = this.separator;
michael@0 1049 if (separator)
michael@0 1050 separator.hidden = true;
michael@0 1051 let overflowMenu = this.overflowMenu;
michael@0 1052 if (overflowMenu)
michael@0 1053 overflowMenu.hidden = true;
michael@0 1054 }
michael@0 1055 else if (visibleCount > MenuManager.overflowThreshold) {
michael@0 1056 this.separator.hidden = false;
michael@0 1057 let overflowPopup = this.overflowPopup;
michael@0 1058 if (overflowPopup)
michael@0 1059 overflowPopup.parentNode.hidden = false;
michael@0 1060
michael@0 1061 if (toplevel.length > 0) {
michael@0 1062 // The overflow menu shouldn't exist here but let's play it safe
michael@0 1063 if (!overflowPopup) {
michael@0 1064 let overflowMenu = this.window.document.createElement("menu");
michael@0 1065 overflowMenu.setAttribute("class", OVERFLOW_MENU_CLASS);
michael@0 1066 overflowMenu.setAttribute("label", OVERFLOW_MENU_LABEL);
michael@0 1067 this.contextMenu.insertBefore(overflowMenu, this.separator.nextSibling);
michael@0 1068
michael@0 1069 overflowPopup = this.window.document.createElement("menupopup");
michael@0 1070 overflowPopup.setAttribute("class", OVERFLOW_POPUP_CLASS);
michael@0 1071 overflowMenu.appendChild(overflowPopup);
michael@0 1072 }
michael@0 1073
michael@0 1074 for (let xulNode of toplevel) {
michael@0 1075 overflowPopup.appendChild(xulNode);
michael@0 1076 this.updateXULClass(xulNode);
michael@0 1077 }
michael@0 1078 }
michael@0 1079 }
michael@0 1080 else {
michael@0 1081 this.separator.hidden = false;
michael@0 1082
michael@0 1083 if (overflow.length > 0) {
michael@0 1084 // Move all the overflow nodes out of the overflow menu and position
michael@0 1085 // them immediately before it
michael@0 1086 for (let xulNode of overflow) {
michael@0 1087 this.contextMenu.insertBefore(xulNode, xulNode.parentNode.parentNode);
michael@0 1088 this.updateXULClass(xulNode);
michael@0 1089 }
michael@0 1090 this.contextMenu.removeChild(this.overflowMenu);
michael@0 1091 }
michael@0 1092 }
michael@0 1093 }
michael@0 1094 catch (e) {
michael@0 1095 console.exception(e);
michael@0 1096 }
michael@0 1097 }
michael@0 1098 });
michael@0 1099
michael@0 1100 // This wraps every window that we've seen
michael@0 1101 let WindowWrapper = Class({
michael@0 1102 initialize: function initialize(window) {
michael@0 1103 this.window = window;
michael@0 1104 this.menus = [
michael@0 1105 new MenuWrapper(this, contentContextMenu, window.document.getElementById("contentAreaContextMenu")),
michael@0 1106 ];
michael@0 1107 },
michael@0 1108
michael@0 1109 destroy: function destroy() {
michael@0 1110 for (let menuWrapper of this.menus)
michael@0 1111 menuWrapper.destroy();
michael@0 1112 },
michael@0 1113
michael@0 1114 getMenuWrapperForItem: function getMenuWrapperForItem(item) {
michael@0 1115 let root = item.parentMenu;
michael@0 1116 while (root.parentMenu)
michael@0 1117 root = root.parentMenu;
michael@0 1118
michael@0 1119 for (let wrapper of this.menus) {
michael@0 1120 if (wrapper.items === root)
michael@0 1121 return wrapper;
michael@0 1122 }
michael@0 1123
michael@0 1124 return null;
michael@0 1125 }
michael@0 1126 });
michael@0 1127
michael@0 1128 let MenuManager = {
michael@0 1129 windowMap: new Map(),
michael@0 1130
michael@0 1131 get overflowThreshold() {
michael@0 1132 let prefs = require("./preferences/service");
michael@0 1133 return prefs.get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT);
michael@0 1134 },
michael@0 1135
michael@0 1136 // When a new window is added start watching it for context menu shows
michael@0 1137 onTrack: function onTrack(window) {
michael@0 1138 if (!isBrowser(window))
michael@0 1139 return;
michael@0 1140
michael@0 1141 // Generally shouldn't happen, but just in case
michael@0 1142 if (this.windowMap.has(window)) {
michael@0 1143 console.warn("Already seen this window");
michael@0 1144 return;
michael@0 1145 }
michael@0 1146
michael@0 1147 let winWrapper = WindowWrapper(window);
michael@0 1148 this.windowMap.set(window, winWrapper);
michael@0 1149 },
michael@0 1150
michael@0 1151 onUntrack: function onUntrack(window) {
michael@0 1152 if (!isBrowser(window))
michael@0 1153 return;
michael@0 1154
michael@0 1155 let winWrapper = this.windowMap.get(window);
michael@0 1156 // This shouldn't happen but protect against it anyway
michael@0 1157 if (!winWrapper)
michael@0 1158 return;
michael@0 1159 winWrapper.destroy();
michael@0 1160
michael@0 1161 this.windowMap.delete(window);
michael@0 1162 },
michael@0 1163
michael@0 1164 // Creates a XUL node for an item in every window we've already populated
michael@0 1165 createItem: function createItem(item, after) {
michael@0 1166 for (let [window, winWrapper] of this.windowMap) {
michael@0 1167 let menuWrapper = winWrapper.getMenuWrapperForItem(item);
michael@0 1168 if (menuWrapper)
michael@0 1169 menuWrapper.createItem(item, after);
michael@0 1170 }
michael@0 1171 },
michael@0 1172
michael@0 1173 // Updates the XUL node for an item in every window we've already populated
michael@0 1174 updateItem: function updateItem(item) {
michael@0 1175 for (let [window, winWrapper] of this.windowMap) {
michael@0 1176 let menuWrapper = winWrapper.getMenuWrapperForItem(item);
michael@0 1177 if (menuWrapper)
michael@0 1178 menuWrapper.updateItem(item);
michael@0 1179 }
michael@0 1180 },
michael@0 1181
michael@0 1182 // Moves the XUL node for an item in every window we've ever populated to its
michael@0 1183 // new place in the hierarchy
michael@0 1184 moveItem: function moveItem(item, after) {
michael@0 1185 for (let [window, winWrapper] of this.windowMap) {
michael@0 1186 let menuWrapper = winWrapper.getMenuWrapperForItem(item);
michael@0 1187 if (menuWrapper)
michael@0 1188 menuWrapper.moveItem(item, after);
michael@0 1189 }
michael@0 1190 },
michael@0 1191
michael@0 1192 // Removes the XUL nodes for an item in every window we've ever populated.
michael@0 1193 removeItem: function removeItem(item) {
michael@0 1194 for (let [window, winWrapper] of this.windowMap) {
michael@0 1195 let menuWrapper = winWrapper.getMenuWrapperForItem(item);
michael@0 1196 if (menuWrapper)
michael@0 1197 menuWrapper.removeItem(item);
michael@0 1198 }
michael@0 1199 }
michael@0 1200 };
michael@0 1201
michael@0 1202 WindowTracker(MenuManager);

mercurial