123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- /* -*- 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 { interfaces: Ci, utils: Cu } = Components;
- const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
- const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
- const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
- const { KeyCodes } = require("devtools/client/shared/keycodes");
- XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
- "resource://devtools/shared/event-emitter.js");
- XPCOMUtils.defineLazyModuleGetter(this, "console",
- "resource://gre/modules/Console.jsm");
- this.EXPORTED_SYMBOLS = ["AbstractTreeItem"];
- /**
- * A very generic and low-level tree view implementation. It is not intended
- * to be used alone, but as a base class that you can extend to build your
- * own custom implementation.
- *
- * Language:
- * - An "item" is an instance of an AbstractTreeItem.
- * - An "element" or "node" is an nsIDOMNode.
- *
- * The following events are emitted by this tree, always from the root item,
- * with the first argument pointing to the affected child item:
- * - "expand": when an item is expanded in the tree
- * - "collapse": when an item is collapsed in the tree
- * - "focus": when an item is selected in the tree
- *
- * For example, you can extend this abstract class like this:
- *
- * function MyCustomTreeItem(dataSrc, properties) {
- * AbstractTreeItem.call(this, properties);
- * this.itemDataSrc = dataSrc;
- * }
- *
- * MyCustomTreeItem.prototype = Heritage.extend(AbstractTreeItem.prototype, {
- * _displaySelf: function(document, arrowNode) {
- * let node = document.createElement("hbox");
- * ...
- * // Append the provided arrow node wherever you want.
- * node.appendChild(arrowNode);
- * ...
- * // Use `this.itemDataSrc` to customize the tree item and
- * // `this.level` to calculate the indentation.
- * node.style.marginInlineStart = (this.level * 10) + "px";
- * node.appendChild(document.createTextNode(this.itemDataSrc.label));
- * ...
- * return node;
- * },
- * _populateSelf: function(children) {
- * ...
- * // Use `this.itemDataSrc` to get the data source for the child items.
- * let someChildDataSrc = this.itemDataSrc.children[0];
- * ...
- * children.push(new MyCustomTreeItem(someChildDataSrc, {
- * parent: this,
- * level: this.level + 1
- * }));
- * ...
- * }
- * });
- *
- * And then you could use it like this:
- *
- * let dataSrc = {
- * label: "root",
- * children: [{
- * label: "foo",
- * children: []
- * }, {
- * label: "bar",
- * children: [{
- * label: "baz",
- * children: []
- * }]
- * }]
- * };
- * let root = new MyCustomTreeItem(dataSrc, { parent: null });
- * root.attachTo(nsIDOMNode);
- * root.expand();
- *
- * The following tree view will be generated (after expanding all nodes):
- * ▼ root
- * ▶ foo
- * ▼ bar
- * ▶ baz
- *
- * The way the data source is implemented is completely up to you. There's
- * no assumptions made and you can use it however you like inside the
- * `_displaySelf` and `populateSelf` methods. If you need to add children to a
- * node at a later date, you just need to modify the data source:
- *
- * dataSrc[...path-to-foo...].children.push({
- * label: "lazily-added-node"
- * children: []
- * });
- *
- * The existing tree view will be modified like so (after expanding `foo`):
- * ▼ root
- * ▼ foo
- * ▶ lazily-added-node
- * ▼ bar
- * ▶ baz
- *
- * Everything else is taken care of automagically!
- *
- * @param AbstractTreeItem parent
- * The parent tree item. Should be null for root items.
- * @param number level
- * The indentation level in the tree. The root item is at level 0.
- */
- function AbstractTreeItem({ parent, level }) {
- this._rootItem = parent ? parent._rootItem : this;
- this._parentItem = parent;
- this._level = level || 0;
- this._childTreeItems = [];
- // Events are always propagated through the root item. Decorating every
- // tree item as an event emitter is a very costly operation.
- if (this == this._rootItem) {
- EventEmitter.decorate(this);
- }
- }
- this.AbstractTreeItem = AbstractTreeItem;
- AbstractTreeItem.prototype = {
- _containerNode: null,
- _targetNode: null,
- _arrowNode: null,
- _constructed: false,
- _populated: false,
- _expanded: false,
- /**
- * Optionally, trees may be allowed to automatically expand a few levels deep
- * to avoid initially displaying a completely collapsed tree.
- */
- autoExpandDepth: 0,
- /**
- * Creates the view for this tree item. Implement this method in the
- * inheriting classes to create the child node displayed in the tree.
- * Use `this.level` and the provided `arrowNode` as you see fit.
- *
- * @param nsIDOMNode document
- * @param nsIDOMNode arrowNode
- * @return nsIDOMNode
- */
- _displaySelf: function (document, arrowNode) {
- throw new Error(
- "The `_displaySelf` method needs to be implemented by inheriting classes.");
- },
- /**
- * Populates this tree item with child items, whenever it's expanded.
- * Implement this method in the inheriting classes to fill the provided
- * `children` array with AbstractTreeItem instances, which will then be
- * magically handled by this tree item.
- *
- * @param array:AbstractTreeItem children
- */
- _populateSelf: function (children) {
- throw new Error(
- "The `_populateSelf` method needs to be implemented by inheriting classes.");
- },
- /**
- * Gets the this tree's owner document.
- * @return Document
- */
- get document() {
- return this._containerNode.ownerDocument;
- },
- /**
- * Gets the root item of this tree.
- * @return AbstractTreeItem
- */
- get root() {
- return this._rootItem;
- },
- /**
- * Gets the parent of this tree item.
- * @return AbstractTreeItem
- */
- get parent() {
- return this._parentItem;
- },
- /**
- * Gets the indentation level of this tree item.
- */
- get level() {
- return this._level;
- },
- /**
- * Gets the element displaying this tree item.
- */
- get target() {
- return this._targetNode;
- },
- /**
- * Gets the element containing all tree items.
- * @return nsIDOMNode
- */
- get container() {
- return this._containerNode;
- },
- /**
- * Returns whether or not this item is populated in the tree.
- * Collapsed items can still be populated.
- * @return boolean
- */
- get populated() {
- return this._populated;
- },
- /**
- * Returns whether or not this item is expanded in the tree.
- * Expanded items with no children aren't consudered `populated`.
- * @return boolean
- */
- get expanded() {
- return this._expanded;
- },
- /**
- * Gets the bounds for this tree's container without flushing.
- * @return object
- */
- get bounds() {
- let win = this.document.defaultView;
- let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
- return utils.getBoundsWithoutFlushing(this._containerNode);
- },
- /**
- * Creates and appends this tree item to the specified parent element.
- *
- * @param nsIDOMNode containerNode
- * The parent element for this tree item (and every other tree item).
- * @param nsIDOMNode fragmentNode [optional]
- * An optional document fragment temporarily holding this tree item in
- * the current batch. Defaults to the `containerNode`.
- * @param nsIDOMNode beforeNode [optional]
- * An optional child element which should succeed this tree item.
- */
- attachTo: function (containerNode, fragmentNode = containerNode, beforeNode = null) {
- this._containerNode = containerNode;
- this._constructTargetNode();
- if (beforeNode) {
- fragmentNode.insertBefore(this._targetNode, beforeNode);
- } else {
- fragmentNode.appendChild(this._targetNode);
- }
- if (this._level < this.autoExpandDepth) {
- this.expand();
- }
- },
- /**
- * Permanently removes this tree item (and all subsequent children) from the
- * parent container.
- */
- remove: function () {
- this._targetNode.remove();
- this._hideChildren();
- this._childTreeItems.length = 0;
- },
- /**
- * Focuses this item in the tree.
- */
- focus: function () {
- this._targetNode.focus();
- },
- /**
- * Expands this item in the tree.
- */
- expand: function () {
- if (this._expanded) {
- return;
- }
- this._expanded = true;
- this._arrowNode.setAttribute("open", "");
- this._targetNode.setAttribute("expanded", "");
- this._toggleChildren(true);
- this._rootItem.emit("expand", this);
- },
- /**
- * Collapses this item in the tree.
- */
- collapse: function () {
- if (!this._expanded) {
- return;
- }
- this._expanded = false;
- this._arrowNode.removeAttribute("open");
- this._targetNode.removeAttribute("expanded", "");
- this._toggleChildren(false);
- this._rootItem.emit("collapse", this);
- },
- /**
- * Returns the child item at the specified index.
- *
- * @param number index
- * @return AbstractTreeItem
- */
- getChild: function (index = 0) {
- return this._childTreeItems[index];
- },
- /**
- * Calls the provided function on all the descendants of this item.
- * If this item was never expanded, then no descendents exist yet.
- * @param function cb
- */
- traverse: function (cb) {
- for (let child of this._childTreeItems) {
- cb(child);
- child.bfs();
- }
- },
- /**
- * Calls the provided function on all descendants of this item until
- * a truthy value is returned by the predicate.
- * @param function predicate
- * @return AbstractTreeItem
- */
- find: function (predicate) {
- for (let child of this._childTreeItems) {
- if (predicate(child) || child.find(predicate)) {
- return child;
- }
- }
- return null;
- },
- /**
- * Shows or hides all the children of this item in the tree. If neessary,
- * populates this item with children.
- *
- * @param boolean visible
- * True if the children should be visible, false otherwise.
- */
- _toggleChildren: function (visible) {
- if (visible) {
- if (!this._populated) {
- this._populateSelf(this._childTreeItems);
- this._populated = this._childTreeItems.length > 0;
- }
- this._showChildren();
- } else {
- this._hideChildren();
- }
- },
- /**
- * Shows all children of this item in the tree.
- */
- _showChildren: function () {
- // If this is the root item and we're not expanding any child nodes,
- // it is safe to append everything at once.
- if (this == this._rootItem && this.autoExpandDepth == 0) {
- this._appendChildrenBatch();
- }
- // Otherwise, append the child items and their descendants successively;
- // if not, the tree will become garbled and nodes will intertwine,
- // since all the tree items are sharing a single container node.
- else {
- this._appendChildrenSuccessive();
- }
- },
- /**
- * Hides all children of this item in the tree.
- */
- _hideChildren: function () {
- for (let item of this._childTreeItems) {
- item._targetNode.remove();
- item._hideChildren();
- }
- },
- /**
- * Appends all children in a single batch.
- * This only works properly for root nodes when no child nodes will expand.
- */
- _appendChildrenBatch: function () {
- if (this._fragment === undefined) {
- this._fragment = this.document.createDocumentFragment();
- }
- let childTreeItems = this._childTreeItems;
- for (let i = 0, len = childTreeItems.length; i < len; i++) {
- childTreeItems[i].attachTo(this._containerNode, this._fragment);
- }
- this._containerNode.appendChild(this._fragment);
- },
- /**
- * Appends all children successively.
- */
- _appendChildrenSuccessive: function () {
- let childTreeItems = this._childTreeItems;
- let expandedChildTreeItems = childTreeItems.filter(e => e._expanded);
- let nextNode = this._getSiblingAtDelta(1);
- for (let i = 0, len = childTreeItems.length; i < len; i++) {
- childTreeItems[i].attachTo(this._containerNode, undefined, nextNode);
- }
- for (let i = 0, len = expandedChildTreeItems.length; i < len; i++) {
- expandedChildTreeItems[i]._showChildren();
- }
- },
- /**
- * Constructs and stores the target node displaying this tree item.
- */
- _constructTargetNode: function () {
- if (this._constructed) {
- return;
- }
- this._onArrowClick = this._onArrowClick.bind(this);
- this._onClick = this._onClick.bind(this);
- this._onDoubleClick = this._onDoubleClick.bind(this);
- this._onKeyPress = this._onKeyPress.bind(this);
- this._onFocus = this._onFocus.bind(this);
- this._onBlur = this._onBlur.bind(this);
- let document = this.document;
- let arrowNode = this._arrowNode = document.createElement("hbox");
- arrowNode.className = "arrow theme-twisty";
- arrowNode.addEventListener("mousedown", this._onArrowClick);
- let targetNode = this._targetNode = this._displaySelf(document, arrowNode);
- targetNode.style.MozUserFocus = "normal";
- targetNode.addEventListener("mousedown", this._onClick);
- targetNode.addEventListener("dblclick", this._onDoubleClick);
- targetNode.addEventListener("keypress", this._onKeyPress);
- targetNode.addEventListener("focus", this._onFocus);
- targetNode.addEventListener("blur", this._onBlur);
- this._constructed = true;
- },
- /**
- * Gets the element displaying an item in the tree at the specified offset
- * relative to this item.
- *
- * @param number delta
- * The offset from this item to the target item.
- * @return nsIDOMNode
- * The element displaying the target item at the specified offset.
- */
- _getSiblingAtDelta: function (delta) {
- let childNodes = this._containerNode.childNodes;
- let indexOfSelf = Array.indexOf(childNodes, this._targetNode);
- if (indexOfSelf + delta >= 0) {
- return childNodes[indexOfSelf + delta];
- }
- return undefined;
- },
- _getNodesPerPageSize: function() {
- let childNodes = this._containerNode.childNodes;
- let nodeHeight = this._getHeight(childNodes[childNodes.length - 1]);
- let containerHeight = this.bounds.height;
- return Math.ceil(containerHeight / nodeHeight);
- },
- _getHeight: function(elem) {
- let win = this.document.defaultView;
- let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils);
- return utils.getBoundsWithoutFlushing(elem).height;
- },
- /**
- * Focuses the first item in this tree.
- */
- _focusFirstNode: function () {
- let childNodes = this._containerNode.childNodes;
- // The root node of the tree may be hidden in practice, so uses for-loop
- // here to find the next visible node.
- for (let i = 0; i < childNodes.length; i++) {
- // The height will be 0 if an element is invisible.
- if (this._getHeight(childNodes[i])) {
- childNodes[i].focus();
- return;
- }
- }
- },
- /**
- * Focuses the last item in this tree.
- */
- _focusLastNode: function () {
- let childNodes = this._containerNode.childNodes;
- childNodes[childNodes.length - 1].focus();
- },
- /**
- * Focuses the next item in this tree.
- */
- _focusNextNode: function () {
- let nextElement = this._getSiblingAtDelta(1);
- if (nextElement) nextElement.focus(); // nsIDOMNode
- },
- /**
- * Focuses the previous item in this tree.
- */
- _focusPrevNode: function () {
- let prevElement = this._getSiblingAtDelta(-1);
- if (prevElement) prevElement.focus(); // nsIDOMNode
- },
- /**
- * Focuses the parent item in this tree.
- *
- * The parent item is not always the previous item, because any tree item
- * may have multiple children.
- */
- _focusParentNode: function () {
- let parentItem = this._parentItem;
- if (parentItem) parentItem.focus(); // AbstractTreeItem
- },
- /**
- * Handler for the "click" event on the arrow node of this tree item.
- */
- _onArrowClick: function (e) {
- if (!this._expanded) {
- this.expand();
- } else {
- this.collapse();
- }
- },
- /**
- * Handler for the "click" event on the element displaying this tree item.
- */
- _onClick: function (e) {
- e.stopPropagation();
- this.focus();
- },
- /**
- * Handler for the "dblclick" event on the element displaying this tree item.
- */
- _onDoubleClick: function (e) {
- // Ignore dblclick on the arrow as it has already recived and handled two
- // click events.
- if (!e.target.classList.contains("arrow")) {
- this._onArrowClick(e);
- }
- this.focus();
- },
- /**
- * Handler for the "keypress" event on the element displaying this tree item.
- */
- _onKeyPress: function (e) {
- // Prevent scrolling when pressing navigation keys.
- ViewHelpers.preventScrolling(e);
- switch (e.keyCode) {
- case KeyCodes.DOM_VK_UP:
- this._focusPrevNode();
- return;
- case KeyCodes.DOM_VK_DOWN:
- this._focusNextNode();
- return;
- case KeyCodes.DOM_VK_LEFT:
- if (this._expanded && this._populated) {
- this.collapse();
- } else {
- this._focusParentNode();
- }
- return;
- case KeyCodes.DOM_VK_RIGHT:
- if (!this._expanded) {
- this.expand();
- } else {
- this._focusNextNode();
- }
- return;
- case KeyCodes.DOM_VK_PAGE_UP:
- let pageUpElement =
- this._getSiblingAtDelta(-this._getNodesPerPageSize());
- // There's a chance that the root node is hidden. In this case, its
- // height will be 0.
- if (pageUpElement && this._getHeight(pageUpElement)) {
- pageUpElement.focus();
- } else {
- this._focusFirstNode();
- }
- return;
- case KeyCodes.DOM_VK_PAGE_DOWN:
- let pageDownElement =
- this._getSiblingAtDelta(this._getNodesPerPageSize());
- if (pageDownElement) {
- pageDownElement.focus();
- } else {
- this._focusLastNode();
- }
- return;
- case KeyCodes.DOM_VK_HOME:
- this._focusFirstNode();
- return;
- case KeyCodes.DOM_VK_END:
- this._focusLastNode();
- return;
- }
- },
- /**
- * Handler for the "focus" event on the element displaying this tree item.
- */
- _onFocus: function (e) {
- this._rootItem.emit("focus", this);
- },
- /**
- * Handler for the "blur" event on the element displaying this tree item.
- */
- _onBlur: function (e) {
- this._rootItem.emit("blur", this);
- }
- };
|