diff options
Diffstat (limited to 'devtools/client/shared/widgets/FastListWidget.js')
-rw-r--r-- | devtools/client/shared/widgets/FastListWidget.js | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/devtools/client/shared/widgets/FastListWidget.js b/devtools/client/shared/widgets/FastListWidget.js new file mode 100644 index 000000000..d005ead51 --- /dev/null +++ b/devtools/client/shared/widgets/FastListWidget.js @@ -0,0 +1,249 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 EventEmitter = require("devtools/shared/event-emitter"); +const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers"); + +/** + * A list menu widget that attempts to be very fast. + * + * Note: this widget should be used in tandem with the WidgetMethods in + * view-helpers.js. + * + * @param nsIDOMNode aNode + * The element associated with the widget. + */ +const FastListWidget = module.exports = function FastListWidget(node) { + this.document = node.ownerDocument; + this.window = this.document.defaultView; + this._parent = node; + this._fragment = this.document.createDocumentFragment(); + + // This is a prototype element that each item added to the list clones. + this._templateElement = this.document.createElement("hbox"); + + // Create an internal scrollbox container. + this._list = this.document.createElement("scrollbox"); + this._list.className = "fast-list-widget-container theme-body"; + this._list.setAttribute("flex", "1"); + this._list.setAttribute("orient", "vertical"); + this._list.setAttribute("tabindex", "0"); + this._list.addEventListener("keypress", e => this.emit("keyPress", e), false); + this._list.addEventListener("mousedown", e => this.emit("mousePress", e), + false); + this._parent.appendChild(this._list); + + this._orderedMenuElementsArray = []; + this._itemsByElement = new Map(); + + // This widget emits events that can be handled in a MenuContainer. + EventEmitter.decorate(this); + + // Delegate some of the associated node's methods to satisfy the interface + // required by MenuContainer instances. + ViewHelpers.delegateWidgetAttributeMethods(this, node); + ViewHelpers.delegateWidgetEventMethods(this, node); +}; + +FastListWidget.prototype = { + /** + * Inserts an item in this container at the specified index, optionally + * grouping by name. + * + * @param number aIndex + * The position in the container intended for this item. + * @param nsIDOMNode aContents + * The node to be displayed in the container. + * @param Object aAttachment [optional] + * Extra data for the user. + * @return nsIDOMNode + * The element associated with the displayed item. + */ + insertItemAt: function (index, contents, attachment = {}) { + let element = this._templateElement.cloneNode(); + element.appendChild(contents); + + if (index >= 0) { + throw new Error("FastListWidget only supports appending items."); + } + + this._fragment.appendChild(element); + this._orderedMenuElementsArray.push(element); + this._itemsByElement.set(element, this); + + return element; + }, + + /** + * This is a non-standard widget implementation method. When appending items, + * they are queued in a document fragment. This method appends the document + * fragment to the dom. + */ + flush: function () { + this._list.appendChild(this._fragment); + }, + + /** + * Removes all of the child nodes from this container. + */ + removeAllItems: function () { + let list = this._list; + + while (list.hasChildNodes()) { + list.firstChild.remove(); + } + + this._selectedItem = null; + + this._orderedMenuElementsArray.length = 0; + this._itemsByElement.clear(); + }, + + /** + * Remove the given item. + */ + removeChild: function (child) { + throw new Error("Not yet implemented"); + }, + + /** + * Gets the currently selected child node in this container. + * @return nsIDOMNode + */ + get selectedItem() { + return this._selectedItem; + }, + + /** + * Sets the currently selected child node in this container. + * @param nsIDOMNode child + */ + set selectedItem(child) { + let menuArray = this._orderedMenuElementsArray; + + if (!child) { + this._selectedItem = null; + } + for (let node of menuArray) { + if (node == child) { + node.classList.add("selected"); + this._selectedItem = node; + } else { + node.classList.remove("selected"); + } + } + + this.ensureElementIsVisible(this.selectedItem); + }, + + /** + * Returns the child node in this container situated at the specified index. + * + * @param number index + * The position in the container intended for this item. + * @return nsIDOMNode + * The element associated with the displayed item. + */ + getItemAtIndex: function (index) { + return this._orderedMenuElementsArray[index]; + }, + + /** + * Adds a new attribute or changes an existing attribute on this container. + * + * @param string name + * The name of the attribute. + * @param string value + * The desired attribute value. + */ + setAttribute: function (name, value) { + this._parent.setAttribute(name, value); + + if (name == "emptyText") { + this._textWhenEmpty = value; + } + }, + + /** + * Removes an attribute on this container. + * + * @param string name + * The name of the attribute. + */ + removeAttribute: function (name) { + this._parent.removeAttribute(name); + + if (name == "emptyText") { + this._removeEmptyText(); + } + }, + + /** + * Ensures the specified element is visible. + * + * @param nsIDOMNode element + * The element to make visible. + */ + ensureElementIsVisible: function (element) { + if (!element) { + return; + } + + // Ensure the element is visible but not scrolled horizontally. + let boxObject = this._list.boxObject; + boxObject.ensureElementIsVisible(element); + boxObject.scrollBy(-this._list.clientWidth, 0); + }, + + /** + * Sets the text displayed in this container when empty. + * @param string aValue + */ + set _textWhenEmpty(value) { + if (this._emptyTextNode) { + this._emptyTextNode.setAttribute("value", value); + } + this._emptyTextValue = value; + this._showEmptyText(); + }, + + /** + * Creates and appends a label signaling that this container is empty. + */ + _showEmptyText: function () { + if (this._emptyTextNode || !this._emptyTextValue) { + return; + } + let label = this.document.createElement("label"); + label.className = "plain fast-list-widget-empty-text"; + label.setAttribute("value", this._emptyTextValue); + + this._parent.insertBefore(label, this._list); + this._emptyTextNode = label; + }, + + /** + * Removes the label signaling that this container is empty. + */ + _removeEmptyText: function () { + if (!this._emptyTextNode) { + return; + } + this._parent.removeChild(this._emptyTextNode); + this._emptyTextNode = null; + }, + + window: null, + document: null, + _parent: null, + _list: null, + _selectedItem: null, + _orderedMenuElementsArray: null, + _itemsByElement: null, + _emptyTextNode: null, + _emptyTextValue: "" +}; |