base_tree_viewer.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. /* Copyright 2020 Mozilla Foundation
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. import { removeNullCharacters } from "./ui_utils.js";
  16. const TREEITEM_OFFSET_TOP = -100; // px
  17. const TREEITEM_SELECTED_CLASS = "selected";
  18. class BaseTreeViewer {
  19. constructor(options) {
  20. if (this.constructor === BaseTreeViewer) {
  21. throw new Error("Cannot initialize BaseTreeViewer.");
  22. }
  23. this.container = options.container;
  24. this.eventBus = options.eventBus;
  25. this.reset();
  26. }
  27. reset() {
  28. this._pdfDocument = null;
  29. this._lastToggleIsShow = true;
  30. this._currentTreeItem = null;
  31. // Remove the tree from the DOM.
  32. this.container.textContent = "";
  33. // Ensure that the left (right in RTL locales) margin is always reset,
  34. // to prevent incorrect tree alignment if a new document is opened.
  35. this.container.classList.remove("treeWithDeepNesting");
  36. }
  37. /**
  38. * @private
  39. */
  40. _dispatchEvent(count) {
  41. throw new Error("Not implemented: _dispatchEvent");
  42. }
  43. /**
  44. * @private
  45. */
  46. _bindLink(element, params) {
  47. throw new Error("Not implemented: _bindLink");
  48. }
  49. /**
  50. * @private
  51. */
  52. _normalizeTextContent(str) {
  53. // Chars in range [0x01-0x1F] will be replaced with a white space
  54. // and 0x00 by "".
  55. return (
  56. removeNullCharacters(str, /* replaceInvisible */ true) ||
  57. /* en dash = */ "\u2013"
  58. );
  59. }
  60. /**
  61. * Prepend a button before a tree item which allows the user to collapse or
  62. * expand all tree items at that level; see `_toggleTreeItem`.
  63. * @private
  64. */
  65. _addToggleButton(div, hidden = false) {
  66. const toggler = document.createElement("div");
  67. toggler.className = "treeItemToggler";
  68. if (hidden) {
  69. toggler.classList.add("treeItemsHidden");
  70. }
  71. toggler.onclick = evt => {
  72. evt.stopPropagation();
  73. toggler.classList.toggle("treeItemsHidden");
  74. if (evt.shiftKey) {
  75. const shouldShowAll = !toggler.classList.contains("treeItemsHidden");
  76. this._toggleTreeItem(div, shouldShowAll);
  77. }
  78. };
  79. div.prepend(toggler);
  80. }
  81. /**
  82. * Collapse or expand the subtree of a tree item.
  83. *
  84. * @param {Element} root - the root of the item (sub)tree.
  85. * @param {boolean} show - whether to show the item (sub)tree. If false,
  86. * the item subtree rooted at `root` will be collapsed.
  87. * @private
  88. */
  89. _toggleTreeItem(root, show = false) {
  90. this._lastToggleIsShow = show;
  91. for (const toggler of root.querySelectorAll(".treeItemToggler")) {
  92. toggler.classList.toggle("treeItemsHidden", !show);
  93. }
  94. }
  95. /**
  96. * Collapse or expand all subtrees of the `container`.
  97. * @private
  98. */
  99. _toggleAllTreeItems() {
  100. this._toggleTreeItem(this.container, !this._lastToggleIsShow);
  101. }
  102. /**
  103. * @private
  104. */
  105. _finishRendering(fragment, count, hasAnyNesting = false) {
  106. if (hasAnyNesting) {
  107. this.container.classList.add("treeWithDeepNesting");
  108. this._lastToggleIsShow = !fragment.querySelector(".treeItemsHidden");
  109. }
  110. this.container.append(fragment);
  111. this._dispatchEvent(count);
  112. }
  113. render(params) {
  114. throw new Error("Not implemented: render");
  115. }
  116. /**
  117. * @private
  118. */
  119. _updateCurrentTreeItem(treeItem = null) {
  120. if (this._currentTreeItem) {
  121. // Ensure that the current treeItem-selection is always removed.
  122. this._currentTreeItem.classList.remove(TREEITEM_SELECTED_CLASS);
  123. this._currentTreeItem = null;
  124. }
  125. if (treeItem) {
  126. treeItem.classList.add(TREEITEM_SELECTED_CLASS);
  127. this._currentTreeItem = treeItem;
  128. }
  129. }
  130. /**
  131. * @private
  132. */
  133. _scrollToCurrentTreeItem(treeItem) {
  134. if (!treeItem) {
  135. return;
  136. }
  137. // Ensure that the treeItem is *fully* expanded, such that it will first of
  138. // all be visible and secondly that scrolling it into view works correctly.
  139. let currentNode = treeItem.parentNode;
  140. while (currentNode && currentNode !== this.container) {
  141. if (currentNode.classList.contains("treeItem")) {
  142. const toggler = currentNode.firstElementChild;
  143. toggler?.classList.remove("treeItemsHidden");
  144. }
  145. currentNode = currentNode.parentNode;
  146. }
  147. this._updateCurrentTreeItem(treeItem);
  148. this.container.scrollTo(
  149. treeItem.offsetLeft,
  150. treeItem.offsetTop + TREEITEM_OFFSET_TOP
  151. );
  152. }
  153. }
  154. export { BaseTreeViewer };