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
@@ -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
 
@@ -29,13 +29,13 @@ var TreeElement = (function () {
29
29
  */
30
30
  let createClassNames = ({
31
31
  classPrefix,
32
- commonClassName,
33
- treeClassName
32
+ commonClassName = `${classPrefix}-common`,
33
+ treeClassName = classPrefix
34
34
  }) => ({
35
35
  border: `${classPrefix}-border`,
36
36
  circle: `${classPrefix}-circle`,
37
37
  closed: `${classPrefix}-closed`,
38
- common: commonClassName ?? `${classPrefix}-common`,
38
+ common: commonClassName,
39
39
  dnd: `${classPrefix}-dnd`,
40
40
  dragging: `${classPrefix}-dragging`,
41
41
  element: `${classPrefix}-element`,
@@ -54,7 +54,7 @@ var TreeElement = (function () {
54
54
  toggler: `${classPrefix}-toggler`,
55
55
  togglerLeft: `${classPrefix}-toggler-left`,
56
56
  togglerRight: `${classPrefix}-toggler-right`,
57
- tree: treeClassName ?? classPrefix
57
+ tree: treeClassName
58
58
  });
59
59
 
60
60
  class DataLoader {
@@ -80,15 +80,18 @@ var TreeElement = (function () {
80
80
  element.classList.add(this._classNames.loading);
81
81
 
82
82
  // Tell screen readers that the children of the folder are loading
83
- let titleSpan = node ? this._getTitleSpan(element) : null;
84
- titleSpan?.setAttribute("aria-busy", "true");
83
+ if (node) {
84
+ element.setAttribute("aria-busy", "true");
85
+ }
85
86
  this._triggerEvent("tree.loading_data", {
86
87
  element,
87
88
  node
88
89
  });
89
90
  let stopLoading = () => {
90
91
  element.classList.remove(this._classNames.loading);
91
- titleSpan?.removeAttribute("aria-busy");
92
+ if (node) {
93
+ element.removeAttribute("aria-busy");
94
+ }
92
95
  this._triggerEvent("tree.loaded_data", {
93
96
  element,
94
97
  node
@@ -108,9 +111,12 @@ var TreeElement = (function () {
108
111
  };
109
112
  let signal = this._abortController.signal;
110
113
  url._setSearchParam("_", Date.now().toString());
114
+
115
+ // Accept, not Content-Type: the request has no body. A cross-origin request
116
+ // with only an Accept header also needs no CORS preflight.
111
117
  return fetch(url._value(), {
112
118
  headers: {
113
- "Content-Type": "application/json"
119
+ Accept: "application/json"
114
120
  },
115
121
  signal
116
122
  }).then(handleResponse).catch(error => {
@@ -124,9 +130,6 @@ var TreeElement = (function () {
124
130
  });
125
131
  });
126
132
  }
127
- _getTitleSpan(element) {
128
- return element.querySelector(`:scope > .${this._classNames.element} > .${this._classNames.title}`);
129
- }
130
133
  }
131
134
 
132
135
  // Get the top position of the HTML element.
@@ -262,8 +265,9 @@ var TreeElement = (function () {
262
265
  lastTop = top;
263
266
  };
264
267
  let handleAfterOpenFolder = (node, nextNode) => {
265
- if (node === currentNode || nextNode === currentNode) {
266
- // Cannot move before or after current item
268
+ // Not called for the current node: handleOpenFolder stops iterating there
269
+ if (nextNode === currentNode) {
270
+ // Dropping the current node after this folder would leave it where it is
267
271
  addHitPosition(node, null, lastTop);
268
272
  } else {
269
273
  addHitPosition(node, "after", lastTop);
@@ -391,7 +395,6 @@ var TreeElement = (function () {
391
395
  _openFolderDelay: openFolderDelay,
392
396
  _openNode: openNode,
393
397
  _refreshElements: refreshElements,
394
- _slide: slide,
395
398
  _treeElement: treeElement,
396
399
  _triggerEvent: triggerEvent
397
400
  }) {
@@ -409,7 +412,6 @@ var TreeElement = (function () {
409
412
  this._openFolderDelay = openFolderDelay;
410
413
  this._openNode = openNode;
411
414
  this._refreshElements = refreshElements;
412
- this._slide = slide;
413
415
  this._treeElement = treeElement;
414
416
  this._triggerEvent = triggerEvent;
415
417
  this._hoveredArea = null;
@@ -442,9 +444,6 @@ var TreeElement = (function () {
442
444
  this._dragElement._move(positionInfo.pageX, positionInfo.pageY);
443
445
  let area = this._findHoveredArea(positionInfo.pageX, positionInfo.pageY);
444
446
  if (area && this._canMoveToArea(area, this._currentItem)) {
445
- if (!area.node.isFolder()) {
446
- this._stopOpenFolderTimer();
447
- }
448
447
  if (this._hoveredArea !== area) {
449
448
  this._hoveredArea = area;
450
449
 
@@ -503,7 +502,6 @@ var TreeElement = (function () {
503
502
  if (!this._hoveredArea && currentItem && this._onDragStop) {
504
503
  this._onDragStop(currentItem._node, positionInfo.originalEvent);
505
504
  }
506
- return false;
507
505
  }
508
506
  _refresh() {
509
507
  this._removeHitAreas();
@@ -594,8 +592,7 @@ var TreeElement = (function () {
594
592
  }
595
593
  }
596
594
  _mustCaptureElement(element) {
597
- let nodeName = element.nodeName;
598
- return nodeName !== "INPUT" && nodeName !== "SELECT" && nodeName !== "TEXTAREA";
595
+ return !["INPUT", "SELECT", "TEXTAREA"].includes(element.nodeName);
599
596
  }
600
597
  _mustOpenFolderTimer(area) {
601
598
  let node = area.node;
@@ -614,7 +611,7 @@ var TreeElement = (function () {
614
611
  }
615
612
  _startOpenFolderTimer(folder) {
616
613
  let openFolder = () => {
617
- void this._openNode(folder, this._slide).then(() => {
614
+ void this._openNode(folder).then(() => {
618
615
  this._refresh();
619
616
  this._updateDropHint();
620
617
  });
@@ -649,6 +646,24 @@ var TreeElement = (function () {
649
646
  let isInt = n => Number.isInteger(n);
650
647
  let getBoolString = value => value ? "true" : "false";
651
648
 
649
+ /* The open state of a folder is shown on the toggler and on the <li>.
650
+ * These are used both when a folder is rendered and when it is opened or
651
+ * closed, so a rendered folder looks the same as a toggled one.
652
+ */
653
+
654
+ let setTogglerOpenState = (toggler, isOpen, classNames, icons) => {
655
+ toggler.classList.toggle(classNames.closed, !isOpen);
656
+ toggler.replaceChildren();
657
+ let iconElement = isOpen ? icons.opened : icons.closed;
658
+ if (iconElement) {
659
+ toggler.append(iconElement.cloneNode(true));
660
+ }
661
+ };
662
+ let setFolderLiOpenState = (li, isOpen, classNames) => {
663
+ li.classList.toggle(classNames.closed, !isOpen);
664
+ li.setAttribute("aria-expanded", getBoolString(isOpen));
665
+ };
666
+
652
667
  /* Renders the tree to html.
653
668
  *
654
669
  * The children of a closed folder are not rendered. They are rendered when
@@ -672,6 +687,7 @@ var TreeElement = (function () {
672
687
  _getTree: getTree,
673
688
  _htmlElement: htmlElement,
674
689
  _isNodeSelected: isNodeSelected,
690
+ _multiSelect: multiSelect,
675
691
  _onCreateLi: onCreateLi,
676
692
  _openedIcon: openedIcon = "+",
677
693
  _rtl: rtl,
@@ -688,18 +704,22 @@ var TreeElement = (function () {
688
704
  this._getTree = getTree;
689
705
  this._htmlElement = htmlElement;
690
706
  this._isNodeSelected = isNodeSelected;
707
+ this._multiSelect = multiSelect;
691
708
  this._onCreateLi = onCreateLi;
692
709
  this._rtl = rtl;
693
710
  this._setNodeElement = setNodeElement;
694
711
  this._showEmptyFolder = showEmptyFolder;
695
712
  this._tabIndex = tabIndex;
696
- this._openedIconElement = this._createButtonElement(openedIcon);
697
- this._closedIconElement = this._createButtonElement(closedIcon);
698
- this._rootUlTemplate = this._createUlTemplate(true);
699
- this._groupUlTemplate = this._createUlTemplate(false);
700
- this._nodeTemplate = this._createNodeTemplate();
701
- this._openedFolderTemplate = this._createFolderTemplate(true);
702
- this._closedFolderTemplate = this._createFolderTemplate(false);
713
+ this._icons = {
714
+ closed: this._createButtonElement(closedIcon),
715
+ opened: this._createButtonElement(openedIcon)
716
+ };
717
+ let templates = this._createTemplates();
718
+ this._closedFolderTemplate = templates.closedFolderTemplate;
719
+ this._groupUlTemplate = templates.groupUlTemplate;
720
+ this._nodeTemplate = templates.nodeTemplate;
721
+ this._openedFolderTemplate = templates.openedFolderTemplate;
722
+ this._rootUlTemplate = templates.rootUlTemplate;
703
723
  }
704
724
  _render(fromNode) {
705
725
  if (fromNode?.parent) {
@@ -713,7 +733,7 @@ var TreeElement = (function () {
713
733
  * null when the folder is not rendered or has no children.
714
734
  */
715
735
  _renderChildren(node) {
716
- return !node.element || !node.hasChildren() ? null : this._createDomElements(node.element, node.children, false, node.getLevel() + 1);
736
+ return node.element && node.hasChildren() ? this._createDomElements(node.element, node.children, false, node.getLevel() + 1) : null;
717
737
  }
718
738
  _renderFromNode(node) {
719
739
  if (!node.element) {
@@ -733,6 +753,43 @@ var TreeElement = (function () {
733
753
  this._createDomElements(this._htmlElement, tree.children, true, 1);
734
754
  }
735
755
  }
756
+
757
+ /* Change dragAndDrop after construction. The rendered lists and the
758
+ * templates for new lists get or lose the dnd class.
759
+ */
760
+ _setDragAndDrop(dragAndDrop) {
761
+ this._dragAndDrop = dragAndDrop;
762
+ this._updateTemplates();
763
+ for (let ul of this._htmlElement.querySelectorAll(`ul.${this._classNames.common}`)) {
764
+ ul.classList.toggle(this._classNames.dnd, dragAndDrop);
765
+ }
766
+ }
767
+
768
+ /* Change multiSelect after construction. Only the templates are updated;
769
+ * the tree must be rendered again.
770
+ */
771
+ _setMultiSelect(multiSelect) {
772
+ this._multiSelect = multiSelect;
773
+ this._updateTemplates();
774
+ }
775
+
776
+ /* Change tabIndex after construction. The rendered nodes and the
777
+ * templates for new nodes get the new tabindex. The tab stop is updated by
778
+ * TreeElement.updateTabStop.
779
+ */
780
+ _setTabIndex(tabIndex) {
781
+ this._tabIndex = tabIndex;
782
+ this._updateTemplates();
783
+ for (let li of this._htmlElement.querySelectorAll("li[role=treeitem]")) {
784
+ if (tabIndex === undefined) {
785
+ li.removeAttribute("tabindex");
786
+ } else if (!this._multiSelect && li.getAttribute("aria-selected") === "true") {
787
+ li.setAttribute("tabindex", String(tabIndex));
788
+ } else {
789
+ li.setAttribute("tabindex", "-1");
790
+ }
791
+ }
792
+ }
736
793
  _attachNodeData(node, li) {
737
794
  node.element = li;
738
795
  this._setNodeElement(li, node);
@@ -774,10 +831,10 @@ var TreeElement = (function () {
774
831
  // li > div > [toggler], title span, [toggler]
775
832
  let div = li.firstChild;
776
833
  let titleSpan = div.childNodes[this._buttonLeft ? 1 : 0];
777
- this._fillTitleSpan(titleSpan, node.name, isSelected, level);
834
+ this._fillNode(li, titleSpan, node.name, isSelected, level);
778
835
  if (node.is_loading) {
779
836
  li.classList.add(this._classNames.loading);
780
- titleSpan.setAttribute("aria-busy", "true");
837
+ li.setAttribute("aria-busy", "true");
781
838
  }
782
839
  return li;
783
840
  }
@@ -788,30 +845,22 @@ var TreeElement = (function () {
788
845
  * state, so there is a template for each.
789
846
  */
790
847
  _createFolderTemplate(isOpen) {
791
- let liClasses = [this._classNames.common, this._classNames.folder];
792
- if (!isOpen) {
793
- liClasses.push(this._classNames.closed);
794
- }
795
- let li = this._createLiTemplate(liClasses.join(" "));
848
+ let li = this._createLiTemplate(`${this._classNames.common} ${this._classNames.folder}`);
849
+ setFolderLiOpenState(li, isOpen, this._classNames);
796
850
  let div = li.firstChild;
797
851
 
798
852
  // The toggler is hidden from screen readers: the icon is decoration, and
799
853
  // aria-expanded on the treeitem has the open state.
800
854
  let toggler = document.createElement("span");
801
- toggler.className = this._getButtonClasses(isOpen);
855
+ toggler.className = this._getButtonClasses();
802
856
  toggler.setAttribute("aria-hidden", "true");
803
- let iconElement = isOpen ? this._openedIconElement : this._closedIconElement;
804
- if (iconElement) {
805
- toggler.append(iconElement.cloneNode(true));
806
- }
857
+ setTogglerOpenState(toggler, isOpen, this._classNames, this._icons);
807
858
  if (this._buttonLeft) {
808
859
  div.append(toggler);
809
860
  }
810
861
 
811
862
  // title span
812
- let titleSpan = this._createTitleSpanTemplate(true);
813
- titleSpan.setAttribute("aria-expanded", getBoolString(isOpen));
814
- div.append(titleSpan);
863
+ div.append(this._createTitleSpanTemplate(true));
815
864
  if (!this._buttonLeft) {
816
865
  div.append(toggler);
817
866
  }
@@ -835,11 +884,22 @@ var TreeElement = (function () {
835
884
 
836
885
  /*
837
886
  Create the outer part of a <li> template: li > div
887
+ The li is the treeitem, so the ul of its children is inside the treeitem.
838
888
  */
839
889
  _createLiTemplate(liClasses) {
840
890
  let li = document.createElement("li");
841
891
  li.className = liClasses;
842
- li.setAttribute("role", "none");
892
+ li.setAttribute("role", "treeitem");
893
+
894
+ // Only a multiselect tree marks the nodes that are not selected
895
+ if (this._multiSelect) {
896
+ li.setAttribute("aria-selected", "false");
897
+ }
898
+
899
+ // Every node is focusable, but only the tab stop is in the tab order
900
+ if (this._tabIndex !== undefined) {
901
+ li.setAttribute("tabindex", "-1");
902
+ }
843
903
  let div = document.createElement("div");
844
904
  div.className = `${this._classNames.element} ${this._classNames.common}`;
845
905
  div.setAttribute("role", "none");
@@ -855,7 +915,7 @@ var TreeElement = (function () {
855
915
  // li > div > title span
856
916
  let div = li.firstChild;
857
917
  let titleSpan = div.firstChild;
858
- this._fillTitleSpan(titleSpan, node.name, isSelected, level);
918
+ this._fillNode(li, titleSpan, node.name, isSelected, level);
859
919
  return li;
860
920
  }
861
921
 
@@ -868,6 +928,15 @@ var TreeElement = (function () {
868
928
  div.append(this._createTitleSpanTemplate(false));
869
929
  return li;
870
930
  }
931
+ _createTemplates() {
932
+ return {
933
+ closedFolderTemplate: this._createFolderTemplate(false),
934
+ groupUlTemplate: this._createUlTemplate(false),
935
+ nodeTemplate: this._createNodeTemplate(),
936
+ openedFolderTemplate: this._createFolderTemplate(true),
937
+ rootUlTemplate: this._createUlTemplate(true)
938
+ };
939
+ }
871
940
  _createTitleSpanTemplate(isFolder) {
872
941
  let titleSpan = document.createElement("span");
873
942
  let classes = `${this._classNames.title} ${this._classNames.common}`;
@@ -876,13 +945,6 @@ var TreeElement = (function () {
876
945
  }
877
946
  classes += ` ${this._buttonLeft ? this._classNames.titleButtonLeft : this._classNames.titleButtonRight}`;
878
947
  titleSpan.className = classes;
879
- titleSpan.setAttribute("role", "treeitem");
880
- titleSpan.setAttribute("aria-selected", "false");
881
-
882
- // Every node is focusable, but only the tab stop is in the tab order
883
- if (this._tabIndex !== undefined) {
884
- titleSpan.setAttribute("tabindex", "-1");
885
- }
886
948
  return titleSpan;
887
949
  }
888
950
  _createUlTemplate(isRootNode) {
@@ -911,20 +973,25 @@ var TreeElement = (function () {
911
973
  if (this._ariaLabelledBy !== undefined) {
912
974
  ul.setAttribute("aria-labelledby", this._ariaLabelledBy);
913
975
  }
976
+ if (this._multiSelect) {
977
+ ul.setAttribute("aria-multiselectable", "true");
978
+ }
914
979
  }
915
980
  return ul;
916
981
  }
917
982
 
918
983
  /*
919
- Set the parts of a cloned title span that differ per node
984
+ Set the parts of a cloned li and title span that differ per node
920
985
  */
921
- _fillTitleSpan(titleSpan, nodeName, isSelected, level) {
922
- titleSpan.setAttribute("aria-level", String(level));
986
+ _fillNode(li, titleSpan, nodeName, isSelected, level) {
987
+ li.setAttribute("aria-level", String(level));
923
988
  if (isSelected) {
924
- titleSpan.setAttribute("aria-selected", "true");
989
+ li.setAttribute("aria-selected", "true");
925
990
  let tabIndex = this._tabIndex;
926
- if (tabIndex !== undefined) {
927
- titleSpan.setAttribute("tabindex", String(tabIndex));
991
+
992
+ // With multiSelect the tab stop is the active node, see updateTabStop
993
+ if (tabIndex !== undefined && !this._multiSelect) {
994
+ li.setAttribute("tabindex", String(tabIndex));
928
995
  }
929
996
  }
930
997
  if (this._autoEscape) {
@@ -933,12 +1000,13 @@ var TreeElement = (function () {
933
1000
  // eslint-disable-next-line no-unsanitized/property -- the user opted out of escaping with autoEscape: false
934
1001
  titleSpan.innerHTML = nodeName;
935
1002
  }
1003
+
1004
+ // The treeitem is named by its title, without the html when autoEscape
1005
+ // is false. Otherwise the name would contain everything in the row.
1006
+ li.setAttribute("aria-label", titleSpan.textContent);
936
1007
  }
937
- _getButtonClasses(isOpen) {
1008
+ _getButtonClasses() {
938
1009
  let classes = [this._classNames.toggler, this._classNames.common];
939
- if (!isOpen) {
940
- classes.push(this._classNames.closed);
941
- }
942
1010
  if (this._buttonLeft) {
943
1011
  classes.push(this._classNames.togglerLeft);
944
1012
  } else {
@@ -953,26 +1021,47 @@ var TreeElement = (function () {
953
1021
  _mustRenderChildren(node) {
954
1022
  return node.hasChildren() && node.is_open === true;
955
1023
  }
1024
+ _updateTemplates() {
1025
+ let templates = this._createTemplates();
1026
+ this._closedFolderTemplate = templates.closedFolderTemplate;
1027
+ this._groupUlTemplate = templates.groupUlTemplate;
1028
+ this._nodeTemplate = templates.nodeTemplate;
1029
+ this._openedFolderTemplate = templates.openedFolderTemplate;
1030
+ this._rootUlTemplate = templates.rootUlTemplate;
1031
+ }
956
1032
  }
957
1033
 
1034
+ /* The keys move from the selected node and select the node they move to.
1035
+ * With multiSelect they move from the focused node and only move the focus;
1036
+ * Space adds the focused node to the selection or removes it.
1037
+ */
1038
+
958
1039
  class KeyHandler {
959
1040
  constructor({
960
1041
  _closeNode: closeNode,
1042
+ _focusNode: focusNode,
1043
+ _getActiveNode: getActiveNode,
961
1044
  _getFocusedNode: getFocusedNode,
962
1045
  _getSelectedNode: getSelectedNode,
963
1046
  _getTree: getTree,
964
1047
  _keyboardSupport: keyboardSupport,
1048
+ _multiSelect: multiSelect,
965
1049
  _openNode: openNode,
966
1050
  _selectNode: selectNode,
1051
+ _toggleNodeInSelection: toggleNodeInSelection,
967
1052
  _triggerEvent: triggerEvent
968
1053
  }) {
969
1054
  this._closeNode = closeNode;
1055
+ this._focusNode = focusNode;
1056
+ this._getActiveNode = getActiveNode;
970
1057
  this._getFocusedNode = getFocusedNode;
971
1058
  this._getSelectedNode = getSelectedNode;
972
1059
  this._getTree = getTree;
973
1060
  this._keyboardSupport = keyboardSupport;
1061
+ this._multiSelect = multiSelect;
974
1062
  this._openNode = openNode;
975
1063
  this._originalSelectNode = selectNode;
1064
+ this._toggleNodeInSelection = toggleNodeInSelection;
976
1065
  this._triggerEvent = triggerEvent;
977
1066
  if (keyboardSupport) {
978
1067
  document.addEventListener("keydown", this._handleKeyDown);
@@ -984,10 +1073,28 @@ var TreeElement = (function () {
984
1073
  }
985
1074
  }
986
1075
  _moveDown() {
987
- return this._selectNode(this._getSelectedNode()?.getNextVisibleNode());
1076
+ return this._goToNode(this._getCurrentNode()?.getNextVisibleNode());
988
1077
  }
989
1078
  _moveUp() {
990
- return this._selectNode(this._getSelectedNode()?.getPreviousVisibleNode());
1079
+ return this._goToNode(this._getCurrentNode()?.getPreviousVisibleNode());
1080
+ }
1081
+
1082
+ // Change keyboardSupport after construction
1083
+ _setKeyboardSupport(keyboardSupport) {
1084
+ if (keyboardSupport === this._keyboardSupport) {
1085
+ return;
1086
+ }
1087
+ this._keyboardSupport = keyboardSupport;
1088
+ if (keyboardSupport) {
1089
+ document.addEventListener("keydown", this._handleKeyDown);
1090
+ } else {
1091
+ document.removeEventListener("keydown", this._handleKeyDown);
1092
+ }
1093
+ }
1094
+
1095
+ // Change multiSelect after construction
1096
+ _setMultiSelect(multiSelect) {
1097
+ this._multiSelect = multiSelect;
991
1098
  }
992
1099
 
993
1100
  // Activate the node, for example to open its edit page.
@@ -998,6 +1105,26 @@ var TreeElement = (function () {
998
1105
  });
999
1106
  return true;
1000
1107
  }
1108
+
1109
+ // No node is selected. These keys select the focused node, which is
1110
+ // usually the first node, see TreeElement.updateTabStop.
1111
+ _getCurrentNode() {
1112
+ return this._multiSelect ? this._getActiveNode() : this._getSelectedNode();
1113
+ }
1114
+
1115
+ // Move to the node: select it, or with multiSelect focus it.
1116
+ // Result: there was a node to move to.
1117
+ _goToNode(node) {
1118
+ if (!node) {
1119
+ return false;
1120
+ }
1121
+ if (this._multiSelect) {
1122
+ this._focusNode(node);
1123
+ } else {
1124
+ this._originalSelectNode(node);
1125
+ }
1126
+ return true;
1127
+ }
1001
1128
  _handleArrowKey(key) {
1002
1129
  switch (key) {
1003
1130
  case "ArrowDown":
@@ -1022,49 +1149,53 @@ var TreeElement = (function () {
1022
1149
  }
1023
1150
  }
1024
1151
  }
1152
+ _handleKey(key, focusedNode) {
1153
+ return this._getCurrentNode() ? this._handleArrowKey(key) : this._handleKeyWithoutSelection(key, focusedNode);
1154
+ }
1025
1155
  _handleKeyDown = e => {
1026
1156
  let focusedNode = this._getFocusedNode();
1027
1157
  if (!focusedNode) {
1028
1158
  return;
1029
1159
  }
1030
1160
  let isKeyHandled;
1031
- let selectedNode = this._getSelectedNode();
1032
1161
  switch (e.key) {
1162
+ case " ":
1163
+ {
1164
+ isKeyHandled = this._multiSelect ? this._toggle(focusedNode) : this._handleKey(e.key, focusedNode);
1165
+ break;
1166
+ }
1033
1167
  case "End":
1034
1168
  {
1035
- isKeyHandled = this._selectNode(this._getTree()?.getLastChild());
1169
+ isKeyHandled = this._goToNode(this._getTree()?.getLastChild());
1036
1170
  break;
1037
1171
  }
1038
1172
  case "Enter":
1039
1173
  {
1040
1174
  // Without a selected node, activate the focused node
1041
- isKeyHandled = this._activate(selectedNode ?? focusedNode, e);
1175
+ isKeyHandled = this._activate(this._multiSelect ? focusedNode : this._getSelectedNode() ?? focusedNode, e);
1042
1176
  break;
1043
1177
  }
1044
1178
  case "Home":
1045
1179
  {
1046
- isKeyHandled = this._selectNode(this._getTree()?.children[0]);
1180
+ isKeyHandled = this._goToNode(this._getTree()?.children[0]);
1047
1181
  break;
1048
1182
  }
1049
1183
  default:
1050
1184
  {
1051
- isKeyHandled = selectedNode ? this._handleArrowKey(e.key) : this._handleKeyWithoutSelection(e.key, focusedNode);
1185
+ isKeyHandled = this._handleKey(e.key, focusedNode);
1052
1186
  }
1053
1187
  }
1054
1188
  if (isKeyHandled) {
1055
1189
  e.preventDefault();
1056
1190
  }
1057
1191
  };
1058
-
1059
- // No node is selected. These keys select the focused node, which is
1060
- // usually the first node, see TreeElement.updateTabStop.
1061
1192
  _handleKeyWithoutSelection(key, focusedNode) {
1062
1193
  switch (key) {
1063
1194
  case " ":
1064
1195
  case "ArrowDown":
1065
1196
  case "ArrowRight":
1066
1197
  {
1067
- return this._selectNode(focusedNode);
1198
+ return this._goToNode(focusedNode);
1068
1199
  }
1069
1200
  default:
1070
1201
  {
@@ -1073,45 +1204,37 @@ var TreeElement = (function () {
1073
1204
  }
1074
1205
  }
1075
1206
  _moveLeft() {
1076
- let selectedNode = this._getSelectedNode();
1207
+ let currentNode = this._getCurrentNode();
1077
1208
 
1078
1209
  /* istanbul ignore if */
1079
- if (!selectedNode) {
1210
+ if (!currentNode) {
1080
1211
  return false;
1081
1212
  }
1082
- if (selectedNode.isFolder() && selectedNode.is_open) {
1213
+ if (currentNode.isFolder() && currentNode.is_open) {
1083
1214
  // Left on an open node closes the node
1084
- void this._closeNode(selectedNode);
1215
+ void this._closeNode(currentNode);
1085
1216
  return true;
1086
1217
  }
1087
1218
  // Left on a closed or end node moves focus to the node's parent
1088
- return this._selectNode(selectedNode.getParent());
1219
+ return this._goToNode(currentNode.getParent());
1089
1220
  }
1090
1221
  _moveRight() {
1091
- let selectedNode = this._getSelectedNode();
1092
- if (selectedNode?.isFolder()) {
1222
+ let currentNode = this._getCurrentNode();
1223
+ if (currentNode?.isFolder()) {
1093
1224
  // folder node
1094
- if (selectedNode.is_open) {
1225
+ if (currentNode.is_open) {
1095
1226
  // Right moves to the first child of an open node
1096
- return this._selectNode(selectedNode.getNextVisibleNode());
1227
+ return this._goToNode(currentNode.getNextVisibleNode());
1097
1228
  }
1098
1229
  // Right expands a closed node
1099
- void this._openNode(selectedNode);
1230
+ void this._openNode(currentNode);
1100
1231
  return true;
1101
1232
  }
1102
1233
  return false;
1103
1234
  }
1104
-
1105
- /* Select the node.
1106
- * Don't do anything if the node is null.
1107
- * Result: a different node was selected.
1108
- */
1109
- _selectNode(node) {
1110
- if (node) {
1111
- this._originalSelectNode(node);
1112
- return true;
1113
- }
1114
- return false;
1235
+ _toggle(node) {
1236
+ this._toggleNodeInSelection(node);
1237
+ return true;
1115
1238
  }
1116
1239
  }
1117
1240
 
@@ -1238,19 +1361,19 @@ var TreeElement = (function () {
1238
1361
  return;
1239
1362
  }
1240
1363
  let div = e.target.closest(`ul.${this._classNames.tree} .${this._classNames.element}`);
1241
- if (div) {
1242
- let node = this._getNode(div);
1243
- if (node) {
1244
- e.preventDefault();
1245
- e.stopPropagation();
1246
- this._triggerEvent("tree.contextmenu", {
1247
- node,
1248
- originalEvent: e
1249
- });
1250
- return false;
1251
- }
1364
+ if (!div) {
1365
+ return;
1252
1366
  }
1253
- return null;
1367
+ let node = this._getNode(div);
1368
+ if (!node) {
1369
+ return;
1370
+ }
1371
+ e.preventDefault();
1372
+ e.stopPropagation();
1373
+ this._triggerEvent("tree.contextmenu", {
1374
+ node,
1375
+ originalEvent: e
1376
+ });
1254
1377
  };
1255
1378
  _handleDblclick = e => {
1256
1379
  if (!e.target) {
@@ -1410,14 +1533,14 @@ var TreeElement = (function () {
1410
1533
  *
1411
1534
  * @groupDescription Searching
1412
1535
  * These work on any node, and search that node's subtree. Called on the root
1413
- * node, they search the whole tree — which is what the tree's own methods of
1536
+ * node, they search the whole tree—which is what the tree's own methods of
1414
1537
  * the same name do.
1415
1538
  *
1416
1539
  * @groupDescription Changing the tree
1417
1540
  * ::: warning
1418
1541
  * These methods change the data without re-rendering. The tree's
1419
- * [methods](/reference/methods) — `appendNode`, `removeNode`, `moveNode` and
1420
- * friends — do the same and refresh the display, so prefer those. If you do
1542
+ * [methods](/reference/methods)—`appendNode`, `removeNode`, `moveNode` and
1543
+ * friends—do the same and refresh the display, so prefer those. If you do
1421
1544
  * use these, call `tree.refresh()` afterwards.
1422
1545
  * :::
1423
1546
  */
@@ -1466,7 +1589,7 @@ var TreeElement = (function () {
1466
1589
  */
1467
1590
  nodeClass;
1468
1591
  /**
1469
- The parent; `null` for the root node.
1592
+ The parent, or `null` for the root node.
1470
1593
  */
1471
1594
  parent;
1472
1595
  /**
@@ -1558,7 +1681,9 @@ var TreeElement = (function () {
1558
1681
  }
1559
1682
 
1560
1683
  /**
1561
- * Inserts a new parent between this node and its current parent.
1684
+ * Inserts a new parent between this node and its current parent. Its
1685
+ * siblings move along: the new node takes all children of the current
1686
+ * parent, and becomes its only child.
1562
1687
  *
1563
1688
  * @returns The new node, or `null` when the node has no parent.
1564
1689
  * @group Changing the tree
@@ -1628,7 +1753,7 @@ var TreeElement = (function () {
1628
1753
  * Returns the subtree as plain data, ready for `JSON.stringify`.
1629
1754
  * Internal properties (`parent`, `children`, `element`, `tree`,
1630
1755
  * `idMapping`, `nodeClass`, `load_on_demand`, `isEmptyFolder`) are left
1631
- * out; your own properties are kept.
1756
+ * out. Your own properties are kept.
1632
1757
  *
1633
1758
  * ```js
1634
1759
  * tree.getTree().getData();
@@ -1720,7 +1845,7 @@ var TreeElement = (function () {
1720
1845
  }
1721
1846
 
1722
1847
  /**
1723
- * Like `getNextNode`, but skipping nodes inside closed folders — this is
1848
+ * Like `getNextNode`, but skipping nodes inside closed folders. This is
1724
1849
  * what the arrow keys use.
1725
1850
  *
1726
1851
  * @group Moving around the tree
@@ -1804,7 +1929,7 @@ var TreeElement = (function () {
1804
1929
  }
1805
1930
 
1806
1931
  /**
1807
- * Returns the parent: `null` for a top-level node — the root node is not
1932
+ * Returns the parent, or `null` for a top-level node: the root node is not
1808
1933
  * returned.
1809
1934
  *
1810
1935
  * @group Moving around the tree
@@ -1852,8 +1977,8 @@ var TreeElement = (function () {
1852
1977
  }
1853
1978
 
1854
1979
  /**
1855
- * Like `getPreviousNode`, but skipping nodes inside closed folders —
1856
- * this is what the arrow keys use.
1980
+ * Like `getPreviousNode`, but skipping nodes inside closed folders.
1981
+ * This is what the arrow keys use.
1857
1982
  *
1858
1983
  * @group Moving around the tree
1859
1984
  */
@@ -2217,12 +2342,14 @@ var TreeElement = (function () {
2217
2342
  constructor({
2218
2343
  _classNames: classNames,
2219
2344
  _getScrollLeft: getScrollLeft,
2345
+ _multiSelect: multiSelect = false,
2220
2346
  _node: node,
2221
2347
  _tabIndex: tabIndex,
2222
2348
  _treeElement: treeElement
2223
2349
  }) {
2224
2350
  this._classNames = classNames;
2225
2351
  this._getScrollLeft = getScrollLeft;
2352
+ this._multiSelect = multiSelect;
2226
2353
  this._node = node;
2227
2354
  this._tabIndex = tabIndex;
2228
2355
  this._treeElement = treeElement;
@@ -2238,35 +2365,36 @@ var TreeElement = (function () {
2238
2365
  return;
2239
2366
  }
2240
2367
  this._element.classList.remove(this._classNames.selected);
2241
- let titleSpan = this._getTitleSpan();
2368
+
2369
+ // Only a multiselect tree marks the nodes that are not selected
2370
+ if (this._multiSelect) {
2371
+ this._element.setAttribute("aria-selected", "false");
2372
+ return;
2373
+ }
2374
+ this._element.removeAttribute("aria-selected");
2242
2375
 
2243
2376
  // Check for null or undefined
2244
2377
  if (this._tabIndex != null) {
2245
- titleSpan.setAttribute("tabindex", "-1");
2378
+ this._element.setAttribute("tabindex", "-1");
2246
2379
  }
2247
- titleSpan.setAttribute("aria-selected", "false");
2248
- titleSpan.blur();
2380
+ this._element.blur();
2249
2381
  }
2250
2382
  _select(mustSetFocus) {
2251
2383
  if (!this._isRendered()) {
2252
2384
  return;
2253
2385
  }
2254
2386
  this._element.classList.add(this._classNames.selected);
2255
- let titleSpan = this._getTitleSpan();
2387
+ this._element.setAttribute("aria-selected", "true");
2256
2388
  let tabIndex = this._tabIndex;
2257
2389
 
2258
2390
  // Check for null or undefined
2259
- if (tabIndex != null) {
2260
- titleSpan.setAttribute("tabindex", tabIndex.toString());
2391
+ if (tabIndex != null && !this._multiSelect) {
2392
+ this._element.setAttribute("tabindex", tabIndex.toString());
2261
2393
  }
2262
- titleSpan.setAttribute("aria-selected", "true");
2263
2394
  if (mustSetFocus) {
2264
- titleSpan.focus();
2395
+ this._element.focus();
2265
2396
  }
2266
2397
  }
2267
- _getTitleSpan() {
2268
- return this._element.querySelector(`:scope > .${this._classNames.element} > span.${this._classNames.title}`);
2269
- }
2270
2398
  _getUl() {
2271
2399
  return this._element.querySelector(":scope > ul");
2272
2400
  }
@@ -2326,10 +2454,10 @@ var TreeElement = (function () {
2326
2454
  class FolderElement extends NodeElement {
2327
2455
  constructor({
2328
2456
  _classNames: classNames,
2329
- _closedIconElement: closedIconElement,
2330
2457
  _getScrollLeft: getScrollLeft,
2458
+ _icons: icons,
2459
+ _multiSelect: multiSelect,
2331
2460
  _node: node,
2332
- _openedIconElement: openedIconElement,
2333
2461
  _renderChildren: renderChildren,
2334
2462
  _tabIndex: tabIndex,
2335
2463
  _treeElement: treeElement,
@@ -2338,12 +2466,12 @@ var TreeElement = (function () {
2338
2466
  super({
2339
2467
  _classNames: classNames,
2340
2468
  _getScrollLeft: getScrollLeft,
2469
+ _multiSelect: multiSelect,
2341
2470
  _node: node,
2342
2471
  _tabIndex: tabIndex,
2343
2472
  _treeElement: treeElement
2344
2473
  });
2345
- this._closedIconElement = closedIconElement;
2346
- this._openedIconElement = openedIconElement;
2474
+ this._icons = icons;
2347
2475
  this._renderChildren = renderChildren;
2348
2476
  this._triggerEvent = triggerEvent;
2349
2477
  }
@@ -2352,31 +2480,11 @@ var TreeElement = (function () {
2352
2480
  return;
2353
2481
  }
2354
2482
  this._node.is_open = false;
2355
- if (!this._isRendered()) {
2356
- this._triggerEvent("tree.close", {
2357
- node: this._node
2358
- });
2359
- return;
2360
- }
2361
- let button = this._getButton();
2362
- button.classList.add(this._classNames.closed);
2363
- button.replaceChildren();
2364
- let closedIconElement = this._closedIconElement;
2365
- if (closedIconElement) {
2366
- let icon = closedIconElement.cloneNode(true);
2367
- button.append(icon);
2368
- }
2369
- let ul = this._getUl();
2370
- if (ul) {
2371
- if (slide) {
2372
- await slideUp(ul, animationSpeed);
2373
- } else {
2374
- ul.style.display = "none";
2375
- }
2483
+ if (this._isRendered()) {
2484
+ this._setButtonState(false);
2485
+ await this._setUlState(this._getUl(), false, slide, animationSpeed);
2486
+ this._setElementState(false);
2376
2487
  }
2377
- this._element.classList.add(this._classNames.closed);
2378
- let titleSpan = this._getTitleSpan();
2379
- titleSpan.setAttribute("aria-expanded", "false");
2380
2488
  this._triggerEvent("tree.close", {
2381
2489
  node: this._node
2382
2490
  });
@@ -2386,34 +2494,17 @@ var TreeElement = (function () {
2386
2494
  return;
2387
2495
  }
2388
2496
  this._node.is_open = true;
2389
- if (!this._isRendered()) {
2390
- // The folder is rendered open when its parent is opened
2391
- this._triggerEvent("tree.open", {
2392
- node: this._node
2393
- });
2394
- return;
2395
- }
2396
- let button = this._getButton();
2397
- button.classList.remove(this._classNames.closed);
2398
- button.replaceChildren();
2399
- let openedIconElement = this._openedIconElement;
2400
- if (openedIconElement) {
2401
- let icon = openedIconElement.cloneNode(true);
2402
- button.append(icon);
2403
- }
2404
2497
 
2405
- // The children are rendered the first time the folder is opened
2406
- let ul = this._getUl() ?? this._renderChildren(this._node);
2407
- if (ul) {
2408
- if (slide) {
2409
- await slideDown(ul, animationSpeed);
2410
- } else {
2411
- ul.style.display = "block";
2412
- }
2498
+ // A folder that is not rendered is rendered open when its parent is
2499
+ // opened
2500
+ if (this._isRendered()) {
2501
+ this._setButtonState(true);
2502
+
2503
+ // The children are rendered the first time the folder is opened
2504
+ let ul = this._getUl() ?? this._renderChildren(this._node);
2505
+ await this._setUlState(ul, true, slide, animationSpeed);
2506
+ this._setElementState(true);
2413
2507
  }
2414
- this._element.classList.remove(this._classNames.closed);
2415
- let titleSpan = this._getTitleSpan();
2416
- titleSpan.setAttribute("aria-expanded", "true");
2417
2508
  this._triggerEvent("tree.open", {
2418
2509
  node: this._node
2419
2510
  });
@@ -2424,8 +2515,34 @@ var TreeElement = (function () {
2424
2515
  _getButton() {
2425
2516
  return this._element.querySelector(`:scope > .${this._classNames.element} > .${this._classNames.toggler}`);
2426
2517
  }
2518
+ _setButtonState(isOpen) {
2519
+ setTogglerOpenState(this._getButton(), isOpen, this._classNames, this._icons);
2520
+ }
2521
+ _setElementState(isOpen) {
2522
+ setFolderLiOpenState(this._element, isOpen, this._classNames);
2523
+ }
2524
+ async _setUlState(ul, isOpen, slide, animationSpeed) {
2525
+ if (!ul) {
2526
+ return;
2527
+ }
2528
+ if (slide) {
2529
+ await (isOpen ? slideDown : slideUp)(ul, animationSpeed);
2530
+ } else {
2531
+ ul.style.display = isOpen ? "block" : "none";
2532
+ }
2533
+ }
2427
2534
  }
2428
2535
 
2536
+ /* The options that setOption can change. The others are read once, in the
2537
+ * constructor, so changing them later would have no effect or leave the tree
2538
+ * in an inconsistent state.
2539
+ */
2540
+ let changeableOptions = ["animationSpeed", "dataUrl", "dragAndDrop", "keyboardSupport", "multiSelect", "onCanSelectNode", "selectable", "slide", "startDndDelay", "tabIndex"];
2541
+
2542
+ /**
2543
+ * The options that `setOption` can change.
2544
+ */
2545
+
2429
2546
  // Url of a request. A relative url is resolved against the page, like a link
2430
2547
  // or `fetch` would, so it also works when the page is in a subdirectory.
2431
2548
 
@@ -2466,9 +2583,7 @@ var TreeElement = (function () {
2466
2583
  this._saveStateOption = saveState;
2467
2584
  }
2468
2585
  _getNodeIdToBeSelected() {
2469
- if (!this._saveStateOption) {
2470
- return null;
2471
- }
2586
+ // Without the saveState option there is no state
2472
2587
  let state = this._getStateFromStorage();
2473
2588
  return state?.selected_node ? state.selected_node[0] ?? null : null;
2474
2589
  }
@@ -2597,17 +2712,19 @@ var TreeElement = (function () {
2597
2712
  this._removeFromSelection(node);
2598
2713
  }
2599
2714
  }
2715
+
2716
+ // result: is a node selected?
2600
2717
  _selectInitialNodes(nodeIds) {
2601
- let selectCount = 0;
2718
+ let isNodeSelected = false;
2602
2719
  for (let nodeId of nodeIds) {
2603
2720
  let node = this._getNodeById(nodeId);
2604
2721
  if (!node) {
2605
2722
  continue;
2606
2723
  }
2607
- selectCount += 1;
2608
2724
  this._addToSelection(node);
2725
+ isNodeSelected = true;
2609
2726
  }
2610
- return selectCount !== 0;
2727
+ return isNodeSelected;
2611
2728
  }
2612
2729
  }
2613
2730
 
@@ -2921,9 +3038,9 @@ var TreeElement = (function () {
2921
3038
  } else {
2922
3039
  this._selectedNodes.delete(node.id);
2923
3040
  if (includeChildren) {
2924
- node.iterate(() => {
2925
- if (node.id != null) {
2926
- this._selectedNodes.delete(node.id);
3041
+ node.iterate(child => {
3042
+ if (child.id != null) {
3043
+ this._selectedNodes.delete(child.id);
2927
3044
  }
2928
3045
  return true;
2929
3046
  });
@@ -2942,6 +3059,9 @@ var TreeElement = (function () {
2942
3059
  * mustToggle: support deselecting the selected node
2943
3060
  */
2944
3061
  _selectSingleNode(node, optionsParam) {
3062
+ if (!this._canSelect(node)) {
3063
+ return;
3064
+ }
2945
3065
  let defaultOptions = {
2946
3066
  mustSetFocus: true,
2947
3067
  mustToggle: true
@@ -2950,35 +3070,39 @@ var TreeElement = (function () {
2950
3070
  ...defaultOptions,
2951
3071
  ...optionsParam
2952
3072
  };
2953
- let canSelect = () => {
2954
- if (!this._getSelectable()) {
2955
- return false;
2956
- }
2957
- let onCanSelectNode = this._getOnCanSelectNode();
2958
- return onCanSelectNode ? onCanSelectNode(node) : true;
2959
- };
2960
- if (!canSelect()) {
2961
- return;
2962
- }
2963
- let deselectCurrentNode = deselectedNode => {
2964
- this._removeFromSelection(deselectedNode);
2965
- this._getNodeElementForNode(deselectedNode)._deselect();
2966
- };
3073
+ let otherSelectedNodes = this._getSelectedNodes().filter(selectedNode => selectedNode !== node);
2967
3074
  if (this._isNodeSelected(node)) {
2968
- if (selectOptions.mustToggle) {
2969
- deselectCurrentNode(node);
3075
+ if (otherSelectedNodes.length) {
3076
+ // Part of a multiple selection: keep only this node selected
3077
+ for (let otherNode of otherSelectedNodes) {
3078
+ this._deselectNode(otherNode);
3079
+ }
3080
+ for (let otherNode of otherSelectedNodes) {
3081
+ this._triggerEvent("tree.deselect", {
3082
+ node: otherNode
3083
+ });
3084
+ }
3085
+ } else if (selectOptions.mustToggle) {
3086
+ this._deselectNode(node);
2970
3087
  this._triggerEvent("tree.deselect", {
2971
3088
  node
2972
3089
  });
2973
3090
  }
2974
3091
  } else {
2975
- let deselectedNode = this._getSelectedNode();
2976
- if (deselectedNode) {
2977
- deselectCurrentNode(deselectedNode);
3092
+ // tree.select reports the first deselected node; the others, from a
3093
+ // multiple selection, get a tree.deselect.
3094
+ let [deselectedNode = null, ...alsoDeselectedNodes] = otherSelectedNodes;
3095
+ for (let otherNode of otherSelectedNodes) {
3096
+ this._deselectNode(otherNode);
2978
3097
  }
2979
3098
  this._addToSelection(node);
2980
3099
  this._openParents(node);
2981
3100
  this._getNodeElementForNode(node)._select(selectOptions.mustSetFocus);
3101
+ for (let otherNode of alsoDeselectedNodes) {
3102
+ this._triggerEvent("tree.deselect", {
3103
+ node: otherNode
3104
+ });
3105
+ }
2982
3106
  this._triggerEvent("tree.select", {
2983
3107
  deselectedNode,
2984
3108
  node
@@ -2987,6 +3111,43 @@ var TreeElement = (function () {
2987
3111
  this._updateTabStop();
2988
3112
  this._saveState();
2989
3113
  }
3114
+
3115
+ /* Add a node to the selection, or remove it when it is selected, without
3116
+ * changing the other selected nodes.
3117
+ * Checks if the node is selectable and saves the state.
3118
+ */
3119
+ _toggleNodeInSelection(node, mustSetFocus = true) {
3120
+ if (!this._canSelect(node)) {
3121
+ return;
3122
+ }
3123
+ if (this._isNodeSelected(node)) {
3124
+ this._deselectNode(node);
3125
+ this._triggerEvent("tree.deselect", {
3126
+ node
3127
+ });
3128
+ } else {
3129
+ this._addToSelection(node);
3130
+ this._openParents(node);
3131
+ this._getNodeElementForNode(node)._select(mustSetFocus);
3132
+ this._triggerEvent("tree.select", {
3133
+ deselectedNode: null,
3134
+ node
3135
+ });
3136
+ }
3137
+ this._updateTabStop();
3138
+ this._saveState();
3139
+ }
3140
+ _canSelect(node) {
3141
+ if (!this._getSelectable()) {
3142
+ return false;
3143
+ }
3144
+ let onCanSelectNode = this._getOnCanSelectNode();
3145
+ return onCanSelectNode ? onCanSelectNode(node) : true;
3146
+ }
3147
+ _deselectNode(node) {
3148
+ this._removeFromSelection(node);
3149
+ this._getNodeElementForNode(node)._deselect();
3150
+ }
2990
3151
  }
2991
3152
 
2992
3153
  /* The default icons are svg instead of text: the triangle characters come
@@ -3013,6 +3174,7 @@ var TreeElement = (function () {
3013
3174
  // dataUrl: undefined,
3014
3175
  dragAndDrop: false,
3015
3176
  keyboardSupport: true,
3177
+ multiSelect: false,
3016
3178
  nodeClass: Node,
3017
3179
  // onCanMove: undefined, // Can this node be moved?
3018
3180
  // onCanMoveTo: undefined, // Can this node be moved to this position? function(moved_node, target_node, position)
@@ -3078,7 +3240,7 @@ var TreeElement = (function () {
3078
3240
  };
3079
3241
 
3080
3242
  // Generated by config/generateVersion.mjs. Do not edit.
3081
- let version = "0.3.2";
3243
+ let version = "0.4.1";
3082
3244
 
3083
3245
  // The types that appear in the public api. Consumers cannot import them from
3084
3246
  // the submodules directly, because those are not exposed in package.json.
@@ -3086,6 +3248,8 @@ var TreeElement = (function () {
3086
3248
  // as its global, instead of an object of named exports.
3087
3249
 
3088
3250
  class TreeElement {
3251
+ // With multiSelect: the node that is the tab stop, see updateTabStop
3252
+ _activeNode;
3089
3253
  _classNames;
3090
3254
  _dataLoader;
3091
3255
  _dndHandler;
@@ -3119,6 +3283,7 @@ var TreeElement = (function () {
3119
3283
  this._tree = new Node({}, true);
3120
3284
  this._nodeMap = new WeakMap();
3121
3285
  this._fallbackTabStop = null;
3286
+ this._activeNode = null;
3122
3287
  let {
3123
3288
  ariaLabel,
3124
3289
  ariaLabelledBy,
@@ -3128,6 +3293,7 @@ var TreeElement = (function () {
3128
3293
  dataFilter,
3129
3294
  dragAndDrop,
3130
3295
  keyboardSupport,
3296
+ multiSelect,
3131
3297
  onCanMove,
3132
3298
  onCanMoveTo,
3133
3299
  onCreateLi,
@@ -3141,7 +3307,6 @@ var TreeElement = (function () {
3141
3307
  rtl,
3142
3308
  saveState: saveStateOption,
3143
3309
  showEmptyFolder,
3144
- slide,
3145
3310
  tabIndex
3146
3311
  } = this._options;
3147
3312
  let classNames = this._classNames;
@@ -3179,6 +3344,17 @@ var TreeElement = (function () {
3179
3344
  let isNodeSelected = selectNodeHandler._isNodeSelected.bind(selectNodeHandler);
3180
3345
  let removeFromSelection = selectNodeHandler._removeFromSelection.bind(selectNodeHandler);
3181
3346
  let selectNode = selectNodeHandler._selectSingleNode.bind(selectNodeHandler);
3347
+ let toggleNodeInSelection = selectNodeHandler._toggleNodeInSelection.bind(selectNodeHandler);
3348
+
3349
+ // With multiSelect, a click adds a node to the selection or removes it
3350
+ let onClickTitle = node => {
3351
+ if (this._options.multiSelect) {
3352
+ toggleNodeInSelection(node, false);
3353
+ this._focusNode(node);
3354
+ } else {
3355
+ selectNode(node);
3356
+ }
3357
+ };
3182
3358
  let getMouseDelay = () => this._options.startDndDelay ?? 0;
3183
3359
  let dataLoader = new DataLoader({
3184
3360
  _classNames: classNames,
@@ -3219,18 +3395,21 @@ var TreeElement = (function () {
3219
3395
  _openFolderDelay: openFolderDelay,
3220
3396
  _openNode: openNode,
3221
3397
  _refreshElements: refreshElements,
3222
- _slide: slide,
3223
3398
  _treeElement: treeElement,
3224
3399
  _triggerEvent: triggerEvent
3225
3400
  });
3226
3401
  let keyHandler = new KeyHandler({
3227
3402
  _closeNode: closeNode,
3403
+ _focusNode: this._focusNode.bind(this),
3404
+ _getActiveNode: this._getActiveNode.bind(this),
3228
3405
  _getFocusedNode: getFocusedNode,
3229
3406
  _getSelectedNode: getSelectedNode,
3230
3407
  _getTree: getTree,
3231
3408
  _keyboardSupport: keyboardSupport,
3409
+ _multiSelect: multiSelect,
3232
3410
  _openNode: openNode,
3233
3411
  _selectNode: selectNode,
3412
+ _toggleNodeInSelection: toggleNodeInSelection,
3234
3413
  _triggerEvent: triggerEvent
3235
3414
  });
3236
3415
  let renderer = new ElementsRenderer({
@@ -3244,6 +3423,7 @@ var TreeElement = (function () {
3244
3423
  _getTree: getTree,
3245
3424
  _htmlElement: treeElement,
3246
3425
  _isNodeSelected: isNodeSelected,
3426
+ _multiSelect: multiSelect,
3247
3427
  _onCreateLi: onCreateLi,
3248
3428
  _openedIcon: openedIcon,
3249
3429
  _rtl: rtl,
@@ -3262,7 +3442,7 @@ var TreeElement = (function () {
3262
3442
  _getMouseDelay: getMouseDelay,
3263
3443
  _getNode: getNode,
3264
3444
  _onClickButton: this.toggle.bind(this),
3265
- _onClickTitle: selectNode,
3445
+ _onClickTitle: onClickTitle,
3266
3446
  _onMouseCapture: onMouseCapture,
3267
3447
  _onMouseDrag: onMouseDrag,
3268
3448
  _onMouseStart: onMouseStart,
@@ -3278,6 +3458,9 @@ var TreeElement = (function () {
3278
3458
  this._saveStateHandler = saveStateHandler;
3279
3459
  this._scrollHandler = scrollHandler;
3280
3460
  this._selectNodeHandler = selectNodeHandler;
3461
+ if (multiSelect) {
3462
+ treeElement.addEventListener("focusin", this._handleFocusIn);
3463
+ }
3281
3464
  this._initData();
3282
3465
  }
3283
3466
 
@@ -3310,8 +3493,9 @@ var TreeElement = (function () {
3310
3493
  }
3311
3494
 
3312
3495
  /**
3313
- * Inserts a new node between a node and its parent, taking the node as its
3314
- * child.
3496
+ * Inserts a new node between a node and its parent. The siblings of the node
3497
+ * move along: the new node takes all children of the parent, and becomes its
3498
+ * only child.
3315
3499
  *
3316
3500
  * @returns The new node, or `null` when the node has no parent.
3317
3501
  * @group Changing the tree
@@ -3387,6 +3571,7 @@ var TreeElement = (function () {
3387
3571
  this._dataLoader._deinit();
3388
3572
  this._keyHandler._deinit();
3389
3573
  this._mouseHandler._deinit();
3574
+ this._htmlElement.removeEventListener("focusin", this._handleFocusIn);
3390
3575
  this._tree = new Node({}, true);
3391
3576
  }
3392
3577
 
@@ -3480,7 +3665,7 @@ var TreeElement = (function () {
3480
3665
  }
3481
3666
 
3482
3667
  /**
3483
- * Returns the current state — `open_nodes` and `selected_node` — whether or
3668
+ * Returns the current state—`open_nodes` and `selected_node`—whether or
3484
3669
  * not `saveState` is enabled.
3485
3670
  *
3486
3671
  * @group State
@@ -3499,7 +3684,7 @@ var TreeElement = (function () {
3499
3684
  }
3500
3685
 
3501
3686
  /**
3502
- * Returns the root node. It is not rendered; its `children` are the
3687
+ * Returns the root node. It is not rendered, and its `children` are the
3503
3688
  * top-level nodes. Also available as the `tree` property.
3504
3689
  *
3505
3690
  * @group Finding nodes
@@ -3574,7 +3759,8 @@ var TreeElement = (function () {
3574
3759
  }
3575
3760
 
3576
3761
  /**
3577
- * Selects the next visible node, like the down arrow key.
3762
+ * Selects the next visible node, like the down arrow key. With
3763
+ * `multiSelect` it moves the focus instead.
3578
3764
  *
3579
3765
  * @group Selection
3580
3766
  */
@@ -3599,7 +3785,8 @@ var TreeElement = (function () {
3599
3785
  }
3600
3786
 
3601
3787
  /**
3602
- * Selects the previous visible node, like the up arrow key.
3788
+ * Selects the previous visible node, like the up arrow key. With
3789
+ * `multiSelect` it moves the focus instead.
3603
3790
  *
3604
3791
  * @group Selection
3605
3792
  */
@@ -3608,7 +3795,7 @@ var TreeElement = (function () {
3608
3795
  }
3609
3796
 
3610
3797
  /**
3611
- * Opens a folder, and the folders above it. A node marked `load_on_demand`
3798
+ * Opens a folder, and the folders that contain it. A node marked `load_on_demand`
3612
3799
  * is fetched first, so await the promise when you need to know it is really
3613
3800
  * open.
3614
3801
  *
@@ -3624,7 +3811,7 @@ var TreeElement = (function () {
3624
3811
  async openNode(node, slide) {
3625
3812
  let mustSlide = slide ?? this._options.slide;
3626
3813
  let doOpenNode = async (openedNode, slideOption) => {
3627
- if (!node.children.length) {
3814
+ if (!openedNode.children.length) {
3628
3815
  return;
3629
3816
  }
3630
3817
  let folderElement = this._createFolderElement(openedNode);
@@ -3729,8 +3916,8 @@ var TreeElement = (function () {
3729
3916
  */
3730
3917
  selectNode(node, options) {
3731
3918
  if (!node) {
3732
- // Called with empty node -> deselect current node
3733
- this._deselectCurrentNode();
3919
+ // Called with empty node -> deselect all nodes
3920
+ this._deselectAllNodes();
3734
3921
  this._saveStateHandler._saveState();
3735
3922
  return;
3736
3923
  }
@@ -3743,13 +3930,43 @@ var TreeElement = (function () {
3743
3930
  }
3744
3931
 
3745
3932
  /**
3746
- * Changes an option after construction. See
3747
- * [Options](/reference/options#changing-an-option-later) for the caveats.
3933
+ * Changes an option after construction. Only some options can be changed:
3934
+ * the others throw an error. See
3935
+ * [Options](/reference/options#changing-an-option-later).
3748
3936
  *
3749
3937
  * @group Other
3750
3938
  */
3751
3939
  setOption(option, value) {
3940
+ if (!changeableOptions.includes(option)) {
3941
+ throw new Error(`The option ${option} can only be set in the constructor`);
3942
+ }
3752
3943
  this._options[option] = value;
3944
+ switch (option) {
3945
+ case "dragAndDrop":
3946
+ {
3947
+ this._renderer._setDragAndDrop(Boolean(value));
3948
+ break;
3949
+ }
3950
+ case "keyboardSupport":
3951
+ {
3952
+ this._keyHandler._setKeyboardSupport(Boolean(value));
3953
+ break;
3954
+ }
3955
+ case "multiSelect":
3956
+ {
3957
+ this._setMultiSelect(Boolean(value));
3958
+ break;
3959
+ }
3960
+ case "tabIndex":
3961
+ {
3962
+ this._options.tabIndex = value ?? undefined;
3963
+ this._renderer._setTabIndex(this._options.tabIndex);
3964
+ this._fallbackTabStop = null;
3965
+ this._updateTabStop();
3966
+ break;
3967
+ }
3968
+ // No default
3969
+ }
3753
3970
  }
3754
3971
 
3755
3972
  /**
@@ -3782,8 +3999,8 @@ var TreeElement = (function () {
3782
3999
  }
3783
4000
 
3784
4001
  /**
3785
- * Returns the tree as a json string, including changes made through the
3786
- * api.
4002
+ * Returns the tree as a JSON string, including changes made through the
4003
+ * API.
3787
4004
  *
3788
4005
  * @group Other
3789
4006
  */
@@ -3796,7 +4013,7 @@ var TreeElement = (function () {
3796
4013
  * name.
3797
4014
  *
3798
4015
  * Changing the `id` re-indexes the node. `children` and `parent` are
3799
- * ignored — use `loadData` or `moveNode` for those.
4016
+ * ignored—use `loadData` or `moveNode` for those.
3800
4017
  *
3801
4018
  * @example
3802
4019
  * ```js
@@ -3847,9 +4064,7 @@ var TreeElement = (function () {
3847
4064
  }
3848
4065
  _createFolderElement(node) {
3849
4066
  let classNames = this._classNames;
3850
- let closedIconElement = this._renderer._closedIconElement;
3851
4067
  let getScrollLeft = this._scrollHandler._getScrollLeft.bind(this._scrollHandler);
3852
- let openedIconElement = this._renderer._openedIconElement;
3853
4068
  let tabIndex = this._options.tabIndex;
3854
4069
  let treeElement = this._htmlElement;
3855
4070
  let triggerEvent = this._triggerEvent.bind(this);
@@ -3864,10 +4079,10 @@ var TreeElement = (function () {
3864
4079
  };
3865
4080
  return new FolderElement({
3866
4081
  _classNames: classNames,
3867
- _closedIconElement: closedIconElement,
3868
4082
  _getScrollLeft: getScrollLeft,
4083
+ _icons: this._renderer._icons,
4084
+ _multiSelect: this._options.multiSelect,
3869
4085
  _node: node,
3870
- _openedIconElement: openedIconElement,
3871
4086
  _renderChildren: renderChildren,
3872
4087
  _tabIndex: tabIndex,
3873
4088
  _treeElement: treeElement,
@@ -3882,6 +4097,7 @@ var TreeElement = (function () {
3882
4097
  return new NodeElement({
3883
4098
  _classNames: classNames,
3884
4099
  _getScrollLeft: getScrollLeft,
4100
+ _multiSelect: this._options.multiSelect,
3885
4101
  _node: node,
3886
4102
  _tabIndex: tabIndex,
3887
4103
  _treeElement: treeElement
@@ -3911,9 +4127,8 @@ var TreeElement = (function () {
3911
4127
  }
3912
4128
  return requestUrl;
3913
4129
  }
3914
- _deselectCurrentNode() {
3915
- let node = this.getSelectedNode();
3916
- if (node) {
4130
+ _deselectAllNodes() {
4131
+ for (let node of this.getSelectedNodes()) {
3917
4132
  this.removeFromSelection(node);
3918
4133
  }
3919
4134
  }
@@ -3926,6 +4141,20 @@ var TreeElement = (function () {
3926
4141
  }
3927
4142
  }
3928
4143
 
4144
+ // The treeitem of the first node, or null when the tree is empty
4145
+ // Move the focus to a node, without selecting it
4146
+ _focusNode(node) {
4147
+ this._activeNode = node;
4148
+ this._updateTabStop();
4149
+ node.element?.focus();
4150
+ }
4151
+
4152
+ // With multiSelect: the active node, if it is still rendered in the tree
4153
+ _getActiveNode() {
4154
+ let element = this._activeNode?.element;
4155
+ return element && this._htmlElement.contains(element) ? this._activeNode : null;
4156
+ }
4157
+
3929
4158
  // Get the maximum level for auto open
3930
4159
  _getAutoOpenMaxLevel() {
3931
4160
  if (this._options.autoOpen === true) {
@@ -3941,25 +4170,16 @@ var TreeElement = (function () {
3941
4170
  /* istanbul ignore next @preserve */
3942
4171
  return 0;
3943
4172
  }
3944
-
3945
- // The title span of the first node, or null when the tree is empty
3946
- _getFirstTitleSpan() {
3947
- let firstElement = this._tree.children[0]?.element;
3948
- return firstElement ? firstElement.querySelector(`:scope > .${this._classNames.element} > span.${this._classNames.title}`) : null;
4173
+ _getFirstTreeItem() {
4174
+ return this._tree.children[0]?.element ?? null;
3949
4175
  }
3950
4176
 
3951
4177
  // The node of the HTML element that has the focus, if it is in this tree
3952
4178
  _getFocusedNode() {
3953
4179
  let activeElement = document.activeElement;
3954
4180
 
3955
- /* istanbul ignore if */
3956
- if (!activeElement) {
3957
- return null;
3958
- }
3959
-
3960
- // The keyboard must still work for input elements.
3961
- let tagName = activeElement.tagName;
3962
- if (tagName !== "A" && tagName !== "SPAN") {
4181
+ // An active element must exist and the keyboard must still work for input elements.
4182
+ if (!activeElement || !["A", "LI", "SPAN"].includes(activeElement.tagName)) {
3963
4183
  return null;
3964
4184
  }
3965
4185
  let node = this.getNode(activeElement);
@@ -3975,6 +4195,16 @@ var TreeElement = (function () {
3975
4195
  _getNodeIdToBeSelected() {
3976
4196
  return this._saveStateHandler._getNodeIdToBeSelected();
3977
4197
  }
4198
+
4199
+ // With multiSelect, the focused node becomes the tab stop
4200
+ _handleFocusIn = () => {
4201
+ let node = this._getFocusedNode();
4202
+ if (!node || node === this._activeNode) {
4203
+ return;
4204
+ }
4205
+ this._activeNode = node;
4206
+ this._updateTabStop();
4207
+ };
3978
4208
  _hasRenderedSelectedNode() {
3979
4209
  return this.getSelectedNodes().some(node => Boolean(node.element));
3980
4210
  }
@@ -3998,11 +4228,17 @@ var TreeElement = (function () {
3998
4228
  this._isInitialized = true;
3999
4229
  this._triggerEvent("tree.init");
4000
4230
  };
4231
+
4232
+ // Before the tree is replaced: then the focused node is no longer in it
4233
+ let focusedNode = this._getFocusedNode();
4001
4234
  this._tree = new this._options.nodeClass(null, true, this._options.nodeClass);
4002
4235
  this._selectNodeHandler._clear();
4003
4236
  this._tree.loadFromData(data);
4004
4237
  let mustLoadOnDemand = this._setInitialState();
4005
4238
  this._refreshElements(null);
4239
+ if (focusedNode) {
4240
+ this._restoreFocus(focusedNode);
4241
+ }
4006
4242
  if (mustLoadOnDemand) {
4007
4243
  // Load data on demand and then init the tree
4008
4244
  void this._setInitialStateOnDemand().then(doInit);
@@ -4010,15 +4246,6 @@ var TreeElement = (function () {
4010
4246
  doInit();
4011
4247
  }
4012
4248
  }
4013
-
4014
- // Does an HTML element of the tree have the focus?
4015
- _isFocusOnTree() {
4016
- return this._getFocusedNode() !== null;
4017
- }
4018
- _isSelectedNodeInSubtree(subtree) {
4019
- let selectedNode = this.getSelectedNode();
4020
- return selectedNode ? subtree === selectedNode || subtree.isParentOf(selectedNode) : false;
4021
- }
4022
4249
  async _loadFolderOnDemand(node, slide) {
4023
4250
  node.is_loading = true;
4024
4251
  await this.loadDataFromUrl(undefined, node);
@@ -4051,7 +4278,6 @@ var TreeElement = (function () {
4051
4278
  }
4052
4279
  _mouseStart(positionInfo) {
4053
4280
  /* istanbul ignore if */
4054
- // eslint-disable-next-line unicorn/prefer-ternary
4055
4281
  if (!this._options.dragAndDrop) {
4056
4282
  return false;
4057
4283
  }
@@ -4060,10 +4286,10 @@ var TreeElement = (function () {
4060
4286
  _mouseStop(positionInfo) {
4061
4287
  /* istanbul ignore if */
4062
4288
  if (!this._options.dragAndDrop) {
4063
- return false;
4289
+ return;
4064
4290
  }
4065
4291
  this._scrollHandler._stopScrolling();
4066
- return this._dndHandler._mouseStop(positionInfo);
4292
+ this._dndHandler._mouseStop(positionInfo);
4067
4293
  }
4068
4294
  _openParents(node) {
4069
4295
  let parent = node.parent;
@@ -4077,25 +4303,32 @@ var TreeElement = (function () {
4077
4303
  from_node: redraw this subtree
4078
4304
  */
4079
4305
  _refreshElements(fromNode) {
4080
- let mustSetFocus = this._isFocusOnTree();
4081
- let mustSelect = fromNode ? this._isSelectedNodeInSubtree(fromNode) : false;
4306
+ let focusedNode = this._getFocusedNode();
4307
+
4308
+ // The renderer marks the selected nodes, so only the focus needs restoring
4082
4309
  this._renderer._render(fromNode);
4083
- if (mustSelect) {
4084
- this._selectCurrentNode(mustSetFocus);
4310
+ if (focusedNode) {
4311
+ this._restoreFocus(focusedNode);
4085
4312
  }
4086
4313
  this._updateTabStop();
4087
4314
  this._triggerEvent("tree.refresh");
4088
4315
  }
4089
- _saveState() {
4090
- this._saveStateHandler._saveState();
4091
- }
4092
- _selectCurrentNode(mustSetFocus) {
4093
- let node = this.getSelectedNode();
4094
- if (!node) {
4316
+
4317
+ // Rendering replaces the elements of the nodes, so a focused element can be
4318
+ // gone. Then move the focus to the new element of the same node. After
4319
+ // loadData the nodes are new objects, so look the node up by its id.
4320
+ _restoreFocus(focusedNode) {
4321
+ if (this._htmlElement.contains(document.activeElement)) {
4095
4322
  return;
4096
4323
  }
4097
- let nodeElement = this._getNodeElementForNode(node);
4098
- nodeElement._select(mustSetFocus);
4324
+ let node = (focusedNode.id == null ? null : this.getNodeById(focusedNode.id)) ?? focusedNode;
4325
+ let element = node.element;
4326
+ if (element && this._htmlElement.contains(element)) {
4327
+ element.focus();
4328
+ }
4329
+ }
4330
+ _saveState() {
4331
+ this._saveStateHandler._saveState();
4099
4332
  }
4100
4333
 
4101
4334
  // Set initial state, either by restoring the state or auto-opening nodes
@@ -4150,6 +4383,25 @@ var TreeElement = (function () {
4150
4383
  }
4151
4384
  }
4152
4385
 
4386
+ /* Change multiSelect after construction. The attributes of the nodes
4387
+ * depend on it, so the tree is rendered again. The selection is kept: a
4388
+ * tree without multiSelect can also have several selected nodes, see
4389
+ * addToSelection.
4390
+ */
4391
+ _setMultiSelect(multiSelect) {
4392
+ let focusedNode = this._getFocusedNode();
4393
+ this._keyHandler._setMultiSelect(multiSelect);
4394
+ this._renderer._setMultiSelect(multiSelect);
4395
+ if (multiSelect) {
4396
+ this._htmlElement.addEventListener("focusin", this._handleFocusIn);
4397
+ } else {
4398
+ this._htmlElement.removeEventListener("focusin", this._handleFocusIn);
4399
+ }
4400
+ this._activeNode = multiSelect ? focusedNode : null;
4401
+ this._fallbackTabStop = null;
4402
+ this._refreshElements(null);
4403
+ }
4404
+
4153
4405
  // Set this HTML element to this node in the node map.
4154
4406
  _setNodeElement(element, node) {
4155
4407
  this._nodeMap.set(element, node);
@@ -4161,12 +4413,22 @@ var TreeElement = (function () {
4161
4413
  /* Keyboard users reach the tree through the tab stop. The selected node is
4162
4414
  * the tab stop, see NodeElement.select. Without a selected node, the first
4163
4415
  * node is the tab stop, so the tree can always be reached.
4416
+ * With multiSelect the tab stop is the active node: the node that was
4417
+ * focused last. Without one, it is the first node.
4164
4418
  */
4165
4419
  _updateTabStop() {
4166
4420
  let tabIndex = this._options.tabIndex;
4421
+ let multiSelect = this._options.multiSelect;
4167
4422
  let previousTabStop = this._fallbackTabStop;
4168
- let tabStop = tabIndex == null || this._hasRenderedSelectedNode() ? null : this._getFirstTitleSpan();
4169
- if (previousTabStop && previousTabStop !== tabStop && previousTabStop.getAttribute("aria-selected") !== "true") {
4423
+ let tabStop;
4424
+ if (tabIndex == null) {
4425
+ tabStop = null;
4426
+ } else if (multiSelect) {
4427
+ tabStop = this._getActiveNode()?.element ?? this._getFirstTreeItem();
4428
+ } else {
4429
+ tabStop = this._hasRenderedSelectedNode() ? null : this._getFirstTreeItem();
4430
+ }
4431
+ if (previousTabStop && previousTabStop !== tabStop && (multiSelect || previousTabStop.getAttribute("aria-selected") !== "true")) {
4170
4432
  previousTabStop.setAttribute("tabindex", "-1");
4171
4433
  }
4172
4434
  if (tabStop && tabIndex != null) {