tree-element 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +43 -31
  2. package/lib/animation.d.ts +2 -2
  3. package/lib/animation.d.ts.map +1 -1
  4. package/lib/animation.js +16 -12
  5. package/lib/animation.js.map +1 -1
  6. package/lib/classNames.d.ts.map +1 -1
  7. package/lib/classNames.js +2 -2
  8. package/lib/classNames.js.map +1 -1
  9. package/lib/dataLoader.d.ts +1 -0
  10. package/lib/dataLoader.d.ts.map +1 -1
  11. package/lib/dataLoader.js +22 -20
  12. package/lib/dataLoader.js.map +1 -1
  13. package/lib/dragAndDropHandler/binarySearch.js +3 -3
  14. package/lib/dragAndDropHandler/binarySearch.js.map +1 -1
  15. package/lib/dragAndDropHandler/dragElement.d.ts.map +1 -1
  16. package/lib/dragAndDropHandler/dragElement.js +11 -13
  17. package/lib/dragAndDropHandler/dragElement.js.map +1 -1
  18. package/lib/dragAndDropHandler/generateHitAreas.d.ts.map +1 -1
  19. package/lib/dragAndDropHandler/generateHitAreas.js +25 -25
  20. package/lib/dragAndDropHandler/generateHitAreas.js.map +1 -1
  21. package/lib/dragAndDropHandler/index.d.ts.map +1 -1
  22. package/lib/dragAndDropHandler/index.js +125 -158
  23. package/lib/dragAndDropHandler/index.js.map +1 -1
  24. package/lib/dragAndDropHandler/iterateVisibleNodes.d.ts.map +1 -1
  25. package/lib/dragAndDropHandler/iterateVisibleNodes.js +23 -22
  26. package/lib/dragAndDropHandler/iterateVisibleNodes.js.map +1 -1
  27. package/lib/dragAndDropHandler/types.d.ts.map +1 -1
  28. package/lib/elementsRenderer.d.ts +20 -7
  29. package/lib/elementsRenderer.d.ts.map +1 -1
  30. package/lib/elementsRenderer.js +195 -136
  31. package/lib/elementsRenderer.js.map +1 -1
  32. package/lib/events.d.ts +44 -11
  33. package/lib/events.d.ts.map +1 -1
  34. package/lib/index.d.ts +36 -13
  35. package/lib/index.d.ts.map +1 -1
  36. package/lib/index.js +386 -349
  37. package/lib/index.js.map +1 -1
  38. package/lib/keyHandler.d.ts +13 -8
  39. package/lib/keyHandler.d.ts.map +1 -1
  40. package/lib/keyHandler.js +106 -51
  41. package/lib/keyHandler.js.map +1 -1
  42. package/lib/methodTypes.d.ts +3 -3
  43. package/lib/methodTypes.d.ts.map +1 -1
  44. package/lib/mouseHandler.d.ts +1 -1
  45. package/lib/mouseHandler.d.ts.map +1 -1
  46. package/lib/mouseHandler.js +36 -49
  47. package/lib/mouseHandler.js.map +1 -1
  48. package/lib/mouseUtils.d.ts.map +1 -1
  49. package/lib/mouseUtils.js +2 -2
  50. package/lib/mouseUtils.js.map +1 -1
  51. package/lib/node.d.ts +47 -16
  52. package/lib/node.d.ts.map +1 -1
  53. package/lib/node.js +169 -171
  54. package/lib/node.js.map +1 -1
  55. package/lib/nodeElement/borderDropHint.d.ts.map +1 -1
  56. package/lib/nodeElement/borderDropHint.js +4 -5
  57. package/lib/nodeElement/borderDropHint.js.map +1 -1
  58. package/lib/nodeElement/folderElement.d.ts +8 -5
  59. package/lib/nodeElement/folderElement.d.ts.map +1 -1
  60. package/lib/nodeElement/folderElement.js +75 -67
  61. package/lib/nodeElement/folderElement.js.map +1 -1
  62. package/lib/nodeElement/ghostDropHint.d.ts.map +1 -1
  63. package/lib/nodeElement/ghostDropHint.js +13 -13
  64. package/lib/nodeElement/ghostDropHint.js.map +1 -1
  65. package/lib/nodeElement/index.d.ts +2 -1
  66. package/lib/nodeElement/index.d.ts.map +1 -1
  67. package/lib/nodeElement/index.js +38 -30
  68. package/lib/nodeElement/index.js.map +1 -1
  69. package/lib/nodeUtils.d.ts.map +1 -1
  70. package/lib/nodeUtils.js +1 -1
  71. package/lib/nodeUtils.js.map +1 -1
  72. package/lib/options.d.ts +19 -3
  73. package/lib/options.d.ts.map +1 -1
  74. package/lib/positionUtils.js +3 -3
  75. package/lib/positionUtils.js.map +1 -1
  76. package/lib/requestUrl.d.ts +1 -1
  77. package/lib/requestUrl.d.ts.map +1 -1
  78. package/lib/requestUrl.js +6 -17
  79. package/lib/requestUrl.js.map +1 -1
  80. package/lib/saveStateHandler.d.ts.map +1 -1
  81. package/lib/saveStateHandler.js +59 -89
  82. package/lib/saveStateHandler.js.map +1 -1
  83. package/lib/scrollHandler/containerScrollParent.d.ts.map +1 -1
  84. package/lib/scrollHandler/containerScrollParent.js +11 -18
  85. package/lib/scrollHandler/containerScrollParent.js.map +1 -1
  86. package/lib/scrollHandler/createScrollParent.d.ts.map +1 -1
  87. package/lib/scrollHandler/createScrollParent.js +12 -13
  88. package/lib/scrollHandler/createScrollParent.js.map +1 -1
  89. package/lib/scrollHandler/documentScrollParent.d.ts.map +1 -1
  90. package/lib/scrollHandler/documentScrollParent.js +18 -27
  91. package/lib/scrollHandler/documentScrollParent.js.map +1 -1
  92. package/lib/scrollHandler/scrollParent.d.ts.map +1 -1
  93. package/lib/scrollHandler/scrollParent.js +30 -34
  94. package/lib/scrollHandler/scrollParent.js.map +1 -1
  95. package/lib/scrollHandler.d.ts.map +1 -1
  96. package/lib/scrollHandler.js +11 -14
  97. package/lib/scrollHandler.js.map +1 -1
  98. package/lib/selectNodeHandler.d.ts +4 -2
  99. package/lib/selectNodeHandler.d.ts.map +1 -1
  100. package/lib/selectNodeHandler.js +64 -83
  101. package/lib/selectNodeHandler.js.map +1 -1
  102. package/lib/setDefaultOptions.d.ts.map +1 -1
  103. package/lib/setDefaultOptions.js +34 -38
  104. package/lib/setDefaultOptions.js.map +1 -1
  105. package/lib/triggerCustomEvent.d.ts.map +1 -1
  106. package/lib/triggerCustomEvent.js +2 -2
  107. package/lib/triggerCustomEvent.js.map +1 -1
  108. package/lib/util.d.ts.map +1 -1
  109. package/lib/util.js +2 -2
  110. package/lib/util.js.map +1 -1
  111. package/lib/version.d.ts +1 -1
  112. package/lib/version.js +1 -1
  113. package/lib/version.js.map +1 -1
  114. package/package.json +43 -32
  115. package/src/animation.ts +14 -12
  116. package/src/classNames.ts +52 -52
  117. package/src/dataLoader.ts +79 -71
  118. package/src/dragAndDropHandler/binarySearch.ts +17 -17
  119. package/src/dragAndDropHandler/dragElement.ts +51 -50
  120. package/src/dragAndDropHandler/generateHitAreas.ts +156 -161
  121. package/src/dragAndDropHandler/index.ts +334 -348
  122. package/src/dragAndDropHandler/iterateVisibleNodes.ts +61 -63
  123. package/src/dragAndDropHandler/types.ts +5 -5
  124. package/src/elementsRenderer.ts +377 -294
  125. package/src/events.ts +241 -208
  126. package/src/header.txt +1 -1
  127. package/src/index.ts +259 -229
  128. package/src/keyHandler.ts +179 -115
  129. package/src/methodTypes.ts +7 -10
  130. package/src/mouseHandler.ts +316 -316
  131. package/src/mouseUtils.ts +14 -14
  132. package/src/node.ts +773 -771
  133. package/src/nodeElement/borderDropHint.ts +25 -25
  134. package/src/nodeElement/folderElement.ts +116 -102
  135. package/src/nodeElement/ghostDropHint.ts +64 -62
  136. package/src/nodeElement/index.ts +81 -73
  137. package/src/nodeUtils.ts +5 -5
  138. package/src/options.ts +25 -5
  139. package/src/requestUrl.ts +7 -14
  140. package/src/saveStateHandler.ts +191 -207
  141. package/src/scrollHandler/containerScrollParent.ts +40 -49
  142. package/src/scrollHandler/createScrollParent.ts +26 -27
  143. package/src/scrollHandler/documentScrollParent.ts +66 -72
  144. package/src/scrollHandler/scrollParent.ts +90 -86
  145. package/src/scrollHandler.ts +45 -45
  146. package/src/selectNodeHandler.ts +171 -165
  147. package/src/setDefaultOptions.ts +44 -32
  148. package/src/triggerCustomEvent.ts +7 -3
  149. package/src/util.ts +2 -3
  150. package/src/version.ts +1 -1
  151. package/tree_element.css +87 -69
  152. package/tree_element.debug.js +3866 -3831
  153. package/tree_element.debug.js.map +1 -1
  154. package/tree_element.js +2 -2
  155. package/tree_element.js.map +1 -1
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ClassNames } from "./classNames";
2
- import type { MoveInfo, TreeEvent, TreeEventName, TreeEvents } from "./events";
2
+ import type { TreeEventName, TreeEvents } from "./events";
3
3
  import type { PositionInfo } from "./mouseUtils";
4
4
  import type { NodeData, NodeId, Position } from "./node";
5
5
  import type { TreeElementOptions } from "./options";
@@ -28,19 +28,6 @@ import __version__ from "./version";
28
28
  // the submodules directly, because those are not exposed in package.json.
29
29
  // Type only, so that the iife build keeps exposing the TreeElement class itself
30
30
  // as its global, instead of an object of named exports.
31
- export type {
32
- MoveInfo,
33
- Node,
34
- NodeData,
35
- NodeId,
36
- Position,
37
- SavedState,
38
- SelectNodeOptions,
39
- TreeElementOptions,
40
- TreeEvent,
41
- TreeEventName,
42
- TreeEvents,
43
- };
44
31
 
45
32
  export type TriggerEventProvider = (
46
33
  element: HTMLElement,
@@ -49,21 +36,21 @@ export type TriggerEventProvider = (
49
36
  ) => boolean;
50
37
 
51
38
  interface TreeElementParams extends Partial<TreeElementOptions> {
52
- /** The element the tree is rendered into. Its content is replaced. */
39
+ /**
40
+ The element the tree is rendered into. Its content is replaced.
41
+ */
53
42
  htmlElement: HTMLElement;
54
43
  /** Replaces how events are dispatched. It must return whether the event
55
44
  * was not cancelled. This exists for tests and for integrating with
56
45
  * another event system. */
57
- overrideTriggerEventProvider?: TriggerEventProvider,
46
+ overrideTriggerEventProvider?: TriggerEventProvider;
58
47
  }
59
48
 
60
49
  export default class TreeElement {
61
- /** @hidden */
62
- public tree: Node;
63
-
64
50
  private classNames: ClassNames;
65
51
  private dataLoader: DataLoader;
66
52
  private dndHandler: DragAndDropHandler;
53
+ private fallbackTabStop: HTMLElement | null;
67
54
  private htmlElement: HTMLElement;
68
55
  private isInitialized: boolean;
69
56
  private keyHandler: KeyHandler;
@@ -74,20 +61,31 @@ export default class TreeElement {
74
61
  private saveStateHandler: SaveStateHandler;
75
62
  private scrollHandler: ScrollHandler;
76
63
  private selectNodeHandler: SelectNodeHandler;
64
+ private tree: Node;
77
65
  private triggerEventProvider: TriggerEventProvider;
78
66
 
79
- /** @hidden */
80
- constructor({ htmlElement, overrideTriggerEventProvider, ...options }: TreeElementParams) {
67
+ /**
68
+ @hidden
69
+ */
70
+ constructor({
71
+ htmlElement,
72
+ overrideTriggerEventProvider,
73
+ ...options
74
+ }: TreeElementParams) {
81
75
  this.htmlElement = htmlElement;
82
76
  this.options = setDefaultOptions(htmlElement, options);
83
77
  this.classNames = createClassNames(this.options);
84
- this.triggerEventProvider = overrideTriggerEventProvider ?? triggerCustomEvent;
78
+ this.triggerEventProvider =
79
+ overrideTriggerEventProvider ?? triggerCustomEvent;
85
80
 
86
81
  this.isInitialized = false;
87
82
  this.tree = new Node({}, true);
88
83
  this.nodeMap = new WeakMap();
84
+ this.fallbackTabStop = null;
89
85
 
90
86
  const {
87
+ ariaLabel,
88
+ ariaLabelledBy,
91
89
  autoEscape,
92
90
  buttonLeft,
93
91
  closedIcon,
@@ -115,10 +113,10 @@ export default class TreeElement {
115
113
  const closeNode = this.closeNode.bind(this);
116
114
  const getNodeElement = this.getNodeElement.bind(this);
117
115
  const getNodeElementForNode = this.getNodeElementForNode.bind(this);
116
+ const getFocusedNode = this.getFocusedNode.bind(this);
118
117
  const getNodeById = this.getNodeById.bind(this);
119
118
  const getSelectedNode = this.getSelectedNode.bind(this);
120
119
  const getTree = this.getTree.bind(this);
121
- const isFocusOnTree = this.isFocusOnTree.bind(this);
122
120
  const loadData = this.loadData.bind(this);
123
121
  const openNode = this.openNode.bind(this);
124
122
  const openParents = this.openParents.bind(this);
@@ -127,10 +125,11 @@ export default class TreeElement {
127
125
  const setNodeElement = this.setNodeElement.bind(this);
128
126
  const treeElement = this.htmlElement;
129
127
  const triggerEvent = this.triggerEvent.bind(this);
128
+ const updateTabStop = this.updateTabStop.bind(this);
130
129
 
131
130
  const saveState = () => {
132
131
  saveStateHandler.saveState();
133
- }
132
+ };
134
133
 
135
134
  const selectNodeHandler = new SelectNodeHandler({
136
135
  getNodeById,
@@ -139,7 +138,8 @@ export default class TreeElement {
139
138
  getSelectable: () => this.options.selectable,
140
139
  openParents,
141
140
  saveState,
142
- triggerEvent
141
+ triggerEvent,
142
+ updateTabStop,
143
143
  });
144
144
 
145
145
  const addToSelection =
@@ -150,7 +150,8 @@ export default class TreeElement {
150
150
  selectNodeHandler.isNodeSelected.bind(selectNodeHandler);
151
151
  const removeFromSelection =
152
152
  selectNodeHandler.removeFromSelection.bind(selectNodeHandler);
153
- const selectNode = selectNodeHandler.selectSingleNode.bind(selectNodeHandler);
153
+ const selectNode =
154
+ selectNodeHandler.selectSingleNode.bind(selectNodeHandler);
154
155
 
155
156
  const getMouseDelay = () => this.options.startDndDelay ?? 0;
156
157
 
@@ -204,14 +205,18 @@ export default class TreeElement {
204
205
 
205
206
  const keyHandler = new KeyHandler({
206
207
  closeNode,
208
+ getFocusedNode,
207
209
  getSelectedNode,
208
- isFocusOnTree,
210
+ getTree,
209
211
  keyboardSupport,
210
212
  openNode,
211
213
  selectNode,
214
+ triggerEvent,
212
215
  });
213
216
 
214
217
  const renderer = new ElementsRenderer({
218
+ ariaLabel,
219
+ ariaLabelledBy,
215
220
  autoEscape,
216
221
  buttonLeft,
217
222
  classNames,
@@ -267,10 +272,7 @@ export default class TreeElement {
267
272
  * @returns The new node, or `null` when the node has no parent.
268
273
  * @group Changing the tree
269
274
  */
270
- public addNodeAfter(
271
- nodeData: NodeData,
272
- existingNode: Node,
273
- ): Node | null {
275
+ public addNodeAfter(nodeData: NodeData, existingNode: Node): Node | null {
274
276
  const newNode = existingNode.addAfter(nodeData);
275
277
 
276
278
  if (newNode) {
@@ -286,10 +288,7 @@ export default class TreeElement {
286
288
  * @returns The new node, or `null` when the node has no parent.
287
289
  * @group Changing the tree
288
290
  */
289
- public addNodeBefore(
290
- nodeData: NodeData,
291
- existingNode: Node,
292
- ): Node | null {
291
+ public addNodeBefore(nodeData: NodeData, existingNode: Node): Node | null {
293
292
  const newNode = existingNode.addBefore(nodeData);
294
293
 
295
294
  if (newNode) {
@@ -306,10 +305,7 @@ export default class TreeElement {
306
305
  * @returns The new node, or `null` when the node has no parent.
307
306
  * @group Changing the tree
308
307
  */
309
- public addParentNode(
310
- nodeData: NodeData,
311
- existingNode: Node,
312
- ): Node | null {
308
+ public addParentNode(nodeData: NodeData, existingNode: Node): Node | null {
313
309
  const newNode = existingNode.addParent(nodeData);
314
310
 
315
311
  if (newNode) {
@@ -325,12 +321,13 @@ export default class TreeElement {
325
321
  * @param mustSetFocus - Move the focus to the node. Default `true`.
326
322
  * @group Selection
327
323
  */
328
- public addToSelection(node: Node, mustSetFocus?: boolean) {
324
+ public addToSelection(node: Node, mustSetFocus = true) {
329
325
  this.selectNodeHandler.addToSelection(node);
330
326
  this.openParents(node);
331
327
 
332
- this.getNodeElementForNode(node).select(mustSetFocus ?? true);
328
+ this.getNodeElementForNode(node).select(mustSetFocus);
333
329
 
330
+ this.updateTabStop();
334
331
  this.saveState();
335
332
  }
336
333
 
@@ -354,20 +351,29 @@ export default class TreeElement {
354
351
  }
355
352
 
356
353
  /**
357
- * Closes a folder.
354
+ * Closes a folder. Await the promise when you need to know the closing
355
+ * animation has finished.
356
+ *
357
+ * @example
358
+ * ```js
359
+ * await tree.closeNode(node);
360
+ * console.log("closed");
361
+ * ```
358
362
  *
359
363
  * @param slide - Override the `slide` option for this call.
360
364
  * @group Opening and closing
361
365
  */
362
- public closeNode(node: Node, slide?: boolean): void {
363
- if (node.isFolder() || node.isEmptyFolder) {
364
- this.createFolderElement(node).close(
365
- slide ?? this.options.slide,
366
- this.options.animationSpeed,
367
- );
368
-
369
- this.saveState();
366
+ public async closeNode(node: Node, slide?: boolean): Promise<void> {
367
+ if (!(node.isFolder() || node.isEmptyFolder)) {
368
+ return;
370
369
  }
370
+
371
+ await this.createFolderElement(node).close(
372
+ slide ?? this.options.slide,
373
+ this.options.animationSpeed,
374
+ );
375
+
376
+ this.saveState();
371
377
  }
372
378
 
373
379
  /**
@@ -377,7 +383,7 @@ export default class TreeElement {
377
383
  * @group Other
378
384
  */
379
385
  public deinit(): void {
380
- this.htmlElement.textContent = '';
386
+ this.htmlElement.textContent = "";
381
387
 
382
388
  this.dataLoader.deinit();
383
389
  this.keyHandler.deinit();
@@ -396,11 +402,7 @@ export default class TreeElement {
396
402
  `li.${this.classNames.common}`,
397
403
  );
398
404
 
399
- if (liElement) {
400
- return this.nodeMap.get(liElement) ?? null;
401
- } else {
402
- return null;
403
- }
405
+ return liElement ? (this.nodeMap.get(liElement) ?? null) : null;
404
406
  }
405
407
 
406
408
  /**
@@ -465,11 +467,11 @@ export default class TreeElement {
465
467
  }
466
468
 
467
469
  /**
468
- * Returns the selected node, or `false` when nothing is selected.
470
+ * Returns the selected node, or `null` when nothing is selected.
469
471
  *
470
472
  * @group Selection
471
473
  */
472
- public getSelectedNode(): false | Node {
474
+ public getSelectedNode(): Node | null {
473
475
  return this.selectNodeHandler.getSelectedNode();
474
476
  }
475
477
 
@@ -571,11 +573,10 @@ export default class TreeElement {
571
573
  * @param url - Defaults to the `dataUrl` option.
572
574
  * @group Loading data
573
575
  */
574
- public async loadDataFromUrl(
575
- url?: string,
576
- parentNode?: Node
577
- ): Promise<void> {
578
- const requestUrl = url ? new RequestUrl(url) : this.createRequestUrl(parentNode);
576
+ public async loadDataFromUrl(url?: string, parentNode?: Node): Promise<void> {
577
+ const requestUrl = url
578
+ ? new RequestUrl(url)
579
+ : this.createRequestUrl(parentNode);
579
580
 
580
581
  if (requestUrl) {
581
582
  await this.dataLoader.loadFromUrl(requestUrl, parentNode);
@@ -588,10 +589,7 @@ export default class TreeElement {
588
589
  * @group Selection
589
590
  */
590
591
  public moveDown() {
591
- const selectedNode = this.getSelectedNode();
592
- if (selectedNode) {
593
- this.keyHandler.moveDown(selectedNode);
594
- }
592
+ this.keyHandler.moveDown();
595
593
  }
596
594
 
597
595
  /**
@@ -605,11 +603,7 @@ export default class TreeElement {
605
603
  * @param position - `"before"`, `"after"` or `"inside"`.
606
604
  * @group Changing the tree
607
605
  */
608
- public moveNode(
609
- node: Node,
610
- targetNode: Node,
611
- position: Position,
612
- ): void {
606
+ public moveNode(node: Node, targetNode: Node, position: Position): void {
613
607
  this.tree.moveNode(node, targetNode, position);
614
608
  this.refreshElements(null);
615
609
  }
@@ -620,10 +614,7 @@ export default class TreeElement {
620
614
  * @group Selection
621
615
  */
622
616
  public moveUp() {
623
- const selectedNode = this.getSelectedNode();
624
- if (selectedNode) {
625
- this.keyHandler.moveUp(selectedNode);
626
- }
617
+ this.keyHandler.moveUp();
627
618
  }
628
619
 
629
620
  /**
@@ -640,15 +631,12 @@ export default class TreeElement {
640
631
  * @param slide - Override the `slide` option for this call.
641
632
  * @group Opening and closing
642
633
  */
643
- public async openNode(
644
- node: Node,
645
- slide?: boolean
646
- ): Promise<void> {
634
+ public async openNode(node: Node, slide?: boolean): Promise<void> {
647
635
  const mustSlide = slide ?? this.options.slide;
648
636
 
649
637
  const doOpenNode = async (
650
638
  openedNode: Node,
651
- slideOption: boolean
639
+ slideOption: boolean,
652
640
  ): Promise<void> => {
653
641
  if (!node.children.length) {
654
642
  return;
@@ -656,10 +644,7 @@ export default class TreeElement {
656
644
 
657
645
  const folderElement = this.createFolderElement(openedNode);
658
646
 
659
- await folderElement.open(
660
- slideOption,
661
- this.options.animationSpeed,
662
- );
647
+ await folderElement.open(slideOption, this.options.animationSpeed);
663
648
  };
664
649
 
665
650
  if (node.isFolder() || node.isEmptyFolder) {
@@ -726,6 +711,7 @@ export default class TreeElement {
726
711
  this.selectNodeHandler.removeFromSelection(node);
727
712
 
728
713
  this.getNodeElementForNode(node).deselect();
714
+ this.updateTabStop();
729
715
  this.saveState();
730
716
  }
731
717
 
@@ -752,9 +738,7 @@ export default class TreeElement {
752
738
  return;
753
739
  }
754
740
 
755
- const top =
756
- getOffsetTop(node.element) -
757
- getOffsetTop(this.htmlElement);
741
+ const top = getOffsetTop(node.element) - getOffsetTop(this.htmlElement);
758
742
 
759
743
  this.scrollHandler.scrollToY(top);
760
744
  }
@@ -768,10 +752,7 @@ export default class TreeElement {
768
752
  * `false`.
769
753
  * @group Selection
770
754
  */
771
- public selectNode(
772
- node: Node | null,
773
- options?: SelectNodeOptions,
774
- ): void {
755
+ public selectNode(node: Node | null, options?: SelectNodeOptions): void {
775
756
  if (!node) {
776
757
  // Called with empty node -> deselect current node
777
758
  this.deselectCurrentNode();
@@ -779,7 +760,11 @@ export default class TreeElement {
779
760
  return;
780
761
  }
781
762
 
782
- this.selectNodeHandler.selectSingleNode(node, options);
763
+ // Unlike a click, selecting the selected node again keeps it selected
764
+ this.selectNodeHandler.selectSingleNode(node, {
765
+ mustToggle: false,
766
+ ...options,
767
+ });
783
768
  }
784
769
 
785
770
  /**
@@ -793,28 +778,32 @@ export default class TreeElement {
793
778
  }
794
779
 
795
780
  /**
796
- * Applies a state to the tree.
781
+ * Applies a state to the tree, and saves it to storage when `saveState` is
782
+ * enabled.
797
783
  *
798
784
  * @group State
799
785
  */
800
786
  public setState(state: SavedState) {
801
787
  this.saveStateHandler.setInitialState(state);
802
788
  this.refreshElements(null);
789
+ this.saveState();
803
790
  }
804
791
 
805
792
  /**
806
- * Closes an open node and opens a closed one.
793
+ * Closes an open node and opens a closed one. Await the promise when you
794
+ * need to know the node is really open or closed, like with `openNode` and
795
+ * `closeNode`.
807
796
  *
808
797
  * @param slide - Override the `slide` option for this call.
809
798
  * @group Opening and closing
810
799
  */
811
- public toggle(node: Node, slide: boolean | null = null) {
800
+ public async toggle(node: Node, slide: boolean | null = null): Promise<void> {
812
801
  const mustSlide = slide ?? this.options.slide;
813
802
 
814
803
  if (node.is_open) {
815
- this.closeNode(node, mustSlide);
804
+ await this.closeNode(node, mustSlide);
816
805
  } else {
817
- void this.openNode(node, mustSlide);
806
+ await this.openNode(node, mustSlide);
818
807
  }
819
808
  }
820
809
 
@@ -860,7 +849,7 @@ export default class TreeElement {
860
849
  if (
861
850
  typeof data === "object" &&
862
851
  data.children &&
863
- data.children instanceof Array
852
+ Array.isArray(data.children)
864
853
  ) {
865
854
  node.removeChildren();
866
855
 
@@ -872,6 +861,34 @@ export default class TreeElement {
872
861
  this.refreshElements(node);
873
862
  }
874
863
 
864
+ // Open the nodes up to the autoOpen level. A folder that is loaded on
865
+ // demand is fetched first, and its children are opened when the data has
866
+ // arrived.
867
+ private async autoOpenNodesOnDemand(): Promise<void> {
868
+ const maxLevel = this.getAutoOpenMaxLevel();
869
+
870
+ const openNodes = async (): Promise<void> => {
871
+ const loading: Promise<void>[] = [];
872
+
873
+ this.tree.iterate((node: Node, level: number) => {
874
+ if (node.load_on_demand) {
875
+ if (!node.is_loading) {
876
+ loading.push(this.openNode(node, false).then(openNodes));
877
+ }
878
+
879
+ return false;
880
+ }
881
+ void this.openNode(node, false);
882
+
883
+ return level !== maxLevel;
884
+ });
885
+
886
+ await Promise.all(loading);
887
+ };
888
+
889
+ await openNodes();
890
+ }
891
+
875
892
  private createFolderElement(node: Node) {
876
893
  const classNames = this.classNames;
877
894
  const closedIconElement = this.renderer.closedIconElement;
@@ -883,12 +900,22 @@ export default class TreeElement {
883
900
  const treeElement = this.htmlElement;
884
901
  const triggerEvent = this.triggerEvent.bind(this);
885
902
 
903
+ // A selected node in a closed folder is rendered when the folder is
904
+ // opened, which can be after it was selected. It becomes the tab stop
905
+ // then, so the fallback tab stop must go.
906
+ const renderChildren = (folder: Node) => {
907
+ const ul = this.renderer.renderChildren(folder);
908
+ this.updateTabStop();
909
+ return ul;
910
+ };
911
+
886
912
  return new FolderElement({
887
913
  classNames,
888
914
  closedIconElement,
889
915
  getScrollLeft,
890
916
  node,
891
917
  openedIconElement,
918
+ renderChildren,
892
919
  tabIndex,
893
920
  treeElement,
894
921
  triggerEvent,
@@ -913,19 +940,13 @@ export default class TreeElement {
913
940
  }
914
941
 
915
942
  /* Create a RequestUrl based on the url in the options.
916
- * Add a 'node' query parameter for loading on demand
917
- * Add a 'selected_node' query parameter if a node is selected.
918
- */
943
+ * Add a 'node' query parameter for loading on demand
944
+ * Add a 'selected_node' query parameter if a node is selected.
945
+ */
919
946
  private createRequestUrl(node?: Node): null | RequestUrl {
920
947
  const dataUrl = this.options.dataUrl;
921
948
 
922
- let url;
923
-
924
- if (typeof dataUrl === "function") {
925
- url = dataUrl(node);
926
- } else {
927
- url = dataUrl;
928
- }
949
+ const url = typeof dataUrl === "function" ? dataUrl(node) : dataUrl;
929
950
 
930
951
  if (!url) {
931
952
  return null;
@@ -935,12 +956,12 @@ export default class TreeElement {
935
956
 
936
957
  if (node?.id) {
937
958
  // Load on demand of a subtree; add node parameter
938
- requestUrl.setSearchParam('node', node.id.toString());
959
+ requestUrl.setSearchParam("node", node.id.toString());
939
960
  } else {
940
961
  // Add selected_node parameter
941
962
  const selectedNodeId = this.getNodeIdToBeSelected();
942
963
  if (selectedNodeId) {
943
- requestUrl.setSearchParam('selected_node', selectedNodeId.toString());
964
+ requestUrl.setSearchParam("selected_node", selectedNodeId.toString());
944
965
  }
945
966
  }
946
967
 
@@ -967,9 +988,11 @@ export default class TreeElement {
967
988
  private getAutoOpenMaxLevel(): number {
968
989
  if (this.options.autoOpen === true) {
969
990
  return -1;
970
- } else if (typeof this.options.autoOpen === "number") {
991
+ }
992
+ if (typeof this.options.autoOpen === "number") {
971
993
  return this.options.autoOpen;
972
- } else if (typeof this.options.autoOpen === "string") {
994
+ }
995
+ if (typeof this.options.autoOpen === "string") {
973
996
  return parseInt(this.options.autoOpen, 10);
974
997
  }
975
998
 
@@ -977,27 +1000,55 @@ export default class TreeElement {
977
1000
  return 0;
978
1001
  }
979
1002
 
980
- private getNodeElement(element: HTMLElement): NodeElement | null {
981
- const node = this.getNode(element);
982
- if (node) {
983
- return this.getNodeElementForNode(node);
984
- } else {
1003
+ // The title span of the first node, or null when the tree is empty
1004
+ private getFirstTitleSpan(): HTMLElement | null {
1005
+ const firstElement = this.tree.children[0]?.element;
1006
+
1007
+ return firstElement
1008
+ ? firstElement.querySelector<HTMLElement>(
1009
+ `:scope > .${this.classNames.element} > span.${this.classNames.title}`,
1010
+ )
1011
+ : null;
1012
+ }
1013
+
1014
+ // The node of the HTML element that has the focus, if it is in this tree
1015
+ private getFocusedNode(): Node | null {
1016
+ const activeElement = document.activeElement;
1017
+
1018
+ /* istanbul ignore if */
1019
+ if (!activeElement) {
1020
+ return null;
1021
+ }
1022
+
1023
+ // The keyboard must still work for input elements.
1024
+ const tagName = activeElement.tagName;
1025
+ if (tagName !== "A" && tagName !== "SPAN") {
985
1026
  return null;
986
1027
  }
1028
+
1029
+ const node = this.getNode(activeElement as HTMLElement);
1030
+ return node?.tree === this.tree ? node : null;
1031
+ }
1032
+
1033
+ private getNodeElement(element: HTMLElement): NodeElement | null {
1034
+ const node = this.getNode(element);
1035
+ return node ? this.getNodeElementForNode(node) : null;
987
1036
  }
988
1037
 
989
1038
  private getNodeElementForNode(node: Node): NodeElement {
990
- if (node.isFolder()) {
991
- return this.createFolderElement(node);
992
- } else {
993
- return this.createNodeElement(node);
994
- }
1039
+ return node.isFolder()
1040
+ ? this.createFolderElement(node)
1041
+ : this.createNodeElement(node);
995
1042
  }
996
1043
 
997
1044
  private getNodeIdToBeSelected(): NodeId | null {
998
1045
  return this.saveStateHandler.getNodeIdToBeSelected();
999
1046
  }
1000
1047
 
1048
+ private hasRenderedSelectedNode(): boolean {
1049
+ return this.getSelectedNodes().some((node) => Boolean(node.element));
1050
+ }
1051
+
1001
1052
  private initData(): void {
1002
1053
  if (this.options.data) {
1003
1054
  this.loadData(this.options.data);
@@ -1014,17 +1065,15 @@ export default class TreeElement {
1014
1065
 
1015
1066
  private initTree(data: NodeData[]): void {
1016
1067
  const doInit = (): void => {
1017
- if (!this.isInitialized) {
1018
- this.isInitialized = true;
1019
- this.triggerEvent("tree.init");
1068
+ if (this.isInitialized) {
1069
+ return;
1020
1070
  }
1071
+
1072
+ this.isInitialized = true;
1073
+ this.triggerEvent("tree.init");
1021
1074
  };
1022
1075
 
1023
- this.tree = new this.options.nodeClass(
1024
- null,
1025
- true,
1026
- this.options.nodeClass,
1027
- );
1076
+ this.tree = new this.options.nodeClass(null, true, this.options.nodeClass);
1028
1077
 
1029
1078
  this.selectNodeHandler.clear();
1030
1079
 
@@ -1044,41 +1093,29 @@ export default class TreeElement {
1044
1093
 
1045
1094
  // Does an HTML element of the tree have the focus?
1046
1095
  private isFocusOnTree(): boolean {
1047
- const activeElement = document.activeElement;
1048
-
1049
- /* istanbul ignore if */
1050
- if (!activeElement) {
1051
- return false;
1052
- }
1053
-
1054
- // The keyboard must still work for input elements.
1055
- const tagName = activeElement.tagName;
1056
- if (tagName !== "A" && tagName !== "SPAN") {
1057
- return false;
1058
- }
1059
-
1060
- const node = this.getNode(activeElement as HTMLElement);
1061
- return node?.tree === this.tree;
1096
+ return this.getFocusedNode() !== null;
1062
1097
  }
1063
1098
 
1064
1099
  private isSelectedNodeInSubtree(subtree: Node): boolean {
1065
1100
  const selectedNode = this.getSelectedNode();
1066
1101
 
1067
- if (!selectedNode) {
1068
- return false;
1069
- } else {
1070
- return subtree === selectedNode || subtree.isParentOf(selectedNode);
1071
- }
1102
+ return selectedNode
1103
+ ? subtree === selectedNode || subtree.isParentOf(selectedNode)
1104
+ : false;
1072
1105
  }
1073
1106
 
1074
- private async loadFolderOnDemand(
1075
- node: Node,
1076
- slide: boolean,
1077
- ): Promise<void> {
1107
+ private async loadFolderOnDemand(node: Node, slide: boolean): Promise<void> {
1078
1108
  node.is_loading = true;
1079
1109
 
1080
1110
  await this.loadDataFromUrl(undefined, node);
1081
1111
 
1112
+ // Loading failed, or there is no url. Opening the node now would load it
1113
+ // again, without end. Leave it closed so the user can try again.
1114
+ if (node.load_on_demand) {
1115
+ node.is_loading = false;
1116
+ return;
1117
+ }
1118
+
1082
1119
  await this.openNode(node, slide);
1083
1120
  }
1084
1121
 
@@ -1092,11 +1129,9 @@ export default class TreeElement {
1092
1129
  }
1093
1130
 
1094
1131
  private mouseCapture(positionInfo: PositionInfo): boolean | null {
1095
- if (!this.options.dragAndDrop) {
1096
- return false;
1097
- }
1098
-
1099
- return this.dndHandler.mouseCapture(positionInfo);
1132
+ return this.options.dragAndDrop
1133
+ ? this.dndHandler.mouseCapture(positionInfo)
1134
+ : false;
1100
1135
  }
1101
1136
 
1102
1137
  private mouseDrag(positionInfo: PositionInfo): boolean {
@@ -1112,6 +1147,7 @@ export default class TreeElement {
1112
1147
 
1113
1148
  private mouseStart(positionInfo: PositionInfo): boolean {
1114
1149
  /* istanbul ignore if */
1150
+ // eslint-disable-next-line unicorn/prefer-ternary
1115
1151
  if (!this.options.dragAndDrop) {
1116
1152
  return false;
1117
1153
  }
@@ -1153,6 +1189,8 @@ export default class TreeElement {
1153
1189
  this.selectCurrentNode(mustSetFocus);
1154
1190
  }
1155
1191
 
1192
+ this.updateTabStop();
1193
+
1156
1194
  this.triggerEvent("tree.refresh");
1157
1195
  }
1158
1196
 
@@ -1162,10 +1200,12 @@ export default class TreeElement {
1162
1200
 
1163
1201
  private selectCurrentNode(mustSetFocus: boolean): void {
1164
1202
  const node = this.getSelectedNode();
1165
- if (node) {
1166
- const nodeElement = this.getNodeElementForNode(node);
1167
- nodeElement.select(mustSetFocus);
1203
+ if (!node) {
1204
+ return;
1168
1205
  }
1206
+
1207
+ const nodeElement = this.getNodeElementForNode(node);
1208
+ nodeElement.select(mustSetFocus);
1169
1209
  }
1170
1210
 
1171
1211
  // Set initial state, either by restoring the state or auto-opening nodes
@@ -1175,15 +1215,13 @@ export default class TreeElement {
1175
1215
  // result: is state restored, must load on demand?
1176
1216
  const state = this.saveStateHandler.getStateFromStorage();
1177
1217
 
1178
- if (!state) {
1179
- return [false, false];
1180
- } else {
1181
- const mustLoadOnDemand =
1182
- this.saveStateHandler.setInitialState(state);
1218
+ if (state) {
1219
+ const mustLoadOnDemand = this.saveStateHandler.setInitialState(state);
1183
1220
 
1184
1221
  // return true: the state is restored
1185
1222
  return [true, mustLoadOnDemand];
1186
1223
  }
1224
+ return [false, false];
1187
1225
  };
1188
1226
 
1189
1227
  const autoOpenNodes = (): boolean => {
@@ -1199,12 +1237,12 @@ export default class TreeElement {
1199
1237
  if (node.load_on_demand) {
1200
1238
  mustLoadOnDemand = true;
1201
1239
  return false;
1202
- } else if (!node.hasChildren()) {
1203
- return false;
1204
- } else {
1240
+ }
1241
+ if (node.hasChildren()) {
1205
1242
  node.is_open = true;
1206
1243
  return level !== maxLevel;
1207
1244
  }
1245
+ return false;
1208
1246
  });
1209
1247
 
1210
1248
  return mustLoadOnDemand;
@@ -1221,62 +1259,13 @@ export default class TreeElement {
1221
1259
 
1222
1260
  // Set the initial state for nodes that are loaded on demand
1223
1261
  private async setInitialStateOnDemand(): Promise<void> {
1224
- return new Promise(resolve => {
1225
- const restoreState = (): boolean => {
1226
- const state = this.saveStateHandler.getStateFromStorage();
1227
-
1228
- if (!state) {
1229
- return false;
1230
- } else {
1231
- void this.saveStateHandler.setInitialStateOnDemand(
1232
- state,
1233
- ).then(() => {
1234
- resolve();
1235
- });
1236
-
1237
- return true;
1238
- }
1239
- };
1240
-
1241
- const autoOpenNodes = (): void => {
1242
- const maxLevel = this.getAutoOpenMaxLevel();
1243
- let loadingCount = 0;
1244
-
1245
- const loadAndOpenNode = (node: Node): void => {
1246
- loadingCount += 1;
1247
- void this.openNode(node, false).then(() => {
1248
- loadingCount -= 1;
1249
- openNodes();
1250
- });
1251
- };
1252
-
1253
- const openNodes = (): void => {
1254
- this.tree.iterate((node: Node, level: number) => {
1255
- if (node.load_on_demand) {
1256
- if (!node.is_loading) {
1257
- loadAndOpenNode(node);
1258
- }
1259
-
1260
- return false;
1261
- } else {
1262
- void this.openNode(node, false);
1263
-
1264
- return level !== maxLevel;
1265
- }
1266
- });
1267
-
1268
- if (loadingCount === 0) {
1269
- resolve();
1270
- }
1271
- };
1272
-
1273
- openNodes();
1274
- };
1262
+ const state = this.saveStateHandler.getStateFromStorage();
1275
1263
 
1276
- if (!restoreState()) {
1277
- autoOpenNodes();
1278
- }
1279
- });
1264
+ if (state) {
1265
+ await this.saveStateHandler.setInitialStateOnDemand(state);
1266
+ } else {
1267
+ await this.autoOpenNodesOnDemand();
1268
+ }
1280
1269
  }
1281
1270
 
1282
1271
  // Set this HTML element to this node in the node map.
@@ -1284,7 +1273,48 @@ export default class TreeElement {
1284
1273
  this.nodeMap.set(element, node);
1285
1274
  }
1286
1275
 
1287
- private triggerEvent<Name extends TreeEventName>(eventName: Name, values?: TreeEvents[Name]): boolean {
1288
- return this.triggerEventProvider(this.htmlElement, eventName, values)
1276
+ private triggerEvent<Name extends TreeEventName>(
1277
+ eventName: Name,
1278
+ values?: TreeEvents[Name],
1279
+ ): boolean {
1280
+ return this.triggerEventProvider(this.htmlElement, eventName, values);
1289
1281
  }
1290
- }
1282
+
1283
+ /* Keyboard users reach the tree through the tab stop. The selected node is
1284
+ * the tab stop, see NodeElement.select. Without a selected node, the first
1285
+ * node is the tab stop, so the tree can always be reached.
1286
+ */
1287
+ private updateTabStop(): void {
1288
+ const tabIndex = this.options.tabIndex;
1289
+ const previousTabStop = this.fallbackTabStop;
1290
+ const tabStop =
1291
+ tabIndex == null || this.hasRenderedSelectedNode()
1292
+ ? null
1293
+ : this.getFirstTitleSpan();
1294
+
1295
+ if (
1296
+ previousTabStop &&
1297
+ previousTabStop !== tabStop &&
1298
+ previousTabStop.getAttribute("aria-selected") !== "true"
1299
+ ) {
1300
+ previousTabStop.setAttribute("tabindex", "-1");
1301
+ }
1302
+
1303
+ if (tabStop && tabIndex != null) {
1304
+ tabStop.setAttribute("tabindex", String(tabIndex));
1305
+ }
1306
+
1307
+ this.fallbackTabStop = tabStop;
1308
+ }
1309
+ }
1310
+
1311
+ export {
1312
+ type MoveInfo,
1313
+ type TreeEvent,
1314
+ type TreeEventName,
1315
+ type TreeEvents,
1316
+ } from "./events";
1317
+ export { type Node, type NodeData, type NodeId, type Position } from "./node";
1318
+ export { type TreeElementOptions } from "./options";
1319
+ export { type SavedState } from "./saveStateHandler";
1320
+ export { type SelectNodeOptions } from "./selectNodeHandler";