AbstractTreeItem.jsm 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. const { interfaces: Ci, utils: Cu } = Components;
  7. const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
  8. const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
  9. const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
  10. const { KeyCodes } = require("devtools/client/shared/keycodes");
  11. XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
  12. "resource://devtools/shared/event-emitter.js");
  13. XPCOMUtils.defineLazyModuleGetter(this, "console",
  14. "resource://gre/modules/Console.jsm");
  15. this.EXPORTED_SYMBOLS = ["AbstractTreeItem"];
  16. /**
  17. * A very generic and low-level tree view implementation. It is not intended
  18. * to be used alone, but as a base class that you can extend to build your
  19. * own custom implementation.
  20. *
  21. * Language:
  22. * - An "item" is an instance of an AbstractTreeItem.
  23. * - An "element" or "node" is an nsIDOMNode.
  24. *
  25. * The following events are emitted by this tree, always from the root item,
  26. * with the first argument pointing to the affected child item:
  27. * - "expand": when an item is expanded in the tree
  28. * - "collapse": when an item is collapsed in the tree
  29. * - "focus": when an item is selected in the tree
  30. *
  31. * For example, you can extend this abstract class like this:
  32. *
  33. * function MyCustomTreeItem(dataSrc, properties) {
  34. * AbstractTreeItem.call(this, properties);
  35. * this.itemDataSrc = dataSrc;
  36. * }
  37. *
  38. * MyCustomTreeItem.prototype = Heritage.extend(AbstractTreeItem.prototype, {
  39. * _displaySelf: function(document, arrowNode) {
  40. * let node = document.createElement("hbox");
  41. * ...
  42. * // Append the provided arrow node wherever you want.
  43. * node.appendChild(arrowNode);
  44. * ...
  45. * // Use `this.itemDataSrc` to customize the tree item and
  46. * // `this.level` to calculate the indentation.
  47. * node.style.marginInlineStart = (this.level * 10) + "px";
  48. * node.appendChild(document.createTextNode(this.itemDataSrc.label));
  49. * ...
  50. * return node;
  51. * },
  52. * _populateSelf: function(children) {
  53. * ...
  54. * // Use `this.itemDataSrc` to get the data source for the child items.
  55. * let someChildDataSrc = this.itemDataSrc.children[0];
  56. * ...
  57. * children.push(new MyCustomTreeItem(someChildDataSrc, {
  58. * parent: this,
  59. * level: this.level + 1
  60. * }));
  61. * ...
  62. * }
  63. * });
  64. *
  65. * And then you could use it like this:
  66. *
  67. * let dataSrc = {
  68. * label: "root",
  69. * children: [{
  70. * label: "foo",
  71. * children: []
  72. * }, {
  73. * label: "bar",
  74. * children: [{
  75. * label: "baz",
  76. * children: []
  77. * }]
  78. * }]
  79. * };
  80. * let root = new MyCustomTreeItem(dataSrc, { parent: null });
  81. * root.attachTo(nsIDOMNode);
  82. * root.expand();
  83. *
  84. * The following tree view will be generated (after expanding all nodes):
  85. * ▼ root
  86. * ▶ foo
  87. * ▼ bar
  88. * ▶ baz
  89. *
  90. * The way the data source is implemented is completely up to you. There's
  91. * no assumptions made and you can use it however you like inside the
  92. * `_displaySelf` and `populateSelf` methods. If you need to add children to a
  93. * node at a later date, you just need to modify the data source:
  94. *
  95. * dataSrc[...path-to-foo...].children.push({
  96. * label: "lazily-added-node"
  97. * children: []
  98. * });
  99. *
  100. * The existing tree view will be modified like so (after expanding `foo`):
  101. * ▼ root
  102. * ▼ foo
  103. * ▶ lazily-added-node
  104. * ▼ bar
  105. * ▶ baz
  106. *
  107. * Everything else is taken care of automagically!
  108. *
  109. * @param AbstractTreeItem parent
  110. * The parent tree item. Should be null for root items.
  111. * @param number level
  112. * The indentation level in the tree. The root item is at level 0.
  113. */
  114. function AbstractTreeItem({ parent, level }) {
  115. this._rootItem = parent ? parent._rootItem : this;
  116. this._parentItem = parent;
  117. this._level = level || 0;
  118. this._childTreeItems = [];
  119. // Events are always propagated through the root item. Decorating every
  120. // tree item as an event emitter is a very costly operation.
  121. if (this == this._rootItem) {
  122. EventEmitter.decorate(this);
  123. }
  124. }
  125. this.AbstractTreeItem = AbstractTreeItem;
  126. AbstractTreeItem.prototype = {
  127. _containerNode: null,
  128. _targetNode: null,
  129. _arrowNode: null,
  130. _constructed: false,
  131. _populated: false,
  132. _expanded: false,
  133. /**
  134. * Optionally, trees may be allowed to automatically expand a few levels deep
  135. * to avoid initially displaying a completely collapsed tree.
  136. */
  137. autoExpandDepth: 0,
  138. /**
  139. * Creates the view for this tree item. Implement this method in the
  140. * inheriting classes to create the child node displayed in the tree.
  141. * Use `this.level` and the provided `arrowNode` as you see fit.
  142. *
  143. * @param nsIDOMNode document
  144. * @param nsIDOMNode arrowNode
  145. * @return nsIDOMNode
  146. */
  147. _displaySelf: function (document, arrowNode) {
  148. throw new Error(
  149. "The `_displaySelf` method needs to be implemented by inheriting classes.");
  150. },
  151. /**
  152. * Populates this tree item with child items, whenever it's expanded.
  153. * Implement this method in the inheriting classes to fill the provided
  154. * `children` array with AbstractTreeItem instances, which will then be
  155. * magically handled by this tree item.
  156. *
  157. * @param array:AbstractTreeItem children
  158. */
  159. _populateSelf: function (children) {
  160. throw new Error(
  161. "The `_populateSelf` method needs to be implemented by inheriting classes.");
  162. },
  163. /**
  164. * Gets the this tree's owner document.
  165. * @return Document
  166. */
  167. get document() {
  168. return this._containerNode.ownerDocument;
  169. },
  170. /**
  171. * Gets the root item of this tree.
  172. * @return AbstractTreeItem
  173. */
  174. get root() {
  175. return this._rootItem;
  176. },
  177. /**
  178. * Gets the parent of this tree item.
  179. * @return AbstractTreeItem
  180. */
  181. get parent() {
  182. return this._parentItem;
  183. },
  184. /**
  185. * Gets the indentation level of this tree item.
  186. */
  187. get level() {
  188. return this._level;
  189. },
  190. /**
  191. * Gets the element displaying this tree item.
  192. */
  193. get target() {
  194. return this._targetNode;
  195. },
  196. /**
  197. * Gets the element containing all tree items.
  198. * @return nsIDOMNode
  199. */
  200. get container() {
  201. return this._containerNode;
  202. },
  203. /**
  204. * Returns whether or not this item is populated in the tree.
  205. * Collapsed items can still be populated.
  206. * @return boolean
  207. */
  208. get populated() {
  209. return this._populated;
  210. },
  211. /**
  212. * Returns whether or not this item is expanded in the tree.
  213. * Expanded items with no children aren't consudered `populated`.
  214. * @return boolean
  215. */
  216. get expanded() {
  217. return this._expanded;
  218. },
  219. /**
  220. * Gets the bounds for this tree's container without flushing.
  221. * @return object
  222. */
  223. get bounds() {
  224. let win = this.document.defaultView;
  225. let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  226. return utils.getBoundsWithoutFlushing(this._containerNode);
  227. },
  228. /**
  229. * Creates and appends this tree item to the specified parent element.
  230. *
  231. * @param nsIDOMNode containerNode
  232. * The parent element for this tree item (and every other tree item).
  233. * @param nsIDOMNode fragmentNode [optional]
  234. * An optional document fragment temporarily holding this tree item in
  235. * the current batch. Defaults to the `containerNode`.
  236. * @param nsIDOMNode beforeNode [optional]
  237. * An optional child element which should succeed this tree item.
  238. */
  239. attachTo: function (containerNode, fragmentNode = containerNode, beforeNode = null) {
  240. this._containerNode = containerNode;
  241. this._constructTargetNode();
  242. if (beforeNode) {
  243. fragmentNode.insertBefore(this._targetNode, beforeNode);
  244. } else {
  245. fragmentNode.appendChild(this._targetNode);
  246. }
  247. if (this._level < this.autoExpandDepth) {
  248. this.expand();
  249. }
  250. },
  251. /**
  252. * Permanently removes this tree item (and all subsequent children) from the
  253. * parent container.
  254. */
  255. remove: function () {
  256. this._targetNode.remove();
  257. this._hideChildren();
  258. this._childTreeItems.length = 0;
  259. },
  260. /**
  261. * Focuses this item in the tree.
  262. */
  263. focus: function () {
  264. this._targetNode.focus();
  265. },
  266. /**
  267. * Expands this item in the tree.
  268. */
  269. expand: function () {
  270. if (this._expanded) {
  271. return;
  272. }
  273. this._expanded = true;
  274. this._arrowNode.setAttribute("open", "");
  275. this._targetNode.setAttribute("expanded", "");
  276. this._toggleChildren(true);
  277. this._rootItem.emit("expand", this);
  278. },
  279. /**
  280. * Collapses this item in the tree.
  281. */
  282. collapse: function () {
  283. if (!this._expanded) {
  284. return;
  285. }
  286. this._expanded = false;
  287. this._arrowNode.removeAttribute("open");
  288. this._targetNode.removeAttribute("expanded", "");
  289. this._toggleChildren(false);
  290. this._rootItem.emit("collapse", this);
  291. },
  292. /**
  293. * Returns the child item at the specified index.
  294. *
  295. * @param number index
  296. * @return AbstractTreeItem
  297. */
  298. getChild: function (index = 0) {
  299. return this._childTreeItems[index];
  300. },
  301. /**
  302. * Calls the provided function on all the descendants of this item.
  303. * If this item was never expanded, then no descendents exist yet.
  304. * @param function cb
  305. */
  306. traverse: function (cb) {
  307. for (let child of this._childTreeItems) {
  308. cb(child);
  309. child.bfs();
  310. }
  311. },
  312. /**
  313. * Calls the provided function on all descendants of this item until
  314. * a truthy value is returned by the predicate.
  315. * @param function predicate
  316. * @return AbstractTreeItem
  317. */
  318. find: function (predicate) {
  319. for (let child of this._childTreeItems) {
  320. if (predicate(child) || child.find(predicate)) {
  321. return child;
  322. }
  323. }
  324. return null;
  325. },
  326. /**
  327. * Shows or hides all the children of this item in the tree. If neessary,
  328. * populates this item with children.
  329. *
  330. * @param boolean visible
  331. * True if the children should be visible, false otherwise.
  332. */
  333. _toggleChildren: function (visible) {
  334. if (visible) {
  335. if (!this._populated) {
  336. this._populateSelf(this._childTreeItems);
  337. this._populated = this._childTreeItems.length > 0;
  338. }
  339. this._showChildren();
  340. } else {
  341. this._hideChildren();
  342. }
  343. },
  344. /**
  345. * Shows all children of this item in the tree.
  346. */
  347. _showChildren: function () {
  348. // If this is the root item and we're not expanding any child nodes,
  349. // it is safe to append everything at once.
  350. if (this == this._rootItem && this.autoExpandDepth == 0) {
  351. this._appendChildrenBatch();
  352. }
  353. // Otherwise, append the child items and their descendants successively;
  354. // if not, the tree will become garbled and nodes will intertwine,
  355. // since all the tree items are sharing a single container node.
  356. else {
  357. this._appendChildrenSuccessive();
  358. }
  359. },
  360. /**
  361. * Hides all children of this item in the tree.
  362. */
  363. _hideChildren: function () {
  364. for (let item of this._childTreeItems) {
  365. item._targetNode.remove();
  366. item._hideChildren();
  367. }
  368. },
  369. /**
  370. * Appends all children in a single batch.
  371. * This only works properly for root nodes when no child nodes will expand.
  372. */
  373. _appendChildrenBatch: function () {
  374. if (this._fragment === undefined) {
  375. this._fragment = this.document.createDocumentFragment();
  376. }
  377. let childTreeItems = this._childTreeItems;
  378. for (let i = 0, len = childTreeItems.length; i < len; i++) {
  379. childTreeItems[i].attachTo(this._containerNode, this._fragment);
  380. }
  381. this._containerNode.appendChild(this._fragment);
  382. },
  383. /**
  384. * Appends all children successively.
  385. */
  386. _appendChildrenSuccessive: function () {
  387. let childTreeItems = this._childTreeItems;
  388. let expandedChildTreeItems = childTreeItems.filter(e => e._expanded);
  389. let nextNode = this._getSiblingAtDelta(1);
  390. for (let i = 0, len = childTreeItems.length; i < len; i++) {
  391. childTreeItems[i].attachTo(this._containerNode, undefined, nextNode);
  392. }
  393. for (let i = 0, len = expandedChildTreeItems.length; i < len; i++) {
  394. expandedChildTreeItems[i]._showChildren();
  395. }
  396. },
  397. /**
  398. * Constructs and stores the target node displaying this tree item.
  399. */
  400. _constructTargetNode: function () {
  401. if (this._constructed) {
  402. return;
  403. }
  404. this._onArrowClick = this._onArrowClick.bind(this);
  405. this._onClick = this._onClick.bind(this);
  406. this._onDoubleClick = this._onDoubleClick.bind(this);
  407. this._onKeyPress = this._onKeyPress.bind(this);
  408. this._onFocus = this._onFocus.bind(this);
  409. this._onBlur = this._onBlur.bind(this);
  410. let document = this.document;
  411. let arrowNode = this._arrowNode = document.createElement("hbox");
  412. arrowNode.className = "arrow theme-twisty";
  413. arrowNode.addEventListener("mousedown", this._onArrowClick);
  414. let targetNode = this._targetNode = this._displaySelf(document, arrowNode);
  415. targetNode.style.MozUserFocus = "normal";
  416. targetNode.addEventListener("mousedown", this._onClick);
  417. targetNode.addEventListener("dblclick", this._onDoubleClick);
  418. targetNode.addEventListener("keypress", this._onKeyPress);
  419. targetNode.addEventListener("focus", this._onFocus);
  420. targetNode.addEventListener("blur", this._onBlur);
  421. this._constructed = true;
  422. },
  423. /**
  424. * Gets the element displaying an item in the tree at the specified offset
  425. * relative to this item.
  426. *
  427. * @param number delta
  428. * The offset from this item to the target item.
  429. * @return nsIDOMNode
  430. * The element displaying the target item at the specified offset.
  431. */
  432. _getSiblingAtDelta: function (delta) {
  433. let childNodes = this._containerNode.childNodes;
  434. let indexOfSelf = Array.indexOf(childNodes, this._targetNode);
  435. if (indexOfSelf + delta >= 0) {
  436. return childNodes[indexOfSelf + delta];
  437. }
  438. return undefined;
  439. },
  440. _getNodesPerPageSize: function() {
  441. let childNodes = this._containerNode.childNodes;
  442. let nodeHeight = this._getHeight(childNodes[childNodes.length - 1]);
  443. let containerHeight = this.bounds.height;
  444. return Math.ceil(containerHeight / nodeHeight);
  445. },
  446. _getHeight: function(elem) {
  447. let win = this.document.defaultView;
  448. let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
  449. .getInterface(Ci.nsIDOMWindowUtils);
  450. return utils.getBoundsWithoutFlushing(elem).height;
  451. },
  452. /**
  453. * Focuses the first item in this tree.
  454. */
  455. _focusFirstNode: function () {
  456. let childNodes = this._containerNode.childNodes;
  457. // The root node of the tree may be hidden in practice, so uses for-loop
  458. // here to find the next visible node.
  459. for (let i = 0; i < childNodes.length; i++) {
  460. // The height will be 0 if an element is invisible.
  461. if (this._getHeight(childNodes[i])) {
  462. childNodes[i].focus();
  463. return;
  464. }
  465. }
  466. },
  467. /**
  468. * Focuses the last item in this tree.
  469. */
  470. _focusLastNode: function () {
  471. let childNodes = this._containerNode.childNodes;
  472. childNodes[childNodes.length - 1].focus();
  473. },
  474. /**
  475. * Focuses the next item in this tree.
  476. */
  477. _focusNextNode: function () {
  478. let nextElement = this._getSiblingAtDelta(1);
  479. if (nextElement) nextElement.focus(); // nsIDOMNode
  480. },
  481. /**
  482. * Focuses the previous item in this tree.
  483. */
  484. _focusPrevNode: function () {
  485. let prevElement = this._getSiblingAtDelta(-1);
  486. if (prevElement) prevElement.focus(); // nsIDOMNode
  487. },
  488. /**
  489. * Focuses the parent item in this tree.
  490. *
  491. * The parent item is not always the previous item, because any tree item
  492. * may have multiple children.
  493. */
  494. _focusParentNode: function () {
  495. let parentItem = this._parentItem;
  496. if (parentItem) parentItem.focus(); // AbstractTreeItem
  497. },
  498. /**
  499. * Handler for the "click" event on the arrow node of this tree item.
  500. */
  501. _onArrowClick: function (e) {
  502. if (!this._expanded) {
  503. this.expand();
  504. } else {
  505. this.collapse();
  506. }
  507. },
  508. /**
  509. * Handler for the "click" event on the element displaying this tree item.
  510. */
  511. _onClick: function (e) {
  512. e.stopPropagation();
  513. this.focus();
  514. },
  515. /**
  516. * Handler for the "dblclick" event on the element displaying this tree item.
  517. */
  518. _onDoubleClick: function (e) {
  519. // Ignore dblclick on the arrow as it has already recived and handled two
  520. // click events.
  521. if (!e.target.classList.contains("arrow")) {
  522. this._onArrowClick(e);
  523. }
  524. this.focus();
  525. },
  526. /**
  527. * Handler for the "keypress" event on the element displaying this tree item.
  528. */
  529. _onKeyPress: function (e) {
  530. // Prevent scrolling when pressing navigation keys.
  531. ViewHelpers.preventScrolling(e);
  532. switch (e.keyCode) {
  533. case KeyCodes.DOM_VK_UP:
  534. this._focusPrevNode();
  535. return;
  536. case KeyCodes.DOM_VK_DOWN:
  537. this._focusNextNode();
  538. return;
  539. case KeyCodes.DOM_VK_LEFT:
  540. if (this._expanded && this._populated) {
  541. this.collapse();
  542. } else {
  543. this._focusParentNode();
  544. }
  545. return;
  546. case KeyCodes.DOM_VK_RIGHT:
  547. if (!this._expanded) {
  548. this.expand();
  549. } else {
  550. this._focusNextNode();
  551. }
  552. return;
  553. case KeyCodes.DOM_VK_PAGE_UP:
  554. let pageUpElement =
  555. this._getSiblingAtDelta(-this._getNodesPerPageSize());
  556. // There's a chance that the root node is hidden. In this case, its
  557. // height will be 0.
  558. if (pageUpElement && this._getHeight(pageUpElement)) {
  559. pageUpElement.focus();
  560. } else {
  561. this._focusFirstNode();
  562. }
  563. return;
  564. case KeyCodes.DOM_VK_PAGE_DOWN:
  565. let pageDownElement =
  566. this._getSiblingAtDelta(this._getNodesPerPageSize());
  567. if (pageDownElement) {
  568. pageDownElement.focus();
  569. } else {
  570. this._focusLastNode();
  571. }
  572. return;
  573. case KeyCodes.DOM_VK_HOME:
  574. this._focusFirstNode();
  575. return;
  576. case KeyCodes.DOM_VK_END:
  577. this._focusLastNode();
  578. return;
  579. }
  580. },
  581. /**
  582. * Handler for the "focus" event on the element displaying this tree item.
  583. */
  584. _onFocus: function (e) {
  585. this._rootItem.emit("focus", this);
  586. },
  587. /**
  588. * Handler for the "blur" event on the element displaying this tree item.
  589. */
  590. _onBlur: function (e) {
  591. this._rootItem.emit("blur", this);
  592. }
  593. };