12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625 |
- /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- "use strict";
- const {KeyCodes} = require("devtools/client/shared/keycodes");
- const PANE_APPEARANCE_DELAY = 50;
- const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
- const WIDGET_FOCUSABLE_NODES = new Set(["vbox", "hbox"]);
- var namedTimeoutsStore = new Map();
- /**
- * Inheritance helpers from the addon SDK's core/heritage.
- * Remove these when all devtools are loadered.
- */
- exports.Heritage = {
- /**
- * @see extend in sdk/core/heritage.
- */
- extend: function (prototype, properties = {}) {
- return Object.create(prototype, this.getOwnPropertyDescriptors(properties));
- },
- /**
- * @see getOwnPropertyDescriptors in sdk/core/heritage.
- */
- getOwnPropertyDescriptors: function (object) {
- return Object.getOwnPropertyNames(object).reduce((descriptor, name) => {
- descriptor[name] = Object.getOwnPropertyDescriptor(object, name);
- return descriptor;
- }, {});
- }
- };
- /**
- * Helper for draining a rapid succession of events and invoking a callback
- * once everything settles down.
- *
- * @param string id
- * A string identifier for the named timeout.
- * @param number wait
- * The amount of milliseconds to wait after no more events are fired.
- * @param function callback
- * Invoked when no more events are fired after the specified time.
- */
- const setNamedTimeout = function setNamedTimeout(id, wait, callback) {
- clearNamedTimeout(id);
- namedTimeoutsStore.set(id, setTimeout(() =>
- namedTimeoutsStore.delete(id) && callback(), wait));
- };
- exports.setNamedTimeout = setNamedTimeout;
- /**
- * Clears a named timeout.
- * @see setNamedTimeout
- *
- * @param string id
- * A string identifier for the named timeout.
- */
- const clearNamedTimeout = function clearNamedTimeout(id) {
- if (!namedTimeoutsStore) {
- return;
- }
- clearTimeout(namedTimeoutsStore.get(id));
- namedTimeoutsStore.delete(id);
- };
- exports.clearNamedTimeout = clearNamedTimeout;
- /**
- * Same as `setNamedTimeout`, but invokes the callback only if the provided
- * predicate function returns true. Otherwise, the timeout is re-triggered.
- *
- * @param string id
- * A string identifier for the conditional timeout.
- * @param number wait
- * The amount of milliseconds to wait after no more events are fired.
- * @param function predicate
- * The predicate function used to determine whether the timeout restarts.
- * @param function callback
- * Invoked when no more events are fired after the specified time, and
- * the provided predicate function returns true.
- */
- const setConditionalTimeout = function setConditionalTimeout(id, wait,
- predicate,
- callback) {
- setNamedTimeout(id, wait, function maybeCallback() {
- if (predicate()) {
- callback();
- return;
- }
- setConditionalTimeout(id, wait, predicate, callback);
- });
- };
- exports.setConditionalTimeout = setConditionalTimeout;
- /**
- * Clears a conditional timeout.
- * @see setConditionalTimeout
- *
- * @param string id
- * A string identifier for the conditional timeout.
- */
- const clearConditionalTimeout = function clearConditionalTimeout(id) {
- clearNamedTimeout(id);
- };
- exports.clearConditionalTimeout = clearConditionalTimeout;
- /**
- * Helpers for creating and messaging between UI components.
- */
- const ViewHelpers = exports.ViewHelpers = {
- /**
- * Convenience method, dispatching a custom event.
- *
- * @param nsIDOMNode target
- * A custom target element to dispatch the event from.
- * @param string type
- * The name of the event.
- * @param any detail
- * The data passed when initializing the event.
- * @return boolean
- * True if the event was cancelled or a registered handler
- * called preventDefault.
- */
- dispatchEvent: function (target, type, detail) {
- if (!(target instanceof Node)) {
- // Event cancelled.
- return true;
- }
- let document = target.ownerDocument || target;
- let dispatcher = target.ownerDocument ? target : document.documentElement;
- let event = document.createEvent("CustomEvent");
- event.initCustomEvent(type, true, true, detail);
- return dispatcher.dispatchEvent(event);
- },
- /**
- * Helper delegating some of the DOM attribute methods of a node to a widget.
- *
- * @param object widget
- * The widget to assign the methods to.
- * @param nsIDOMNode node
- * A node to delegate the methods to.
- */
- delegateWidgetAttributeMethods: function (widget, node) {
- widget.getAttribute =
- widget.getAttribute || node.getAttribute.bind(node);
- widget.setAttribute =
- widget.setAttribute || node.setAttribute.bind(node);
- widget.removeAttribute =
- widget.removeAttribute || node.removeAttribute.bind(node);
- },
- /**
- * Helper delegating some of the DOM event methods of a node to a widget.
- *
- * @param object widget
- * The widget to assign the methods to.
- * @param nsIDOMNode node
- * A node to delegate the methods to.
- */
- delegateWidgetEventMethods: function (widget, node) {
- widget.addEventListener =
- widget.addEventListener || node.addEventListener.bind(node);
- widget.removeEventListener =
- widget.removeEventListener || node.removeEventListener.bind(node);
- },
- /**
- * Checks if the specified object looks like it's been decorated by an
- * event emitter.
- *
- * @return boolean
- * True if it looks, walks and quacks like an event emitter.
- */
- isEventEmitter: function (object) {
- return object && object.on && object.off && object.once && object.emit;
- },
- /**
- * Checks if the specified object is an instance of a DOM node.
- *
- * @return boolean
- * True if it's a node, false otherwise.
- */
- isNode: function (object) {
- return object instanceof Node ||
- object instanceof Element ||
- object instanceof DocumentFragment;
- },
- /**
- * Prevents event propagation when navigation keys are pressed.
- *
- * @param Event e
- * The event to be prevented.
- */
- preventScrolling: function (e) {
- switch (e.keyCode) {
- case KeyCodes.DOM_VK_UP:
- case KeyCodes.DOM_VK_DOWN:
- case KeyCodes.DOM_VK_LEFT:
- case KeyCodes.DOM_VK_RIGHT:
- case KeyCodes.DOM_VK_PAGE_UP:
- case KeyCodes.DOM_VK_PAGE_DOWN:
- case KeyCodes.DOM_VK_HOME:
- case KeyCodes.DOM_VK_END:
- e.preventDefault();
- e.stopPropagation();
- }
- },
- /**
- * Check if the enter key or space was pressed
- *
- * @param event event
- * The event triggered by a keypress on an element
- */
- isSpaceOrReturn: function (event) {
- return event.keyCode === KeyCodes.DOM_VK_SPACE ||
- event.keyCode === KeyCodes.DOM_VK_RETURN;
- },
- /**
- * Sets a toggled pane hidden or visible. The pane can either be displayed on
- * the side (right or left depending on the locale) or at the bottom.
- *
- * @param object flags
- * An object containing some of the following properties:
- * - visible: true if the pane should be shown, false to hide
- * - animated: true to display an animation on toggle
- * - delayed: true to wait a few cycles before toggle
- * - callback: a function to invoke when the toggle finishes
- * @param nsIDOMNode pane
- * The element representing the pane to toggle.
- */
- togglePane: function (flags, pane) {
- // Make sure a pane is actually available first.
- if (!pane) {
- return;
- }
- // Hiding is always handled via margins, not the hidden attribute.
- pane.removeAttribute("hidden");
- // Add a class to the pane to handle min-widths, margins and animations.
- pane.classList.add("generic-toggled-pane");
- // Avoid toggles in the middle of animation.
- if (pane.hasAttribute("animated")) {
- return;
- }
- // Avoid useless toggles.
- if (flags.visible == !pane.classList.contains("pane-collapsed")) {
- if (flags.callback) {
- flags.callback();
- }
- return;
- }
- // The "animated" attributes enables animated toggles (slide in-out).
- if (flags.animated) {
- pane.setAttribute("animated", "");
- } else {
- pane.removeAttribute("animated");
- }
- // Computes and sets the pane margins in order to hide or show it.
- let doToggle = () => {
- // Negative margins are applied to "right" and "left" to support RTL and
- // LTR directions, as well as to "bottom" to support vertical layouts.
- // Unnecessary negative margins are forced to 0 via CSS in widgets.css.
- if (flags.visible) {
- pane.style.marginLeft = "0";
- pane.style.marginRight = "0";
- pane.style.marginBottom = "0";
- pane.classList.remove("pane-collapsed");
- } else {
- let width = Math.floor(pane.getAttribute("width")) + 1;
- let height = Math.floor(pane.getAttribute("height")) + 1;
- pane.style.marginLeft = -width + "px";
- pane.style.marginRight = -width + "px";
- pane.style.marginBottom = -height + "px";
- }
- // Wait for the animation to end before calling afterToggle()
- if (flags.animated) {
- let options = {
- useCapture: false,
- once: true
- };
- pane.addEventListener("transitionend", () => {
- // Prevent unwanted transitions: if the panel is hidden and the layout
- // changes margins will be updated and the panel will pop out.
- pane.removeAttribute("animated");
- if (!flags.visible) {
- pane.classList.add("pane-collapsed");
- }
- if (flags.callback) {
- flags.callback();
- }
- }, options);
- } else {
- if (!flags.visible) {
- pane.classList.add("pane-collapsed");
- }
- // Invoke the callback immediately since there's no transition.
- if (flags.callback) {
- flags.callback();
- }
- }
- };
- // Sometimes it's useful delaying the toggle a few ticks to ensure
- // a smoother slide in-out animation.
- if (flags.delayed) {
- pane.ownerDocument.defaultView.setTimeout(doToggle,
- PANE_APPEARANCE_DELAY);
- } else {
- doToggle();
- }
- }
- };
- /**
- * A generic Item is used to describe children present in a Widget.
- *
- * This is basically a very thin wrapper around an nsIDOMNode, with a few
- * characteristics, like a `value` and an `attachment`.
- *
- * The characteristics are optional, and their meaning is entirely up to you.
- * - The `value` should be a string, passed as an argument.
- * - The `attachment` is any kind of primitive or object, passed as an argument.
- *
- * Iterable via "for (let childItem of parentItem) { }".
- *
- * @param object ownerView
- * The owner view creating this item.
- * @param nsIDOMNode element
- * A prebuilt node to be wrapped.
- * @param string value
- * A string identifying the node.
- * @param any attachment
- * Some attached primitive/object.
- */
- function Item(ownerView, element, value, attachment) {
- this.ownerView = ownerView;
- this.attachment = attachment;
- this._value = value + "";
- this._prebuiltNode = element;
- this._itemsByElement = new Map();
- }
- Item.prototype = {
- get value() {
- return this._value;
- },
- get target() {
- return this._target;
- },
- get prebuiltNode() {
- return this._prebuiltNode;
- },
- /**
- * Immediately appends a child item to this item.
- *
- * @param nsIDOMNode element
- * An nsIDOMNode representing the child element to append.
- * @param object options [optional]
- * Additional options or flags supported by this operation:
- * - attachment: some attached primitive/object for the item
- * - attributes: a batch of attributes set to the displayed element
- * - finalize: function invoked when the child item is removed
- * @return Item
- * The item associated with the displayed element.
- */
- append: function (element, options = {}) {
- let item = new Item(this, element, "", options.attachment);
- // Entangle the item with the newly inserted child node.
- // Make sure this is done with the value returned by appendChild(),
- // to avoid storing a potential DocumentFragment.
- this._entangleItem(item, this._target.appendChild(element));
- // Handle any additional options after entangling the item.
- if (options.attributes) {
- options.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
- }
- if (options.finalize) {
- item.finalize = options.finalize;
- }
- // Return the item associated with the displayed element.
- return item;
- },
- /**
- * Immediately removes the specified child item from this item.
- *
- * @param Item item
- * The item associated with the element to remove.
- */
- remove: function (item) {
- if (!item) {
- return;
- }
- this._target.removeChild(item._target);
- this._untangleItem(item);
- },
- /**
- * Entangles an item (model) with a displayed node element (view).
- *
- * @param Item item
- * The item describing a target element.
- * @param nsIDOMNode element
- * The element displaying the item.
- */
- _entangleItem: function (item, element) {
- this._itemsByElement.set(element, item);
- item._target = element;
- },
- /**
- * Untangles an item (model) from a displayed node element (view).
- *
- * @param Item item
- * The item describing a target element.
- */
- _untangleItem: function (item) {
- if (item.finalize) {
- item.finalize(item);
- }
- for (let childItem of item) {
- item.remove(childItem);
- }
- this._unlinkItem(item);
- item._target = null;
- },
- /**
- * Deletes an item from the its parent's storage maps.
- *
- * @param Item item
- * The item describing a target element.
- */
- _unlinkItem: function (item) {
- this._itemsByElement.delete(item._target);
- },
- /**
- * Returns a string representing the object.
- * Avoid using `toString` to avoid accidental JSONification.
- * @return string
- */
- stringify: function () {
- return JSON.stringify({
- value: this._value,
- target: this._target + "",
- prebuiltNode: this._prebuiltNode + "",
- attachment: this.attachment
- }, null, 2);
- },
- _value: "",
- _target: null,
- _prebuiltNode: null,
- finalize: null,
- attachment: null
- };
- /**
- * Some generic Widget methods handling Item instances.
- * Iterable via "for (let childItem of wrappedView) { }".
- *
- * Usage:
- * function MyView() {
- * this.widget = new MyWidget(document.querySelector(".my-node"));
- * }
- *
- * MyView.prototype = Heritage.extend(WidgetMethods, {
- * myMethod: function() {},
- * ...
- * });
- *
- * See https://gist.github.com/victorporof/5749386 for more details.
- * The devtools/shared/widgets/SimpleListWidget.jsm is an implementation
- * example.
- *
- * Language:
- * - An "item" is an instance of an Item.
- * - An "element" or "node" is a nsIDOMNode.
- *
- * The supplied widget can be any object implementing the following
- * methods:
- * - function:nsIDOMNode insertItemAt(aIndex:number, aNode:nsIDOMNode,
- * aValue:string)
- * - function:nsIDOMNode getItemAtIndex(aIndex:number)
- * - function removeChild(aChild:nsIDOMNode)
- * - function removeAllItems()
- * - get:nsIDOMNode selectedItem()
- * - set selectedItem(aChild:nsIDOMNode)
- * - function getAttribute(aName:string)
- * - function setAttribute(aName:string, aValue:string)
- * - function removeAttribute(aName:string)
- * - function addEventListener(aName:string, aCallback:function,
- * aBubbleFlag:boolean)
- * - function removeEventListener(aName:string, aCallback:function,
- * aBubbleFlag:boolean)
- *
- * Optional methods that can be implemented by the widget:
- * - function ensureElementIsVisible(aChild:nsIDOMNode)
- *
- * Optional attributes that may be handled (when calling
- * get/set/removeAttribute):
- * - "emptyText": label temporarily added when there are no items present
- * - "headerText": label permanently added as a header
- *
- * For automagical keyboard and mouse accessibility, the widget should be an
- * event emitter with the following events:
- * - "keyPress" -> (aName:string, aEvent:KeyboardEvent)
- * - "mousePress" -> (aName:string, aEvent:MouseEvent)
- */
- const WidgetMethods = exports.WidgetMethods = {
- /**
- * Sets the element node or widget associated with this container.
- * @param nsIDOMNode | object widget
- */
- set widget(widget) {
- this._widget = widget;
- // Can't use a WeakMap for _itemsByValue because keys are strings, and
- // can't use one for _itemsByElement either, since it needs to be iterable.
- this._itemsByValue = new Map();
- this._itemsByElement = new Map();
- this._stagedItems = [];
- // Handle internal events emitted by the widget if necessary.
- if (ViewHelpers.isEventEmitter(widget)) {
- widget.on("keyPress", this._onWidgetKeyPress.bind(this));
- widget.on("mousePress", this._onWidgetMousePress.bind(this));
- }
- },
- /**
- * Gets the element node or widget associated with this container.
- * @return nsIDOMNode | object
- */
- get widget() {
- return this._widget;
- },
- /**
- * Prepares an item to be added to this container. This allows, for example,
- * for a large number of items to be batched up before being sorted & added.
- *
- * If the "staged" flag is *not* set to true, the item will be immediately
- * inserted at the correct position in this container, so that all the items
- * still remain sorted. This can (possibly) be much slower than batching up
- * multiple items.
- *
- * By default, this container assumes that all the items should be displayed
- * sorted by their value. This can be overridden with the "index" flag,
- * specifying on which position should an item be appended. The "staged" and
- * "index" flags are mutually exclusive, meaning that all staged items
- * will always be appended.
- *
- * @param nsIDOMNode element
- * A prebuilt node to be wrapped.
- * @param string value
- * A string identifying the node.
- * @param object options [optional]
- * Additional options or flags supported by this operation:
- * - attachment: some attached primitive/object for the item
- * - staged: true to stage the item to be appended later
- * - index: specifies on which position should the item be appended
- * - attributes: a batch of attributes set to the displayed element
- * - finalize: function invoked when the item is removed
- * @return Item
- * The item associated with the displayed element if an unstaged push,
- * undefined if the item was staged for a later commit.
- */
- push: function ([element, value], options = {}) {
- let item = new Item(this, element, value, options.attachment);
- // Batch the item to be added later.
- if (options.staged) {
- // An ulterior commit operation will ignore any specified index, so
- // no reason to keep it around.
- options.index = undefined;
- return void this._stagedItems.push({ item: item, options: options });
- }
- // Find the target position in this container and insert the item there.
- if (!("index" in options)) {
- return this._insertItemAt(this._findExpectedIndexFor(item), item,
- options);
- }
- // Insert the item at the specified index. If negative or out of bounds,
- // the item will be simply appended.
- return this._insertItemAt(options.index, item, options);
- },
- /**
- * Flushes all the prepared items into this container.
- * Any specified index on the items will be ignored. Everything is appended.
- *
- * @param object options [optional]
- * Additional options or flags supported by this operation:
- * - sorted: true to sort all the items before adding them
- */
- commit: function (options = {}) {
- let stagedItems = this._stagedItems;
- // Sort the items before adding them to this container, if preferred.
- if (options.sorted) {
- stagedItems.sort((a, b) => this._currentSortPredicate(a.item, b.item));
- }
- // Append the prepared items to this container.
- for (let { item, opt } of stagedItems) {
- this._insertItemAt(-1, item, opt);
- }
- // Recreate the temporary items list for ulterior pushes.
- this._stagedItems.length = 0;
- },
- /**
- * Immediately removes the specified item from this container.
- *
- * @param Item item
- * The item associated with the element to remove.
- */
- remove: function (item) {
- if (!item) {
- return;
- }
- this._widget.removeChild(item._target);
- this._untangleItem(item);
- if (!this._itemsByElement.size) {
- this._preferredValue = this.selectedValue;
- this._widget.selectedItem = null;
- this._widget.setAttribute("emptyText", this._emptyText);
- }
- },
- /**
- * Removes the item at the specified index from this container.
- *
- * @param number index
- * The index of the item to remove.
- */
- removeAt: function (index) {
- this.remove(this.getItemAtIndex(index));
- },
- /**
- * Removes the items in this container based on a predicate.
- */
- removeForPredicate: function (predicate) {
- let item;
- while ((item = this.getItemForPredicate(predicate))) {
- this.remove(item);
- }
- },
- /**
- * Removes all items from this container.
- */
- empty: function () {
- this._preferredValue = this.selectedValue;
- this._widget.selectedItem = null;
- this._widget.removeAllItems();
- this._widget.setAttribute("emptyText", this._emptyText);
- for (let [, item] of this._itemsByElement) {
- this._untangleItem(item);
- }
- this._itemsByValue.clear();
- this._itemsByElement.clear();
- this._stagedItems.length = 0;
- },
- /**
- * Ensures the specified item is visible in this container.
- *
- * @param Item item
- * The item to bring into view.
- */
- ensureItemIsVisible: function (item) {
- this._widget.ensureElementIsVisible(item._target);
- },
- /**
- * Ensures the item at the specified index is visible in this container.
- *
- * @param number index
- * The index of the item to bring into view.
- */
- ensureIndexIsVisible: function (index) {
- this.ensureItemIsVisible(this.getItemAtIndex(index));
- },
- /**
- * Sugar for ensuring the selected item is visible in this container.
- */
- ensureSelectedItemIsVisible: function () {
- this.ensureItemIsVisible(this.selectedItem);
- },
- /**
- * If supported by the widget, the label string temporarily added to this
- * container when there are no child items present.
- */
- set emptyText(value) {
- this._emptyText = value;
- // Apply the emptyText attribute right now if there are no child items.
- if (!this._itemsByElement.size) {
- this._widget.setAttribute("emptyText", value);
- }
- },
- /**
- * If supported by the widget, the label string permanently added to this
- * container as a header.
- * @param string value
- */
- set headerText(value) {
- this._headerText = value;
- this._widget.setAttribute("headerText", value);
- },
- /**
- * Toggles all the items in this container hidden or visible.
- *
- * This does not change the default filtering predicate, so newly inserted
- * items will always be visible. Use WidgetMethods.filterContents if you care.
- *
- * @param boolean visibleFlag
- * Specifies the intended visibility.
- */
- toggleContents: function (visibleFlag) {
- for (let [element] of this._itemsByElement) {
- element.hidden = !visibleFlag;
- }
- },
- /**
- * Toggles all items in this container hidden or visible based on a predicate.
- *
- * @param function predicate [optional]
- * Items are toggled according to the return value of this function,
- * which will become the new default filtering predicate in this
- * container.
- * If unspecified, all items will be toggled visible.
- */
- filterContents: function (predicate = this._currentFilterPredicate) {
- this._currentFilterPredicate = predicate;
- for (let [element, item] of this._itemsByElement) {
- element.hidden = !predicate(item);
- }
- },
- /**
- * Sorts all the items in this container based on a predicate.
- *
- * @param function predicate [optional]
- * Items are sorted according to the return value of the function,
- * which will become the new default sorting predicate in this
- * container. If unspecified, all items will be sorted by their value.
- */
- sortContents: function (predicate = this._currentSortPredicate) {
- let sortedItems = this.items.sort(this._currentSortPredicate = predicate);
- for (let i = 0, len = sortedItems.length; i < len; i++) {
- this.swapItems(this.getItemAtIndex(i), sortedItems[i]);
- }
- },
- /**
- * Visually swaps two items in this container.
- *
- * @param Item first
- * The first item to be swapped.
- * @param Item second
- * The second item to be swapped.
- */
- swapItems: function (first, second) {
- if (first == second) {
- // We're just dandy, thank you.
- return;
- }
- let { _prebuiltNode: firstPrebuiltTarget, _target: firstTarget } = first;
- let { _prebuiltNode: secondPrebuiltTarget, _target: secondTarget } = second;
- // If the two items were constructed with prebuilt nodes as
- // DocumentFragments, then those DocumentFragments are now
- // empty and need to be reassembled.
- if (firstPrebuiltTarget instanceof DocumentFragment) {
- for (let node of firstTarget.childNodes) {
- firstPrebuiltTarget.appendChild(node.cloneNode(true));
- }
- }
- if (secondPrebuiltTarget instanceof DocumentFragment) {
- for (let node of secondTarget.childNodes) {
- secondPrebuiltTarget.appendChild(node.cloneNode(true));
- }
- }
- // 1. Get the indices of the two items to swap.
- let i = this._indexOfElement(firstTarget);
- let j = this._indexOfElement(secondTarget);
- // 2. Remeber the selection index, to reselect an item, if necessary.
- let selectedTarget = this._widget.selectedItem;
- let selectedIndex = -1;
- if (selectedTarget == firstTarget) {
- selectedIndex = i;
- } else if (selectedTarget == secondTarget) {
- selectedIndex = j;
- }
- // 3. Silently nuke both items, nobody needs to know about this.
- this._widget.removeChild(firstTarget);
- this._widget.removeChild(secondTarget);
- this._unlinkItem(first);
- this._unlinkItem(second);
- // 4. Add the items again, but reversing their indices.
- this._insertItemAt.apply(this, i < j ? [i, second] : [j, first]);
- this._insertItemAt.apply(this, i < j ? [j, first] : [i, second]);
- // 5. Restore the previous selection, if necessary.
- if (selectedIndex == i) {
- this._widget.selectedItem = first._target;
- } else if (selectedIndex == j) {
- this._widget.selectedItem = second._target;
- }
- // 6. Let the outside world know that these two items were swapped.
- ViewHelpers.dispatchEvent(first.target, "swap", [second, first]);
- },
- /**
- * Visually swaps two items in this container at specific indices.
- *
- * @param number first
- * The index of the first item to be swapped.
- * @param number second
- * The index of the second item to be swapped.
- */
- swapItemsAtIndices: function (first, second) {
- this.swapItems(this.getItemAtIndex(first), this.getItemAtIndex(second));
- },
- /**
- * Checks whether an item with the specified value is among the elements
- * shown in this container.
- *
- * @param string value
- * The item's value.
- * @return boolean
- * True if the value is known, false otherwise.
- */
- containsValue: function (value) {
- return this._itemsByValue.has(value) ||
- this._stagedItems.some(({ item }) => item._value == value);
- },
- /**
- * Gets the "preferred value". This is the latest selected item's value,
- * remembered just before emptying this container.
- * @return string
- */
- get preferredValue() {
- return this._preferredValue;
- },
- /**
- * Retrieves the item associated with the selected element.
- * @return Item | null
- */
- get selectedItem() {
- let selectedElement = this._widget.selectedItem;
- if (selectedElement) {
- return this._itemsByElement.get(selectedElement);
- }
- return null;
- },
- /**
- * Retrieves the selected element's index in this container.
- * @return number
- */
- get selectedIndex() {
- let selectedElement = this._widget.selectedItem;
- if (selectedElement) {
- return this._indexOfElement(selectedElement);
- }
- return -1;
- },
- /**
- * Retrieves the value of the selected element.
- * @return string
- */
- get selectedValue() {
- let selectedElement = this._widget.selectedItem;
- if (selectedElement) {
- return this._itemsByElement.get(selectedElement)._value;
- }
- return "";
- },
- /**
- * Retrieves the attachment of the selected element.
- * @return object | null
- */
- get selectedAttachment() {
- let selectedElement = this._widget.selectedItem;
- if (selectedElement) {
- return this._itemsByElement.get(selectedElement).attachment;
- }
- return null;
- },
- _selectItem: function (item) {
- // A falsy item is allowed to invalidate the current selection.
- let targetElement = item ? item._target : null;
- let prevElement = this._widget.selectedItem;
- // Make sure the selected item's target element is focused and visible.
- if (this.autoFocusOnSelection && targetElement) {
- targetElement.focus();
- }
- if (targetElement != prevElement) {
- this._widget.selectedItem = targetElement;
- }
- },
- /**
- * Selects the element with the entangled item in this container.
- * @param Item | function item
- */
- set selectedItem(item) {
- // A predicate is allowed to select a specific item.
- // If no item is matched, then the current selection is removed.
- if (typeof item == "function") {
- item = this.getItemForPredicate(item);
- }
- let targetElement = item ? item._target : null;
- let prevElement = this._widget.selectedItem;
- if (this.maintainSelectionVisible && targetElement) {
- // Some methods are optional. See the WidgetMethods object documentation
- // for a comprehensive list.
- if ("ensureElementIsVisible" in this._widget) {
- this._widget.ensureElementIsVisible(targetElement);
- }
- }
- this._selectItem(item);
- // Prevent selecting the same item again and avoid dispatching
- // a redundant selection event, so return early.
- if (targetElement != prevElement) {
- let dispTarget = targetElement || prevElement;
- let dispName = this.suppressSelectionEvents ? "suppressed-select"
- : "select";
- ViewHelpers.dispatchEvent(dispTarget, dispName, item);
- }
- },
- /**
- * Selects the element at the specified index in this container.
- * @param number index
- */
- set selectedIndex(index) {
- let targetElement = this._widget.getItemAtIndex(index);
- if (targetElement) {
- this.selectedItem = this._itemsByElement.get(targetElement);
- return;
- }
- this.selectedItem = null;
- },
- /**
- * Selects the element with the specified value in this container.
- * @param string value
- */
- set selectedValue(value) {
- this.selectedItem = this._itemsByValue.get(value);
- },
- /**
- * Deselects and re-selects an item in this container.
- *
- * Useful when you want a "select" event to be emitted, even though
- * the specified item was already selected.
- *
- * @param Item | function item
- * @see `set selectedItem`
- */
- forceSelect: function (item) {
- this.selectedItem = null;
- this.selectedItem = item;
- },
- /**
- * Specifies if this container should try to keep the selected item visible.
- * (For example, when new items are added the selection is brought into view).
- */
- maintainSelectionVisible: true,
- /**
- * Specifies if "select" events dispatched from the elements in this container
- * when their respective items are selected should be suppressed or not.
- *
- * If this flag is set to true, then consumers of this container won't
- * be normally notified when items are selected.
- */
- suppressSelectionEvents: false,
- /**
- * Focus this container the first time an element is inserted?
- *
- * If this flag is set to true, then when the first item is inserted in
- * this container (and thus it's the only item available), its corresponding
- * target element is focused as well.
- */
- autoFocusOnFirstItem: true,
- /**
- * Focus on selection?
- *
- * If this flag is set to true, then whenever an item is selected in
- * this container (e.g. via the selectedIndex or selectedItem setters),
- * its corresponding target element is focused as well.
- *
- * You can disable this flag, for example, to maintain a certain node
- * focused but visually indicate a different selection in this container.
- */
- autoFocusOnSelection: true,
- /**
- * Focus on input (e.g. mouse click)?
- *
- * If this flag is set to true, then whenever an item receives user input in
- * this container, its corresponding target element is focused as well.
- */
- autoFocusOnInput: true,
- /**
- * When focusing on input, allow right clicks?
- * @see WidgetMethods.autoFocusOnInput
- */
- allowFocusOnRightClick: false,
- /**
- * The number of elements in this container to jump when Page Up or Page Down
- * keys are pressed. If falsy, then the page size will be based on the
- * number of visible items in the container.
- */
- pageSize: 0,
- /**
- * Focuses the first visible item in this container.
- */
- focusFirstVisibleItem: function () {
- this.focusItemAtDelta(-this.itemCount);
- },
- /**
- * Focuses the last visible item in this container.
- */
- focusLastVisibleItem: function () {
- this.focusItemAtDelta(+this.itemCount);
- },
- /**
- * Focuses the next item in this container.
- */
- focusNextItem: function () {
- this.focusItemAtDelta(+1);
- },
- /**
- * Focuses the previous item in this container.
- */
- focusPrevItem: function () {
- this.focusItemAtDelta(-1);
- },
- /**
- * Focuses another item in this container based on the index distance
- * from the currently focused item.
- *
- * @param number delta
- * A scalar specifying by how many items should the selection change.
- */
- focusItemAtDelta: function (delta) {
- // Make sure the currently selected item is also focused, so that the
- // command dispatcher mechanism has a relative node to work with.
- // If there's no selection, just select an item at a corresponding index
- // (e.g. the first item in this container if delta <= 1).
- let selectedElement = this._widget.selectedItem;
- if (selectedElement) {
- selectedElement.focus();
- } else {
- this.selectedIndex = Math.max(0, delta - 1);
- return;
- }
- let direction = delta > 0 ? "advanceFocus" : "rewindFocus";
- let distance = Math.abs(Math[delta > 0 ? "ceil" : "floor"](delta));
- while (distance--) {
- if (!this._focusChange(direction)) {
- // Out of bounds.
- break;
- }
- }
- // Synchronize the selected item as being the currently focused element.
- this.selectedItem = this.getItemForElement(this._focusedElement);
- },
- /**
- * Focuses the next or previous item in this container.
- *
- * @param string direction
- * Either "advanceFocus" or "rewindFocus".
- * @return boolean
- * False if the focus went out of bounds and the first or last item
- * in this container was focused instead.
- */
- _focusChange: function (direction) {
- let commandDispatcher = this._commandDispatcher;
- let prevFocusedElement = commandDispatcher.focusedElement;
- let currFocusedElement;
- do {
- commandDispatcher.suppressFocusScroll = true;
- commandDispatcher[direction]();
- currFocusedElement = commandDispatcher.focusedElement;
- // Make sure the newly focused item is a part of this container. If the
- // focus goes out of bounds, revert the previously focused item.
- if (!this.getItemForElement(currFocusedElement)) {
- prevFocusedElement.focus();
- return false;
- }
- } while (!WIDGET_FOCUSABLE_NODES.has(currFocusedElement.tagName));
- // Focus remained within bounds.
- return true;
- },
- /**
- * Gets the command dispatcher instance associated with this container's DOM.
- * If there are no items displayed in this container, null is returned.
- * @return nsIDOMXULCommandDispatcher | null
- */
- get _commandDispatcher() {
- if (this._cachedCommandDispatcher) {
- return this._cachedCommandDispatcher;
- }
- let someElement = this._widget.getItemAtIndex(0);
- if (someElement) {
- let commandDispatcher = someElement.ownerDocument.commandDispatcher;
- this._cachedCommandDispatcher = commandDispatcher;
- return commandDispatcher;
- }
- return null;
- },
- /**
- * Gets the currently focused element in this container.
- *
- * @return nsIDOMNode
- * The focused element, or null if nothing is found.
- */
- get _focusedElement() {
- let commandDispatcher = this._commandDispatcher;
- if (commandDispatcher) {
- return commandDispatcher.focusedElement;
- }
- return null;
- },
- /**
- * Gets the item in the container having the specified index.
- *
- * @param number index
- * The index used to identify the element.
- * @return Item
- * The matched item, or null if nothing is found.
- */
- getItemAtIndex: function (index) {
- return this.getItemForElement(this._widget.getItemAtIndex(index));
- },
- /**
- * Gets the item in the container having the specified value.
- *
- * @param string value
- * The value used to identify the element.
- * @return Item
- * The matched item, or null if nothing is found.
- */
- getItemByValue: function (value) {
- return this._itemsByValue.get(value);
- },
- /**
- * Gets the item in the container associated with the specified element.
- *
- * @param nsIDOMNode element
- * The element used to identify the item.
- * @param object flags [optional]
- * Additional options for showing the source. Supported options:
- * - noSiblings: if siblings shouldn't be taken into consideration
- * when searching for the associated item.
- * @return Item
- * The matched item, or null if nothing is found.
- */
- getItemForElement: function (element, flags = {}) {
- while (element) {
- let item = this._itemsByElement.get(element);
- // Also search the siblings if allowed.
- if (!flags.noSiblings) {
- item = item ||
- this._itemsByElement.get(element.nextElementSibling) ||
- this._itemsByElement.get(element.previousElementSibling);
- }
- if (item) {
- return item;
- }
- element = element.parentNode;
- }
- return null;
- },
- /**
- * Gets a visible item in this container validating a specified predicate.
- *
- * @param function predicate
- * The first item which validates this predicate is returned
- * @return Item
- * The matched item, or null if nothing is found.
- */
- getItemForPredicate: function (predicate, owner = this) {
- // Recursively check the items in this widget for a predicate match.
- for (let [element, item] of owner._itemsByElement) {
- let match;
- if (predicate(item) && !element.hidden) {
- match = item;
- } else {
- match = this.getItemForPredicate(predicate, item);
- }
- if (match) {
- return match;
- }
- }
- // Also check the staged items. No need to do this recursively since
- // they're not even appended to the view yet.
- for (let { item } of this._stagedItems) {
- if (predicate(item)) {
- return item;
- }
- }
- return null;
- },
- /**
- * Shortcut function for getItemForPredicate which works on item attachments.
- * @see getItemForPredicate
- */
- getItemForAttachment: function (predicate, owner = this) {
- return this.getItemForPredicate(e => predicate(e.attachment));
- },
- /**
- * Finds the index of an item in the container.
- *
- * @param Item item
- * The item get the index for.
- * @return number
- * The index of the matched item, or -1 if nothing is found.
- */
- indexOfItem: function (item) {
- return this._indexOfElement(item._target);
- },
- /**
- * Finds the index of an element in the container.
- *
- * @param nsIDOMNode element
- * The element get the index for.
- * @return number
- * The index of the matched element, or -1 if nothing is found.
- */
- _indexOfElement: function (element) {
- for (let i = 0; i < this._itemsByElement.size; i++) {
- if (this._widget.getItemAtIndex(i) == element) {
- return i;
- }
- }
- return -1;
- },
- /**
- * Gets the total number of items in this container.
- * @return number
- */
- get itemCount() {
- return this._itemsByElement.size;
- },
- /**
- * Returns a list of items in this container, in the displayed order.
- * @return array
- */
- get items() {
- let store = [];
- let itemCount = this.itemCount;
- for (let i = 0; i < itemCount; i++) {
- store.push(this.getItemAtIndex(i));
- }
- return store;
- },
- /**
- * Returns a list of values in this container, in the displayed order.
- * @return array
- */
- get values() {
- return this.items.map(e => e._value);
- },
- /**
- * Returns a list of attachments in this container, in the displayed order.
- * @return array
- */
- get attachments() {
- return this.items.map(e => e.attachment);
- },
- /**
- * Returns a list of all the visible (non-hidden) items in this container,
- * in the displayed order
- * @return array
- */
- get visibleItems() {
- return this.items.filter(e => !e._target.hidden);
- },
- /**
- * Checks if an item is unique in this container. If an item's value is an
- * empty string, "undefined" or "null", it is considered unique.
- *
- * @param Item item
- * The item for which to verify uniqueness.
- * @return boolean
- * True if the item is unique, false otherwise.
- */
- isUnique: function (item) {
- let value = item._value;
- if (value == "" || value == "undefined" || value == "null") {
- return true;
- }
- return !this._itemsByValue.has(value);
- },
- /**
- * Checks if an item is eligible for this container. By default, this checks
- * whether an item is unique and has a prebuilt target node.
- *
- * @param Item item
- * The item for which to verify eligibility.
- * @return boolean
- * True if the item is eligible, false otherwise.
- */
- isEligible: function (item) {
- return this.isUnique(item) && item._prebuiltNode;
- },
- /**
- * Finds the expected item index in this container based on the default
- * sort predicate.
- *
- * @param Item item
- * The item for which to get the expected index.
- * @return number
- * The expected item index.
- */
- _findExpectedIndexFor: function (item) {
- let itemCount = this.itemCount;
- for (let i = 0; i < itemCount; i++) {
- if (this._currentSortPredicate(this.getItemAtIndex(i), item) > 0) {
- return i;
- }
- }
- return itemCount;
- },
- /**
- * Immediately inserts an item in this container at the specified index.
- *
- * @param number index
- * The position in the container intended for this item.
- * @param Item item
- * The item describing a target element.
- * @param object options [optional]
- * Additional options or flags supported by this operation:
- * - attributes: a batch of attributes set to the displayed element
- * - finalize: function when the item is untangled (removed)
- * @return Item
- * The item associated with the displayed element, null if rejected.
- */
- _insertItemAt: function (index, item, options = {}) {
- if (!this.isEligible(item)) {
- return null;
- }
- // Entangle the item with the newly inserted node.
- // Make sure this is done with the value returned by insertItemAt(),
- // to avoid storing a potential DocumentFragment.
- let node = item._prebuiltNode;
- let attachment = item.attachment;
- this._entangleItem(item,
- this._widget.insertItemAt(index, node, attachment));
- // Handle any additional options after entangling the item.
- if (!this._currentFilterPredicate(item)) {
- item._target.hidden = true;
- }
- if (this.autoFocusOnFirstItem && this._itemsByElement.size == 1) {
- item._target.focus();
- }
- if (options.attributes) {
- options.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
- }
- if (options.finalize) {
- item.finalize = options.finalize;
- }
- // Hide the empty text if the selection wasn't lost.
- this._widget.removeAttribute("emptyText");
- // Return the item associated with the displayed element.
- return item;
- },
- /**
- * Entangles an item (model) with a displayed node element (view).
- *
- * @param Item item
- * The item describing a target element.
- * @param nsIDOMNode element
- * The element displaying the item.
- */
- _entangleItem: function (item, element) {
- this._itemsByValue.set(item._value, item);
- this._itemsByElement.set(element, item);
- item._target = element;
- },
- /**
- * Untangles an item (model) from a displayed node element (view).
- *
- * @param Item item
- * The item describing a target element.
- */
- _untangleItem: function (item) {
- if (item.finalize) {
- item.finalize(item);
- }
- for (let childItem of item) {
- item.remove(childItem);
- }
- this._unlinkItem(item);
- item._target = null;
- },
- /**
- * Deletes an item from the its parent's storage maps.
- *
- * @param Item item
- * The item describing a target element.
- */
- _unlinkItem: function (item) {
- this._itemsByValue.delete(item._value);
- this._itemsByElement.delete(item._target);
- },
- /**
- * The keyPress event listener for this container.
- * @param string name
- * @param KeyboardEvent event
- */
- _onWidgetKeyPress: function (name, event) {
- // Prevent scrolling when pressing navigation keys.
- ViewHelpers.preventScrolling(event);
- switch (event.keyCode) {
- case KeyCodes.DOM_VK_UP:
- case KeyCodes.DOM_VK_LEFT:
- this.focusPrevItem();
- return;
- case KeyCodes.DOM_VK_DOWN:
- case KeyCodes.DOM_VK_RIGHT:
- this.focusNextItem();
- return;
- case KeyCodes.DOM_VK_PAGE_UP:
- this.focusItemAtDelta(-(this.pageSize ||
- (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
- return;
- case KeyCodes.DOM_VK_PAGE_DOWN:
- this.focusItemAtDelta(+(this.pageSize ||
- (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
- return;
- case KeyCodes.DOM_VK_HOME:
- this.focusFirstVisibleItem();
- return;
- case KeyCodes.DOM_VK_END:
- this.focusLastVisibleItem();
- return;
- }
- },
- /**
- * The mousePress event listener for this container.
- * @param string name
- * @param MouseEvent event
- */
- _onWidgetMousePress: function (name, event) {
- if (event.button != 0 && !this.allowFocusOnRightClick) {
- // Only allow left-click to trigger this event.
- return;
- }
- let item = this.getItemForElement(event.target);
- if (item) {
- // The container is not empty and we clicked on an actual item.
- this.selectedItem = item;
- // Make sure the current event's target element is also focused.
- this.autoFocusOnInput && item._target.focus();
- }
- },
- /**
- * The predicate used when filtering items. By default, all items in this
- * view are visible.
- *
- * @param Item item
- * The item passing through the filter.
- * @return boolean
- * True if the item should be visible, false otherwise.
- */
- _currentFilterPredicate: function (item) {
- return true;
- },
- /**
- * The predicate used when sorting items. By default, items in this view
- * are sorted by their label.
- *
- * @param Item first
- * The first item used in the comparison.
- * @param Item second
- * The second item used in the comparison.
- * @return number
- * -1 to sort first to a lower index than second
- * 0 to leave first and second unchanged with respect to each other
- * 1 to sort second to a lower index than first
- */
- _currentSortPredicate: function (first, second) {
- return +(first._value.toLowerCase() > second._value.toLowerCase());
- },
- /**
- * Call a method on this widget named `methodName`. Any further arguments are
- * passed on to the method. Returns the result of the method call.
- *
- * @param String methodName
- * The name of the method you want to call.
- * @param args
- * Optional. Any arguments you want to pass through to the method.
- */
- callMethod: function (methodName, ...args) {
- return this._widget[methodName].apply(this._widget, args);
- },
- _widget: null,
- _emptyText: "",
- _headerText: "",
- _preferredValue: "",
- _cachedCommandDispatcher: null
- };
- /**
- * A generator-iterator over all the items in this container.
- */
- Item.prototype[Symbol.iterator] =
- WidgetMethods[Symbol.iterator] = function* () {
- yield* this._itemsByElement.values();
- };
|