michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: const { Cu, Ci } = require("chrome"); michael@0: const { ViewHelpers } = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {}); michael@0: michael@0: /** michael@0: * A list menu widget that attempts to be very fast. michael@0: * michael@0: * Note: this widget should be used in tandem with the WidgetMethods in michael@0: * ViewHelpers.jsm. michael@0: * michael@0: * @param nsIDOMNode aNode michael@0: * The element associated with the widget. michael@0: */ michael@0: const FastListWidget = module.exports = function FastListWidget(aNode) { michael@0: this.document = aNode.ownerDocument; michael@0: this.window = this.document.defaultView; michael@0: this._parent = aNode; michael@0: this._fragment = this.document.createDocumentFragment(); michael@0: michael@0: // This is a prototype element that each item added to the list clones. michael@0: this._templateElement = this.document.createElement("hbox"); michael@0: michael@0: // Create an internal scrollbox container. michael@0: this._list = this.document.createElement("scrollbox"); michael@0: this._list.className = "fast-list-widget-container theme-body"; michael@0: this._list.setAttribute("flex", "1"); michael@0: this._list.setAttribute("orient", "vertical"); michael@0: this._list.setAttribute("tabindex", "0"); michael@0: this._list.addEventListener("keypress", e => this.emit("keyPress", e), false); michael@0: this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false); michael@0: this._parent.appendChild(this._list); michael@0: michael@0: this._orderedMenuElementsArray = []; michael@0: this._itemsByElement = new Map(); michael@0: michael@0: // This widget emits events that can be handled in a MenuContainer. michael@0: EventEmitter.decorate(this); michael@0: michael@0: // Delegate some of the associated node's methods to satisfy the interface michael@0: // required by MenuContainer instances. michael@0: ViewHelpers.delegateWidgetAttributeMethods(this, aNode); michael@0: ViewHelpers.delegateWidgetEventMethods(this, aNode); michael@0: } michael@0: michael@0: FastListWidget.prototype = { michael@0: /** michael@0: * Inserts an item in this container at the specified index, optionally michael@0: * grouping by name. michael@0: * michael@0: * @param number aIndex michael@0: * The position in the container intended for this item. michael@0: * @param nsIDOMNode aContents michael@0: * The node to be displayed in the container. michael@0: * @param Object aAttachment [optional] michael@0: * Extra data for the user. michael@0: * @return nsIDOMNode michael@0: * The element associated with the displayed item. michael@0: */ michael@0: insertItemAt: function(aIndex, aContents, aAttachment={}) { michael@0: let element = this._templateElement.cloneNode(); michael@0: element.appendChild(aContents); michael@0: michael@0: if (aIndex >= 0) { michael@0: throw new Error("FastListWidget only supports appending items."); michael@0: } michael@0: michael@0: this._fragment.appendChild(element); michael@0: this._orderedMenuElementsArray.push(element); michael@0: this._itemsByElement.set(element, this); michael@0: michael@0: return element; michael@0: }, michael@0: michael@0: /** michael@0: * This is a non-standard widget implementation method. When appending items, michael@0: * they are queued in a document fragment. This method appends the document michael@0: * fragment to the dom. michael@0: */ michael@0: flush: function() { michael@0: this._list.appendChild(this._fragment); michael@0: }, michael@0: michael@0: /** michael@0: * Removes all of the child nodes from this container. michael@0: */ michael@0: removeAllItems: function() { michael@0: let parent = this._parent; michael@0: let list = this._list; michael@0: michael@0: while (list.hasChildNodes()) { michael@0: list.firstChild.remove(); michael@0: } michael@0: michael@0: this._selectedItem = null; michael@0: michael@0: this._orderedMenuElementsArray.length = 0; michael@0: this._itemsByElement.clear(); michael@0: }, michael@0: michael@0: /** michael@0: * Remove the given item. michael@0: */ michael@0: removeChild: function(child) { michael@0: throw new Error("Not yet implemented"); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the currently selected child node in this container. michael@0: * @return nsIDOMNode michael@0: */ michael@0: get selectedItem() { michael@0: return this._selectedItem; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the currently selected child node in this container. michael@0: * @param nsIDOMNode child michael@0: */ michael@0: set selectedItem(child) { michael@0: let menuArray = this._orderedMenuElementsArray; michael@0: michael@0: if (!child) { michael@0: this._selectedItem = null; michael@0: } michael@0: for (let node of menuArray) { michael@0: if (node == child) { michael@0: node.classList.add("selected"); michael@0: this._selectedItem = node; michael@0: } else { michael@0: node.classList.remove("selected"); michael@0: } michael@0: } michael@0: michael@0: this.ensureElementIsVisible(this.selectedItem); michael@0: }, michael@0: michael@0: /** michael@0: * Returns the child node in this container situated at the specified index. michael@0: * michael@0: * @param number index michael@0: * The position in the container intended for this item. michael@0: * @return nsIDOMNode michael@0: * The element associated with the displayed item. michael@0: */ michael@0: getItemAtIndex: function(index) { michael@0: return this._orderedMenuElementsArray[index]; michael@0: }, michael@0: michael@0: /** michael@0: * Adds a new attribute or changes an existing attribute on this container. michael@0: * michael@0: * @param string name michael@0: * The name of the attribute. michael@0: * @param string value michael@0: * The desired attribute value. michael@0: */ michael@0: setAttribute: function(name, value) { michael@0: this._parent.setAttribute(name, value); michael@0: michael@0: if (name == "emptyText") { michael@0: this._textWhenEmpty = value; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Removes an attribute on this container. michael@0: * michael@0: * @param string name michael@0: * The name of the attribute. michael@0: */ michael@0: removeAttribute: function(name) { michael@0: this._parent.removeAttribute(name); michael@0: michael@0: if (name == "emptyText") { michael@0: this._removeEmptyText(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Ensures the specified element is visible. michael@0: * michael@0: * @param nsIDOMNode element michael@0: * The element to make visible. michael@0: */ michael@0: ensureElementIsVisible: function(element) { michael@0: if (!element) { michael@0: return; michael@0: } michael@0: michael@0: // Ensure the element is visible but not scrolled horizontally. michael@0: let boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject); michael@0: boxObject.ensureElementIsVisible(element); michael@0: boxObject.scrollBy(-this._list.clientWidth, 0); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the text displayed in this container when empty. michael@0: * @param string aValue michael@0: */ michael@0: set _textWhenEmpty(aValue) { michael@0: if (this._emptyTextNode) { michael@0: this._emptyTextNode.setAttribute("value", aValue); michael@0: } michael@0: this._emptyTextValue = aValue; michael@0: this._showEmptyText(); michael@0: }, michael@0: michael@0: /** michael@0: * Creates and appends a label signaling that this container is empty. michael@0: */ michael@0: _showEmptyText: function() { michael@0: if (this._emptyTextNode || !this._emptyTextValue) { michael@0: return; michael@0: } michael@0: let label = this.document.createElement("label"); michael@0: label.className = "plain fast-list-widget-empty-text"; michael@0: label.setAttribute("value", this._emptyTextValue); michael@0: michael@0: this._parent.insertBefore(label, this._list); michael@0: this._emptyTextNode = label; michael@0: }, michael@0: michael@0: /** michael@0: * Removes the label signaling that this container is empty. michael@0: */ michael@0: _removeEmptyText: function() { michael@0: if (!this._emptyTextNode) { michael@0: return; michael@0: } michael@0: this._parent.removeChild(this._emptyTextNode); michael@0: this._emptyTextNode = null; michael@0: }, michael@0: michael@0: window: null, michael@0: document: null, michael@0: _parent: null, michael@0: _list: null, michael@0: _selectedItem: null, michael@0: _orderedMenuElementsArray: null, michael@0: _itemsByElement: null, michael@0: _emptyTextNode: null, michael@0: _emptyTextValue: "" michael@0: };