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