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/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- Tree-element 0.3.2
2
+ Tree-element 0.4.1
3
3
 
4
4
  Copyright 2026 Marco Braak
5
5
 
@@ -26,6 +26,7 @@ import MouseHandler from './mouseHandler.js';
26
26
  import { Node } from './node.js';
27
27
  import NodeElement from './nodeElement/index.js';
28
28
  import FolderElement from './nodeElement/folderElement.js';
29
+ import { changeableOptions } from './options.js';
29
30
  import { getOffsetTop } from './positionUtils.js';
30
31
  import RequestUrl from './requestUrl.js';
31
32
  import SaveStateHandler from './saveStateHandler.js';
@@ -41,6 +42,8 @@ import version from './version.js';
41
42
  // as its global, instead of an object of named exports.
42
43
 
43
44
  class TreeElement {
45
+ // With multiSelect: the node that is the tab stop, see updateTabStop
46
+ _activeNode;
44
47
  _classNames;
45
48
  _dataLoader;
46
49
  _dndHandler;
@@ -74,6 +77,7 @@ class TreeElement {
74
77
  this._tree = new Node({}, true);
75
78
  this._nodeMap = new WeakMap();
76
79
  this._fallbackTabStop = null;
80
+ this._activeNode = null;
77
81
  let {
78
82
  ariaLabel,
79
83
  ariaLabelledBy,
@@ -83,6 +87,7 @@ class TreeElement {
83
87
  dataFilter,
84
88
  dragAndDrop,
85
89
  keyboardSupport,
90
+ multiSelect,
86
91
  onCanMove,
87
92
  onCanMoveTo,
88
93
  onCreateLi,
@@ -96,7 +101,6 @@ class TreeElement {
96
101
  rtl,
97
102
  saveState: saveStateOption,
98
103
  showEmptyFolder,
99
- slide,
100
104
  tabIndex
101
105
  } = this._options;
102
106
  let classNames = this._classNames;
@@ -134,6 +138,17 @@ class TreeElement {
134
138
  let isNodeSelected = selectNodeHandler._isNodeSelected.bind(selectNodeHandler);
135
139
  let removeFromSelection = selectNodeHandler._removeFromSelection.bind(selectNodeHandler);
136
140
  let selectNode = selectNodeHandler._selectSingleNode.bind(selectNodeHandler);
141
+ let toggleNodeInSelection = selectNodeHandler._toggleNodeInSelection.bind(selectNodeHandler);
142
+
143
+ // With multiSelect, a click adds a node to the selection or removes it
144
+ let onClickTitle = node => {
145
+ if (this._options.multiSelect) {
146
+ toggleNodeInSelection(node, false);
147
+ this._focusNode(node);
148
+ } else {
149
+ selectNode(node);
150
+ }
151
+ };
137
152
  let getMouseDelay = () => this._options.startDndDelay ?? 0;
138
153
  let dataLoader = new DataLoader({
139
154
  _classNames: classNames,
@@ -174,18 +189,21 @@ class TreeElement {
174
189
  _openFolderDelay: openFolderDelay,
175
190
  _openNode: openNode,
176
191
  _refreshElements: refreshElements,
177
- _slide: slide,
178
192
  _treeElement: treeElement,
179
193
  _triggerEvent: triggerEvent
180
194
  });
181
195
  let keyHandler = new KeyHandler({
182
196
  _closeNode: closeNode,
197
+ _focusNode: this._focusNode.bind(this),
198
+ _getActiveNode: this._getActiveNode.bind(this),
183
199
  _getFocusedNode: getFocusedNode,
184
200
  _getSelectedNode: getSelectedNode,
185
201
  _getTree: getTree,
186
202
  _keyboardSupport: keyboardSupport,
203
+ _multiSelect: multiSelect,
187
204
  _openNode: openNode,
188
205
  _selectNode: selectNode,
206
+ _toggleNodeInSelection: toggleNodeInSelection,
189
207
  _triggerEvent: triggerEvent
190
208
  });
191
209
  let renderer = new ElementsRenderer({
@@ -199,6 +217,7 @@ class TreeElement {
199
217
  _getTree: getTree,
200
218
  _htmlElement: treeElement,
201
219
  _isNodeSelected: isNodeSelected,
220
+ _multiSelect: multiSelect,
202
221
  _onCreateLi: onCreateLi,
203
222
  _openedIcon: openedIcon,
204
223
  _rtl: rtl,
@@ -217,7 +236,7 @@ class TreeElement {
217
236
  _getMouseDelay: getMouseDelay,
218
237
  _getNode: getNode,
219
238
  _onClickButton: this.toggle.bind(this),
220
- _onClickTitle: selectNode,
239
+ _onClickTitle: onClickTitle,
221
240
  _onMouseCapture: onMouseCapture,
222
241
  _onMouseDrag: onMouseDrag,
223
242
  _onMouseStart: onMouseStart,
@@ -233,6 +252,9 @@ class TreeElement {
233
252
  this._saveStateHandler = saveStateHandler;
234
253
  this._scrollHandler = scrollHandler;
235
254
  this._selectNodeHandler = selectNodeHandler;
255
+ if (multiSelect) {
256
+ treeElement.addEventListener("focusin", this._handleFocusIn);
257
+ }
236
258
  this._initData();
237
259
  }
238
260
 
@@ -265,8 +287,9 @@ class TreeElement {
265
287
  }
266
288
 
267
289
  /**
268
- * Inserts a new node between a node and its parent, taking the node as its
269
- * child.
290
+ * Inserts a new node between a node and its parent. The siblings of the node
291
+ * move along: the new node takes all children of the parent, and becomes its
292
+ * only child.
270
293
  *
271
294
  * @returns The new node, or `null` when the node has no parent.
272
295
  * @group Changing the tree
@@ -342,6 +365,7 @@ class TreeElement {
342
365
  this._dataLoader._deinit();
343
366
  this._keyHandler._deinit();
344
367
  this._mouseHandler._deinit();
368
+ this._htmlElement.removeEventListener("focusin", this._handleFocusIn);
345
369
  this._tree = new Node({}, true);
346
370
  }
347
371
 
@@ -435,7 +459,7 @@ class TreeElement {
435
459
  }
436
460
 
437
461
  /**
438
- * Returns the current state — `open_nodes` and `selected_node` — whether or
462
+ * Returns the current state—`open_nodes` and `selected_node`—whether or
439
463
  * not `saveState` is enabled.
440
464
  *
441
465
  * @group State
@@ -454,7 +478,7 @@ class TreeElement {
454
478
  }
455
479
 
456
480
  /**
457
- * Returns the root node. It is not rendered; its `children` are the
481
+ * Returns the root node. It is not rendered, and its `children` are the
458
482
  * top-level nodes. Also available as the `tree` property.
459
483
  *
460
484
  * @group Finding nodes
@@ -529,7 +553,8 @@ class TreeElement {
529
553
  }
530
554
 
531
555
  /**
532
- * Selects the next visible node, like the down arrow key.
556
+ * Selects the next visible node, like the down arrow key. With
557
+ * `multiSelect` it moves the focus instead.
533
558
  *
534
559
  * @group Selection
535
560
  */
@@ -554,7 +579,8 @@ class TreeElement {
554
579
  }
555
580
 
556
581
  /**
557
- * Selects the previous visible node, like the up arrow key.
582
+ * Selects the previous visible node, like the up arrow key. With
583
+ * `multiSelect` it moves the focus instead.
558
584
  *
559
585
  * @group Selection
560
586
  */
@@ -563,7 +589,7 @@ class TreeElement {
563
589
  }
564
590
 
565
591
  /**
566
- * Opens a folder, and the folders above it. A node marked `load_on_demand`
592
+ * Opens a folder, and the folders that contain it. A node marked `load_on_demand`
567
593
  * is fetched first, so await the promise when you need to know it is really
568
594
  * open.
569
595
  *
@@ -579,7 +605,7 @@ class TreeElement {
579
605
  async openNode(node, slide) {
580
606
  let mustSlide = slide ?? this._options.slide;
581
607
  let doOpenNode = async (openedNode, slideOption) => {
582
- if (!node.children.length) {
608
+ if (!openedNode.children.length) {
583
609
  return;
584
610
  }
585
611
  let folderElement = this._createFolderElement(openedNode);
@@ -684,8 +710,8 @@ class TreeElement {
684
710
  */
685
711
  selectNode(node, options) {
686
712
  if (!node) {
687
- // Called with empty node -> deselect current node
688
- this._deselectCurrentNode();
713
+ // Called with empty node -> deselect all nodes
714
+ this._deselectAllNodes();
689
715
  this._saveStateHandler._saveState();
690
716
  return;
691
717
  }
@@ -698,13 +724,43 @@ class TreeElement {
698
724
  }
699
725
 
700
726
  /**
701
- * Changes an option after construction. See
702
- * [Options](/reference/options#changing-an-option-later) for the caveats.
727
+ * Changes an option after construction. Only some options can be changed:
728
+ * the others throw an error. See
729
+ * [Options](/reference/options#changing-an-option-later).
703
730
  *
704
731
  * @group Other
705
732
  */
706
733
  setOption(option, value) {
734
+ if (!changeableOptions.includes(option)) {
735
+ throw new Error(`The option ${option} can only be set in the constructor`);
736
+ }
707
737
  this._options[option] = value;
738
+ switch (option) {
739
+ case "dragAndDrop":
740
+ {
741
+ this._renderer._setDragAndDrop(Boolean(value));
742
+ break;
743
+ }
744
+ case "keyboardSupport":
745
+ {
746
+ this._keyHandler._setKeyboardSupport(Boolean(value));
747
+ break;
748
+ }
749
+ case "multiSelect":
750
+ {
751
+ this._setMultiSelect(Boolean(value));
752
+ break;
753
+ }
754
+ case "tabIndex":
755
+ {
756
+ this._options.tabIndex = value ?? undefined;
757
+ this._renderer._setTabIndex(this._options.tabIndex);
758
+ this._fallbackTabStop = null;
759
+ this._updateTabStop();
760
+ break;
761
+ }
762
+ // No default
763
+ }
708
764
  }
709
765
 
710
766
  /**
@@ -737,8 +793,8 @@ class TreeElement {
737
793
  }
738
794
 
739
795
  /**
740
- * Returns the tree as a json string, including changes made through the
741
- * api.
796
+ * Returns the tree as a JSON string, including changes made through the
797
+ * API.
742
798
  *
743
799
  * @group Other
744
800
  */
@@ -751,7 +807,7 @@ class TreeElement {
751
807
  * name.
752
808
  *
753
809
  * Changing the `id` re-indexes the node. `children` and `parent` are
754
- * ignored — use `loadData` or `moveNode` for those.
810
+ * ignored—use `loadData` or `moveNode` for those.
755
811
  *
756
812
  * @example
757
813
  * ```js
@@ -802,9 +858,7 @@ class TreeElement {
802
858
  }
803
859
  _createFolderElement(node) {
804
860
  let classNames = this._classNames;
805
- let closedIconElement = this._renderer._closedIconElement;
806
861
  let getScrollLeft = this._scrollHandler._getScrollLeft.bind(this._scrollHandler);
807
- let openedIconElement = this._renderer._openedIconElement;
808
862
  let tabIndex = this._options.tabIndex;
809
863
  let treeElement = this._htmlElement;
810
864
  let triggerEvent = this._triggerEvent.bind(this);
@@ -819,10 +873,10 @@ class TreeElement {
819
873
  };
820
874
  return new FolderElement({
821
875
  _classNames: classNames,
822
- _closedIconElement: closedIconElement,
823
876
  _getScrollLeft: getScrollLeft,
877
+ _icons: this._renderer._icons,
878
+ _multiSelect: this._options.multiSelect,
824
879
  _node: node,
825
- _openedIconElement: openedIconElement,
826
880
  _renderChildren: renderChildren,
827
881
  _tabIndex: tabIndex,
828
882
  _treeElement: treeElement,
@@ -837,6 +891,7 @@ class TreeElement {
837
891
  return new NodeElement({
838
892
  _classNames: classNames,
839
893
  _getScrollLeft: getScrollLeft,
894
+ _multiSelect: this._options.multiSelect,
840
895
  _node: node,
841
896
  _tabIndex: tabIndex,
842
897
  _treeElement: treeElement
@@ -866,9 +921,8 @@ class TreeElement {
866
921
  }
867
922
  return requestUrl;
868
923
  }
869
- _deselectCurrentNode() {
870
- let node = this.getSelectedNode();
871
- if (node) {
924
+ _deselectAllNodes() {
925
+ for (let node of this.getSelectedNodes()) {
872
926
  this.removeFromSelection(node);
873
927
  }
874
928
  }
@@ -881,6 +935,20 @@ class TreeElement {
881
935
  }
882
936
  }
883
937
 
938
+ // The treeitem of the first node, or null when the tree is empty
939
+ // Move the focus to a node, without selecting it
940
+ _focusNode(node) {
941
+ this._activeNode = node;
942
+ this._updateTabStop();
943
+ node.element?.focus();
944
+ }
945
+
946
+ // With multiSelect: the active node, if it is still rendered in the tree
947
+ _getActiveNode() {
948
+ let element = this._activeNode?.element;
949
+ return element && this._htmlElement.contains(element) ? this._activeNode : null;
950
+ }
951
+
884
952
  // Get the maximum level for auto open
885
953
  _getAutoOpenMaxLevel() {
886
954
  if (this._options.autoOpen === true) {
@@ -896,25 +964,16 @@ class TreeElement {
896
964
  /* istanbul ignore next @preserve */
897
965
  return 0;
898
966
  }
899
-
900
- // The title span of the first node, or null when the tree is empty
901
- _getFirstTitleSpan() {
902
- let firstElement = this._tree.children[0]?.element;
903
- return firstElement ? firstElement.querySelector(`:scope > .${this._classNames.element} > span.${this._classNames.title}`) : null;
967
+ _getFirstTreeItem() {
968
+ return this._tree.children[0]?.element ?? null;
904
969
  }
905
970
 
906
971
  // The node of the HTML element that has the focus, if it is in this tree
907
972
  _getFocusedNode() {
908
973
  let activeElement = document.activeElement;
909
974
 
910
- /* istanbul ignore if */
911
- if (!activeElement) {
912
- return null;
913
- }
914
-
915
- // The keyboard must still work for input elements.
916
- let tagName = activeElement.tagName;
917
- if (tagName !== "A" && tagName !== "SPAN") {
975
+ // An active element must exist and the keyboard must still work for input elements.
976
+ if (!activeElement || !["A", "LI", "SPAN"].includes(activeElement.tagName)) {
918
977
  return null;
919
978
  }
920
979
  let node = this.getNode(activeElement);
@@ -930,6 +989,16 @@ class TreeElement {
930
989
  _getNodeIdToBeSelected() {
931
990
  return this._saveStateHandler._getNodeIdToBeSelected();
932
991
  }
992
+
993
+ // With multiSelect, the focused node becomes the tab stop
994
+ _handleFocusIn = () => {
995
+ let node = this._getFocusedNode();
996
+ if (!node || node === this._activeNode) {
997
+ return;
998
+ }
999
+ this._activeNode = node;
1000
+ this._updateTabStop();
1001
+ };
933
1002
  _hasRenderedSelectedNode() {
934
1003
  return this.getSelectedNodes().some(node => Boolean(node.element));
935
1004
  }
@@ -953,11 +1022,17 @@ class TreeElement {
953
1022
  this._isInitialized = true;
954
1023
  this._triggerEvent("tree.init");
955
1024
  };
1025
+
1026
+ // Before the tree is replaced: then the focused node is no longer in it
1027
+ let focusedNode = this._getFocusedNode();
956
1028
  this._tree = new this._options.nodeClass(null, true, this._options.nodeClass);
957
1029
  this._selectNodeHandler._clear();
958
1030
  this._tree.loadFromData(data);
959
1031
  let mustLoadOnDemand = this._setInitialState();
960
1032
  this._refreshElements(null);
1033
+ if (focusedNode) {
1034
+ this._restoreFocus(focusedNode);
1035
+ }
961
1036
  if (mustLoadOnDemand) {
962
1037
  // Load data on demand and then init the tree
963
1038
  void this._setInitialStateOnDemand().then(doInit);
@@ -965,15 +1040,6 @@ class TreeElement {
965
1040
  doInit();
966
1041
  }
967
1042
  }
968
-
969
- // Does an HTML element of the tree have the focus?
970
- _isFocusOnTree() {
971
- return this._getFocusedNode() !== null;
972
- }
973
- _isSelectedNodeInSubtree(subtree) {
974
- let selectedNode = this.getSelectedNode();
975
- return selectedNode ? subtree === selectedNode || subtree.isParentOf(selectedNode) : false;
976
- }
977
1043
  async _loadFolderOnDemand(node, slide) {
978
1044
  node.is_loading = true;
979
1045
  await this.loadDataFromUrl(undefined, node);
@@ -1006,7 +1072,6 @@ class TreeElement {
1006
1072
  }
1007
1073
  _mouseStart(positionInfo) {
1008
1074
  /* istanbul ignore if */
1009
- // eslint-disable-next-line unicorn/prefer-ternary
1010
1075
  if (!this._options.dragAndDrop) {
1011
1076
  return false;
1012
1077
  }
@@ -1015,10 +1080,10 @@ class TreeElement {
1015
1080
  _mouseStop(positionInfo) {
1016
1081
  /* istanbul ignore if */
1017
1082
  if (!this._options.dragAndDrop) {
1018
- return false;
1083
+ return;
1019
1084
  }
1020
1085
  this._scrollHandler._stopScrolling();
1021
- return this._dndHandler._mouseStop(positionInfo);
1086
+ this._dndHandler._mouseStop(positionInfo);
1022
1087
  }
1023
1088
  _openParents(node) {
1024
1089
  let parent = node.parent;
@@ -1032,25 +1097,32 @@ class TreeElement {
1032
1097
  from_node: redraw this subtree
1033
1098
  */
1034
1099
  _refreshElements(fromNode) {
1035
- let mustSetFocus = this._isFocusOnTree();
1036
- let mustSelect = fromNode ? this._isSelectedNodeInSubtree(fromNode) : false;
1100
+ let focusedNode = this._getFocusedNode();
1101
+
1102
+ // The renderer marks the selected nodes, so only the focus needs restoring
1037
1103
  this._renderer._render(fromNode);
1038
- if (mustSelect) {
1039
- this._selectCurrentNode(mustSetFocus);
1104
+ if (focusedNode) {
1105
+ this._restoreFocus(focusedNode);
1040
1106
  }
1041
1107
  this._updateTabStop();
1042
1108
  this._triggerEvent("tree.refresh");
1043
1109
  }
1044
- _saveState() {
1045
- this._saveStateHandler._saveState();
1046
- }
1047
- _selectCurrentNode(mustSetFocus) {
1048
- let node = this.getSelectedNode();
1049
- if (!node) {
1110
+
1111
+ // Rendering replaces the elements of the nodes, so a focused element can be
1112
+ // gone. Then move the focus to the new element of the same node. After
1113
+ // loadData the nodes are new objects, so look the node up by its id.
1114
+ _restoreFocus(focusedNode) {
1115
+ if (this._htmlElement.contains(document.activeElement)) {
1050
1116
  return;
1051
1117
  }
1052
- let nodeElement = this._getNodeElementForNode(node);
1053
- nodeElement._select(mustSetFocus);
1118
+ let node = (focusedNode.id == null ? null : this.getNodeById(focusedNode.id)) ?? focusedNode;
1119
+ let element = node.element;
1120
+ if (element && this._htmlElement.contains(element)) {
1121
+ element.focus();
1122
+ }
1123
+ }
1124
+ _saveState() {
1125
+ this._saveStateHandler._saveState();
1054
1126
  }
1055
1127
 
1056
1128
  // Set initial state, either by restoring the state or auto-opening nodes
@@ -1105,6 +1177,25 @@ class TreeElement {
1105
1177
  }
1106
1178
  }
1107
1179
 
1180
+ /* Change multiSelect after construction. The attributes of the nodes
1181
+ * depend on it, so the tree is rendered again. The selection is kept: a
1182
+ * tree without multiSelect can also have several selected nodes, see
1183
+ * addToSelection.
1184
+ */
1185
+ _setMultiSelect(multiSelect) {
1186
+ let focusedNode = this._getFocusedNode();
1187
+ this._keyHandler._setMultiSelect(multiSelect);
1188
+ this._renderer._setMultiSelect(multiSelect);
1189
+ if (multiSelect) {
1190
+ this._htmlElement.addEventListener("focusin", this._handleFocusIn);
1191
+ } else {
1192
+ this._htmlElement.removeEventListener("focusin", this._handleFocusIn);
1193
+ }
1194
+ this._activeNode = multiSelect ? focusedNode : null;
1195
+ this._fallbackTabStop = null;
1196
+ this._refreshElements(null);
1197
+ }
1198
+
1108
1199
  // Set this HTML element to this node in the node map.
1109
1200
  _setNodeElement(element, node) {
1110
1201
  this._nodeMap.set(element, node);
@@ -1116,12 +1207,22 @@ class TreeElement {
1116
1207
  /* Keyboard users reach the tree through the tab stop. The selected node is
1117
1208
  * the tab stop, see NodeElement.select. Without a selected node, the first
1118
1209
  * node is the tab stop, so the tree can always be reached.
1210
+ * With multiSelect the tab stop is the active node: the node that was
1211
+ * focused last. Without one, it is the first node.
1119
1212
  */
1120
1213
  _updateTabStop() {
1121
1214
  let tabIndex = this._options.tabIndex;
1215
+ let multiSelect = this._options.multiSelect;
1122
1216
  let previousTabStop = this._fallbackTabStop;
1123
- let tabStop = tabIndex == null || this._hasRenderedSelectedNode() ? null : this._getFirstTitleSpan();
1124
- if (previousTabStop && previousTabStop !== tabStop && previousTabStop.getAttribute("aria-selected") !== "true") {
1217
+ let tabStop;
1218
+ if (tabIndex == null) {
1219
+ tabStop = null;
1220
+ } else if (multiSelect) {
1221
+ tabStop = this._getActiveNode()?.element ?? this._getFirstTreeItem();
1222
+ } else {
1223
+ tabStop = this._hasRenderedSelectedNode() ? null : this._getFirstTreeItem();
1224
+ }
1225
+ if (previousTabStop && previousTabStop !== tabStop && (multiSelect || previousTabStop.getAttribute("aria-selected") !== "true")) {
1125
1226
  previousTabStop.setAttribute("tabindex", "-1");
1126
1227
  }
1127
1228
  if (tabStop && tabIndex != null) {