tree-element 0.3.2 → 0.4.1

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 (91) hide show
  1. package/README.md +23 -20
  2. package/lib/classNames.js +4 -4
  3. package/lib/classNames.js.map +1 -1
  4. package/lib/dataLoader.d.ts +0 -1
  5. package/lib/dataLoader.d.ts.map +1 -1
  6. package/lib/dataLoader.js +10 -7
  7. package/lib/dataLoader.js.map +1 -1
  8. package/lib/dragAndDropHandler/generateHitAreas.d.ts.map +1 -1
  9. package/lib/dragAndDropHandler/generateHitAreas.js +3 -2
  10. package/lib/dragAndDropHandler/generateHitAreas.js.map +1 -1
  11. package/lib/dragAndDropHandler/index.d.ts +2 -4
  12. package/lib/dragAndDropHandler/index.d.ts.map +1 -1
  13. package/lib/dragAndDropHandler/index.js +2 -9
  14. package/lib/dragAndDropHandler/index.js.map +1 -1
  15. package/lib/elementsRenderer.d.ts +11 -4
  16. package/lib/elementsRenderer.d.ts.map +1 -1
  17. package/lib/elementsRenderer.js +104 -43
  18. package/lib/elementsRenderer.js.map +1 -1
  19. package/lib/events.d.ts +13 -7
  20. package/lib/events.d.ts.map +1 -1
  21. package/lib/folderOpenState.d.ts +8 -0
  22. package/lib/folderOpenState.d.ts.map +1 -0
  23. package/lib/folderOpenState.js +22 -0
  24. package/lib/folderOpenState.js.map +1 -0
  25. package/lib/index.d.ts +27 -20
  26. package/lib/index.d.ts.map +1 -1
  27. package/lib/index.js +166 -65
  28. package/lib/index.js.map +1 -1
  29. package/lib/keyHandler.d.ts +16 -3
  30. package/lib/keyHandler.d.ts.map +1 -1
  31. package/lib/keyHandler.js +79 -32
  32. package/lib/keyHandler.js.map +1 -1
  33. package/lib/methodTypes.d.ts +3 -0
  34. package/lib/methodTypes.d.ts.map +1 -1
  35. package/lib/mouseHandler.d.ts.map +1 -1
  36. package/lib/mouseHandler.js +12 -12
  37. package/lib/mouseHandler.js.map +1 -1
  38. package/lib/node.d.ts +12 -10
  39. package/lib/node.d.ts.map +1 -1
  40. package/lib/node.js +12 -10
  41. package/lib/node.js.map +1 -1
  42. package/lib/nodeElement/folderElement.d.ts +7 -5
  43. package/lib/nodeElement/folderElement.d.ts.map +1 -1
  44. package/lib/nodeElement/folderElement.js +35 -55
  45. package/lib/nodeElement/folderElement.js.map +1 -1
  46. package/lib/nodeElement/index.d.ts +3 -2
  47. package/lib/nodeElement/index.d.ts.map +1 -1
  48. package/lib/nodeElement/index.js +15 -12
  49. package/lib/nodeElement/index.js.map +1 -1
  50. package/lib/options.d.ts +29 -14
  51. package/lib/options.d.ts.map +1 -1
  52. package/lib/options.js +12 -0
  53. package/lib/options.js.map +1 -0
  54. package/lib/saveStateHandler.d.ts.map +1 -1
  55. package/lib/saveStateHandler.js +6 -6
  56. package/lib/saveStateHandler.js.map +1 -1
  57. package/lib/selectNodeHandler.d.ts +3 -0
  58. package/lib/selectNodeHandler.d.ts.map +1 -1
  59. package/lib/selectNodeHandler.js +66 -22
  60. package/lib/selectNodeHandler.js.map +1 -1
  61. package/lib/setDefaultOptions.d.ts.map +1 -1
  62. package/lib/setDefaultOptions.js +1 -0
  63. package/lib/setDefaultOptions.js.map +1 -1
  64. package/lib/version.d.ts +1 -1
  65. package/lib/version.js +1 -1
  66. package/lib/version.js.map +1 -1
  67. package/package.json +38 -32
  68. package/src/classNames.ts +4 -4
  69. package/src/dataLoader.ts +9 -10
  70. package/src/dragAndDropHandler/generateHitAreas.ts +3 -2
  71. package/src/dragAndDropHandler/index.ts +3 -17
  72. package/src/elementsRenderer.ts +124 -55
  73. package/src/events.ts +13 -7
  74. package/src/folderOpenState.ts +38 -0
  75. package/src/index.ts +191 -82
  76. package/src/keyHandler.ts +104 -34
  77. package/src/methodTypes.ts +6 -0
  78. package/src/mouseHandler.ts +15 -13
  79. package/src/node.ts +12 -10
  80. package/src/nodeElement/folderElement.ts +47 -72
  81. package/src/nodeElement/index.ts +20 -17
  82. package/src/options.ts +46 -14
  83. package/src/saveStateHandler.ts +5 -8
  84. package/src/selectNodeHandler.ts +69 -25
  85. package/src/setDefaultOptions.ts +1 -0
  86. package/src/version.ts +1 -1
  87. package/tree_element.css +48 -5
  88. package/tree_element.debug.js +540 -278
  89. package/tree_element.debug.js.map +1 -1
  90. package/tree_element.js +2 -2
  91. package/tree_element.js.map +1 -1
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import type { ClassNames } from "./classNames";
2
2
  import type { TreeEventName, TreeEvents } from "./events";
3
3
  import type { PositionInfo } from "./mouseUtils";
4
4
  import type { NodeData, NodeId, Position } from "./node";
5
- import type { TreeElementOptions } from "./options";
5
+ import type { ChangeableOption, TreeElementOptions } from "./options";
6
6
  import type { SavedState } from "./saveStateHandler";
7
7
  import type { SelectNodeOptions } from "./selectNodeHandler";
8
8
 
@@ -15,6 +15,7 @@ import MouseHandler from "./mouseHandler";
15
15
  import { Node } from "./node";
16
16
  import NodeElement from "./nodeElement";
17
17
  import FolderElement from "./nodeElement/folderElement";
18
+ import { changeableOptions } from "./options";
18
19
  import { getOffsetTop } from "./positionUtils";
19
20
  import RequestUrl from "./requestUrl";
20
21
  import SaveStateHandler from "./saveStateHandler";
@@ -47,6 +48,8 @@ interface TreeElementParams extends Partial<TreeElementOptions> {
47
48
  }
48
49
 
49
50
  export default class TreeElement {
51
+ // With multiSelect: the node that is the tab stop, see updateTabStop
52
+ private activeNode: Node | null;
50
53
  private classNames: ClassNames;
51
54
  private dataLoader: DataLoader;
52
55
  private dndHandler: DragAndDropHandler;
@@ -82,6 +85,7 @@ export default class TreeElement {
82
85
  this.tree = new Node({}, true);
83
86
  this.nodeMap = new WeakMap();
84
87
  this.fallbackTabStop = null;
88
+ this.activeNode = null;
85
89
 
86
90
  const {
87
91
  ariaLabel,
@@ -92,6 +96,7 @@ export default class TreeElement {
92
96
  dataFilter,
93
97
  dragAndDrop,
94
98
  keyboardSupport,
99
+ multiSelect,
95
100
  onCanMove,
96
101
  onCanMoveTo,
97
102
  onCreateLi,
@@ -105,7 +110,6 @@ export default class TreeElement {
105
110
  rtl,
106
111
  saveState: saveStateOption,
107
112
  showEmptyFolder,
108
- slide,
109
113
  tabIndex,
110
114
  } = this.options;
111
115
 
@@ -153,6 +157,19 @@ export default class TreeElement {
153
157
  const selectNode =
154
158
  selectNodeHandler.selectSingleNode.bind(selectNodeHandler);
155
159
 
160
+ const toggleNodeInSelection =
161
+ selectNodeHandler.toggleNodeInSelection.bind(selectNodeHandler);
162
+
163
+ // With multiSelect, a click adds a node to the selection or removes it
164
+ const onClickTitle = (node: Node) => {
165
+ if (this.options.multiSelect) {
166
+ toggleNodeInSelection(node, false);
167
+ this.focusNode(node);
168
+ } else {
169
+ selectNode(node);
170
+ }
171
+ };
172
+
156
173
  const getMouseDelay = () => this.options.startDndDelay ?? 0;
157
174
 
158
175
  const dataLoader = new DataLoader({
@@ -198,19 +215,22 @@ export default class TreeElement {
198
215
  openFolderDelay,
199
216
  openNode,
200
217
  refreshElements,
201
- slide,
202
218
  treeElement,
203
219
  triggerEvent,
204
220
  });
205
221
 
206
222
  const keyHandler = new KeyHandler({
207
223
  closeNode,
224
+ focusNode: this.focusNode.bind(this),
225
+ getActiveNode: this.getActiveNode.bind(this),
208
226
  getFocusedNode,
209
227
  getSelectedNode,
210
228
  getTree,
211
229
  keyboardSupport,
230
+ multiSelect,
212
231
  openNode,
213
232
  selectNode,
233
+ toggleNodeInSelection,
214
234
  triggerEvent,
215
235
  });
216
236
 
@@ -225,6 +245,7 @@ export default class TreeElement {
225
245
  getTree,
226
246
  htmlElement: treeElement,
227
247
  isNodeSelected,
248
+ multiSelect,
228
249
  onCreateLi,
229
250
  openedIcon,
230
251
  rtl,
@@ -245,7 +266,7 @@ export default class TreeElement {
245
266
  getMouseDelay,
246
267
  getNode,
247
268
  onClickButton: this.toggle.bind(this),
248
- onClickTitle: selectNode,
269
+ onClickTitle,
249
270
  onMouseCapture,
250
271
  onMouseDrag,
251
272
  onMouseStart,
@@ -263,6 +284,10 @@ export default class TreeElement {
263
284
  this.scrollHandler = scrollHandler;
264
285
  this.selectNodeHandler = selectNodeHandler;
265
286
 
287
+ if (multiSelect) {
288
+ treeElement.addEventListener("focusin", this.handleFocusIn);
289
+ }
290
+
266
291
  this.initData();
267
292
  }
268
293
 
@@ -299,8 +324,9 @@ export default class TreeElement {
299
324
  }
300
325
 
301
326
  /**
302
- * Inserts a new node between a node and its parent, taking the node as its
303
- * child.
327
+ * Inserts a new node between a node and its parent. The siblings of the node
328
+ * move along: the new node takes all children of the parent, and becomes its
329
+ * only child.
304
330
  *
305
331
  * @returns The new node, or `null` when the node has no parent.
306
332
  * @group Changing the tree
@@ -388,6 +414,7 @@ export default class TreeElement {
388
414
  this.dataLoader.deinit();
389
415
  this.keyHandler.deinit();
390
416
  this.mouseHandler.deinit();
417
+ this.htmlElement.removeEventListener("focusin", this.handleFocusIn);
391
418
 
392
419
  this.tree = new Node({}, true);
393
420
  }
@@ -485,7 +512,7 @@ export default class TreeElement {
485
512
  }
486
513
 
487
514
  /**
488
- * Returns the current state — `open_nodes` and `selected_node` — whether or
515
+ * Returns the current state—`open_nodes` and `selected_node`—whether or
489
516
  * not `saveState` is enabled.
490
517
  *
491
518
  * @group State
@@ -504,7 +531,7 @@ export default class TreeElement {
504
531
  }
505
532
 
506
533
  /**
507
- * Returns the root node. It is not rendered; its `children` are the
534
+ * Returns the root node. It is not rendered, and its `children` are the
508
535
  * top-level nodes. Also available as the `tree` property.
509
536
  *
510
537
  * @group Finding nodes
@@ -584,7 +611,8 @@ export default class TreeElement {
584
611
  }
585
612
 
586
613
  /**
587
- * Selects the next visible node, like the down arrow key.
614
+ * Selects the next visible node, like the down arrow key. With
615
+ * `multiSelect` it moves the focus instead.
588
616
  *
589
617
  * @group Selection
590
618
  */
@@ -609,7 +637,8 @@ export default class TreeElement {
609
637
  }
610
638
 
611
639
  /**
612
- * Selects the previous visible node, like the up arrow key.
640
+ * Selects the previous visible node, like the up arrow key. With
641
+ * `multiSelect` it moves the focus instead.
613
642
  *
614
643
  * @group Selection
615
644
  */
@@ -618,7 +647,7 @@ export default class TreeElement {
618
647
  }
619
648
 
620
649
  /**
621
- * Opens a folder, and the folders above it. A node marked `load_on_demand`
650
+ * Opens a folder, and the folders that contain it. A node marked `load_on_demand`
622
651
  * is fetched first, so await the promise when you need to know it is really
623
652
  * open.
624
653
  *
@@ -638,7 +667,7 @@ export default class TreeElement {
638
667
  openedNode: Node,
639
668
  slideOption: boolean,
640
669
  ): Promise<void> => {
641
- if (!node.children.length) {
670
+ if (!openedNode.children.length) {
642
671
  return;
643
672
  }
644
673
 
@@ -754,8 +783,8 @@ export default class TreeElement {
754
783
  */
755
784
  public selectNode(node: Node | null, options?: SelectNodeOptions): void {
756
785
  if (!node) {
757
- // Called with empty node -> deselect current node
758
- this.deselectCurrentNode();
786
+ // Called with empty node -> deselect all nodes
787
+ this.deselectAllNodes();
759
788
  this.saveStateHandler.saveState();
760
789
  return;
761
790
  }
@@ -768,13 +797,47 @@ export default class TreeElement {
768
797
  }
769
798
 
770
799
  /**
771
- * Changes an option after construction. See
772
- * [Options](/reference/options#changing-an-option-later) for the caveats.
800
+ * Changes an option after construction. Only some options can be changed:
801
+ * the others throw an error. See
802
+ * [Options](/reference/options#changing-an-option-later).
773
803
  *
774
804
  * @group Other
775
805
  */
776
- public setOption(option: string, value: unknown) {
806
+ public setOption(option: ChangeableOption, value: unknown) {
807
+ if (!(changeableOptions as readonly string[]).includes(option)) {
808
+ throw new Error(
809
+ `The option ${option} can only be set in the constructor`,
810
+ );
811
+ }
812
+
777
813
  (this.options as unknown as Record<string, unknown>)[option] = value;
814
+
815
+ switch (option) {
816
+ case "dragAndDrop": {
817
+ this.renderer.setDragAndDrop(Boolean(value));
818
+
819
+ break;
820
+ }
821
+ case "keyboardSupport": {
822
+ this.keyHandler.setKeyboardSupport(Boolean(value));
823
+
824
+ break;
825
+ }
826
+ case "multiSelect": {
827
+ this.setMultiSelect(Boolean(value));
828
+
829
+ break;
830
+ }
831
+ case "tabIndex": {
832
+ this.options.tabIndex = (value as null | number) ?? undefined;
833
+ this.renderer.setTabIndex(this.options.tabIndex);
834
+ this.fallbackTabStop = null;
835
+ this.updateTabStop();
836
+
837
+ break;
838
+ }
839
+ // No default
840
+ }
778
841
  }
779
842
 
780
843
  /**
@@ -808,8 +871,8 @@ export default class TreeElement {
808
871
  }
809
872
 
810
873
  /**
811
- * Returns the tree as a json string, including changes made through the
812
- * api.
874
+ * Returns the tree as a JSON string, including changes made through the
875
+ * API.
813
876
  *
814
877
  * @group Other
815
878
  */
@@ -822,7 +885,7 @@ export default class TreeElement {
822
885
  * name.
823
886
  *
824
887
  * Changing the `id` re-indexes the node. `children` and `parent` are
825
- * ignored — use `loadData` or `moveNode` for those.
888
+ * ignored—use `loadData` or `moveNode` for those.
826
889
  *
827
890
  * @example
828
891
  * ```js
@@ -891,11 +954,9 @@ export default class TreeElement {
891
954
 
892
955
  private createFolderElement(node: Node) {
893
956
  const classNames = this.classNames;
894
- const closedIconElement = this.renderer.closedIconElement;
895
957
  const getScrollLeft = this.scrollHandler.getScrollLeft.bind(
896
958
  this.scrollHandler,
897
959
  );
898
- const openedIconElement = this.renderer.openedIconElement;
899
960
  const tabIndex = this.options.tabIndex;
900
961
  const treeElement = this.htmlElement;
901
962
  const triggerEvent = this.triggerEvent.bind(this);
@@ -911,10 +972,10 @@ export default class TreeElement {
911
972
 
912
973
  return new FolderElement({
913
974
  classNames,
914
- closedIconElement,
915
975
  getScrollLeft,
976
+ icons: this.renderer.icons,
977
+ multiSelect: this.options.multiSelect,
916
978
  node,
917
- openedIconElement,
918
979
  renderChildren,
919
980
  tabIndex,
920
981
  treeElement,
@@ -933,6 +994,7 @@ export default class TreeElement {
933
994
  return new NodeElement({
934
995
  classNames,
935
996
  getScrollLeft,
997
+ multiSelect: this.options.multiSelect,
936
998
  node,
937
999
  tabIndex,
938
1000
  treeElement,
@@ -968,9 +1030,8 @@ export default class TreeElement {
968
1030
  return requestUrl;
969
1031
  }
970
1032
 
971
- private deselectCurrentNode(): void {
972
- const node = this.getSelectedNode();
973
- if (node) {
1033
+ private deselectAllNodes(): void {
1034
+ for (const node of this.getSelectedNodes()) {
974
1035
  this.removeFromSelection(node);
975
1036
  }
976
1037
  }
@@ -984,6 +1045,23 @@ export default class TreeElement {
984
1045
  }
985
1046
  }
986
1047
 
1048
+ // The treeitem of the first node, or null when the tree is empty
1049
+ // Move the focus to a node, without selecting it
1050
+ private focusNode(node: Node): void {
1051
+ this.activeNode = node;
1052
+ this.updateTabStop();
1053
+ node.element?.focus();
1054
+ }
1055
+
1056
+ // With multiSelect: the active node, if it is still rendered in the tree
1057
+ private getActiveNode(): Node | null {
1058
+ const element = this.activeNode?.element;
1059
+
1060
+ return element && this.htmlElement.contains(element)
1061
+ ? this.activeNode
1062
+ : null;
1063
+ }
1064
+
987
1065
  // Get the maximum level for auto open
988
1066
  private getAutoOpenMaxLevel(): number {
989
1067
  if (this.options.autoOpen === true) {
@@ -1000,29 +1078,19 @@ export default class TreeElement {
1000
1078
  return 0;
1001
1079
  }
1002
1080
 
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;
1081
+ private getFirstTreeItem(): HTMLElement | null {
1082
+ return this.tree.children[0]?.element ?? null;
1012
1083
  }
1013
1084
 
1014
1085
  // The node of the HTML element that has the focus, if it is in this tree
1015
1086
  private getFocusedNode(): Node | null {
1016
1087
  const activeElement = document.activeElement;
1017
1088
 
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") {
1089
+ // An active element must exist and the keyboard must still work for input elements.
1090
+ if (
1091
+ !activeElement ||
1092
+ !["A", "LI", "SPAN"].includes(activeElement.tagName)
1093
+ ) {
1026
1094
  return null;
1027
1095
  }
1028
1096
 
@@ -1045,6 +1113,18 @@ export default class TreeElement {
1045
1113
  return this.saveStateHandler.getNodeIdToBeSelected();
1046
1114
  }
1047
1115
 
1116
+ // With multiSelect, the focused node becomes the tab stop
1117
+ private handleFocusIn = (): void => {
1118
+ const node = this.getFocusedNode();
1119
+
1120
+ if (!node || node === this.activeNode) {
1121
+ return;
1122
+ }
1123
+
1124
+ this.activeNode = node;
1125
+ this.updateTabStop();
1126
+ };
1127
+
1048
1128
  private hasRenderedSelectedNode(): boolean {
1049
1129
  return this.getSelectedNodes().some((node) => Boolean(node.element));
1050
1130
  }
@@ -1073,6 +1153,9 @@ export default class TreeElement {
1073
1153
  this.triggerEvent("tree.init");
1074
1154
  };
1075
1155
 
1156
+ // Before the tree is replaced: then the focused node is no longer in it
1157
+ const focusedNode = this.getFocusedNode();
1158
+
1076
1159
  this.tree = new this.options.nodeClass(null, true, this.options.nodeClass);
1077
1160
 
1078
1161
  this.selectNodeHandler.clear();
@@ -1083,6 +1166,10 @@ export default class TreeElement {
1083
1166
 
1084
1167
  this.refreshElements(null);
1085
1168
 
1169
+ if (focusedNode) {
1170
+ this.restoreFocus(focusedNode);
1171
+ }
1172
+
1086
1173
  if (mustLoadOnDemand) {
1087
1174
  // Load data on demand and then init the tree
1088
1175
  void this.setInitialStateOnDemand().then(doInit);
@@ -1091,19 +1178,6 @@ export default class TreeElement {
1091
1178
  }
1092
1179
  }
1093
1180
 
1094
- // Does an HTML element of the tree have the focus?
1095
- private isFocusOnTree(): boolean {
1096
- return this.getFocusedNode() !== null;
1097
- }
1098
-
1099
- private isSelectedNodeInSubtree(subtree: Node): boolean {
1100
- const selectedNode = this.getSelectedNode();
1101
-
1102
- return selectedNode
1103
- ? subtree === selectedNode || subtree.isParentOf(selectedNode)
1104
- : false;
1105
- }
1106
-
1107
1181
  private async loadFolderOnDemand(node: Node, slide: boolean): Promise<void> {
1108
1182
  node.is_loading = true;
1109
1183
 
@@ -1147,7 +1221,6 @@ export default class TreeElement {
1147
1221
 
1148
1222
  private mouseStart(positionInfo: PositionInfo): boolean {
1149
1223
  /* istanbul ignore if */
1150
- // eslint-disable-next-line unicorn/prefer-ternary
1151
1224
  if (!this.options.dragAndDrop) {
1152
1225
  return false;
1153
1226
  }
@@ -1155,14 +1228,14 @@ export default class TreeElement {
1155
1228
  return this.dndHandler.mouseStart(positionInfo);
1156
1229
  }
1157
1230
 
1158
- private mouseStop(positionInfo: PositionInfo): boolean {
1231
+ private mouseStop(positionInfo: PositionInfo): void {
1159
1232
  /* istanbul ignore if */
1160
1233
  if (!this.options.dragAndDrop) {
1161
- return false;
1234
+ return;
1162
1235
  }
1163
1236
 
1164
1237
  this.scrollHandler.stopScrolling();
1165
- return this.dndHandler.mouseStop(positionInfo);
1238
+ this.dndHandler.mouseStop(positionInfo);
1166
1239
  }
1167
1240
 
1168
1241
  private openParents(node: Node) {
@@ -1178,15 +1251,13 @@ export default class TreeElement {
1178
1251
  from_node: redraw this subtree
1179
1252
  */
1180
1253
  private refreshElements(fromNode: Node | null): void {
1181
- const mustSetFocus = this.isFocusOnTree();
1182
- const mustSelect = fromNode
1183
- ? this.isSelectedNodeInSubtree(fromNode)
1184
- : false;
1254
+ const focusedNode = this.getFocusedNode();
1185
1255
 
1256
+ // The renderer marks the selected nodes, so only the focus needs restoring
1186
1257
  this.renderer.render(fromNode);
1187
1258
 
1188
- if (mustSelect) {
1189
- this.selectCurrentNode(mustSetFocus);
1259
+ if (focusedNode) {
1260
+ this.restoreFocus(focusedNode);
1190
1261
  }
1191
1262
 
1192
1263
  this.updateTabStop();
@@ -1194,18 +1265,26 @@ export default class TreeElement {
1194
1265
  this.triggerEvent("tree.refresh");
1195
1266
  }
1196
1267
 
1197
- private saveState(): void {
1198
- this.saveStateHandler.saveState();
1199
- }
1200
-
1201
- private selectCurrentNode(mustSetFocus: boolean): void {
1202
- const node = this.getSelectedNode();
1203
- if (!node) {
1268
+ // Rendering replaces the elements of the nodes, so a focused element can be
1269
+ // gone. Then move the focus to the new element of the same node. After
1270
+ // loadData the nodes are new objects, so look the node up by its id.
1271
+ private restoreFocus(focusedNode: Node): void {
1272
+ if (this.htmlElement.contains(document.activeElement)) {
1204
1273
  return;
1205
1274
  }
1206
1275
 
1207
- const nodeElement = this.getNodeElementForNode(node);
1208
- nodeElement.select(mustSetFocus);
1276
+ const node =
1277
+ (focusedNode.id == null ? null : this.getNodeById(focusedNode.id)) ??
1278
+ focusedNode;
1279
+ const element = node.element;
1280
+
1281
+ if (element && this.htmlElement.contains(element)) {
1282
+ element.focus();
1283
+ }
1284
+ }
1285
+
1286
+ private saveState(): void {
1287
+ this.saveStateHandler.saveState();
1209
1288
  }
1210
1289
 
1211
1290
  // Set initial state, either by restoring the state or auto-opening nodes
@@ -1268,6 +1347,28 @@ export default class TreeElement {
1268
1347
  }
1269
1348
  }
1270
1349
 
1350
+ /* Change multiSelect after construction. The attributes of the nodes
1351
+ * depend on it, so the tree is rendered again. The selection is kept: a
1352
+ * tree without multiSelect can also have several selected nodes, see
1353
+ * addToSelection.
1354
+ */
1355
+ private setMultiSelect(multiSelect: boolean): void {
1356
+ const focusedNode = this.getFocusedNode();
1357
+
1358
+ this.keyHandler.setMultiSelect(multiSelect);
1359
+ this.renderer.setMultiSelect(multiSelect);
1360
+
1361
+ if (multiSelect) {
1362
+ this.htmlElement.addEventListener("focusin", this.handleFocusIn);
1363
+ } else {
1364
+ this.htmlElement.removeEventListener("focusin", this.handleFocusIn);
1365
+ }
1366
+
1367
+ this.activeNode = multiSelect ? focusedNode : null;
1368
+ this.fallbackTabStop = null;
1369
+ this.refreshElements(null);
1370
+ }
1371
+
1271
1372
  // Set this HTML element to this node in the node map.
1272
1373
  private setNodeElement(element: HTMLElement, node: Node) {
1273
1374
  this.nodeMap.set(element, node);
@@ -1283,19 +1384,27 @@ export default class TreeElement {
1283
1384
  /* Keyboard users reach the tree through the tab stop. The selected node is
1284
1385
  * the tab stop, see NodeElement.select. Without a selected node, the first
1285
1386
  * node is the tab stop, so the tree can always be reached.
1387
+ * With multiSelect the tab stop is the active node: the node that was
1388
+ * focused last. Without one, it is the first node.
1286
1389
  */
1287
1390
  private updateTabStop(): void {
1288
1391
  const tabIndex = this.options.tabIndex;
1392
+ const multiSelect = this.options.multiSelect;
1289
1393
  const previousTabStop = this.fallbackTabStop;
1290
- const tabStop =
1291
- tabIndex == null || this.hasRenderedSelectedNode()
1292
- ? null
1293
- : this.getFirstTitleSpan();
1394
+ let tabStop: HTMLElement | null;
1395
+
1396
+ if (tabIndex == null) {
1397
+ tabStop = null;
1398
+ } else if (multiSelect) {
1399
+ tabStop = this.getActiveNode()?.element ?? this.getFirstTreeItem();
1400
+ } else {
1401
+ tabStop = this.hasRenderedSelectedNode() ? null : this.getFirstTreeItem();
1402
+ }
1294
1403
 
1295
1404
  if (
1296
1405
  previousTabStop &&
1297
1406
  previousTabStop !== tabStop &&
1298
- previousTabStop.getAttribute("aria-selected") !== "true"
1407
+ (multiSelect || previousTabStop.getAttribute("aria-selected") !== "true")
1299
1408
  ) {
1300
1409
  previousTabStop.setAttribute("tabindex", "-1");
1301
1410
  }
@@ -1315,6 +1424,6 @@ export {
1315
1424
  type TreeEvents,
1316
1425
  } from "./events";
1317
1426
  export { type Node, type NodeData, type NodeId, type Position } from "./node";
1318
- export { type TreeElementOptions } from "./options";
1427
+ export { type ChangeableOption, type TreeElementOptions } from "./options";
1319
1428
  export { type SavedState } from "./saveStateHandler";
1320
1429
  export { type SelectNodeOptions } from "./selectNodeHandler";