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 Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms michael@0: michael@0: Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/event-emitter.js"); michael@0: michael@0: this.EXPORTED_SYMBOLS = ["BreadcrumbsWidget"]; michael@0: michael@0: /** michael@0: * A breadcrumb-like list of items. 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: this.BreadcrumbsWidget = function BreadcrumbsWidget(aNode) { michael@0: this.document = aNode.ownerDocument; michael@0: this.window = this.document.defaultView; michael@0: this._parent = aNode; michael@0: michael@0: // Create an internal arrowscrollbox container. michael@0: this._list = this.document.createElement("arrowscrollbox"); michael@0: this._list.className = "breadcrumbs-widget-container"; michael@0: this._list.setAttribute("flex", "1"); michael@0: this._list.setAttribute("orient", "horizontal"); michael@0: this._list.setAttribute("clicktoscroll", "true") 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: // By default, hide the arrows. We let the arrowscrollbox show them michael@0: // in case of overflow. michael@0: this._list._scrollButtonUp.collapsed = true; michael@0: this._list._scrollButtonDown.collapsed = true; michael@0: this._list.addEventListener("underflow", this._onUnderflow.bind(this), false); michael@0: this._list.addEventListener("overflow", this._onOverflow.bind(this), false); michael@0: michael@0: michael@0: // These separators are used for CSS purposes only, and are positioned michael@0: // off screen, but displayed with -moz-element. michael@0: this._separators = this.document.createElement("box"); michael@0: this._separators.className = "breadcrumb-separator-container"; michael@0: this._separators.innerHTML = michael@0: "" + michael@0: "" + michael@0: ""; michael@0: this._parent.appendChild(this._separators); 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: BreadcrumbsWidget.prototype = { michael@0: /** michael@0: * Inserts an item in this container at the specified index. 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 displayed in the container. michael@0: * @return nsIDOMNode michael@0: * The element associated with the displayed item. michael@0: */ michael@0: insertItemAt: function(aIndex, aContents) { michael@0: let list = this._list; michael@0: let breadcrumb = new Breadcrumb(this, aContents); michael@0: return list.insertBefore(breadcrumb._target, list.childNodes[aIndex]); 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 aIndex 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(aIndex) { michael@0: return this._list.childNodes[aIndex]; michael@0: }, michael@0: michael@0: /** michael@0: * Removes the specified child node from this container. michael@0: * michael@0: * @param nsIDOMNode aChild michael@0: * The element associated with the displayed item. michael@0: */ michael@0: removeChild: function(aChild) { michael@0: this._list.removeChild(aChild); michael@0: michael@0: if (this._selectedItem == aChild) { michael@0: this._selectedItem = null; michael@0: } 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 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: 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 aChild michael@0: */ michael@0: set selectedItem(aChild) { michael@0: let childNodes = this._list.childNodes; michael@0: michael@0: if (!aChild) { michael@0: this._selectedItem = null; michael@0: } michael@0: for (let node of childNodes) { michael@0: if (node == aChild) { michael@0: node.setAttribute("checked", ""); michael@0: this._selectedItem = node; michael@0: } else { michael@0: node.removeAttribute("checked"); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns the value of the named attribute on this container. michael@0: * michael@0: * @param string aName michael@0: * The name of the attribute. michael@0: * @return string michael@0: * The current attribute value. michael@0: */ michael@0: getAttribute: function(aName) { michael@0: if (aName == "scrollPosition") return this._list.scrollPosition; michael@0: if (aName == "scrollWidth") return this._list.scrollWidth; michael@0: return this._parent.getAttribute(aName); michael@0: }, michael@0: michael@0: /** michael@0: * Ensures the specified element is visible. michael@0: * michael@0: * @param nsIDOMNode aElement michael@0: * The element to make visible. michael@0: */ michael@0: ensureElementIsVisible: function(aElement) { michael@0: if (!aElement) { michael@0: return; michael@0: } michael@0: michael@0: // Repeated calls to ensureElementIsVisible would interfere with each other michael@0: // and may sometimes result in incorrect scroll positions. michael@0: setNamedTimeout("breadcrumb-select", ENSURE_SELECTION_VISIBLE_DELAY, () => { michael@0: if (this._list.ensureElementIsVisible) { michael@0: this._list.ensureElementIsVisible(aElement); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * The underflow and overflow listener for the arrowscrollbox container. michael@0: */ michael@0: _onUnderflow: function({ target }) { michael@0: if (target != this._list) { michael@0: return; michael@0: } michael@0: target._scrollButtonUp.collapsed = true; michael@0: target._scrollButtonDown.collapsed = true; michael@0: target.removeAttribute("overflows"); michael@0: }, michael@0: michael@0: /** michael@0: * The underflow and overflow listener for the arrowscrollbox container. michael@0: */ michael@0: _onOverflow: function({ target }) { michael@0: if (target != this._list) { michael@0: return; michael@0: } michael@0: target._scrollButtonUp.collapsed = false; michael@0: target._scrollButtonDown.collapsed = false; michael@0: target.setAttribute("overflows", ""); 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: }; michael@0: michael@0: /** michael@0: * A Breadcrumb constructor for the BreadcrumbsWidget. michael@0: * michael@0: * @param BreadcrumbsWidget aWidget michael@0: * The widget to contain this breadcrumb. michael@0: * @param nsIDOMNode aContents michael@0: * The node displayed in the container. michael@0: */ michael@0: function Breadcrumb(aWidget, aContents) { michael@0: this.document = aWidget.document; michael@0: this.window = aWidget.window; michael@0: this.ownerView = aWidget; michael@0: michael@0: this._target = this.document.createElement("hbox"); michael@0: this._target.className = "breadcrumbs-widget-item"; michael@0: this._target.setAttribute("align", "center"); michael@0: this.contents = aContents; michael@0: } michael@0: michael@0: Breadcrumb.prototype = { michael@0: /** michael@0: * Sets the contents displayed in this item's view. michael@0: * michael@0: * @param string | nsIDOMNode aContents michael@0: * The string or node displayed in the container. michael@0: */ michael@0: set contents(aContents) { michael@0: // If there are already some contents displayed, replace them. michael@0: if (this._target.hasChildNodes()) { michael@0: this._target.replaceChild(aContents, this._target.firstChild); michael@0: return; michael@0: } michael@0: // These are the first contents ever displayed. michael@0: this._target.appendChild(aContents); michael@0: }, michael@0: michael@0: window: null, michael@0: document: null, michael@0: ownerView: null, michael@0: _target: null michael@0: };