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: };