browser/devtools/shared/widgets/ViewHelpers.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6 "use strict";
michael@0 7
michael@0 8 const Cc = Components.classes;
michael@0 9 const Ci = Components.interfaces;
michael@0 10 const Cu = Components.utils;
michael@0 11
michael@0 12 const PANE_APPEARANCE_DELAY = 50;
michael@0 13 const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
michael@0 14 const WIDGET_FOCUSABLE_NODES = new Set(["vbox", "hbox"]);
michael@0 15
michael@0 16 Cu.import("resource://gre/modules/Services.jsm");
michael@0 17 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 18 Cu.import("resource://gre/modules/Timer.jsm");
michael@0 19 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
michael@0 20
michael@0 21 this.EXPORTED_SYMBOLS = [
michael@0 22 "Heritage", "ViewHelpers", "WidgetMethods",
michael@0 23 "setNamedTimeout", "clearNamedTimeout",
michael@0 24 "setConditionalTimeout", "clearConditionalTimeout",
michael@0 25 ];
michael@0 26
michael@0 27 /**
michael@0 28 * Inheritance helpers from the addon SDK's core/heritage.
michael@0 29 * Remove these when all devtools are loadered.
michael@0 30 */
michael@0 31 this.Heritage = {
michael@0 32 /**
michael@0 33 * @see extend in sdk/core/heritage.
michael@0 34 */
michael@0 35 extend: function(aPrototype, aProperties = {}) {
michael@0 36 return Object.create(aPrototype, this.getOwnPropertyDescriptors(aProperties));
michael@0 37 },
michael@0 38
michael@0 39 /**
michael@0 40 * @see getOwnPropertyDescriptors in sdk/core/heritage.
michael@0 41 */
michael@0 42 getOwnPropertyDescriptors: function(aObject) {
michael@0 43 return Object.getOwnPropertyNames(aObject).reduce((aDescriptor, aName) => {
michael@0 44 aDescriptor[aName] = Object.getOwnPropertyDescriptor(aObject, aName);
michael@0 45 return aDescriptor;
michael@0 46 }, {});
michael@0 47 }
michael@0 48 };
michael@0 49
michael@0 50 /**
michael@0 51 * Helper for draining a rapid succession of events and invoking a callback
michael@0 52 * once everything settles down.
michael@0 53 *
michael@0 54 * @param string aId
michael@0 55 * A string identifier for the named timeout.
michael@0 56 * @param number aWait
michael@0 57 * The amount of milliseconds to wait after no more events are fired.
michael@0 58 * @param function aCallback
michael@0 59 * Invoked when no more events are fired after the specified time.
michael@0 60 */
michael@0 61 this.setNamedTimeout = function setNamedTimeout(aId, aWait, aCallback) {
michael@0 62 clearNamedTimeout(aId);
michael@0 63
michael@0 64 namedTimeoutsStore.set(aId, setTimeout(() =>
michael@0 65 namedTimeoutsStore.delete(aId) && aCallback(), aWait));
michael@0 66 };
michael@0 67
michael@0 68 /**
michael@0 69 * Clears a named timeout.
michael@0 70 * @see setNamedTimeout
michael@0 71 *
michael@0 72 * @param string aId
michael@0 73 * A string identifier for the named timeout.
michael@0 74 */
michael@0 75 this.clearNamedTimeout = function clearNamedTimeout(aId) {
michael@0 76 if (!namedTimeoutsStore) {
michael@0 77 return;
michael@0 78 }
michael@0 79 clearTimeout(namedTimeoutsStore.get(aId));
michael@0 80 namedTimeoutsStore.delete(aId);
michael@0 81 };
michael@0 82
michael@0 83 /**
michael@0 84 * Same as `setNamedTimeout`, but invokes the callback only if the provided
michael@0 85 * predicate function returns true. Otherwise, the timeout is re-triggered.
michael@0 86 *
michael@0 87 * @param string aId
michael@0 88 * A string identifier for the conditional timeout.
michael@0 89 * @param number aWait
michael@0 90 * The amount of milliseconds to wait after no more events are fired.
michael@0 91 * @param function aPredicate
michael@0 92 * The predicate function used to determine whether the timeout restarts.
michael@0 93 * @param function aCallback
michael@0 94 * Invoked when no more events are fired after the specified time, and
michael@0 95 * the provided predicate function returns true.
michael@0 96 */
michael@0 97 this.setConditionalTimeout = function setConditionalTimeout(aId, aWait, aPredicate, aCallback) {
michael@0 98 setNamedTimeout(aId, aWait, function maybeCallback() {
michael@0 99 if (aPredicate()) {
michael@0 100 aCallback();
michael@0 101 return;
michael@0 102 }
michael@0 103 setConditionalTimeout(aId, aWait, aPredicate, aCallback);
michael@0 104 });
michael@0 105 };
michael@0 106
michael@0 107 /**
michael@0 108 * Clears a conditional timeout.
michael@0 109 * @see setConditionalTimeout
michael@0 110 *
michael@0 111 * @param string aId
michael@0 112 * A string identifier for the conditional timeout.
michael@0 113 */
michael@0 114 this.clearConditionalTimeout = function clearConditionalTimeout(aId) {
michael@0 115 clearNamedTimeout(aId);
michael@0 116 };
michael@0 117
michael@0 118 XPCOMUtils.defineLazyGetter(this, "namedTimeoutsStore", () => new Map());
michael@0 119
michael@0 120 /**
michael@0 121 * Helpers for creating and messaging between UI components.
michael@0 122 */
michael@0 123 this.ViewHelpers = {
michael@0 124 /**
michael@0 125 * Convenience method, dispatching a custom event.
michael@0 126 *
michael@0 127 * @param nsIDOMNode aTarget
michael@0 128 * A custom target element to dispatch the event from.
michael@0 129 * @param string aType
michael@0 130 * The name of the event.
michael@0 131 * @param any aDetail
michael@0 132 * The data passed when initializing the event.
michael@0 133 * @return boolean
michael@0 134 * True if the event was cancelled or a registered handler
michael@0 135 * called preventDefault.
michael@0 136 */
michael@0 137 dispatchEvent: function(aTarget, aType, aDetail) {
michael@0 138 if (!(aTarget instanceof Ci.nsIDOMNode)) {
michael@0 139 return true; // Event cancelled.
michael@0 140 }
michael@0 141 let document = aTarget.ownerDocument || aTarget;
michael@0 142 let dispatcher = aTarget.ownerDocument ? aTarget : document.documentElement;
michael@0 143
michael@0 144 let event = document.createEvent("CustomEvent");
michael@0 145 event.initCustomEvent(aType, true, true, aDetail);
michael@0 146 return dispatcher.dispatchEvent(event);
michael@0 147 },
michael@0 148
michael@0 149 /**
michael@0 150 * Helper delegating some of the DOM attribute methods of a node to a widget.
michael@0 151 *
michael@0 152 * @param object aWidget
michael@0 153 * The widget to assign the methods to.
michael@0 154 * @param nsIDOMNode aNode
michael@0 155 * A node to delegate the methods to.
michael@0 156 */
michael@0 157 delegateWidgetAttributeMethods: function(aWidget, aNode) {
michael@0 158 aWidget.getAttribute =
michael@0 159 aWidget.getAttribute || aNode.getAttribute.bind(aNode);
michael@0 160 aWidget.setAttribute =
michael@0 161 aWidget.setAttribute || aNode.setAttribute.bind(aNode);
michael@0 162 aWidget.removeAttribute =
michael@0 163 aWidget.removeAttribute || aNode.removeAttribute.bind(aNode);
michael@0 164 },
michael@0 165
michael@0 166 /**
michael@0 167 * Helper delegating some of the DOM event methods of a node to a widget.
michael@0 168 *
michael@0 169 * @param object aWidget
michael@0 170 * The widget to assign the methods to.
michael@0 171 * @param nsIDOMNode aNode
michael@0 172 * A node to delegate the methods to.
michael@0 173 */
michael@0 174 delegateWidgetEventMethods: function(aWidget, aNode) {
michael@0 175 aWidget.addEventListener =
michael@0 176 aWidget.addEventListener || aNode.addEventListener.bind(aNode);
michael@0 177 aWidget.removeEventListener =
michael@0 178 aWidget.removeEventListener || aNode.removeEventListener.bind(aNode);
michael@0 179 },
michael@0 180
michael@0 181 /**
michael@0 182 * Checks if the specified object looks like it's been decorated by an
michael@0 183 * event emitter.
michael@0 184 *
michael@0 185 * @return boolean
michael@0 186 * True if it looks, walks and quacks like an event emitter.
michael@0 187 */
michael@0 188 isEventEmitter: function(aObject) {
michael@0 189 return aObject && aObject.on && aObject.off && aObject.once && aObject.emit;
michael@0 190 },
michael@0 191
michael@0 192 /**
michael@0 193 * Checks if the specified object is an instance of a DOM node.
michael@0 194 *
michael@0 195 * @return boolean
michael@0 196 * True if it's a node, false otherwise.
michael@0 197 */
michael@0 198 isNode: function(aObject) {
michael@0 199 return aObject instanceof Ci.nsIDOMNode ||
michael@0 200 aObject instanceof Ci.nsIDOMElement ||
michael@0 201 aObject instanceof Ci.nsIDOMDocumentFragment;
michael@0 202 },
michael@0 203
michael@0 204 /**
michael@0 205 * Prevents event propagation when navigation keys are pressed.
michael@0 206 *
michael@0 207 * @param Event e
michael@0 208 * The event to be prevented.
michael@0 209 */
michael@0 210 preventScrolling: function(e) {
michael@0 211 switch (e.keyCode) {
michael@0 212 case e.DOM_VK_UP:
michael@0 213 case e.DOM_VK_DOWN:
michael@0 214 case e.DOM_VK_LEFT:
michael@0 215 case e.DOM_VK_RIGHT:
michael@0 216 case e.DOM_VK_PAGE_UP:
michael@0 217 case e.DOM_VK_PAGE_DOWN:
michael@0 218 case e.DOM_VK_HOME:
michael@0 219 case e.DOM_VK_END:
michael@0 220 e.preventDefault();
michael@0 221 e.stopPropagation();
michael@0 222 }
michael@0 223 },
michael@0 224
michael@0 225 /**
michael@0 226 * Sets a side pane hidden or visible.
michael@0 227 *
michael@0 228 * @param object aFlags
michael@0 229 * An object containing some of the following properties:
michael@0 230 * - visible: true if the pane should be shown, false to hide
michael@0 231 * - animated: true to display an animation on toggle
michael@0 232 * - delayed: true to wait a few cycles before toggle
michael@0 233 * - callback: a function to invoke when the toggle finishes
michael@0 234 * @param nsIDOMNode aPane
michael@0 235 * The element representing the pane to toggle.
michael@0 236 */
michael@0 237 togglePane: function(aFlags, aPane) {
michael@0 238 // Make sure a pane is actually available first.
michael@0 239 if (!aPane) {
michael@0 240 return;
michael@0 241 }
michael@0 242
michael@0 243 // Hiding is always handled via margins, not the hidden attribute.
michael@0 244 aPane.removeAttribute("hidden");
michael@0 245
michael@0 246 // Add a class to the pane to handle min-widths, margins and animations.
michael@0 247 if (!aPane.classList.contains("generic-toggled-side-pane")) {
michael@0 248 aPane.classList.add("generic-toggled-side-pane");
michael@0 249 }
michael@0 250
michael@0 251 // Avoid useless toggles.
michael@0 252 if (aFlags.visible == !aPane.hasAttribute("pane-collapsed")) {
michael@0 253 if (aFlags.callback) aFlags.callback();
michael@0 254 return;
michael@0 255 }
michael@0 256
michael@0 257 // The "animated" attributes enables animated toggles (slide in-out).
michael@0 258 if (aFlags.animated) {
michael@0 259 aPane.setAttribute("animated", "");
michael@0 260 } else {
michael@0 261 aPane.removeAttribute("animated");
michael@0 262 }
michael@0 263
michael@0 264 // Computes and sets the pane margins in order to hide or show it.
michael@0 265 let doToggle = () => {
michael@0 266 if (aFlags.visible) {
michael@0 267 aPane.style.marginLeft = "0";
michael@0 268 aPane.style.marginRight = "0";
michael@0 269 aPane.removeAttribute("pane-collapsed");
michael@0 270 } else {
michael@0 271 let margin = ~~(aPane.getAttribute("width")) + 1;
michael@0 272 aPane.style.marginLeft = -margin + "px";
michael@0 273 aPane.style.marginRight = -margin + "px";
michael@0 274 aPane.setAttribute("pane-collapsed", "");
michael@0 275 }
michael@0 276
michael@0 277 // Invoke the callback when the transition ended.
michael@0 278 if (aFlags.animated) {
michael@0 279 aPane.addEventListener("transitionend", function onEvent() {
michael@0 280 aPane.removeEventListener("transitionend", onEvent, false);
michael@0 281 if (aFlags.callback) aFlags.callback();
michael@0 282 }, false);
michael@0 283 }
michael@0 284 // Invoke the callback immediately since there's no transition.
michael@0 285 else {
michael@0 286 if (aFlags.callback) aFlags.callback();
michael@0 287 }
michael@0 288 }
michael@0 289
michael@0 290 // Sometimes it's useful delaying the toggle a few ticks to ensure
michael@0 291 // a smoother slide in-out animation.
michael@0 292 if (aFlags.delayed) {
michael@0 293 aPane.ownerDocument.defaultView.setTimeout(doToggle, PANE_APPEARANCE_DELAY);
michael@0 294 } else {
michael@0 295 doToggle();
michael@0 296 }
michael@0 297 }
michael@0 298 };
michael@0 299
michael@0 300 /**
michael@0 301 * Localization convenience methods.
michael@0 302 *
michael@0 303 * @param string aStringBundleName
michael@0 304 * The desired string bundle's name.
michael@0 305 */
michael@0 306 ViewHelpers.L10N = function(aStringBundleName) {
michael@0 307 XPCOMUtils.defineLazyGetter(this, "stringBundle", () =>
michael@0 308 Services.strings.createBundle(aStringBundleName));
michael@0 309
michael@0 310 XPCOMUtils.defineLazyGetter(this, "ellipsis", () =>
michael@0 311 Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data);
michael@0 312 };
michael@0 313
michael@0 314 ViewHelpers.L10N.prototype = {
michael@0 315 stringBundle: null,
michael@0 316
michael@0 317 /**
michael@0 318 * L10N shortcut function.
michael@0 319 *
michael@0 320 * @param string aName
michael@0 321 * @return string
michael@0 322 */
michael@0 323 getStr: function(aName) {
michael@0 324 return this.stringBundle.GetStringFromName(aName);
michael@0 325 },
michael@0 326
michael@0 327 /**
michael@0 328 * L10N shortcut function.
michael@0 329 *
michael@0 330 * @param string aName
michael@0 331 * @param array aArgs
michael@0 332 * @return string
michael@0 333 */
michael@0 334 getFormatStr: function(aName, ...aArgs) {
michael@0 335 return this.stringBundle.formatStringFromName(aName, aArgs, aArgs.length);
michael@0 336 },
michael@0 337
michael@0 338 /**
michael@0 339 * L10N shortcut function for numeric arguments that need to be formatted.
michael@0 340 * All numeric arguments will be fixed to 2 decimals and given a localized
michael@0 341 * decimal separator. Other arguments will be left alone.
michael@0 342 *
michael@0 343 * @param string aName
michael@0 344 * @param array aArgs
michael@0 345 * @return string
michael@0 346 */
michael@0 347 getFormatStrWithNumbers: function(aName, ...aArgs) {
michael@0 348 let newArgs = aArgs.map(x => typeof x == "number" ? this.numberWithDecimals(x, 2) : x);
michael@0 349 return this.stringBundle.formatStringFromName(aName, newArgs, newArgs.length);
michael@0 350 },
michael@0 351
michael@0 352 /**
michael@0 353 * Converts a number to a locale-aware string format and keeps a certain
michael@0 354 * number of decimals.
michael@0 355 *
michael@0 356 * @param number aNumber
michael@0 357 * The number to convert.
michael@0 358 * @param number aDecimals [optional]
michael@0 359 * Total decimals to keep.
michael@0 360 * @return string
michael@0 361 * The localized number as a string.
michael@0 362 */
michael@0 363 numberWithDecimals: function(aNumber, aDecimals = 0) {
michael@0 364 // If this is an integer, don't do anything special.
michael@0 365 if (aNumber == (aNumber | 0)) {
michael@0 366 return aNumber;
michael@0 367 }
michael@0 368 // Remove {n} trailing decimals. Can't use toFixed(n) because
michael@0 369 // toLocaleString converts the number to a string. Also can't use
michael@0 370 // toLocaleString(, { maximumFractionDigits: n }) because it's not
michael@0 371 // implemented on OS X (bug 368838). Gross.
michael@0 372 let localized = aNumber.toLocaleString(); // localize
michael@0 373 let padded = localized + new Array(aDecimals).join("0"); // pad with zeros
michael@0 374 let match = padded.match("([^]*?\\d{" + aDecimals + "})\\d*$");
michael@0 375 return match.pop();
michael@0 376 }
michael@0 377 };
michael@0 378
michael@0 379 /**
michael@0 380 * Shortcuts for lazily accessing and setting various preferences.
michael@0 381 * Usage:
michael@0 382 * let prefs = new ViewHelpers.Prefs("root.path.to.branch", {
michael@0 383 * myIntPref: ["Int", "leaf.path.to.my-int-pref"],
michael@0 384 * myCharPref: ["Char", "leaf.path.to.my-char-pref"],
michael@0 385 * ...
michael@0 386 * });
michael@0 387 *
michael@0 388 * prefs.myCharPref = "foo";
michael@0 389 * let aux = prefs.myCharPref;
michael@0 390 *
michael@0 391 * @param string aPrefsRoot
michael@0 392 * The root path to the required preferences branch.
michael@0 393 * @param object aPrefsObject
michael@0 394 * An object containing { accessorName: [prefType, prefName] } keys.
michael@0 395 */
michael@0 396 ViewHelpers.Prefs = function(aPrefsRoot = "", aPrefsObject = {}) {
michael@0 397 this.root = aPrefsRoot;
michael@0 398
michael@0 399 for (let accessorName in aPrefsObject) {
michael@0 400 let [prefType, prefName] = aPrefsObject[accessorName];
michael@0 401 this.map(accessorName, prefType, prefName);
michael@0 402 }
michael@0 403 };
michael@0 404
michael@0 405 ViewHelpers.Prefs.prototype = {
michael@0 406 /**
michael@0 407 * Helper method for getting a pref value.
michael@0 408 *
michael@0 409 * @param string aType
michael@0 410 * @param string aPrefName
michael@0 411 * @return any
michael@0 412 */
michael@0 413 _get: function(aType, aPrefName) {
michael@0 414 if (this[aPrefName] === undefined) {
michael@0 415 this[aPrefName] = Services.prefs["get" + aType + "Pref"](aPrefName);
michael@0 416 }
michael@0 417 return this[aPrefName];
michael@0 418 },
michael@0 419
michael@0 420 /**
michael@0 421 * Helper method for setting a pref value.
michael@0 422 *
michael@0 423 * @param string aType
michael@0 424 * @param string aPrefName
michael@0 425 * @param any aValue
michael@0 426 */
michael@0 427 _set: function(aType, aPrefName, aValue) {
michael@0 428 Services.prefs["set" + aType + "Pref"](aPrefName, aValue);
michael@0 429 this[aPrefName] = aValue;
michael@0 430 },
michael@0 431
michael@0 432 /**
michael@0 433 * Maps a property name to a pref, defining lazy getters and setters.
michael@0 434 * Supported types are "Bool", "Char", "Int" and "Json" (which is basically
michael@0 435 * just sugar for "Char" using the standard JSON serializer).
michael@0 436 *
michael@0 437 * @param string aAccessorName
michael@0 438 * @param string aType
michael@0 439 * @param string aPrefName
michael@0 440 * @param array aSerializer
michael@0 441 */
michael@0 442 map: function(aAccessorName, aType, aPrefName, aSerializer = { in: e => e, out: e => e }) {
michael@0 443 if (aType == "Json") {
michael@0 444 this.map(aAccessorName, "Char", aPrefName, { in: JSON.parse, out: JSON.stringify });
michael@0 445 return;
michael@0 446 }
michael@0 447
michael@0 448 Object.defineProperty(this, aAccessorName, {
michael@0 449 get: () => aSerializer.in(this._get(aType, [this.root, aPrefName].join("."))),
michael@0 450 set: (e) => this._set(aType, [this.root, aPrefName].join("."), aSerializer.out(e))
michael@0 451 });
michael@0 452 }
michael@0 453 };
michael@0 454
michael@0 455 /**
michael@0 456 * A generic Item is used to describe children present in a Widget.
michael@0 457 *
michael@0 458 * This is basically a very thin wrapper around an nsIDOMNode, with a few
michael@0 459 * characteristics, like a `value` and an `attachment`.
michael@0 460 *
michael@0 461 * The characteristics are optional, and their meaning is entirely up to you.
michael@0 462 * - The `value` should be a string, passed as an argument.
michael@0 463 * - The `attachment` is any kind of primitive or object, passed as an argument.
michael@0 464 *
michael@0 465 * Iterable via "for (let childItem of parentItem) { }".
michael@0 466 *
michael@0 467 * @param object aOwnerView
michael@0 468 * The owner view creating this item.
michael@0 469 * @param nsIDOMNode aElement
michael@0 470 * A prebuilt node to be wrapped.
michael@0 471 * @param string aValue
michael@0 472 * A string identifying the node.
michael@0 473 * @param any aAttachment
michael@0 474 * Some attached primitive/object.
michael@0 475 */
michael@0 476 function Item(aOwnerView, aElement, aValue, aAttachment) {
michael@0 477 this.ownerView = aOwnerView;
michael@0 478 this.attachment = aAttachment;
michael@0 479 this._value = aValue + "";
michael@0 480 this._prebuiltNode = aElement;
michael@0 481 };
michael@0 482
michael@0 483 Item.prototype = {
michael@0 484 get value() { return this._value; },
michael@0 485 get target() { return this._target; },
michael@0 486
michael@0 487 /**
michael@0 488 * Immediately appends a child item to this item.
michael@0 489 *
michael@0 490 * @param nsIDOMNode aElement
michael@0 491 * An nsIDOMNode representing the child element to append.
michael@0 492 * @param object aOptions [optional]
michael@0 493 * Additional options or flags supported by this operation:
michael@0 494 * - attachment: some attached primitive/object for the item
michael@0 495 * - attributes: a batch of attributes set to the displayed element
michael@0 496 * - finalize: function invoked when the child item is removed
michael@0 497 * @return Item
michael@0 498 * The item associated with the displayed element.
michael@0 499 */
michael@0 500 append: function(aElement, aOptions = {}) {
michael@0 501 let item = new Item(this, aElement, "", aOptions.attachment);
michael@0 502
michael@0 503 // Entangle the item with the newly inserted child node.
michael@0 504 // Make sure this is done with the value returned by appendChild(),
michael@0 505 // to avoid storing a potential DocumentFragment.
michael@0 506 this._entangleItem(item, this._target.appendChild(aElement));
michael@0 507
michael@0 508 // Handle any additional options after entangling the item.
michael@0 509 if (aOptions.attributes) {
michael@0 510 aOptions.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
michael@0 511 }
michael@0 512 if (aOptions.finalize) {
michael@0 513 item.finalize = aOptions.finalize;
michael@0 514 }
michael@0 515
michael@0 516 // Return the item associated with the displayed element.
michael@0 517 return item;
michael@0 518 },
michael@0 519
michael@0 520 /**
michael@0 521 * Immediately removes the specified child item from this item.
michael@0 522 *
michael@0 523 * @param Item aItem
michael@0 524 * The item associated with the element to remove.
michael@0 525 */
michael@0 526 remove: function(aItem) {
michael@0 527 if (!aItem) {
michael@0 528 return;
michael@0 529 }
michael@0 530 this._target.removeChild(aItem._target);
michael@0 531 this._untangleItem(aItem);
michael@0 532 },
michael@0 533
michael@0 534 /**
michael@0 535 * Entangles an item (model) with a displayed node element (view).
michael@0 536 *
michael@0 537 * @param Item aItem
michael@0 538 * The item describing a target element.
michael@0 539 * @param nsIDOMNode aElement
michael@0 540 * The element displaying the item.
michael@0 541 */
michael@0 542 _entangleItem: function(aItem, aElement) {
michael@0 543 this._itemsByElement.set(aElement, aItem);
michael@0 544 aItem._target = aElement;
michael@0 545 },
michael@0 546
michael@0 547 /**
michael@0 548 * Untangles an item (model) from a displayed node element (view).
michael@0 549 *
michael@0 550 * @param Item aItem
michael@0 551 * The item describing a target element.
michael@0 552 */
michael@0 553 _untangleItem: function(aItem) {
michael@0 554 if (aItem.finalize) {
michael@0 555 aItem.finalize(aItem);
michael@0 556 }
michael@0 557 for (let childItem of aItem) {
michael@0 558 aItem.remove(childItem);
michael@0 559 }
michael@0 560
michael@0 561 this._unlinkItem(aItem);
michael@0 562 aItem._target = null;
michael@0 563 },
michael@0 564
michael@0 565 /**
michael@0 566 * Deletes an item from the its parent's storage maps.
michael@0 567 *
michael@0 568 * @param Item aItem
michael@0 569 * The item describing a target element.
michael@0 570 */
michael@0 571 _unlinkItem: function(aItem) {
michael@0 572 this._itemsByElement.delete(aItem._target);
michael@0 573 },
michael@0 574
michael@0 575 /**
michael@0 576 * Returns a string representing the object.
michael@0 577 * @return string
michael@0 578 */
michael@0 579 toString: function() {
michael@0 580 return this._value + " :: " + this._target + " :: " + this.attachment;
michael@0 581 },
michael@0 582
michael@0 583 _value: "",
michael@0 584 _target: null,
michael@0 585 _prebuiltNode: null,
michael@0 586 finalize: null,
michael@0 587 attachment: null
michael@0 588 };
michael@0 589
michael@0 590 // Creating maps thousands of times for widgets with a large number of children
michael@0 591 // fills up a lot of memory. Make sure these are instantiated only if needed.
michael@0 592 DevToolsUtils.defineLazyPrototypeGetter(Item.prototype, "_itemsByElement", Map);
michael@0 593
michael@0 594 /**
michael@0 595 * Some generic Widget methods handling Item instances.
michael@0 596 * Iterable via "for (let childItem of wrappedView) { }".
michael@0 597 *
michael@0 598 * Usage:
michael@0 599 * function MyView() {
michael@0 600 * this.widget = new MyWidget(document.querySelector(".my-node"));
michael@0 601 * }
michael@0 602 *
michael@0 603 * MyView.prototype = Heritage.extend(WidgetMethods, {
michael@0 604 * myMethod: function() {},
michael@0 605 * ...
michael@0 606 * });
michael@0 607 *
michael@0 608 * See https://gist.github.com/victorporof/5749386 for more details.
michael@0 609 * The devtools/shared/widgets/SimpleListWidget.jsm is an implementation example.
michael@0 610 *
michael@0 611 * Language:
michael@0 612 * - An "item" is an instance of an Item.
michael@0 613 * - An "element" or "node" is a nsIDOMNode.
michael@0 614 *
michael@0 615 * The supplied widget can be any object implementing the following methods:
michael@0 616 * - function:nsIDOMNode insertItemAt(aIndex:number, aNode:nsIDOMNode, aValue:string)
michael@0 617 * - function:nsIDOMNode getItemAtIndex(aIndex:number)
michael@0 618 * - function removeChild(aChild:nsIDOMNode)
michael@0 619 * - function removeAllItems()
michael@0 620 * - get:nsIDOMNode selectedItem()
michael@0 621 * - set selectedItem(aChild:nsIDOMNode)
michael@0 622 * - function getAttribute(aName:string)
michael@0 623 * - function setAttribute(aName:string, aValue:string)
michael@0 624 * - function removeAttribute(aName:string)
michael@0 625 * - function addEventListener(aName:string, aCallback:function, aBubbleFlag:boolean)
michael@0 626 * - function removeEventListener(aName:string, aCallback:function, aBubbleFlag:boolean)
michael@0 627 *
michael@0 628 * Optional methods that can be implemented by the widget:
michael@0 629 * - function ensureElementIsVisible(aChild:nsIDOMNode)
michael@0 630 *
michael@0 631 * Optional attributes that may be handled (when calling get/set/removeAttribute):
michael@0 632 * - "emptyText": label temporarily added when there are no items present
michael@0 633 * - "headerText": label permanently added as a header
michael@0 634 *
michael@0 635 * For automagical keyboard and mouse accessibility, the widget should be an
michael@0 636 * event emitter with the following events:
michael@0 637 * - "keyPress" -> (aName:string, aEvent:KeyboardEvent)
michael@0 638 * - "mousePress" -> (aName:string, aEvent:MouseEvent)
michael@0 639 */
michael@0 640 this.WidgetMethods = {
michael@0 641 /**
michael@0 642 * Sets the element node or widget associated with this container.
michael@0 643 * @param nsIDOMNode | object aWidget
michael@0 644 */
michael@0 645 set widget(aWidget) {
michael@0 646 this._widget = aWidget;
michael@0 647
michael@0 648
michael@0 649 // Can't use a WeakMap for _itemsByValue because keys are strings, and
michael@0 650 // can't use one for _itemsByElement either, since it needs to be iterable.
michael@0 651 XPCOMUtils.defineLazyGetter(this, "_itemsByValue", () => new Map());
michael@0 652 XPCOMUtils.defineLazyGetter(this, "_itemsByElement", () => new Map());
michael@0 653 XPCOMUtils.defineLazyGetter(this, "_stagedItems", () => []);
michael@0 654
michael@0 655 // Handle internal events emitted by the widget if necessary.
michael@0 656 if (ViewHelpers.isEventEmitter(aWidget)) {
michael@0 657 aWidget.on("keyPress", this._onWidgetKeyPress.bind(this));
michael@0 658 aWidget.on("mousePress", this._onWidgetMousePress.bind(this));
michael@0 659 }
michael@0 660 },
michael@0 661
michael@0 662 /**
michael@0 663 * Gets the element node or widget associated with this container.
michael@0 664 * @return nsIDOMNode | object
michael@0 665 */
michael@0 666 get widget() this._widget,
michael@0 667
michael@0 668 /**
michael@0 669 * Prepares an item to be added to this container. This allows, for example,
michael@0 670 * for a large number of items to be batched up before being sorted & added.
michael@0 671 *
michael@0 672 * If the "staged" flag is *not* set to true, the item will be immediately
michael@0 673 * inserted at the correct position in this container, so that all the items
michael@0 674 * still remain sorted. This can (possibly) be much slower than batching up
michael@0 675 * multiple items.
michael@0 676 *
michael@0 677 * By default, this container assumes that all the items should be displayed
michael@0 678 * sorted by their value. This can be overridden with the "index" flag,
michael@0 679 * specifying on which position should an item be appended. The "staged" and
michael@0 680 * "index" flags are mutually exclusive, meaning that all staged items
michael@0 681 * will always be appended.
michael@0 682 *
michael@0 683 * @param nsIDOMNode aElement
michael@0 684 * A prebuilt node to be wrapped.
michael@0 685 * @param string aValue
michael@0 686 * A string identifying the node.
michael@0 687 * @param object aOptions [optional]
michael@0 688 * Additional options or flags supported by this operation:
michael@0 689 * - attachment: some attached primitive/object for the item
michael@0 690 * - staged: true to stage the item to be appended later
michael@0 691 * - index: specifies on which position should the item be appended
michael@0 692 * - attributes: a batch of attributes set to the displayed element
michael@0 693 * - finalize: function invoked when the item is removed
michael@0 694 * @return Item
michael@0 695 * The item associated with the displayed element if an unstaged push,
michael@0 696 * undefined if the item was staged for a later commit.
michael@0 697 */
michael@0 698 push: function([aElement, aValue], aOptions = {}) {
michael@0 699 let item = new Item(this, aElement, aValue, aOptions.attachment);
michael@0 700
michael@0 701 // Batch the item to be added later.
michael@0 702 if (aOptions.staged) {
michael@0 703 // An ulterior commit operation will ignore any specified index, so
michael@0 704 // no reason to keep it around.
michael@0 705 aOptions.index = undefined;
michael@0 706 return void this._stagedItems.push({ item: item, options: aOptions });
michael@0 707 }
michael@0 708 // Find the target position in this container and insert the item there.
michael@0 709 if (!("index" in aOptions)) {
michael@0 710 return this._insertItemAt(this._findExpectedIndexFor(item), item, aOptions);
michael@0 711 }
michael@0 712 // Insert the item at the specified index. If negative or out of bounds,
michael@0 713 // the item will be simply appended.
michael@0 714 return this._insertItemAt(aOptions.index, item, aOptions);
michael@0 715 },
michael@0 716
michael@0 717 /**
michael@0 718 * Flushes all the prepared items into this container.
michael@0 719 * Any specified index on the items will be ignored. Everything is appended.
michael@0 720 *
michael@0 721 * @param object aOptions [optional]
michael@0 722 * Additional options or flags supported by this operation:
michael@0 723 * - sorted: true to sort all the items before adding them
michael@0 724 */
michael@0 725 commit: function(aOptions = {}) {
michael@0 726 let stagedItems = this._stagedItems;
michael@0 727
michael@0 728 // Sort the items before adding them to this container, if preferred.
michael@0 729 if (aOptions.sorted) {
michael@0 730 stagedItems.sort((a, b) => this._currentSortPredicate(a.item, b.item));
michael@0 731 }
michael@0 732 // Append the prepared items to this container.
michael@0 733 for (let { item, options } of stagedItems) {
michael@0 734 this._insertItemAt(-1, item, options);
michael@0 735 }
michael@0 736 // Recreate the temporary items list for ulterior pushes.
michael@0 737 this._stagedItems.length = 0;
michael@0 738 },
michael@0 739
michael@0 740 /**
michael@0 741 * Immediately removes the specified item from this container.
michael@0 742 *
michael@0 743 * @param Item aItem
michael@0 744 * The item associated with the element to remove.
michael@0 745 */
michael@0 746 remove: function(aItem) {
michael@0 747 if (!aItem) {
michael@0 748 return;
michael@0 749 }
michael@0 750 this._widget.removeChild(aItem._target);
michael@0 751 this._untangleItem(aItem);
michael@0 752
michael@0 753 if (!this._itemsByElement.size) {
michael@0 754 this._preferredValue = this.selectedValue;
michael@0 755 this._widget.selectedItem = null;
michael@0 756 this._widget.setAttribute("emptyText", this._emptyText);
michael@0 757 }
michael@0 758 },
michael@0 759
michael@0 760 /**
michael@0 761 * Removes the item at the specified index from this container.
michael@0 762 *
michael@0 763 * @param number aIndex
michael@0 764 * The index of the item to remove.
michael@0 765 */
michael@0 766 removeAt: function(aIndex) {
michael@0 767 this.remove(this.getItemAtIndex(aIndex));
michael@0 768 },
michael@0 769
michael@0 770 /**
michael@0 771 * Removes all items from this container.
michael@0 772 */
michael@0 773 empty: function() {
michael@0 774 this._preferredValue = this.selectedValue;
michael@0 775 this._widget.selectedItem = null;
michael@0 776 this._widget.removeAllItems();
michael@0 777 this._widget.setAttribute("emptyText", this._emptyText);
michael@0 778
michael@0 779 for (let [, item] of this._itemsByElement) {
michael@0 780 this._untangleItem(item);
michael@0 781 }
michael@0 782
michael@0 783 this._itemsByValue.clear();
michael@0 784 this._itemsByElement.clear();
michael@0 785 this._stagedItems.length = 0;
michael@0 786 },
michael@0 787
michael@0 788 /**
michael@0 789 * Ensures the specified item is visible in this container.
michael@0 790 *
michael@0 791 * @param Item aItem
michael@0 792 * The item to bring into view.
michael@0 793 */
michael@0 794 ensureItemIsVisible: function(aItem) {
michael@0 795 this._widget.ensureElementIsVisible(aItem._target);
michael@0 796 },
michael@0 797
michael@0 798 /**
michael@0 799 * Ensures the item at the specified index is visible in this container.
michael@0 800 *
michael@0 801 * @param number aIndex
michael@0 802 * The index of the item to bring into view.
michael@0 803 */
michael@0 804 ensureIndexIsVisible: function(aIndex) {
michael@0 805 this.ensureItemIsVisible(this.getItemAtIndex(aIndex));
michael@0 806 },
michael@0 807
michael@0 808 /**
michael@0 809 * Sugar for ensuring the selected item is visible in this container.
michael@0 810 */
michael@0 811 ensureSelectedItemIsVisible: function() {
michael@0 812 this.ensureItemIsVisible(this.selectedItem);
michael@0 813 },
michael@0 814
michael@0 815 /**
michael@0 816 * If supported by the widget, the label string temporarily added to this
michael@0 817 * container when there are no child items present.
michael@0 818 */
michael@0 819 set emptyText(aValue) {
michael@0 820 this._emptyText = aValue;
michael@0 821
michael@0 822 // Apply the emptyText attribute right now if there are no child items.
michael@0 823 if (!this._itemsByElement.size) {
michael@0 824 this._widget.setAttribute("emptyText", aValue);
michael@0 825 }
michael@0 826 },
michael@0 827
michael@0 828 /**
michael@0 829 * If supported by the widget, the label string permanently added to this
michael@0 830 * container as a header.
michael@0 831 * @param string aValue
michael@0 832 */
michael@0 833 set headerText(aValue) {
michael@0 834 this._headerText = aValue;
michael@0 835 this._widget.setAttribute("headerText", aValue);
michael@0 836 },
michael@0 837
michael@0 838 /**
michael@0 839 * Toggles all the items in this container hidden or visible.
michael@0 840 *
michael@0 841 * This does not change the default filtering predicate, so newly inserted
michael@0 842 * items will always be visible. Use WidgetMethods.filterContents if you care.
michael@0 843 *
michael@0 844 * @param boolean aVisibleFlag
michael@0 845 * Specifies the intended visibility.
michael@0 846 */
michael@0 847 toggleContents: function(aVisibleFlag) {
michael@0 848 for (let [element, item] of this._itemsByElement) {
michael@0 849 element.hidden = !aVisibleFlag;
michael@0 850 }
michael@0 851 },
michael@0 852
michael@0 853 /**
michael@0 854 * Toggles all items in this container hidden or visible based on a predicate.
michael@0 855 *
michael@0 856 * @param function aPredicate [optional]
michael@0 857 * Items are toggled according to the return value of this function,
michael@0 858 * which will become the new default filtering predicate in this container.
michael@0 859 * If unspecified, all items will be toggled visible.
michael@0 860 */
michael@0 861 filterContents: function(aPredicate = this._currentFilterPredicate) {
michael@0 862 this._currentFilterPredicate = aPredicate;
michael@0 863
michael@0 864 for (let [element, item] of this._itemsByElement) {
michael@0 865 element.hidden = !aPredicate(item);
michael@0 866 }
michael@0 867 },
michael@0 868
michael@0 869 /**
michael@0 870 * Sorts all the items in this container based on a predicate.
michael@0 871 *
michael@0 872 * @param function aPredicate [optional]
michael@0 873 * Items are sorted according to the return value of the function,
michael@0 874 * which will become the new default sorting predicate in this container.
michael@0 875 * If unspecified, all items will be sorted by their value.
michael@0 876 */
michael@0 877 sortContents: function(aPredicate = this._currentSortPredicate) {
michael@0 878 let sortedItems = this.items.sort(this._currentSortPredicate = aPredicate);
michael@0 879
michael@0 880 for (let i = 0, len = sortedItems.length; i < len; i++) {
michael@0 881 this.swapItems(this.getItemAtIndex(i), sortedItems[i]);
michael@0 882 }
michael@0 883 },
michael@0 884
michael@0 885 /**
michael@0 886 * Visually swaps two items in this container.
michael@0 887 *
michael@0 888 * @param Item aFirst
michael@0 889 * The first item to be swapped.
michael@0 890 * @param Item aSecond
michael@0 891 * The second item to be swapped.
michael@0 892 */
michael@0 893 swapItems: function(aFirst, aSecond) {
michael@0 894 if (aFirst == aSecond) { // We're just dandy, thank you.
michael@0 895 return;
michael@0 896 }
michael@0 897 let { _prebuiltNode: firstPrebuiltTarget, _target: firstTarget } = aFirst;
michael@0 898 let { _prebuiltNode: secondPrebuiltTarget, _target: secondTarget } = aSecond;
michael@0 899
michael@0 900 // If the two items were constructed with prebuilt nodes as DocumentFragments,
michael@0 901 // then those DocumentFragments are now empty and need to be reassembled.
michael@0 902 if (firstPrebuiltTarget instanceof Ci.nsIDOMDocumentFragment) {
michael@0 903 for (let node of firstTarget.childNodes) {
michael@0 904 firstPrebuiltTarget.appendChild(node.cloneNode(true));
michael@0 905 }
michael@0 906 }
michael@0 907 if (secondPrebuiltTarget instanceof Ci.nsIDOMDocumentFragment) {
michael@0 908 for (let node of secondTarget.childNodes) {
michael@0 909 secondPrebuiltTarget.appendChild(node.cloneNode(true));
michael@0 910 }
michael@0 911 }
michael@0 912
michael@0 913 // 1. Get the indices of the two items to swap.
michael@0 914 let i = this._indexOfElement(firstTarget);
michael@0 915 let j = this._indexOfElement(secondTarget);
michael@0 916
michael@0 917 // 2. Remeber the selection index, to reselect an item, if necessary.
michael@0 918 let selectedTarget = this._widget.selectedItem;
michael@0 919 let selectedIndex = -1;
michael@0 920 if (selectedTarget == firstTarget) {
michael@0 921 selectedIndex = i;
michael@0 922 } else if (selectedTarget == secondTarget) {
michael@0 923 selectedIndex = j;
michael@0 924 }
michael@0 925
michael@0 926 // 3. Silently nuke both items, nobody needs to know about this.
michael@0 927 this._widget.removeChild(firstTarget);
michael@0 928 this._widget.removeChild(secondTarget);
michael@0 929 this._unlinkItem(aFirst);
michael@0 930 this._unlinkItem(aSecond);
michael@0 931
michael@0 932 // 4. Add the items again, but reversing their indices.
michael@0 933 this._insertItemAt.apply(this, i < j ? [i, aSecond] : [j, aFirst]);
michael@0 934 this._insertItemAt.apply(this, i < j ? [j, aFirst] : [i, aSecond]);
michael@0 935
michael@0 936 // 5. Restore the previous selection, if necessary.
michael@0 937 if (selectedIndex == i) {
michael@0 938 this._widget.selectedItem = aFirst._target;
michael@0 939 } else if (selectedIndex == j) {
michael@0 940 this._widget.selectedItem = aSecond._target;
michael@0 941 }
michael@0 942
michael@0 943 // 6. Let the outside world know that these two items were swapped.
michael@0 944 ViewHelpers.dispatchEvent(aFirst.target, "swap", [aSecond, aFirst]);
michael@0 945 },
michael@0 946
michael@0 947 /**
michael@0 948 * Visually swaps two items in this container at specific indices.
michael@0 949 *
michael@0 950 * @param number aFirst
michael@0 951 * The index of the first item to be swapped.
michael@0 952 * @param number aSecond
michael@0 953 * The index of the second item to be swapped.
michael@0 954 */
michael@0 955 swapItemsAtIndices: function(aFirst, aSecond) {
michael@0 956 this.swapItems(this.getItemAtIndex(aFirst), this.getItemAtIndex(aSecond));
michael@0 957 },
michael@0 958
michael@0 959 /**
michael@0 960 * Checks whether an item with the specified value is among the elements
michael@0 961 * shown in this container.
michael@0 962 *
michael@0 963 * @param string aValue
michael@0 964 * The item's value.
michael@0 965 * @return boolean
michael@0 966 * True if the value is known, false otherwise.
michael@0 967 */
michael@0 968 containsValue: function(aValue) {
michael@0 969 return this._itemsByValue.has(aValue) ||
michael@0 970 this._stagedItems.some(({ item }) => item._value == aValue);
michael@0 971 },
michael@0 972
michael@0 973 /**
michael@0 974 * Gets the "preferred value". This is the latest selected item's value,
michael@0 975 * remembered just before emptying this container.
michael@0 976 * @return string
michael@0 977 */
michael@0 978 get preferredValue() {
michael@0 979 return this._preferredValue;
michael@0 980 },
michael@0 981
michael@0 982 /**
michael@0 983 * Retrieves the item associated with the selected element.
michael@0 984 * @return Item | null
michael@0 985 */
michael@0 986 get selectedItem() {
michael@0 987 let selectedElement = this._widget.selectedItem;
michael@0 988 if (selectedElement) {
michael@0 989 return this._itemsByElement.get(selectedElement);
michael@0 990 }
michael@0 991 return null;
michael@0 992 },
michael@0 993
michael@0 994 /**
michael@0 995 * Retrieves the selected element's index in this container.
michael@0 996 * @return number
michael@0 997 */
michael@0 998 get selectedIndex() {
michael@0 999 let selectedElement = this._widget.selectedItem;
michael@0 1000 if (selectedElement) {
michael@0 1001 return this._indexOfElement(selectedElement);
michael@0 1002 }
michael@0 1003 return -1;
michael@0 1004 },
michael@0 1005
michael@0 1006 /**
michael@0 1007 * Retrieves the value of the selected element.
michael@0 1008 * @return string
michael@0 1009 */
michael@0 1010 get selectedValue() {
michael@0 1011 let selectedElement = this._widget.selectedItem;
michael@0 1012 if (selectedElement) {
michael@0 1013 return this._itemsByElement.get(selectedElement)._value;
michael@0 1014 }
michael@0 1015 return "";
michael@0 1016 },
michael@0 1017
michael@0 1018 /**
michael@0 1019 * Retrieves the attachment of the selected element.
michael@0 1020 * @return object | null
michael@0 1021 */
michael@0 1022 get selectedAttachment() {
michael@0 1023 let selectedElement = this._widget.selectedItem;
michael@0 1024 if (selectedElement) {
michael@0 1025 return this._itemsByElement.get(selectedElement).attachment;
michael@0 1026 }
michael@0 1027 return null;
michael@0 1028 },
michael@0 1029
michael@0 1030 /**
michael@0 1031 * Selects the element with the entangled item in this container.
michael@0 1032 * @param Item | function aItem
michael@0 1033 */
michael@0 1034 set selectedItem(aItem) {
michael@0 1035 // A predicate is allowed to select a specific item.
michael@0 1036 // If no item is matched, then the current selection is removed.
michael@0 1037 if (typeof aItem == "function") {
michael@0 1038 aItem = this.getItemForPredicate(aItem);
michael@0 1039 }
michael@0 1040
michael@0 1041 // A falsy item is allowed to invalidate the current selection.
michael@0 1042 let targetElement = aItem ? aItem._target : null;
michael@0 1043 let prevElement = this._widget.selectedItem;
michael@0 1044
michael@0 1045 // Make sure the selected item's target element is focused and visible.
michael@0 1046 if (this.autoFocusOnSelection && targetElement) {
michael@0 1047 targetElement.focus();
michael@0 1048 }
michael@0 1049 if (this.maintainSelectionVisible && targetElement) {
michael@0 1050 if ("ensureElementIsVisible" in this._widget) {
michael@0 1051 this._widget.ensureElementIsVisible(targetElement);
michael@0 1052 }
michael@0 1053 }
michael@0 1054
michael@0 1055 // Prevent selecting the same item again and avoid dispatching
michael@0 1056 // a redundant selection event, so return early.
michael@0 1057 if (targetElement != prevElement) {
michael@0 1058 this._widget.selectedItem = targetElement;
michael@0 1059 let dispTarget = targetElement || prevElement;
michael@0 1060 let dispName = this.suppressSelectionEvents ? "suppressed-select" : "select";
michael@0 1061 ViewHelpers.dispatchEvent(dispTarget, dispName, aItem);
michael@0 1062 }
michael@0 1063 },
michael@0 1064
michael@0 1065 /**
michael@0 1066 * Selects the element at the specified index in this container.
michael@0 1067 * @param number aIndex
michael@0 1068 */
michael@0 1069 set selectedIndex(aIndex) {
michael@0 1070 let targetElement = this._widget.getItemAtIndex(aIndex);
michael@0 1071 if (targetElement) {
michael@0 1072 this.selectedItem = this._itemsByElement.get(targetElement);
michael@0 1073 return;
michael@0 1074 }
michael@0 1075 this.selectedItem = null;
michael@0 1076 },
michael@0 1077
michael@0 1078 /**
michael@0 1079 * Selects the element with the specified value in this container.
michael@0 1080 * @param string aValue
michael@0 1081 */
michael@0 1082 set selectedValue(aValue) {
michael@0 1083 this.selectedItem = this._itemsByValue.get(aValue);
michael@0 1084 },
michael@0 1085
michael@0 1086 /**
michael@0 1087 * Specifies if this container should try to keep the selected item visible.
michael@0 1088 * (For example, when new items are added the selection is brought into view).
michael@0 1089 */
michael@0 1090 maintainSelectionVisible: true,
michael@0 1091
michael@0 1092 /**
michael@0 1093 * Specifies if "select" events dispatched from the elements in this container
michael@0 1094 * when their respective items are selected should be suppressed or not.
michael@0 1095 *
michael@0 1096 * If this flag is set to true, then consumers of this container won't
michael@0 1097 * be normally notified when items are selected.
michael@0 1098 */
michael@0 1099 suppressSelectionEvents: false,
michael@0 1100
michael@0 1101 /**
michael@0 1102 * Focus this container the first time an element is inserted?
michael@0 1103 *
michael@0 1104 * If this flag is set to true, then when the first item is inserted in
michael@0 1105 * this container (and thus it's the only item available), its corresponding
michael@0 1106 * target element is focused as well.
michael@0 1107 */
michael@0 1108 autoFocusOnFirstItem: true,
michael@0 1109
michael@0 1110 /**
michael@0 1111 * Focus on selection?
michael@0 1112 *
michael@0 1113 * If this flag is set to true, then whenever an item is selected in
michael@0 1114 * this container (e.g. via the selectedIndex or selectedItem setters),
michael@0 1115 * its corresponding target element is focused as well.
michael@0 1116 *
michael@0 1117 * You can disable this flag, for example, to maintain a certain node
michael@0 1118 * focused but visually indicate a different selection in this container.
michael@0 1119 */
michael@0 1120 autoFocusOnSelection: true,
michael@0 1121
michael@0 1122 /**
michael@0 1123 * Focus on input (e.g. mouse click)?
michael@0 1124 *
michael@0 1125 * If this flag is set to true, then whenever an item receives user input in
michael@0 1126 * this container, its corresponding target element is focused as well.
michael@0 1127 */
michael@0 1128 autoFocusOnInput: true,
michael@0 1129
michael@0 1130 /**
michael@0 1131 * When focusing on input, allow right clicks?
michael@0 1132 * @see WidgetMethods.autoFocusOnInput
michael@0 1133 */
michael@0 1134 allowFocusOnRightClick: false,
michael@0 1135
michael@0 1136 /**
michael@0 1137 * The number of elements in this container to jump when Page Up or Page Down
michael@0 1138 * keys are pressed. If falsy, then the page size will be based on the
michael@0 1139 * number of visible items in the container.
michael@0 1140 */
michael@0 1141 pageSize: 0,
michael@0 1142
michael@0 1143 /**
michael@0 1144 * Focuses the first visible item in this container.
michael@0 1145 */
michael@0 1146 focusFirstVisibleItem: function() {
michael@0 1147 this.focusItemAtDelta(-this.itemCount);
michael@0 1148 },
michael@0 1149
michael@0 1150 /**
michael@0 1151 * Focuses the last visible item in this container.
michael@0 1152 */
michael@0 1153 focusLastVisibleItem: function() {
michael@0 1154 this.focusItemAtDelta(+this.itemCount);
michael@0 1155 },
michael@0 1156
michael@0 1157 /**
michael@0 1158 * Focuses the next item in this container.
michael@0 1159 */
michael@0 1160 focusNextItem: function() {
michael@0 1161 this.focusItemAtDelta(+1);
michael@0 1162 },
michael@0 1163
michael@0 1164 /**
michael@0 1165 * Focuses the previous item in this container.
michael@0 1166 */
michael@0 1167 focusPrevItem: function() {
michael@0 1168 this.focusItemAtDelta(-1);
michael@0 1169 },
michael@0 1170
michael@0 1171 /**
michael@0 1172 * Focuses another item in this container based on the index distance
michael@0 1173 * from the currently focused item.
michael@0 1174 *
michael@0 1175 * @param number aDelta
michael@0 1176 * A scalar specifying by how many items should the selection change.
michael@0 1177 */
michael@0 1178 focusItemAtDelta: function(aDelta) {
michael@0 1179 // Make sure the currently selected item is also focused, so that the
michael@0 1180 // command dispatcher mechanism has a relative node to work with.
michael@0 1181 // If there's no selection, just select an item at a corresponding index
michael@0 1182 // (e.g. the first item in this container if aDelta <= 1).
michael@0 1183 let selectedElement = this._widget.selectedItem;
michael@0 1184 if (selectedElement) {
michael@0 1185 selectedElement.focus();
michael@0 1186 } else {
michael@0 1187 this.selectedIndex = Math.max(0, aDelta - 1);
michael@0 1188 return;
michael@0 1189 }
michael@0 1190
michael@0 1191 let direction = aDelta > 0 ? "advanceFocus" : "rewindFocus";
michael@0 1192 let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta));
michael@0 1193 while (distance--) {
michael@0 1194 if (!this._focusChange(direction)) {
michael@0 1195 break; // Out of bounds.
michael@0 1196 }
michael@0 1197 }
michael@0 1198
michael@0 1199 // Synchronize the selected item as being the currently focused element.
michael@0 1200 this.selectedItem = this.getItemForElement(this._focusedElement);
michael@0 1201 },
michael@0 1202
michael@0 1203 /**
michael@0 1204 * Focuses the next or previous item in this container.
michael@0 1205 *
michael@0 1206 * @param string aDirection
michael@0 1207 * Either "advanceFocus" or "rewindFocus".
michael@0 1208 * @return boolean
michael@0 1209 * False if the focus went out of bounds and the first or last item
michael@0 1210 * in this container was focused instead.
michael@0 1211 */
michael@0 1212 _focusChange: function(aDirection) {
michael@0 1213 let commandDispatcher = this._commandDispatcher;
michael@0 1214 let prevFocusedElement = commandDispatcher.focusedElement;
michael@0 1215 let currFocusedElement;
michael@0 1216
michael@0 1217 do {
michael@0 1218 commandDispatcher.suppressFocusScroll = true;
michael@0 1219 commandDispatcher[aDirection]();
michael@0 1220 currFocusedElement = commandDispatcher.focusedElement;
michael@0 1221
michael@0 1222 // Make sure the newly focused item is a part of this container. If the
michael@0 1223 // focus goes out of bounds, revert the previously focused item.
michael@0 1224 if (!this.getItemForElement(currFocusedElement)) {
michael@0 1225 prevFocusedElement.focus();
michael@0 1226 return false;
michael@0 1227 }
michael@0 1228 } while (!WIDGET_FOCUSABLE_NODES.has(currFocusedElement.tagName));
michael@0 1229
michael@0 1230 // Focus remained within bounds.
michael@0 1231 return true;
michael@0 1232 },
michael@0 1233
michael@0 1234 /**
michael@0 1235 * Gets the command dispatcher instance associated with this container's DOM.
michael@0 1236 * If there are no items displayed in this container, null is returned.
michael@0 1237 * @return nsIDOMXULCommandDispatcher | null
michael@0 1238 */
michael@0 1239 get _commandDispatcher() {
michael@0 1240 if (this._cachedCommandDispatcher) {
michael@0 1241 return this._cachedCommandDispatcher;
michael@0 1242 }
michael@0 1243 let someElement = this._widget.getItemAtIndex(0);
michael@0 1244 if (someElement) {
michael@0 1245 let commandDispatcher = someElement.ownerDocument.commandDispatcher;
michael@0 1246 return this._cachedCommandDispatcher = commandDispatcher;
michael@0 1247 }
michael@0 1248 return null;
michael@0 1249 },
michael@0 1250
michael@0 1251 /**
michael@0 1252 * Gets the currently focused element in this container.
michael@0 1253 *
michael@0 1254 * @return nsIDOMNode
michael@0 1255 * The focused element, or null if nothing is found.
michael@0 1256 */
michael@0 1257 get _focusedElement() {
michael@0 1258 let commandDispatcher = this._commandDispatcher;
michael@0 1259 if (commandDispatcher) {
michael@0 1260 return commandDispatcher.focusedElement;
michael@0 1261 }
michael@0 1262 return null;
michael@0 1263 },
michael@0 1264
michael@0 1265 /**
michael@0 1266 * Gets the item in the container having the specified index.
michael@0 1267 *
michael@0 1268 * @param number aIndex
michael@0 1269 * The index used to identify the element.
michael@0 1270 * @return Item
michael@0 1271 * The matched item, or null if nothing is found.
michael@0 1272 */
michael@0 1273 getItemAtIndex: function(aIndex) {
michael@0 1274 return this.getItemForElement(this._widget.getItemAtIndex(aIndex));
michael@0 1275 },
michael@0 1276
michael@0 1277 /**
michael@0 1278 * Gets the item in the container having the specified value.
michael@0 1279 *
michael@0 1280 * @param string aValue
michael@0 1281 * The value used to identify the element.
michael@0 1282 * @return Item
michael@0 1283 * The matched item, or null if nothing is found.
michael@0 1284 */
michael@0 1285 getItemByValue: function(aValue) {
michael@0 1286 return this._itemsByValue.get(aValue);
michael@0 1287 },
michael@0 1288
michael@0 1289 /**
michael@0 1290 * Gets the item in the container associated with the specified element.
michael@0 1291 *
michael@0 1292 * @param nsIDOMNode aElement
michael@0 1293 * The element used to identify the item.
michael@0 1294 * @param object aFlags [optional]
michael@0 1295 * Additional options for showing the source. Supported options:
michael@0 1296 * - noSiblings: if siblings shouldn't be taken into consideration
michael@0 1297 * when searching for the associated item.
michael@0 1298 * @return Item
michael@0 1299 * The matched item, or null if nothing is found.
michael@0 1300 */
michael@0 1301 getItemForElement: function(aElement, aFlags = {}) {
michael@0 1302 while (aElement) {
michael@0 1303 let item = this._itemsByElement.get(aElement);
michael@0 1304
michael@0 1305 // Also search the siblings if allowed.
michael@0 1306 if (!aFlags.noSiblings) {
michael@0 1307 item = item ||
michael@0 1308 this._itemsByElement.get(aElement.nextElementSibling) ||
michael@0 1309 this._itemsByElement.get(aElement.previousElementSibling);
michael@0 1310 }
michael@0 1311 if (item) {
michael@0 1312 return item;
michael@0 1313 }
michael@0 1314 aElement = aElement.parentNode;
michael@0 1315 }
michael@0 1316 return null;
michael@0 1317 },
michael@0 1318
michael@0 1319 /**
michael@0 1320 * Gets a visible item in this container validating a specified predicate.
michael@0 1321 *
michael@0 1322 * @param function aPredicate
michael@0 1323 * The first item which validates this predicate is returned
michael@0 1324 * @return Item
michael@0 1325 * The matched item, or null if nothing is found.
michael@0 1326 */
michael@0 1327 getItemForPredicate: function(aPredicate, aOwner = this) {
michael@0 1328 // Recursively check the items in this widget for a predicate match.
michael@0 1329 for (let [element, item] of aOwner._itemsByElement) {
michael@0 1330 let match;
michael@0 1331 if (aPredicate(item) && !element.hidden) {
michael@0 1332 match = item;
michael@0 1333 } else {
michael@0 1334 match = this.getItemForPredicate(aPredicate, item);
michael@0 1335 }
michael@0 1336 if (match) {
michael@0 1337 return match;
michael@0 1338 }
michael@0 1339 }
michael@0 1340 // Also check the staged items. No need to do this recursively since
michael@0 1341 // they're not even appended to the view yet.
michael@0 1342 for (let { item } of this._stagedItems) {
michael@0 1343 if (aPredicate(item)) {
michael@0 1344 return item;
michael@0 1345 }
michael@0 1346 }
michael@0 1347 return null;
michael@0 1348 },
michael@0 1349
michael@0 1350 /**
michael@0 1351 * Shortcut function for getItemForPredicate which works on item attachments.
michael@0 1352 * @see getItemForPredicate
michael@0 1353 */
michael@0 1354 getItemForAttachment: function(aPredicate, aOwner = this) {
michael@0 1355 return this.getItemForPredicate(e => aPredicate(e.attachment));
michael@0 1356 },
michael@0 1357
michael@0 1358 /**
michael@0 1359 * Finds the index of an item in the container.
michael@0 1360 *
michael@0 1361 * @param Item aItem
michael@0 1362 * The item get the index for.
michael@0 1363 * @return number
michael@0 1364 * The index of the matched item, or -1 if nothing is found.
michael@0 1365 */
michael@0 1366 indexOfItem: function(aItem) {
michael@0 1367 return this._indexOfElement(aItem._target);
michael@0 1368 },
michael@0 1369
michael@0 1370 /**
michael@0 1371 * Finds the index of an element in the container.
michael@0 1372 *
michael@0 1373 * @param nsIDOMNode aElement
michael@0 1374 * The element get the index for.
michael@0 1375 * @return number
michael@0 1376 * The index of the matched element, or -1 if nothing is found.
michael@0 1377 */
michael@0 1378 _indexOfElement: function(aElement) {
michael@0 1379 for (let i = 0; i < this._itemsByElement.size; i++) {
michael@0 1380 if (this._widget.getItemAtIndex(i) == aElement) {
michael@0 1381 return i;
michael@0 1382 }
michael@0 1383 }
michael@0 1384 return -1;
michael@0 1385 },
michael@0 1386
michael@0 1387 /**
michael@0 1388 * Gets the total number of items in this container.
michael@0 1389 * @return number
michael@0 1390 */
michael@0 1391 get itemCount() {
michael@0 1392 return this._itemsByElement.size;
michael@0 1393 },
michael@0 1394
michael@0 1395 /**
michael@0 1396 * Returns a list of items in this container, in the displayed order.
michael@0 1397 * @return array
michael@0 1398 */
michael@0 1399 get items() {
michael@0 1400 let store = [];
michael@0 1401 let itemCount = this.itemCount;
michael@0 1402 for (let i = 0; i < itemCount; i++) {
michael@0 1403 store.push(this.getItemAtIndex(i));
michael@0 1404 }
michael@0 1405 return store;
michael@0 1406 },
michael@0 1407
michael@0 1408 /**
michael@0 1409 * Returns a list of values in this container, in the displayed order.
michael@0 1410 * @return array
michael@0 1411 */
michael@0 1412 get values() {
michael@0 1413 return this.items.map(e => e._value);
michael@0 1414 },
michael@0 1415
michael@0 1416 /**
michael@0 1417 * Returns a list of attachments in this container, in the displayed order.
michael@0 1418 * @return array
michael@0 1419 */
michael@0 1420 get attachments() {
michael@0 1421 return this.items.map(e => e.attachment);
michael@0 1422 },
michael@0 1423
michael@0 1424 /**
michael@0 1425 * Returns a list of all the visible (non-hidden) items in this container,
michael@0 1426 * in the displayed order
michael@0 1427 * @return array
michael@0 1428 */
michael@0 1429 get visibleItems() {
michael@0 1430 return this.items.filter(e => !e._target.hidden);
michael@0 1431 },
michael@0 1432
michael@0 1433 /**
michael@0 1434 * Checks if an item is unique in this container. If an item's value is an
michael@0 1435 * empty string, "undefined" or "null", it is considered unique.
michael@0 1436 *
michael@0 1437 * @param Item aItem
michael@0 1438 * The item for which to verify uniqueness.
michael@0 1439 * @return boolean
michael@0 1440 * True if the item is unique, false otherwise.
michael@0 1441 */
michael@0 1442 isUnique: function(aItem) {
michael@0 1443 let value = aItem._value;
michael@0 1444 if (value == "" || value == "undefined" || value == "null") {
michael@0 1445 return true;
michael@0 1446 }
michael@0 1447 return !this._itemsByValue.has(value);
michael@0 1448 },
michael@0 1449
michael@0 1450 /**
michael@0 1451 * Checks if an item is eligible for this container. By default, this checks
michael@0 1452 * whether an item is unique and has a prebuilt target node.
michael@0 1453 *
michael@0 1454 * @param Item aItem
michael@0 1455 * The item for which to verify eligibility.
michael@0 1456 * @return boolean
michael@0 1457 * True if the item is eligible, false otherwise.
michael@0 1458 */
michael@0 1459 isEligible: function(aItem) {
michael@0 1460 return this.isUnique(aItem) && aItem._prebuiltNode;
michael@0 1461 },
michael@0 1462
michael@0 1463 /**
michael@0 1464 * Finds the expected item index in this container based on the default
michael@0 1465 * sort predicate.
michael@0 1466 *
michael@0 1467 * @param Item aItem
michael@0 1468 * The item for which to get the expected index.
michael@0 1469 * @return number
michael@0 1470 * The expected item index.
michael@0 1471 */
michael@0 1472 _findExpectedIndexFor: function(aItem) {
michael@0 1473 let itemCount = this.itemCount;
michael@0 1474 for (let i = 0; i < itemCount; i++) {
michael@0 1475 if (this._currentSortPredicate(this.getItemAtIndex(i), aItem) > 0) {
michael@0 1476 return i;
michael@0 1477 }
michael@0 1478 }
michael@0 1479 return itemCount;
michael@0 1480 },
michael@0 1481
michael@0 1482 /**
michael@0 1483 * Immediately inserts an item in this container at the specified index.
michael@0 1484 *
michael@0 1485 * @param number aIndex
michael@0 1486 * The position in the container intended for this item.
michael@0 1487 * @param Item aItem
michael@0 1488 * The item describing a target element.
michael@0 1489 * @param object aOptions [optional]
michael@0 1490 * Additional options or flags supported by this operation:
michael@0 1491 * - attributes: a batch of attributes set to the displayed element
michael@0 1492 * - finalize: function when the item is untangled (removed)
michael@0 1493 * @return Item
michael@0 1494 * The item associated with the displayed element, null if rejected.
michael@0 1495 */
michael@0 1496 _insertItemAt: function(aIndex, aItem, aOptions = {}) {
michael@0 1497 if (!this.isEligible(aItem)) {
michael@0 1498 return null;
michael@0 1499 }
michael@0 1500
michael@0 1501 // Entangle the item with the newly inserted node.
michael@0 1502 // Make sure this is done with the value returned by insertItemAt(),
michael@0 1503 // to avoid storing a potential DocumentFragment.
michael@0 1504 let node = aItem._prebuiltNode;
michael@0 1505 let attachment = aItem.attachment;
michael@0 1506 this._entangleItem(aItem, this._widget.insertItemAt(aIndex, node, attachment));
michael@0 1507
michael@0 1508 // Handle any additional options after entangling the item.
michael@0 1509 if (!this._currentFilterPredicate(aItem)) {
michael@0 1510 aItem._target.hidden = true;
michael@0 1511 }
michael@0 1512 if (this.autoFocusOnFirstItem && this._itemsByElement.size == 1) {
michael@0 1513 aItem._target.focus();
michael@0 1514 }
michael@0 1515 if (aOptions.attributes) {
michael@0 1516 aOptions.attributes.forEach(e => aItem._target.setAttribute(e[0], e[1]));
michael@0 1517 }
michael@0 1518 if (aOptions.finalize) {
michael@0 1519 aItem.finalize = aOptions.finalize;
michael@0 1520 }
michael@0 1521
michael@0 1522 // Hide the empty text if the selection wasn't lost.
michael@0 1523 this._widget.removeAttribute("emptyText");
michael@0 1524
michael@0 1525 // Return the item associated with the displayed element.
michael@0 1526 return aItem;
michael@0 1527 },
michael@0 1528
michael@0 1529 /**
michael@0 1530 * Entangles an item (model) with a displayed node element (view).
michael@0 1531 *
michael@0 1532 * @param Item aItem
michael@0 1533 * The item describing a target element.
michael@0 1534 * @param nsIDOMNode aElement
michael@0 1535 * The element displaying the item.
michael@0 1536 */
michael@0 1537 _entangleItem: function(aItem, aElement) {
michael@0 1538 this._itemsByValue.set(aItem._value, aItem);
michael@0 1539 this._itemsByElement.set(aElement, aItem);
michael@0 1540 aItem._target = aElement;
michael@0 1541 },
michael@0 1542
michael@0 1543 /**
michael@0 1544 * Untangles an item (model) from a displayed node element (view).
michael@0 1545 *
michael@0 1546 * @param Item aItem
michael@0 1547 * The item describing a target element.
michael@0 1548 */
michael@0 1549 _untangleItem: function(aItem) {
michael@0 1550 if (aItem.finalize) {
michael@0 1551 aItem.finalize(aItem);
michael@0 1552 }
michael@0 1553 for (let childItem of aItem) {
michael@0 1554 aItem.remove(childItem);
michael@0 1555 }
michael@0 1556
michael@0 1557 this._unlinkItem(aItem);
michael@0 1558 aItem._target = null;
michael@0 1559 },
michael@0 1560
michael@0 1561 /**
michael@0 1562 * Deletes an item from the its parent's storage maps.
michael@0 1563 *
michael@0 1564 * @param Item aItem
michael@0 1565 * The item describing a target element.
michael@0 1566 */
michael@0 1567 _unlinkItem: function(aItem) {
michael@0 1568 this._itemsByValue.delete(aItem._value);
michael@0 1569 this._itemsByElement.delete(aItem._target);
michael@0 1570 },
michael@0 1571
michael@0 1572 /**
michael@0 1573 * The keyPress event listener for this container.
michael@0 1574 * @param string aName
michael@0 1575 * @param KeyboardEvent aEvent
michael@0 1576 */
michael@0 1577 _onWidgetKeyPress: function(aName, aEvent) {
michael@0 1578 // Prevent scrolling when pressing navigation keys.
michael@0 1579 ViewHelpers.preventScrolling(aEvent);
michael@0 1580
michael@0 1581 switch (aEvent.keyCode) {
michael@0 1582 case aEvent.DOM_VK_UP:
michael@0 1583 case aEvent.DOM_VK_LEFT:
michael@0 1584 this.focusPrevItem();
michael@0 1585 return;
michael@0 1586 case aEvent.DOM_VK_DOWN:
michael@0 1587 case aEvent.DOM_VK_RIGHT:
michael@0 1588 this.focusNextItem();
michael@0 1589 return;
michael@0 1590 case aEvent.DOM_VK_PAGE_UP:
michael@0 1591 this.focusItemAtDelta(-(this.pageSize || (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
michael@0 1592 return;
michael@0 1593 case aEvent.DOM_VK_PAGE_DOWN:
michael@0 1594 this.focusItemAtDelta(+(this.pageSize || (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
michael@0 1595 return;
michael@0 1596 case aEvent.DOM_VK_HOME:
michael@0 1597 this.focusFirstVisibleItem();
michael@0 1598 return;
michael@0 1599 case aEvent.DOM_VK_END:
michael@0 1600 this.focusLastVisibleItem();
michael@0 1601 return;
michael@0 1602 }
michael@0 1603 },
michael@0 1604
michael@0 1605 /**
michael@0 1606 * The mousePress event listener for this container.
michael@0 1607 * @param string aName
michael@0 1608 * @param MouseEvent aEvent
michael@0 1609 */
michael@0 1610 _onWidgetMousePress: function(aName, aEvent) {
michael@0 1611 if (aEvent.button != 0 && !this.allowFocusOnRightClick) {
michael@0 1612 // Only allow left-click to trigger this event.
michael@0 1613 return;
michael@0 1614 }
michael@0 1615
michael@0 1616 let item = this.getItemForElement(aEvent.target);
michael@0 1617 if (item) {
michael@0 1618 // The container is not empty and we clicked on an actual item.
michael@0 1619 this.selectedItem = item;
michael@0 1620 // Make sure the current event's target element is also focused.
michael@0 1621 this.autoFocusOnInput && item._target.focus();
michael@0 1622 }
michael@0 1623 },
michael@0 1624
michael@0 1625 /**
michael@0 1626 * The predicate used when filtering items. By default, all items in this
michael@0 1627 * view are visible.
michael@0 1628 *
michael@0 1629 * @param Item aItem
michael@0 1630 * The item passing through the filter.
michael@0 1631 * @return boolean
michael@0 1632 * True if the item should be visible, false otherwise.
michael@0 1633 */
michael@0 1634 _currentFilterPredicate: function(aItem) {
michael@0 1635 return true;
michael@0 1636 },
michael@0 1637
michael@0 1638 /**
michael@0 1639 * The predicate used when sorting items. By default, items in this view
michael@0 1640 * are sorted by their label.
michael@0 1641 *
michael@0 1642 * @param Item aFirst
michael@0 1643 * The first item used in the comparison.
michael@0 1644 * @param Item aSecond
michael@0 1645 * The second item used in the comparison.
michael@0 1646 * @return number
michael@0 1647 * -1 to sort aFirst to a lower index than aSecond
michael@0 1648 * 0 to leave aFirst and aSecond unchanged with respect to each other
michael@0 1649 * 1 to sort aSecond to a lower index than aFirst
michael@0 1650 */
michael@0 1651 _currentSortPredicate: function(aFirst, aSecond) {
michael@0 1652 return +(aFirst._value.toLowerCase() > aSecond._value.toLowerCase());
michael@0 1653 },
michael@0 1654
michael@0 1655 /**
michael@0 1656 * Call a method on this widget named `aMethodName`. Any further arguments are
michael@0 1657 * passed on to the method. Returns the result of the method call.
michael@0 1658 *
michael@0 1659 * @param String aMethodName
michael@0 1660 * The name of the method you want to call.
michael@0 1661 * @param aArgs
michael@0 1662 * Optional. Any arguments you want to pass through to the method.
michael@0 1663 */
michael@0 1664 callMethod: function(aMethodName, ...aArgs) {
michael@0 1665 return this._widget[aMethodName].apply(this._widget, aArgs);
michael@0 1666 },
michael@0 1667
michael@0 1668 _widget: null,
michael@0 1669 _emptyText: "",
michael@0 1670 _headerText: "",
michael@0 1671 _preferredValue: "",
michael@0 1672 _cachedCommandDispatcher: null
michael@0 1673 };
michael@0 1674
michael@0 1675 /**
michael@0 1676 * A generator-iterator over all the items in this container.
michael@0 1677 */
michael@0 1678 Item.prototype["@@iterator"] =
michael@0 1679 WidgetMethods["@@iterator"] = function*() {
michael@0 1680 yield* this._itemsByElement.values();
michael@0 1681 };

mercurial