browser/devtools/shared/widgets/ViewHelpers.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/shared/widgets/ViewHelpers.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1681 @@
     1.4 +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +"use strict";
    1.10 +
    1.11 +const Cc = Components.classes;
    1.12 +const Ci = Components.interfaces;
    1.13 +const Cu = Components.utils;
    1.14 +
    1.15 +const PANE_APPEARANCE_DELAY = 50;
    1.16 +const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
    1.17 +const WIDGET_FOCUSABLE_NODES = new Set(["vbox", "hbox"]);
    1.18 +
    1.19 +Cu.import("resource://gre/modules/Services.jsm");
    1.20 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.21 +Cu.import("resource://gre/modules/Timer.jsm");
    1.22 +Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
    1.23 +
    1.24 +this.EXPORTED_SYMBOLS = [
    1.25 +  "Heritage", "ViewHelpers", "WidgetMethods",
    1.26 +  "setNamedTimeout", "clearNamedTimeout",
    1.27 +  "setConditionalTimeout", "clearConditionalTimeout",
    1.28 +];
    1.29 +
    1.30 +/**
    1.31 + * Inheritance helpers from the addon SDK's core/heritage.
    1.32 + * Remove these when all devtools are loadered.
    1.33 + */
    1.34 +this.Heritage = {
    1.35 +  /**
    1.36 +   * @see extend in sdk/core/heritage.
    1.37 +   */
    1.38 +  extend: function(aPrototype, aProperties = {}) {
    1.39 +    return Object.create(aPrototype, this.getOwnPropertyDescriptors(aProperties));
    1.40 +  },
    1.41 +
    1.42 +  /**
    1.43 +   * @see getOwnPropertyDescriptors in sdk/core/heritage.
    1.44 +   */
    1.45 +  getOwnPropertyDescriptors: function(aObject) {
    1.46 +    return Object.getOwnPropertyNames(aObject).reduce((aDescriptor, aName) => {
    1.47 +      aDescriptor[aName] = Object.getOwnPropertyDescriptor(aObject, aName);
    1.48 +      return aDescriptor;
    1.49 +    }, {});
    1.50 +  }
    1.51 +};
    1.52 +
    1.53 +/**
    1.54 + * Helper for draining a rapid succession of events and invoking a callback
    1.55 + * once everything settles down.
    1.56 + *
    1.57 + * @param string aId
    1.58 + *        A string identifier for the named timeout.
    1.59 + * @param number aWait
    1.60 + *        The amount of milliseconds to wait after no more events are fired.
    1.61 + * @param function aCallback
    1.62 + *        Invoked when no more events are fired after the specified time.
    1.63 + */
    1.64 +this.setNamedTimeout = function setNamedTimeout(aId, aWait, aCallback) {
    1.65 +  clearNamedTimeout(aId);
    1.66 +
    1.67 +  namedTimeoutsStore.set(aId, setTimeout(() =>
    1.68 +    namedTimeoutsStore.delete(aId) && aCallback(), aWait));
    1.69 +};
    1.70 +
    1.71 +/**
    1.72 + * Clears a named timeout.
    1.73 + * @see setNamedTimeout
    1.74 + *
    1.75 + * @param string aId
    1.76 + *        A string identifier for the named timeout.
    1.77 + */
    1.78 +this.clearNamedTimeout = function clearNamedTimeout(aId) {
    1.79 +  if (!namedTimeoutsStore) {
    1.80 +    return;
    1.81 +  }
    1.82 +  clearTimeout(namedTimeoutsStore.get(aId));
    1.83 +  namedTimeoutsStore.delete(aId);
    1.84 +};
    1.85 +
    1.86 +/**
    1.87 + * Same as `setNamedTimeout`, but invokes the callback only if the provided
    1.88 + * predicate function returns true. Otherwise, the timeout is re-triggered.
    1.89 + *
    1.90 + * @param string aId
    1.91 + *        A string identifier for the conditional timeout.
    1.92 + * @param number aWait
    1.93 + *        The amount of milliseconds to wait after no more events are fired.
    1.94 + * @param function aPredicate
    1.95 + *        The predicate function used to determine whether the timeout restarts.
    1.96 + * @param function aCallback
    1.97 + *        Invoked when no more events are fired after the specified time, and
    1.98 + *        the provided predicate function returns true.
    1.99 + */
   1.100 +this.setConditionalTimeout = function setConditionalTimeout(aId, aWait, aPredicate, aCallback) {
   1.101 +  setNamedTimeout(aId, aWait, function maybeCallback() {
   1.102 +    if (aPredicate()) {
   1.103 +      aCallback();
   1.104 +      return;
   1.105 +    }
   1.106 +    setConditionalTimeout(aId, aWait, aPredicate, aCallback);
   1.107 +  });
   1.108 +};
   1.109 +
   1.110 +/**
   1.111 + * Clears a conditional timeout.
   1.112 + * @see setConditionalTimeout
   1.113 + *
   1.114 + * @param string aId
   1.115 + *        A string identifier for the conditional timeout.
   1.116 + */
   1.117 +this.clearConditionalTimeout = function clearConditionalTimeout(aId) {
   1.118 +  clearNamedTimeout(aId);
   1.119 +};
   1.120 +
   1.121 +XPCOMUtils.defineLazyGetter(this, "namedTimeoutsStore", () => new Map());
   1.122 +
   1.123 +/**
   1.124 + * Helpers for creating and messaging between UI components.
   1.125 + */
   1.126 +this.ViewHelpers = {
   1.127 +  /**
   1.128 +   * Convenience method, dispatching a custom event.
   1.129 +   *
   1.130 +   * @param nsIDOMNode aTarget
   1.131 +   *        A custom target element to dispatch the event from.
   1.132 +   * @param string aType
   1.133 +   *        The name of the event.
   1.134 +   * @param any aDetail
   1.135 +   *        The data passed when initializing the event.
   1.136 +   * @return boolean
   1.137 +   *         True if the event was cancelled or a registered handler
   1.138 +   *         called preventDefault.
   1.139 +   */
   1.140 +  dispatchEvent: function(aTarget, aType, aDetail) {
   1.141 +    if (!(aTarget instanceof Ci.nsIDOMNode)) {
   1.142 +      return true; // Event cancelled.
   1.143 +    }
   1.144 +    let document = aTarget.ownerDocument || aTarget;
   1.145 +    let dispatcher = aTarget.ownerDocument ? aTarget : document.documentElement;
   1.146 +
   1.147 +    let event = document.createEvent("CustomEvent");
   1.148 +    event.initCustomEvent(aType, true, true, aDetail);
   1.149 +    return dispatcher.dispatchEvent(event);
   1.150 +  },
   1.151 +
   1.152 +  /**
   1.153 +   * Helper delegating some of the DOM attribute methods of a node to a widget.
   1.154 +   *
   1.155 +   * @param object aWidget
   1.156 +   *        The widget to assign the methods to.
   1.157 +   * @param nsIDOMNode aNode
   1.158 +   *        A node to delegate the methods to.
   1.159 +   */
   1.160 +  delegateWidgetAttributeMethods: function(aWidget, aNode) {
   1.161 +    aWidget.getAttribute =
   1.162 +      aWidget.getAttribute || aNode.getAttribute.bind(aNode);
   1.163 +    aWidget.setAttribute =
   1.164 +      aWidget.setAttribute || aNode.setAttribute.bind(aNode);
   1.165 +    aWidget.removeAttribute =
   1.166 +      aWidget.removeAttribute || aNode.removeAttribute.bind(aNode);
   1.167 +  },
   1.168 +
   1.169 +  /**
   1.170 +   * Helper delegating some of the DOM event methods of a node to a widget.
   1.171 +   *
   1.172 +   * @param object aWidget
   1.173 +   *        The widget to assign the methods to.
   1.174 +   * @param nsIDOMNode aNode
   1.175 +   *        A node to delegate the methods to.
   1.176 +   */
   1.177 +  delegateWidgetEventMethods: function(aWidget, aNode) {
   1.178 +    aWidget.addEventListener =
   1.179 +      aWidget.addEventListener || aNode.addEventListener.bind(aNode);
   1.180 +    aWidget.removeEventListener =
   1.181 +      aWidget.removeEventListener || aNode.removeEventListener.bind(aNode);
   1.182 +  },
   1.183 +
   1.184 +  /**
   1.185 +   * Checks if the specified object looks like it's been decorated by an
   1.186 +   * event emitter.
   1.187 +   *
   1.188 +   * @return boolean
   1.189 +   *         True if it looks, walks and quacks like an event emitter.
   1.190 +   */
   1.191 +  isEventEmitter: function(aObject) {
   1.192 +    return aObject && aObject.on && aObject.off && aObject.once && aObject.emit;
   1.193 +  },
   1.194 +
   1.195 +  /**
   1.196 +   * Checks if the specified object is an instance of a DOM node.
   1.197 +   *
   1.198 +   * @return boolean
   1.199 +   *         True if it's a node, false otherwise.
   1.200 +   */
   1.201 +  isNode: function(aObject) {
   1.202 +    return aObject instanceof Ci.nsIDOMNode ||
   1.203 +           aObject instanceof Ci.nsIDOMElement ||
   1.204 +           aObject instanceof Ci.nsIDOMDocumentFragment;
   1.205 +  },
   1.206 +
   1.207 +  /**
   1.208 +   * Prevents event propagation when navigation keys are pressed.
   1.209 +   *
   1.210 +   * @param Event e
   1.211 +   *        The event to be prevented.
   1.212 +   */
   1.213 +  preventScrolling: function(e) {
   1.214 +    switch (e.keyCode) {
   1.215 +      case e.DOM_VK_UP:
   1.216 +      case e.DOM_VK_DOWN:
   1.217 +      case e.DOM_VK_LEFT:
   1.218 +      case e.DOM_VK_RIGHT:
   1.219 +      case e.DOM_VK_PAGE_UP:
   1.220 +      case e.DOM_VK_PAGE_DOWN:
   1.221 +      case e.DOM_VK_HOME:
   1.222 +      case e.DOM_VK_END:
   1.223 +        e.preventDefault();
   1.224 +        e.stopPropagation();
   1.225 +    }
   1.226 +  },
   1.227 +
   1.228 +  /**
   1.229 +   * Sets a side pane hidden or visible.
   1.230 +   *
   1.231 +   * @param object aFlags
   1.232 +   *        An object containing some of the following properties:
   1.233 +   *        - visible: true if the pane should be shown, false to hide
   1.234 +   *        - animated: true to display an animation on toggle
   1.235 +   *        - delayed: true to wait a few cycles before toggle
   1.236 +   *        - callback: a function to invoke when the toggle finishes
   1.237 +   * @param nsIDOMNode aPane
   1.238 +   *        The element representing the pane to toggle.
   1.239 +   */
   1.240 +  togglePane: function(aFlags, aPane) {
   1.241 +    // Make sure a pane is actually available first.
   1.242 +    if (!aPane) {
   1.243 +      return;
   1.244 +    }
   1.245 +
   1.246 +    // Hiding is always handled via margins, not the hidden attribute.
   1.247 +    aPane.removeAttribute("hidden");
   1.248 +
   1.249 +    // Add a class to the pane to handle min-widths, margins and animations.
   1.250 +    if (!aPane.classList.contains("generic-toggled-side-pane")) {
   1.251 +      aPane.classList.add("generic-toggled-side-pane");
   1.252 +    }
   1.253 +
   1.254 +    // Avoid useless toggles.
   1.255 +    if (aFlags.visible == !aPane.hasAttribute("pane-collapsed")) {
   1.256 +      if (aFlags.callback) aFlags.callback();
   1.257 +      return;
   1.258 +    }
   1.259 +
   1.260 +    // The "animated" attributes enables animated toggles (slide in-out).
   1.261 +    if (aFlags.animated) {
   1.262 +      aPane.setAttribute("animated", "");
   1.263 +    } else {
   1.264 +      aPane.removeAttribute("animated");
   1.265 +    }
   1.266 +
   1.267 +    // Computes and sets the pane margins in order to hide or show it.
   1.268 +    let doToggle = () => {
   1.269 +      if (aFlags.visible) {
   1.270 +        aPane.style.marginLeft = "0";
   1.271 +        aPane.style.marginRight = "0";
   1.272 +        aPane.removeAttribute("pane-collapsed");
   1.273 +      } else {
   1.274 +        let margin = ~~(aPane.getAttribute("width")) + 1;
   1.275 +        aPane.style.marginLeft = -margin + "px";
   1.276 +        aPane.style.marginRight = -margin + "px";
   1.277 +        aPane.setAttribute("pane-collapsed", "");
   1.278 +      }
   1.279 +
   1.280 +      // Invoke the callback when the transition ended.
   1.281 +      if (aFlags.animated) {
   1.282 +        aPane.addEventListener("transitionend", function onEvent() {
   1.283 +          aPane.removeEventListener("transitionend", onEvent, false);
   1.284 +          if (aFlags.callback) aFlags.callback();
   1.285 +        }, false);
   1.286 +      }
   1.287 +      // Invoke the callback immediately since there's no transition.
   1.288 +      else {
   1.289 +        if (aFlags.callback) aFlags.callback();
   1.290 +      }
   1.291 +    }
   1.292 +
   1.293 +    // Sometimes it's useful delaying the toggle a few ticks to ensure
   1.294 +    // a smoother slide in-out animation.
   1.295 +    if (aFlags.delayed) {
   1.296 +      aPane.ownerDocument.defaultView.setTimeout(doToggle, PANE_APPEARANCE_DELAY);
   1.297 +    } else {
   1.298 +      doToggle();
   1.299 +    }
   1.300 +  }
   1.301 +};
   1.302 +
   1.303 +/**
   1.304 + * Localization convenience methods.
   1.305 + *
   1.306 + * @param string aStringBundleName
   1.307 + *        The desired string bundle's name.
   1.308 + */
   1.309 +ViewHelpers.L10N = function(aStringBundleName) {
   1.310 +  XPCOMUtils.defineLazyGetter(this, "stringBundle", () =>
   1.311 +    Services.strings.createBundle(aStringBundleName));
   1.312 +
   1.313 +  XPCOMUtils.defineLazyGetter(this, "ellipsis", () =>
   1.314 +    Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data);
   1.315 +};
   1.316 +
   1.317 +ViewHelpers.L10N.prototype = {
   1.318 +  stringBundle: null,
   1.319 +
   1.320 +  /**
   1.321 +   * L10N shortcut function.
   1.322 +   *
   1.323 +   * @param string aName
   1.324 +   * @return string
   1.325 +   */
   1.326 +  getStr: function(aName) {
   1.327 +    return this.stringBundle.GetStringFromName(aName);
   1.328 +  },
   1.329 +
   1.330 +  /**
   1.331 +   * L10N shortcut function.
   1.332 +   *
   1.333 +   * @param string aName
   1.334 +   * @param array aArgs
   1.335 +   * @return string
   1.336 +   */
   1.337 +  getFormatStr: function(aName, ...aArgs) {
   1.338 +    return this.stringBundle.formatStringFromName(aName, aArgs, aArgs.length);
   1.339 +  },
   1.340 +
   1.341 +  /**
   1.342 +   * L10N shortcut function for numeric arguments that need to be formatted.
   1.343 +   * All numeric arguments will be fixed to 2 decimals and given a localized
   1.344 +   * decimal separator. Other arguments will be left alone.
   1.345 +   *
   1.346 +   * @param string aName
   1.347 +   * @param array aArgs
   1.348 +   * @return string
   1.349 +   */
   1.350 +  getFormatStrWithNumbers: function(aName, ...aArgs) {
   1.351 +    let newArgs = aArgs.map(x => typeof x == "number" ? this.numberWithDecimals(x, 2) : x);
   1.352 +    return this.stringBundle.formatStringFromName(aName, newArgs, newArgs.length);
   1.353 +  },
   1.354 +
   1.355 +  /**
   1.356 +   * Converts a number to a locale-aware string format and keeps a certain
   1.357 +   * number of decimals.
   1.358 +   *
   1.359 +   * @param number aNumber
   1.360 +   *        The number to convert.
   1.361 +   * @param number aDecimals [optional]
   1.362 +   *        Total decimals to keep.
   1.363 +   * @return string
   1.364 +   *         The localized number as a string.
   1.365 +   */
   1.366 +  numberWithDecimals: function(aNumber, aDecimals = 0) {
   1.367 +    // If this is an integer, don't do anything special.
   1.368 +    if (aNumber == (aNumber | 0)) {
   1.369 +      return aNumber;
   1.370 +    }
   1.371 +    // Remove {n} trailing decimals. Can't use toFixed(n) because
   1.372 +    // toLocaleString converts the number to a string. Also can't use
   1.373 +    // toLocaleString(, { maximumFractionDigits: n }) because it's not
   1.374 +    // implemented on OS X (bug 368838). Gross.
   1.375 +    let localized = aNumber.toLocaleString(); // localize
   1.376 +    let padded = localized + new Array(aDecimals).join("0"); // pad with zeros
   1.377 +    let match = padded.match("([^]*?\\d{" + aDecimals + "})\\d*$");
   1.378 +    return match.pop();
   1.379 +  }
   1.380 +};
   1.381 +
   1.382 +/**
   1.383 + * Shortcuts for lazily accessing and setting various preferences.
   1.384 + * Usage:
   1.385 + *   let prefs = new ViewHelpers.Prefs("root.path.to.branch", {
   1.386 + *     myIntPref: ["Int", "leaf.path.to.my-int-pref"],
   1.387 + *     myCharPref: ["Char", "leaf.path.to.my-char-pref"],
   1.388 + *     ...
   1.389 + *   });
   1.390 + *
   1.391 + *   prefs.myCharPref = "foo";
   1.392 + *   let aux = prefs.myCharPref;
   1.393 + *
   1.394 + * @param string aPrefsRoot
   1.395 + *        The root path to the required preferences branch.
   1.396 + * @param object aPrefsObject
   1.397 + *        An object containing { accessorName: [prefType, prefName] } keys.
   1.398 + */
   1.399 +ViewHelpers.Prefs = function(aPrefsRoot = "", aPrefsObject = {}) {
   1.400 +  this.root = aPrefsRoot;
   1.401 +
   1.402 +  for (let accessorName in aPrefsObject) {
   1.403 +    let [prefType, prefName] = aPrefsObject[accessorName];
   1.404 +    this.map(accessorName, prefType, prefName);
   1.405 +  }
   1.406 +};
   1.407 +
   1.408 +ViewHelpers.Prefs.prototype = {
   1.409 +  /**
   1.410 +   * Helper method for getting a pref value.
   1.411 +   *
   1.412 +   * @param string aType
   1.413 +   * @param string aPrefName
   1.414 +   * @return any
   1.415 +   */
   1.416 +  _get: function(aType, aPrefName) {
   1.417 +    if (this[aPrefName] === undefined) {
   1.418 +      this[aPrefName] = Services.prefs["get" + aType + "Pref"](aPrefName);
   1.419 +    }
   1.420 +    return this[aPrefName];
   1.421 +  },
   1.422 +
   1.423 +  /**
   1.424 +   * Helper method for setting a pref value.
   1.425 +   *
   1.426 +   * @param string aType
   1.427 +   * @param string aPrefName
   1.428 +   * @param any aValue
   1.429 +   */
   1.430 +  _set: function(aType, aPrefName, aValue) {
   1.431 +    Services.prefs["set" + aType + "Pref"](aPrefName, aValue);
   1.432 +    this[aPrefName] = aValue;
   1.433 +  },
   1.434 +
   1.435 +  /**
   1.436 +   * Maps a property name to a pref, defining lazy getters and setters.
   1.437 +   * Supported types are "Bool", "Char", "Int" and "Json" (which is basically
   1.438 +   * just sugar for "Char" using the standard JSON serializer).
   1.439 +   *
   1.440 +   * @param string aAccessorName
   1.441 +   * @param string aType
   1.442 +   * @param string aPrefName
   1.443 +   * @param array aSerializer
   1.444 +   */
   1.445 +  map: function(aAccessorName, aType, aPrefName, aSerializer = { in: e => e, out: e => e }) {
   1.446 +    if (aType == "Json") {
   1.447 +      this.map(aAccessorName, "Char", aPrefName, { in: JSON.parse, out: JSON.stringify });
   1.448 +      return;
   1.449 +    }
   1.450 +
   1.451 +    Object.defineProperty(this, aAccessorName, {
   1.452 +      get: () => aSerializer.in(this._get(aType, [this.root, aPrefName].join("."))),
   1.453 +      set: (e) => this._set(aType, [this.root, aPrefName].join("."), aSerializer.out(e))
   1.454 +    });
   1.455 +  }
   1.456 +};
   1.457 +
   1.458 +/**
   1.459 + * A generic Item is used to describe children present in a Widget.
   1.460 + *
   1.461 + * This is basically a very thin wrapper around an nsIDOMNode, with a few
   1.462 + * characteristics, like a `value` and an `attachment`.
   1.463 + *
   1.464 + * The characteristics are optional, and their meaning is entirely up to you.
   1.465 + * - The `value` should be a string, passed as an argument.
   1.466 + * - The `attachment` is any kind of primitive or object, passed as an argument.
   1.467 + *
   1.468 + * Iterable via "for (let childItem of parentItem) { }".
   1.469 + *
   1.470 + * @param object aOwnerView
   1.471 + *        The owner view creating this item.
   1.472 + * @param nsIDOMNode aElement
   1.473 + *        A prebuilt node to be wrapped.
   1.474 + * @param string aValue
   1.475 + *        A string identifying the node.
   1.476 + * @param any aAttachment
   1.477 + *        Some attached primitive/object.
   1.478 + */
   1.479 +function Item(aOwnerView, aElement, aValue, aAttachment) {
   1.480 +  this.ownerView = aOwnerView;
   1.481 +  this.attachment = aAttachment;
   1.482 +  this._value = aValue + "";
   1.483 +  this._prebuiltNode = aElement;
   1.484 +};
   1.485 +
   1.486 +Item.prototype = {
   1.487 +  get value() { return this._value; },
   1.488 +  get target() { return this._target; },
   1.489 +
   1.490 +  /**
   1.491 +   * Immediately appends a child item to this item.
   1.492 +   *
   1.493 +   * @param nsIDOMNode aElement
   1.494 +   *        An nsIDOMNode representing the child element to append.
   1.495 +   * @param object aOptions [optional]
   1.496 +   *        Additional options or flags supported by this operation:
   1.497 +   *          - attachment: some attached primitive/object for the item
   1.498 +   *          - attributes: a batch of attributes set to the displayed element
   1.499 +   *          - finalize: function invoked when the child item is removed
   1.500 +   * @return Item
   1.501 +   *         The item associated with the displayed element.
   1.502 +   */
   1.503 +  append: function(aElement, aOptions = {}) {
   1.504 +    let item = new Item(this, aElement, "", aOptions.attachment);
   1.505 +
   1.506 +    // Entangle the item with the newly inserted child node.
   1.507 +    // Make sure this is done with the value returned by appendChild(),
   1.508 +    // to avoid storing a potential DocumentFragment.
   1.509 +    this._entangleItem(item, this._target.appendChild(aElement));
   1.510 +
   1.511 +    // Handle any additional options after entangling the item.
   1.512 +    if (aOptions.attributes) {
   1.513 +      aOptions.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
   1.514 +    }
   1.515 +    if (aOptions.finalize) {
   1.516 +      item.finalize = aOptions.finalize;
   1.517 +    }
   1.518 +
   1.519 +    // Return the item associated with the displayed element.
   1.520 +    return item;
   1.521 +  },
   1.522 +
   1.523 +  /**
   1.524 +   * Immediately removes the specified child item from this item.
   1.525 +   *
   1.526 +   * @param Item aItem
   1.527 +   *        The item associated with the element to remove.
   1.528 +   */
   1.529 +  remove: function(aItem) {
   1.530 +    if (!aItem) {
   1.531 +      return;
   1.532 +    }
   1.533 +    this._target.removeChild(aItem._target);
   1.534 +    this._untangleItem(aItem);
   1.535 +  },
   1.536 +
   1.537 +  /**
   1.538 +   * Entangles an item (model) with a displayed node element (view).
   1.539 +   *
   1.540 +   * @param Item aItem
   1.541 +   *        The item describing a target element.
   1.542 +   * @param nsIDOMNode aElement
   1.543 +   *        The element displaying the item.
   1.544 +   */
   1.545 +  _entangleItem: function(aItem, aElement) {
   1.546 +    this._itemsByElement.set(aElement, aItem);
   1.547 +    aItem._target = aElement;
   1.548 +  },
   1.549 +
   1.550 +  /**
   1.551 +   * Untangles an item (model) from a displayed node element (view).
   1.552 +   *
   1.553 +   * @param Item aItem
   1.554 +   *        The item describing a target element.
   1.555 +   */
   1.556 +  _untangleItem: function(aItem) {
   1.557 +    if (aItem.finalize) {
   1.558 +      aItem.finalize(aItem);
   1.559 +    }
   1.560 +    for (let childItem of aItem) {
   1.561 +      aItem.remove(childItem);
   1.562 +    }
   1.563 +
   1.564 +    this._unlinkItem(aItem);
   1.565 +    aItem._target = null;
   1.566 +  },
   1.567 +
   1.568 +  /**
   1.569 +   * Deletes an item from the its parent's storage maps.
   1.570 +   *
   1.571 +   * @param Item aItem
   1.572 +   *        The item describing a target element.
   1.573 +   */
   1.574 +  _unlinkItem: function(aItem) {
   1.575 +    this._itemsByElement.delete(aItem._target);
   1.576 +  },
   1.577 +
   1.578 +  /**
   1.579 +   * Returns a string representing the object.
   1.580 +   * @return string
   1.581 +   */
   1.582 +  toString: function() {
   1.583 +    return this._value + " :: " + this._target + " :: " + this.attachment;
   1.584 +  },
   1.585 +
   1.586 +  _value: "",
   1.587 +  _target: null,
   1.588 +  _prebuiltNode: null,
   1.589 +  finalize: null,
   1.590 +  attachment: null
   1.591 +};
   1.592 +
   1.593 +// Creating maps thousands of times for widgets with a large number of children
   1.594 +// fills up a lot of memory. Make sure these are instantiated only if needed.
   1.595 +DevToolsUtils.defineLazyPrototypeGetter(Item.prototype, "_itemsByElement", Map);
   1.596 +
   1.597 +/**
   1.598 + * Some generic Widget methods handling Item instances.
   1.599 + * Iterable via "for (let childItem of wrappedView) { }".
   1.600 + *
   1.601 + * Usage:
   1.602 + *   function MyView() {
   1.603 + *     this.widget = new MyWidget(document.querySelector(".my-node"));
   1.604 + *   }
   1.605 + *
   1.606 + *   MyView.prototype = Heritage.extend(WidgetMethods, {
   1.607 + *     myMethod: function() {},
   1.608 + *     ...
   1.609 + *   });
   1.610 + *
   1.611 + * See https://gist.github.com/victorporof/5749386 for more details.
   1.612 + * The devtools/shared/widgets/SimpleListWidget.jsm is an implementation example.
   1.613 + *
   1.614 + * Language:
   1.615 + *   - An "item" is an instance of an Item.
   1.616 + *   - An "element" or "node" is a nsIDOMNode.
   1.617 + *
   1.618 + * The supplied widget can be any object implementing the following methods:
   1.619 + *   - function:nsIDOMNode insertItemAt(aIndex:number, aNode:nsIDOMNode, aValue:string)
   1.620 + *   - function:nsIDOMNode getItemAtIndex(aIndex:number)
   1.621 + *   - function removeChild(aChild:nsIDOMNode)
   1.622 + *   - function removeAllItems()
   1.623 + *   - get:nsIDOMNode selectedItem()
   1.624 + *   - set selectedItem(aChild:nsIDOMNode)
   1.625 + *   - function getAttribute(aName:string)
   1.626 + *   - function setAttribute(aName:string, aValue:string)
   1.627 + *   - function removeAttribute(aName:string)
   1.628 + *   - function addEventListener(aName:string, aCallback:function, aBubbleFlag:boolean)
   1.629 + *   - function removeEventListener(aName:string, aCallback:function, aBubbleFlag:boolean)
   1.630 + *
   1.631 + * Optional methods that can be implemented by the widget:
   1.632 + *   - function ensureElementIsVisible(aChild:nsIDOMNode)
   1.633 + *
   1.634 + * Optional attributes that may be handled (when calling get/set/removeAttribute):
   1.635 + *   - "emptyText": label temporarily added when there are no items present
   1.636 + *   - "headerText": label permanently added as a header
   1.637 + *
   1.638 + * For automagical keyboard and mouse accessibility, the widget should be an
   1.639 + * event emitter with the following events:
   1.640 + *   - "keyPress" -> (aName:string, aEvent:KeyboardEvent)
   1.641 + *   - "mousePress" -> (aName:string, aEvent:MouseEvent)
   1.642 + */
   1.643 +this.WidgetMethods = {
   1.644 +  /**
   1.645 +   * Sets the element node or widget associated with this container.
   1.646 +   * @param nsIDOMNode | object aWidget
   1.647 +   */
   1.648 +  set widget(aWidget) {
   1.649 +    this._widget = aWidget;
   1.650 +
   1.651 +
   1.652 +    // Can't use a WeakMap for _itemsByValue because keys are strings, and
   1.653 +    // can't use one for _itemsByElement either, since it needs to be iterable.
   1.654 +    XPCOMUtils.defineLazyGetter(this, "_itemsByValue", () => new Map());
   1.655 +    XPCOMUtils.defineLazyGetter(this, "_itemsByElement", () => new Map());
   1.656 +    XPCOMUtils.defineLazyGetter(this, "_stagedItems", () => []);
   1.657 +
   1.658 +    // Handle internal events emitted by the widget if necessary.
   1.659 +    if (ViewHelpers.isEventEmitter(aWidget)) {
   1.660 +      aWidget.on("keyPress", this._onWidgetKeyPress.bind(this));
   1.661 +      aWidget.on("mousePress", this._onWidgetMousePress.bind(this));
   1.662 +    }
   1.663 +  },
   1.664 +
   1.665 +  /**
   1.666 +   * Gets the element node or widget associated with this container.
   1.667 +   * @return nsIDOMNode | object
   1.668 +   */
   1.669 +  get widget() this._widget,
   1.670 +
   1.671 +  /**
   1.672 +   * Prepares an item to be added to this container. This allows, for example,
   1.673 +   * for a large number of items to be batched up before being sorted & added.
   1.674 +   *
   1.675 +   * If the "staged" flag is *not* set to true, the item will be immediately
   1.676 +   * inserted at the correct position in this container, so that all the items
   1.677 +   * still remain sorted. This can (possibly) be much slower than batching up
   1.678 +   * multiple items.
   1.679 +   *
   1.680 +   * By default, this container assumes that all the items should be displayed
   1.681 +   * sorted by their value. This can be overridden with the "index" flag,
   1.682 +   * specifying on which position should an item be appended. The "staged" and
   1.683 +   * "index" flags are mutually exclusive, meaning that all staged items
   1.684 +   * will always be appended.
   1.685 +   *
   1.686 +   * @param nsIDOMNode aElement
   1.687 +   *        A prebuilt node to be wrapped.
   1.688 +   * @param string aValue
   1.689 +   *        A string identifying the node.
   1.690 +   * @param object aOptions [optional]
   1.691 +   *        Additional options or flags supported by this operation:
   1.692 +   *          - attachment: some attached primitive/object for the item
   1.693 +   *          - staged: true to stage the item to be appended later
   1.694 +   *          - index: specifies on which position should the item be appended
   1.695 +   *          - attributes: a batch of attributes set to the displayed element
   1.696 +   *          - finalize: function invoked when the item is removed
   1.697 +   * @return Item
   1.698 +   *         The item associated with the displayed element if an unstaged push,
   1.699 +   *         undefined if the item was staged for a later commit.
   1.700 +   */
   1.701 +  push: function([aElement, aValue], aOptions = {}) {
   1.702 +    let item = new Item(this, aElement, aValue, aOptions.attachment);
   1.703 +
   1.704 +    // Batch the item to be added later.
   1.705 +    if (aOptions.staged) {
   1.706 +      // An ulterior commit operation will ignore any specified index, so
   1.707 +      // no reason to keep it around.
   1.708 +      aOptions.index = undefined;
   1.709 +      return void this._stagedItems.push({ item: item, options: aOptions });
   1.710 +    }
   1.711 +    // Find the target position in this container and insert the item there.
   1.712 +    if (!("index" in aOptions)) {
   1.713 +      return this._insertItemAt(this._findExpectedIndexFor(item), item, aOptions);
   1.714 +    }
   1.715 +    // Insert the item at the specified index. If negative or out of bounds,
   1.716 +    // the item will be simply appended.
   1.717 +    return this._insertItemAt(aOptions.index, item, aOptions);
   1.718 +  },
   1.719 +
   1.720 +  /**
   1.721 +   * Flushes all the prepared items into this container.
   1.722 +   * Any specified index on the items will be ignored. Everything is appended.
   1.723 +   *
   1.724 +   * @param object aOptions [optional]
   1.725 +   *        Additional options or flags supported by this operation:
   1.726 +   *          - sorted: true to sort all the items before adding them
   1.727 +   */
   1.728 +  commit: function(aOptions = {}) {
   1.729 +    let stagedItems = this._stagedItems;
   1.730 +
   1.731 +    // Sort the items before adding them to this container, if preferred.
   1.732 +    if (aOptions.sorted) {
   1.733 +      stagedItems.sort((a, b) => this._currentSortPredicate(a.item, b.item));
   1.734 +    }
   1.735 +    // Append the prepared items to this container.
   1.736 +    for (let { item, options } of stagedItems) {
   1.737 +      this._insertItemAt(-1, item, options);
   1.738 +    }
   1.739 +    // Recreate the temporary items list for ulterior pushes.
   1.740 +    this._stagedItems.length = 0;
   1.741 +  },
   1.742 +
   1.743 +  /**
   1.744 +   * Immediately removes the specified item from this container.
   1.745 +   *
   1.746 +   * @param Item aItem
   1.747 +   *        The item associated with the element to remove.
   1.748 +   */
   1.749 +  remove: function(aItem) {
   1.750 +    if (!aItem) {
   1.751 +      return;
   1.752 +    }
   1.753 +    this._widget.removeChild(aItem._target);
   1.754 +    this._untangleItem(aItem);
   1.755 +
   1.756 +    if (!this._itemsByElement.size) {
   1.757 +      this._preferredValue = this.selectedValue;
   1.758 +      this._widget.selectedItem = null;
   1.759 +      this._widget.setAttribute("emptyText", this._emptyText);
   1.760 +    }
   1.761 +  },
   1.762 +
   1.763 +  /**
   1.764 +   * Removes the item at the specified index from this container.
   1.765 +   *
   1.766 +   * @param number aIndex
   1.767 +   *        The index of the item to remove.
   1.768 +   */
   1.769 +  removeAt: function(aIndex) {
   1.770 +    this.remove(this.getItemAtIndex(aIndex));
   1.771 +  },
   1.772 +
   1.773 +  /**
   1.774 +   * Removes all items from this container.
   1.775 +   */
   1.776 +  empty: function() {
   1.777 +    this._preferredValue = this.selectedValue;
   1.778 +    this._widget.selectedItem = null;
   1.779 +    this._widget.removeAllItems();
   1.780 +    this._widget.setAttribute("emptyText", this._emptyText);
   1.781 +
   1.782 +    for (let [, item] of this._itemsByElement) {
   1.783 +      this._untangleItem(item);
   1.784 +    }
   1.785 +
   1.786 +    this._itemsByValue.clear();
   1.787 +    this._itemsByElement.clear();
   1.788 +    this._stagedItems.length = 0;
   1.789 +  },
   1.790 +
   1.791 +  /**
   1.792 +   * Ensures the specified item is visible in this container.
   1.793 +   *
   1.794 +   * @param Item aItem
   1.795 +   *        The item to bring into view.
   1.796 +   */
   1.797 +  ensureItemIsVisible: function(aItem) {
   1.798 +    this._widget.ensureElementIsVisible(aItem._target);
   1.799 +  },
   1.800 +
   1.801 +  /**
   1.802 +   * Ensures the item at the specified index is visible in this container.
   1.803 +   *
   1.804 +   * @param number aIndex
   1.805 +   *        The index of the item to bring into view.
   1.806 +   */
   1.807 +  ensureIndexIsVisible: function(aIndex) {
   1.808 +    this.ensureItemIsVisible(this.getItemAtIndex(aIndex));
   1.809 +  },
   1.810 +
   1.811 +  /**
   1.812 +   * Sugar for ensuring the selected item is visible in this container.
   1.813 +   */
   1.814 +  ensureSelectedItemIsVisible: function() {
   1.815 +    this.ensureItemIsVisible(this.selectedItem);
   1.816 +  },
   1.817 +
   1.818 +  /**
   1.819 +   * If supported by the widget, the label string temporarily added to this
   1.820 +   * container when there are no child items present.
   1.821 +   */
   1.822 +  set emptyText(aValue) {
   1.823 +    this._emptyText = aValue;
   1.824 +
   1.825 +    // Apply the emptyText attribute right now if there are no child items.
   1.826 +    if (!this._itemsByElement.size) {
   1.827 +      this._widget.setAttribute("emptyText", aValue);
   1.828 +    }
   1.829 +  },
   1.830 +
   1.831 +  /**
   1.832 +   * If supported by the widget, the label string permanently added to this
   1.833 +   * container as a header.
   1.834 +   * @param string aValue
   1.835 +   */
   1.836 +  set headerText(aValue) {
   1.837 +    this._headerText = aValue;
   1.838 +    this._widget.setAttribute("headerText", aValue);
   1.839 +  },
   1.840 +
   1.841 +  /**
   1.842 +   * Toggles all the items in this container hidden or visible.
   1.843 +   *
   1.844 +   * This does not change the default filtering predicate, so newly inserted
   1.845 +   * items will always be visible. Use WidgetMethods.filterContents if you care.
   1.846 +   *
   1.847 +   * @param boolean aVisibleFlag
   1.848 +   *        Specifies the intended visibility.
   1.849 +   */
   1.850 +  toggleContents: function(aVisibleFlag) {
   1.851 +    for (let [element, item] of this._itemsByElement) {
   1.852 +      element.hidden = !aVisibleFlag;
   1.853 +    }
   1.854 +  },
   1.855 +
   1.856 +  /**
   1.857 +   * Toggles all items in this container hidden or visible based on a predicate.
   1.858 +   *
   1.859 +   * @param function aPredicate [optional]
   1.860 +   *        Items are toggled according to the return value of this function,
   1.861 +   *        which will become the new default filtering predicate in this container.
   1.862 +   *        If unspecified, all items will be toggled visible.
   1.863 +   */
   1.864 +  filterContents: function(aPredicate = this._currentFilterPredicate) {
   1.865 +    this._currentFilterPredicate = aPredicate;
   1.866 +
   1.867 +    for (let [element, item] of this._itemsByElement) {
   1.868 +      element.hidden = !aPredicate(item);
   1.869 +    }
   1.870 +  },
   1.871 +
   1.872 +  /**
   1.873 +   * Sorts all the items in this container based on a predicate.
   1.874 +   *
   1.875 +   * @param function aPredicate [optional]
   1.876 +   *        Items are sorted according to the return value of the function,
   1.877 +   *        which will become the new default sorting predicate in this container.
   1.878 +   *        If unspecified, all items will be sorted by their value.
   1.879 +   */
   1.880 +  sortContents: function(aPredicate = this._currentSortPredicate) {
   1.881 +    let sortedItems = this.items.sort(this._currentSortPredicate = aPredicate);
   1.882 +
   1.883 +    for (let i = 0, len = sortedItems.length; i < len; i++) {
   1.884 +      this.swapItems(this.getItemAtIndex(i), sortedItems[i]);
   1.885 +    }
   1.886 +  },
   1.887 +
   1.888 +  /**
   1.889 +   * Visually swaps two items in this container.
   1.890 +   *
   1.891 +   * @param Item aFirst
   1.892 +   *        The first item to be swapped.
   1.893 +   * @param Item aSecond
   1.894 +   *        The second item to be swapped.
   1.895 +   */
   1.896 +  swapItems: function(aFirst, aSecond) {
   1.897 +    if (aFirst == aSecond) { // We're just dandy, thank you.
   1.898 +      return;
   1.899 +    }
   1.900 +    let { _prebuiltNode: firstPrebuiltTarget, _target: firstTarget } = aFirst;
   1.901 +    let { _prebuiltNode: secondPrebuiltTarget, _target: secondTarget } = aSecond;
   1.902 +
   1.903 +    // If the two items were constructed with prebuilt nodes as DocumentFragments,
   1.904 +    // then those DocumentFragments are now empty and need to be reassembled.
   1.905 +    if (firstPrebuiltTarget instanceof Ci.nsIDOMDocumentFragment) {
   1.906 +      for (let node of firstTarget.childNodes) {
   1.907 +        firstPrebuiltTarget.appendChild(node.cloneNode(true));
   1.908 +      }
   1.909 +    }
   1.910 +    if (secondPrebuiltTarget instanceof Ci.nsIDOMDocumentFragment) {
   1.911 +      for (let node of secondTarget.childNodes) {
   1.912 +        secondPrebuiltTarget.appendChild(node.cloneNode(true));
   1.913 +      }
   1.914 +    }
   1.915 +
   1.916 +    // 1. Get the indices of the two items to swap.
   1.917 +    let i = this._indexOfElement(firstTarget);
   1.918 +    let j = this._indexOfElement(secondTarget);
   1.919 +
   1.920 +    // 2. Remeber the selection index, to reselect an item, if necessary.
   1.921 +    let selectedTarget = this._widget.selectedItem;
   1.922 +    let selectedIndex = -1;
   1.923 +    if (selectedTarget == firstTarget) {
   1.924 +      selectedIndex = i;
   1.925 +    } else if (selectedTarget == secondTarget) {
   1.926 +      selectedIndex = j;
   1.927 +    }
   1.928 +
   1.929 +    // 3. Silently nuke both items, nobody needs to know about this.
   1.930 +    this._widget.removeChild(firstTarget);
   1.931 +    this._widget.removeChild(secondTarget);
   1.932 +    this._unlinkItem(aFirst);
   1.933 +    this._unlinkItem(aSecond);
   1.934 +
   1.935 +    // 4. Add the items again, but reversing their indices.
   1.936 +    this._insertItemAt.apply(this, i < j ? [i, aSecond] : [j, aFirst]);
   1.937 +    this._insertItemAt.apply(this, i < j ? [j, aFirst] : [i, aSecond]);
   1.938 +
   1.939 +    // 5. Restore the previous selection, if necessary.
   1.940 +    if (selectedIndex == i) {
   1.941 +      this._widget.selectedItem = aFirst._target;
   1.942 +    } else if (selectedIndex == j) {
   1.943 +      this._widget.selectedItem = aSecond._target;
   1.944 +    }
   1.945 +
   1.946 +    // 6. Let the outside world know that these two items were swapped.
   1.947 +    ViewHelpers.dispatchEvent(aFirst.target, "swap", [aSecond, aFirst]);
   1.948 +  },
   1.949 +
   1.950 +  /**
   1.951 +   * Visually swaps two items in this container at specific indices.
   1.952 +   *
   1.953 +   * @param number aFirst
   1.954 +   *        The index of the first item to be swapped.
   1.955 +   * @param number aSecond
   1.956 +   *        The index of the second item to be swapped.
   1.957 +   */
   1.958 +  swapItemsAtIndices: function(aFirst, aSecond) {
   1.959 +    this.swapItems(this.getItemAtIndex(aFirst), this.getItemAtIndex(aSecond));
   1.960 +  },
   1.961 +
   1.962 +  /**
   1.963 +   * Checks whether an item with the specified value is among the elements
   1.964 +   * shown in this container.
   1.965 +   *
   1.966 +   * @param string aValue
   1.967 +   *        The item's value.
   1.968 +   * @return boolean
   1.969 +   *         True if the value is known, false otherwise.
   1.970 +   */
   1.971 +  containsValue: function(aValue) {
   1.972 +    return this._itemsByValue.has(aValue) ||
   1.973 +           this._stagedItems.some(({ item }) => item._value == aValue);
   1.974 +  },
   1.975 +
   1.976 +  /**
   1.977 +   * Gets the "preferred value". This is the latest selected item's value,
   1.978 +   * remembered just before emptying this container.
   1.979 +   * @return string
   1.980 +   */
   1.981 +  get preferredValue() {
   1.982 +    return this._preferredValue;
   1.983 +  },
   1.984 +
   1.985 +  /**
   1.986 +   * Retrieves the item associated with the selected element.
   1.987 +   * @return Item | null
   1.988 +   */
   1.989 +  get selectedItem() {
   1.990 +    let selectedElement = this._widget.selectedItem;
   1.991 +    if (selectedElement) {
   1.992 +      return this._itemsByElement.get(selectedElement);
   1.993 +    }
   1.994 +    return null;
   1.995 +  },
   1.996 +
   1.997 +  /**
   1.998 +   * Retrieves the selected element's index in this container.
   1.999 +   * @return number
  1.1000 +   */
  1.1001 +  get selectedIndex() {
  1.1002 +    let selectedElement = this._widget.selectedItem;
  1.1003 +    if (selectedElement) {
  1.1004 +      return this._indexOfElement(selectedElement);
  1.1005 +    }
  1.1006 +    return -1;
  1.1007 +  },
  1.1008 +
  1.1009 +  /**
  1.1010 +   * Retrieves the value of the selected element.
  1.1011 +   * @return string
  1.1012 +   */
  1.1013 +  get selectedValue() {
  1.1014 +    let selectedElement = this._widget.selectedItem;
  1.1015 +    if (selectedElement) {
  1.1016 +      return this._itemsByElement.get(selectedElement)._value;
  1.1017 +    }
  1.1018 +    return "";
  1.1019 +  },
  1.1020 +
  1.1021 +  /**
  1.1022 +   * Retrieves the attachment of the selected element.
  1.1023 +   * @return object | null
  1.1024 +   */
  1.1025 +  get selectedAttachment() {
  1.1026 +    let selectedElement = this._widget.selectedItem;
  1.1027 +    if (selectedElement) {
  1.1028 +      return this._itemsByElement.get(selectedElement).attachment;
  1.1029 +    }
  1.1030 +    return null;
  1.1031 +  },
  1.1032 +
  1.1033 +  /**
  1.1034 +   * Selects the element with the entangled item in this container.
  1.1035 +   * @param Item | function aItem
  1.1036 +   */
  1.1037 +  set selectedItem(aItem) {
  1.1038 +    // A predicate is allowed to select a specific item.
  1.1039 +    // If no item is matched, then the current selection is removed.
  1.1040 +    if (typeof aItem == "function") {
  1.1041 +      aItem = this.getItemForPredicate(aItem);
  1.1042 +    }
  1.1043 +
  1.1044 +    // A falsy item is allowed to invalidate the current selection.
  1.1045 +    let targetElement = aItem ? aItem._target : null;
  1.1046 +    let prevElement = this._widget.selectedItem;
  1.1047 +
  1.1048 +    // Make sure the selected item's target element is focused and visible.
  1.1049 +    if (this.autoFocusOnSelection && targetElement) {
  1.1050 +      targetElement.focus();
  1.1051 +    }
  1.1052 +    if (this.maintainSelectionVisible && targetElement) {
  1.1053 +      if ("ensureElementIsVisible" in this._widget) {
  1.1054 +        this._widget.ensureElementIsVisible(targetElement);
  1.1055 +      }
  1.1056 +    }
  1.1057 +
  1.1058 +    // Prevent selecting the same item again and avoid dispatching
  1.1059 +    // a redundant selection event, so return early.
  1.1060 +    if (targetElement != prevElement) {
  1.1061 +      this._widget.selectedItem = targetElement;
  1.1062 +      let dispTarget = targetElement || prevElement;
  1.1063 +      let dispName = this.suppressSelectionEvents ? "suppressed-select" : "select";
  1.1064 +      ViewHelpers.dispatchEvent(dispTarget, dispName, aItem);
  1.1065 +    }
  1.1066 +  },
  1.1067 +
  1.1068 +  /**
  1.1069 +   * Selects the element at the specified index in this container.
  1.1070 +   * @param number aIndex
  1.1071 +   */
  1.1072 +  set selectedIndex(aIndex) {
  1.1073 +    let targetElement = this._widget.getItemAtIndex(aIndex);
  1.1074 +    if (targetElement) {
  1.1075 +      this.selectedItem = this._itemsByElement.get(targetElement);
  1.1076 +      return;
  1.1077 +    }
  1.1078 +    this.selectedItem = null;
  1.1079 +  },
  1.1080 +
  1.1081 +  /**
  1.1082 +   * Selects the element with the specified value in this container.
  1.1083 +   * @param string aValue
  1.1084 +   */
  1.1085 +  set selectedValue(aValue) {
  1.1086 +    this.selectedItem = this._itemsByValue.get(aValue);
  1.1087 +  },
  1.1088 +
  1.1089 +  /**
  1.1090 +   * Specifies if this container should try to keep the selected item visible.
  1.1091 +   * (For example, when new items are added the selection is brought into view).
  1.1092 +   */
  1.1093 +  maintainSelectionVisible: true,
  1.1094 +
  1.1095 +  /**
  1.1096 +   * Specifies if "select" events dispatched from the elements in this container
  1.1097 +   * when their respective items are selected should be suppressed or not.
  1.1098 +   *
  1.1099 +   * If this flag is set to true, then consumers of this container won't
  1.1100 +   * be normally notified when items are selected.
  1.1101 +   */
  1.1102 +  suppressSelectionEvents: false,
  1.1103 +
  1.1104 +  /**
  1.1105 +   * Focus this container the first time an element is inserted?
  1.1106 +   *
  1.1107 +   * If this flag is set to true, then when the first item is inserted in
  1.1108 +   * this container (and thus it's the only item available), its corresponding
  1.1109 +   * target element is focused as well.
  1.1110 +   */
  1.1111 +  autoFocusOnFirstItem: true,
  1.1112 +
  1.1113 +  /**
  1.1114 +   * Focus on selection?
  1.1115 +   *
  1.1116 +   * If this flag is set to true, then whenever an item is selected in
  1.1117 +   * this container (e.g. via the selectedIndex or selectedItem setters),
  1.1118 +   * its corresponding target element is focused as well.
  1.1119 +   *
  1.1120 +   * You can disable this flag, for example, to maintain a certain node
  1.1121 +   * focused but visually indicate a different selection in this container.
  1.1122 +   */
  1.1123 +  autoFocusOnSelection: true,
  1.1124 +
  1.1125 +  /**
  1.1126 +   * Focus on input (e.g. mouse click)?
  1.1127 +   *
  1.1128 +   * If this flag is set to true, then whenever an item receives user input in
  1.1129 +   * this container, its corresponding target element is focused as well.
  1.1130 +   */
  1.1131 +  autoFocusOnInput: true,
  1.1132 +
  1.1133 +  /**
  1.1134 +   * When focusing on input, allow right clicks?
  1.1135 +   * @see WidgetMethods.autoFocusOnInput
  1.1136 +   */
  1.1137 +  allowFocusOnRightClick: false,
  1.1138 +
  1.1139 +  /**
  1.1140 +   * The number of elements in this container to jump when Page Up or Page Down
  1.1141 +   * keys are pressed. If falsy, then the page size will be based on the
  1.1142 +   * number of visible items in the container.
  1.1143 +   */
  1.1144 +  pageSize: 0,
  1.1145 +
  1.1146 +  /**
  1.1147 +   * Focuses the first visible item in this container.
  1.1148 +   */
  1.1149 +  focusFirstVisibleItem: function() {
  1.1150 +    this.focusItemAtDelta(-this.itemCount);
  1.1151 +  },
  1.1152 +
  1.1153 +  /**
  1.1154 +   * Focuses the last visible item in this container.
  1.1155 +   */
  1.1156 +  focusLastVisibleItem: function() {
  1.1157 +    this.focusItemAtDelta(+this.itemCount);
  1.1158 +  },
  1.1159 +
  1.1160 +  /**
  1.1161 +   * Focuses the next item in this container.
  1.1162 +   */
  1.1163 +  focusNextItem: function() {
  1.1164 +    this.focusItemAtDelta(+1);
  1.1165 +  },
  1.1166 +
  1.1167 +  /**
  1.1168 +   * Focuses the previous item in this container.
  1.1169 +   */
  1.1170 +  focusPrevItem: function() {
  1.1171 +    this.focusItemAtDelta(-1);
  1.1172 +  },
  1.1173 +
  1.1174 +  /**
  1.1175 +   * Focuses another item in this container based on the index distance
  1.1176 +   * from the currently focused item.
  1.1177 +   *
  1.1178 +   * @param number aDelta
  1.1179 +   *        A scalar specifying by how many items should the selection change.
  1.1180 +   */
  1.1181 +  focusItemAtDelta: function(aDelta) {
  1.1182 +    // Make sure the currently selected item is also focused, so that the
  1.1183 +    // command dispatcher mechanism has a relative node to work with.
  1.1184 +    // If there's no selection, just select an item at a corresponding index
  1.1185 +    // (e.g. the first item in this container if aDelta <= 1).
  1.1186 +    let selectedElement = this._widget.selectedItem;
  1.1187 +    if (selectedElement) {
  1.1188 +      selectedElement.focus();
  1.1189 +    } else {
  1.1190 +      this.selectedIndex = Math.max(0, aDelta - 1);
  1.1191 +      return;
  1.1192 +    }
  1.1193 +
  1.1194 +    let direction = aDelta > 0 ? "advanceFocus" : "rewindFocus";
  1.1195 +    let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta));
  1.1196 +    while (distance--) {
  1.1197 +      if (!this._focusChange(direction)) {
  1.1198 +        break; // Out of bounds.
  1.1199 +      }
  1.1200 +    }
  1.1201 +
  1.1202 +    // Synchronize the selected item as being the currently focused element.
  1.1203 +    this.selectedItem = this.getItemForElement(this._focusedElement);
  1.1204 +  },
  1.1205 +
  1.1206 +  /**
  1.1207 +   * Focuses the next or previous item in this container.
  1.1208 +   *
  1.1209 +   * @param string aDirection
  1.1210 +   *        Either "advanceFocus" or "rewindFocus".
  1.1211 +   * @return boolean
  1.1212 +   *         False if the focus went out of bounds and the first or last item
  1.1213 +   *         in this container was focused instead.
  1.1214 +   */
  1.1215 +  _focusChange: function(aDirection) {
  1.1216 +    let commandDispatcher = this._commandDispatcher;
  1.1217 +    let prevFocusedElement = commandDispatcher.focusedElement;
  1.1218 +    let currFocusedElement;
  1.1219 +
  1.1220 +    do {
  1.1221 +      commandDispatcher.suppressFocusScroll = true;
  1.1222 +      commandDispatcher[aDirection]();
  1.1223 +      currFocusedElement = commandDispatcher.focusedElement;
  1.1224 +
  1.1225 +      // Make sure the newly focused item is a part of this container. If the
  1.1226 +      // focus goes out of bounds, revert the previously focused item.
  1.1227 +      if (!this.getItemForElement(currFocusedElement)) {
  1.1228 +        prevFocusedElement.focus();
  1.1229 +        return false;
  1.1230 +      }
  1.1231 +    } while (!WIDGET_FOCUSABLE_NODES.has(currFocusedElement.tagName));
  1.1232 +
  1.1233 +    // Focus remained within bounds.
  1.1234 +    return true;
  1.1235 +  },
  1.1236 +
  1.1237 +  /**
  1.1238 +   * Gets the command dispatcher instance associated with this container's DOM.
  1.1239 +   * If there are no items displayed in this container, null is returned.
  1.1240 +   * @return nsIDOMXULCommandDispatcher | null
  1.1241 +   */
  1.1242 +  get _commandDispatcher() {
  1.1243 +    if (this._cachedCommandDispatcher) {
  1.1244 +      return this._cachedCommandDispatcher;
  1.1245 +    }
  1.1246 +    let someElement = this._widget.getItemAtIndex(0);
  1.1247 +    if (someElement) {
  1.1248 +      let commandDispatcher = someElement.ownerDocument.commandDispatcher;
  1.1249 +      return this._cachedCommandDispatcher = commandDispatcher;
  1.1250 +    }
  1.1251 +    return null;
  1.1252 +  },
  1.1253 +
  1.1254 +  /**
  1.1255 +   * Gets the currently focused element in this container.
  1.1256 +   *
  1.1257 +   * @return nsIDOMNode
  1.1258 +   *         The focused element, or null if nothing is found.
  1.1259 +   */
  1.1260 +  get _focusedElement() {
  1.1261 +    let commandDispatcher = this._commandDispatcher;
  1.1262 +    if (commandDispatcher) {
  1.1263 +      return commandDispatcher.focusedElement;
  1.1264 +    }
  1.1265 +    return null;
  1.1266 +  },
  1.1267 +
  1.1268 +  /**
  1.1269 +   * Gets the item in the container having the specified index.
  1.1270 +   *
  1.1271 +   * @param number aIndex
  1.1272 +   *        The index used to identify the element.
  1.1273 +   * @return Item
  1.1274 +   *         The matched item, or null if nothing is found.
  1.1275 +   */
  1.1276 +  getItemAtIndex: function(aIndex) {
  1.1277 +    return this.getItemForElement(this._widget.getItemAtIndex(aIndex));
  1.1278 +  },
  1.1279 +
  1.1280 +  /**
  1.1281 +   * Gets the item in the container having the specified value.
  1.1282 +   *
  1.1283 +   * @param string aValue
  1.1284 +   *        The value used to identify the element.
  1.1285 +   * @return Item
  1.1286 +   *         The matched item, or null if nothing is found.
  1.1287 +   */
  1.1288 +  getItemByValue: function(aValue) {
  1.1289 +    return this._itemsByValue.get(aValue);
  1.1290 +  },
  1.1291 +
  1.1292 +  /**
  1.1293 +   * Gets the item in the container associated with the specified element.
  1.1294 +   *
  1.1295 +   * @param nsIDOMNode aElement
  1.1296 +   *        The element used to identify the item.
  1.1297 +   * @param object aFlags [optional]
  1.1298 +   *        Additional options for showing the source. Supported options:
  1.1299 +   *          - noSiblings: if siblings shouldn't be taken into consideration
  1.1300 +   *                        when searching for the associated item.
  1.1301 +   * @return Item
  1.1302 +   *         The matched item, or null if nothing is found.
  1.1303 +   */
  1.1304 +  getItemForElement: function(aElement, aFlags = {}) {
  1.1305 +    while (aElement) {
  1.1306 +      let item = this._itemsByElement.get(aElement);
  1.1307 +
  1.1308 +      // Also search the siblings if allowed.
  1.1309 +      if (!aFlags.noSiblings) {
  1.1310 +        item = item ||
  1.1311 +          this._itemsByElement.get(aElement.nextElementSibling) ||
  1.1312 +          this._itemsByElement.get(aElement.previousElementSibling);
  1.1313 +      }
  1.1314 +      if (item) {
  1.1315 +        return item;
  1.1316 +      }
  1.1317 +      aElement = aElement.parentNode;
  1.1318 +    }
  1.1319 +    return null;
  1.1320 +  },
  1.1321 +
  1.1322 +  /**
  1.1323 +   * Gets a visible item in this container validating a specified predicate.
  1.1324 +   *
  1.1325 +   * @param function aPredicate
  1.1326 +   *        The first item which validates this predicate is returned
  1.1327 +   * @return Item
  1.1328 +   *         The matched item, or null if nothing is found.
  1.1329 +   */
  1.1330 +  getItemForPredicate: function(aPredicate, aOwner = this) {
  1.1331 +    // Recursively check the items in this widget for a predicate match.
  1.1332 +    for (let [element, item] of aOwner._itemsByElement) {
  1.1333 +      let match;
  1.1334 +      if (aPredicate(item) && !element.hidden) {
  1.1335 +        match = item;
  1.1336 +      } else {
  1.1337 +        match = this.getItemForPredicate(aPredicate, item);
  1.1338 +      }
  1.1339 +      if (match) {
  1.1340 +        return match;
  1.1341 +      }
  1.1342 +    }
  1.1343 +    // Also check the staged items. No need to do this recursively since
  1.1344 +    // they're not even appended to the view yet.
  1.1345 +    for (let { item } of this._stagedItems) {
  1.1346 +      if (aPredicate(item)) {
  1.1347 +        return item;
  1.1348 +      }
  1.1349 +    }
  1.1350 +    return null;
  1.1351 +  },
  1.1352 +
  1.1353 +  /**
  1.1354 +   * Shortcut function for getItemForPredicate which works on item attachments.
  1.1355 +   * @see getItemForPredicate
  1.1356 +   */
  1.1357 +  getItemForAttachment: function(aPredicate, aOwner = this) {
  1.1358 +    return this.getItemForPredicate(e => aPredicate(e.attachment));
  1.1359 +  },
  1.1360 +
  1.1361 +  /**
  1.1362 +   * Finds the index of an item in the container.
  1.1363 +   *
  1.1364 +   * @param Item aItem
  1.1365 +   *        The item get the index for.
  1.1366 +   * @return number
  1.1367 +   *         The index of the matched item, or -1 if nothing is found.
  1.1368 +   */
  1.1369 +  indexOfItem: function(aItem) {
  1.1370 +    return this._indexOfElement(aItem._target);
  1.1371 +  },
  1.1372 +
  1.1373 +  /**
  1.1374 +   * Finds the index of an element in the container.
  1.1375 +   *
  1.1376 +   * @param nsIDOMNode aElement
  1.1377 +   *        The element get the index for.
  1.1378 +   * @return number
  1.1379 +   *         The index of the matched element, or -1 if nothing is found.
  1.1380 +   */
  1.1381 +  _indexOfElement: function(aElement) {
  1.1382 +    for (let i = 0; i < this._itemsByElement.size; i++) {
  1.1383 +      if (this._widget.getItemAtIndex(i) == aElement) {
  1.1384 +        return i;
  1.1385 +      }
  1.1386 +    }
  1.1387 +    return -1;
  1.1388 +  },
  1.1389 +
  1.1390 +  /**
  1.1391 +   * Gets the total number of items in this container.
  1.1392 +   * @return number
  1.1393 +   */
  1.1394 +  get itemCount() {
  1.1395 +    return this._itemsByElement.size;
  1.1396 +  },
  1.1397 +
  1.1398 +  /**
  1.1399 +   * Returns a list of items in this container, in the displayed order.
  1.1400 +   * @return array
  1.1401 +   */
  1.1402 +  get items() {
  1.1403 +    let store = [];
  1.1404 +    let itemCount = this.itemCount;
  1.1405 +    for (let i = 0; i < itemCount; i++) {
  1.1406 +      store.push(this.getItemAtIndex(i));
  1.1407 +    }
  1.1408 +    return store;
  1.1409 +  },
  1.1410 +
  1.1411 +  /**
  1.1412 +   * Returns a list of values in this container, in the displayed order.
  1.1413 +   * @return array
  1.1414 +   */
  1.1415 +  get values() {
  1.1416 +    return this.items.map(e => e._value);
  1.1417 +  },
  1.1418 +
  1.1419 +  /**
  1.1420 +   * Returns a list of attachments in this container, in the displayed order.
  1.1421 +   * @return array
  1.1422 +   */
  1.1423 +  get attachments() {
  1.1424 +    return this.items.map(e => e.attachment);
  1.1425 +  },
  1.1426 +
  1.1427 +  /**
  1.1428 +   * Returns a list of all the visible (non-hidden) items in this container,
  1.1429 +   * in the displayed order
  1.1430 +   * @return array
  1.1431 +   */
  1.1432 +  get visibleItems() {
  1.1433 +    return this.items.filter(e => !e._target.hidden);
  1.1434 +  },
  1.1435 +
  1.1436 +  /**
  1.1437 +   * Checks if an item is unique in this container. If an item's value is an
  1.1438 +   * empty string, "undefined" or "null", it is considered unique.
  1.1439 +   *
  1.1440 +   * @param Item aItem
  1.1441 +   *        The item for which to verify uniqueness.
  1.1442 +   * @return boolean
  1.1443 +   *         True if the item is unique, false otherwise.
  1.1444 +   */
  1.1445 +  isUnique: function(aItem) {
  1.1446 +    let value = aItem._value;
  1.1447 +    if (value == "" || value == "undefined" || value == "null") {
  1.1448 +      return true;
  1.1449 +    }
  1.1450 +    return !this._itemsByValue.has(value);
  1.1451 +  },
  1.1452 +
  1.1453 +  /**
  1.1454 +   * Checks if an item is eligible for this container. By default, this checks
  1.1455 +   * whether an item is unique and has a prebuilt target node.
  1.1456 +   *
  1.1457 +   * @param Item aItem
  1.1458 +   *        The item for which to verify eligibility.
  1.1459 +   * @return boolean
  1.1460 +   *         True if the item is eligible, false otherwise.
  1.1461 +   */
  1.1462 +  isEligible: function(aItem) {
  1.1463 +    return this.isUnique(aItem) && aItem._prebuiltNode;
  1.1464 +  },
  1.1465 +
  1.1466 +  /**
  1.1467 +   * Finds the expected item index in this container based on the default
  1.1468 +   * sort predicate.
  1.1469 +   *
  1.1470 +   * @param Item aItem
  1.1471 +   *        The item for which to get the expected index.
  1.1472 +   * @return number
  1.1473 +   *         The expected item index.
  1.1474 +   */
  1.1475 +  _findExpectedIndexFor: function(aItem) {
  1.1476 +    let itemCount = this.itemCount;
  1.1477 +    for (let i = 0; i < itemCount; i++) {
  1.1478 +      if (this._currentSortPredicate(this.getItemAtIndex(i), aItem) > 0) {
  1.1479 +        return i;
  1.1480 +      }
  1.1481 +    }
  1.1482 +    return itemCount;
  1.1483 +  },
  1.1484 +
  1.1485 +  /**
  1.1486 +   * Immediately inserts an item in this container at the specified index.
  1.1487 +   *
  1.1488 +   * @param number aIndex
  1.1489 +   *        The position in the container intended for this item.
  1.1490 +   * @param Item aItem
  1.1491 +   *        The item describing a target element.
  1.1492 +   * @param object aOptions [optional]
  1.1493 +   *        Additional options or flags supported by this operation:
  1.1494 +   *          - attributes: a batch of attributes set to the displayed element
  1.1495 +   *          - finalize: function when the item is untangled (removed)
  1.1496 +   * @return Item
  1.1497 +   *         The item associated with the displayed element, null if rejected.
  1.1498 +   */
  1.1499 +  _insertItemAt: function(aIndex, aItem, aOptions = {}) {
  1.1500 +    if (!this.isEligible(aItem)) {
  1.1501 +      return null;
  1.1502 +    }
  1.1503 +
  1.1504 +    // Entangle the item with the newly inserted node.
  1.1505 +    // Make sure this is done with the value returned by insertItemAt(),
  1.1506 +    // to avoid storing a potential DocumentFragment.
  1.1507 +    let node = aItem._prebuiltNode;
  1.1508 +    let attachment = aItem.attachment;
  1.1509 +    this._entangleItem(aItem, this._widget.insertItemAt(aIndex, node, attachment));
  1.1510 +
  1.1511 +    // Handle any additional options after entangling the item.
  1.1512 +    if (!this._currentFilterPredicate(aItem)) {
  1.1513 +      aItem._target.hidden = true;
  1.1514 +    }
  1.1515 +    if (this.autoFocusOnFirstItem && this._itemsByElement.size == 1) {
  1.1516 +      aItem._target.focus();
  1.1517 +    }
  1.1518 +    if (aOptions.attributes) {
  1.1519 +      aOptions.attributes.forEach(e => aItem._target.setAttribute(e[0], e[1]));
  1.1520 +    }
  1.1521 +    if (aOptions.finalize) {
  1.1522 +      aItem.finalize = aOptions.finalize;
  1.1523 +    }
  1.1524 +
  1.1525 +    // Hide the empty text if the selection wasn't lost.
  1.1526 +    this._widget.removeAttribute("emptyText");
  1.1527 +
  1.1528 +    // Return the item associated with the displayed element.
  1.1529 +    return aItem;
  1.1530 +  },
  1.1531 +
  1.1532 +  /**
  1.1533 +   * Entangles an item (model) with a displayed node element (view).
  1.1534 +   *
  1.1535 +   * @param Item aItem
  1.1536 +   *        The item describing a target element.
  1.1537 +   * @param nsIDOMNode aElement
  1.1538 +   *        The element displaying the item.
  1.1539 +   */
  1.1540 +  _entangleItem: function(aItem, aElement) {
  1.1541 +    this._itemsByValue.set(aItem._value, aItem);
  1.1542 +    this._itemsByElement.set(aElement, aItem);
  1.1543 +    aItem._target = aElement;
  1.1544 +  },
  1.1545 +
  1.1546 +  /**
  1.1547 +   * Untangles an item (model) from a displayed node element (view).
  1.1548 +   *
  1.1549 +   * @param Item aItem
  1.1550 +   *        The item describing a target element.
  1.1551 +   */
  1.1552 +  _untangleItem: function(aItem) {
  1.1553 +    if (aItem.finalize) {
  1.1554 +      aItem.finalize(aItem);
  1.1555 +    }
  1.1556 +    for (let childItem of aItem) {
  1.1557 +      aItem.remove(childItem);
  1.1558 +    }
  1.1559 +
  1.1560 +    this._unlinkItem(aItem);
  1.1561 +    aItem._target = null;
  1.1562 +  },
  1.1563 +
  1.1564 +  /**
  1.1565 +   * Deletes an item from the its parent's storage maps.
  1.1566 +   *
  1.1567 +   * @param Item aItem
  1.1568 +   *        The item describing a target element.
  1.1569 +   */
  1.1570 +  _unlinkItem: function(aItem) {
  1.1571 +    this._itemsByValue.delete(aItem._value);
  1.1572 +    this._itemsByElement.delete(aItem._target);
  1.1573 +  },
  1.1574 +
  1.1575 +  /**
  1.1576 +   * The keyPress event listener for this container.
  1.1577 +   * @param string aName
  1.1578 +   * @param KeyboardEvent aEvent
  1.1579 +   */
  1.1580 +  _onWidgetKeyPress: function(aName, aEvent) {
  1.1581 +    // Prevent scrolling when pressing navigation keys.
  1.1582 +    ViewHelpers.preventScrolling(aEvent);
  1.1583 +
  1.1584 +    switch (aEvent.keyCode) {
  1.1585 +      case aEvent.DOM_VK_UP:
  1.1586 +      case aEvent.DOM_VK_LEFT:
  1.1587 +        this.focusPrevItem();
  1.1588 +        return;
  1.1589 +      case aEvent.DOM_VK_DOWN:
  1.1590 +      case aEvent.DOM_VK_RIGHT:
  1.1591 +        this.focusNextItem();
  1.1592 +        return;
  1.1593 +      case aEvent.DOM_VK_PAGE_UP:
  1.1594 +        this.focusItemAtDelta(-(this.pageSize || (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
  1.1595 +        return;
  1.1596 +      case aEvent.DOM_VK_PAGE_DOWN:
  1.1597 +        this.focusItemAtDelta(+(this.pageSize || (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
  1.1598 +        return;
  1.1599 +      case aEvent.DOM_VK_HOME:
  1.1600 +        this.focusFirstVisibleItem();
  1.1601 +        return;
  1.1602 +      case aEvent.DOM_VK_END:
  1.1603 +        this.focusLastVisibleItem();
  1.1604 +        return;
  1.1605 +    }
  1.1606 +  },
  1.1607 +
  1.1608 +  /**
  1.1609 +   * The mousePress event listener for this container.
  1.1610 +   * @param string aName
  1.1611 +   * @param MouseEvent aEvent
  1.1612 +   */
  1.1613 +  _onWidgetMousePress: function(aName, aEvent) {
  1.1614 +    if (aEvent.button != 0 && !this.allowFocusOnRightClick) {
  1.1615 +      // Only allow left-click to trigger this event.
  1.1616 +      return;
  1.1617 +    }
  1.1618 +
  1.1619 +    let item = this.getItemForElement(aEvent.target);
  1.1620 +    if (item) {
  1.1621 +      // The container is not empty and we clicked on an actual item.
  1.1622 +      this.selectedItem = item;
  1.1623 +      // Make sure the current event's target element is also focused.
  1.1624 +      this.autoFocusOnInput && item._target.focus();
  1.1625 +    }
  1.1626 +  },
  1.1627 +
  1.1628 +  /**
  1.1629 +   * The predicate used when filtering items. By default, all items in this
  1.1630 +   * view are visible.
  1.1631 +   *
  1.1632 +   * @param Item aItem
  1.1633 +   *        The item passing through the filter.
  1.1634 +   * @return boolean
  1.1635 +   *         True if the item should be visible, false otherwise.
  1.1636 +   */
  1.1637 +  _currentFilterPredicate: function(aItem) {
  1.1638 +    return true;
  1.1639 +  },
  1.1640 +
  1.1641 +  /**
  1.1642 +   * The predicate used when sorting items. By default, items in this view
  1.1643 +   * are sorted by their label.
  1.1644 +   *
  1.1645 +   * @param Item aFirst
  1.1646 +   *        The first item used in the comparison.
  1.1647 +   * @param Item aSecond
  1.1648 +   *        The second item used in the comparison.
  1.1649 +   * @return number
  1.1650 +   *         -1 to sort aFirst to a lower index than aSecond
  1.1651 +   *          0 to leave aFirst and aSecond unchanged with respect to each other
  1.1652 +   *          1 to sort aSecond to a lower index than aFirst
  1.1653 +   */
  1.1654 +  _currentSortPredicate: function(aFirst, aSecond) {
  1.1655 +    return +(aFirst._value.toLowerCase() > aSecond._value.toLowerCase());
  1.1656 +  },
  1.1657 +
  1.1658 +  /**
  1.1659 +   * Call a method on this widget named `aMethodName`. Any further arguments are
  1.1660 +   * passed on to the method. Returns the result of the method call.
  1.1661 +   *
  1.1662 +   * @param String aMethodName
  1.1663 +   *        The name of the method you want to call.
  1.1664 +   * @param aArgs
  1.1665 +   *        Optional. Any arguments you want to pass through to the method.
  1.1666 +   */
  1.1667 +  callMethod: function(aMethodName, ...aArgs) {
  1.1668 +    return this._widget[aMethodName].apply(this._widget, aArgs);
  1.1669 +  },
  1.1670 +
  1.1671 +  _widget: null,
  1.1672 +  _emptyText: "",
  1.1673 +  _headerText: "",
  1.1674 +  _preferredValue: "",
  1.1675 +  _cachedCommandDispatcher: null
  1.1676 +};
  1.1677 +
  1.1678 +/**
  1.1679 + * A generator-iterator over all the items in this container.
  1.1680 + */
  1.1681 +Item.prototype["@@iterator"] =
  1.1682 +WidgetMethods["@@iterator"] = function*() {
  1.1683 +  yield* this._itemsByElement.values();
  1.1684 +};

mercurial