tree-element 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +1 -0
  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 +6 -6
  7. package/lib/dataLoader.js.map +1 -1
  8. package/lib/elementsRenderer.d.ts +4 -2
  9. package/lib/elementsRenderer.d.ts.map +1 -1
  10. package/lib/elementsRenderer.js +35 -21
  11. package/lib/elementsRenderer.js.map +1 -1
  12. package/lib/index.d.ts +10 -4
  13. package/lib/index.d.ts.map +1 -1
  14. package/lib/index.js +75 -18
  15. package/lib/index.js.map +1 -1
  16. package/lib/keyHandler.d.ts +14 -3
  17. package/lib/keyHandler.d.ts.map +1 -1
  18. package/lib/keyHandler.js +61 -32
  19. package/lib/keyHandler.js.map +1 -1
  20. package/lib/methodTypes.d.ts +3 -0
  21. package/lib/methodTypes.d.ts.map +1 -1
  22. package/lib/nodeElement/folderElement.d.ts +1 -1
  23. package/lib/nodeElement/folderElement.d.ts.map +1 -1
  24. package/lib/nodeElement/folderElement.js +4 -4
  25. package/lib/nodeElement/folderElement.js.map +1 -1
  26. package/lib/nodeElement/index.d.ts +3 -2
  27. package/lib/nodeElement/index.d.ts.map +1 -1
  28. package/lib/nodeElement/index.js +15 -12
  29. package/lib/nodeElement/index.js.map +1 -1
  30. package/lib/options.d.ts +13 -0
  31. package/lib/options.d.ts.map +1 -1
  32. package/lib/selectNodeHandler.d.ts +3 -0
  33. package/lib/selectNodeHandler.d.ts.map +1 -1
  34. package/lib/selectNodeHandler.js +54 -22
  35. package/lib/selectNodeHandler.js.map +1 -1
  36. package/lib/setDefaultOptions.d.ts.map +1 -1
  37. package/lib/setDefaultOptions.js +1 -0
  38. package/lib/setDefaultOptions.js.map +1 -1
  39. package/lib/version.d.ts +1 -1
  40. package/lib/version.js +1 -1
  41. package/lib/version.js.map +1 -1
  42. package/package.json +19 -19
  43. package/src/classNames.ts +4 -4
  44. package/src/dataLoader.ts +6 -9
  45. package/src/elementsRenderer.ts +42 -24
  46. package/src/index.ts +83 -24
  47. package/src/keyHandler.ts +84 -34
  48. package/src/methodTypes.ts +6 -0
  49. package/src/nodeElement/folderElement.ts +4 -4
  50. package/src/nodeElement/index.ts +20 -17
  51. package/src/options.ts +13 -0
  52. package/src/selectNodeHandler.ts +58 -25
  53. package/src/setDefaultOptions.ts +1 -0
  54. package/src/version.ts +1 -1
  55. package/tree_element.css +16 -0
  56. package/tree_element.debug.js +256 -120
  57. package/tree_element.debug.js.map +1 -1
  58. package/tree_element.js +2 -2
  59. package/tree_element.js.map +1 -1
@@ -1,5 +1,5 @@
1
1
  /*
2
- Tree-element 0.3.2
2
+ Tree-element 0.4.0
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
@@ -124,9 +127,6 @@ var TreeElement = (function () {
124
127
  });
125
128
  });
126
129
  }
127
- _getTitleSpan(element) {
128
- return element.querySelector(`:scope > .${this._classNames.element} > .${this._classNames.title}`);
129
- }
130
130
  }
131
131
 
132
132
  // Get the top position of the HTML element.
@@ -672,6 +672,7 @@ var TreeElement = (function () {
672
672
  _getTree: getTree,
673
673
  _htmlElement: htmlElement,
674
674
  _isNodeSelected: isNodeSelected,
675
+ _multiSelect: multiSelect,
675
676
  _onCreateLi: onCreateLi,
676
677
  _openedIcon: openedIcon = "+",
677
678
  _rtl: rtl,
@@ -688,6 +689,7 @@ var TreeElement = (function () {
688
689
  this._getTree = getTree;
689
690
  this._htmlElement = htmlElement;
690
691
  this._isNodeSelected = isNodeSelected;
692
+ this._multiSelect = multiSelect;
691
693
  this._onCreateLi = onCreateLi;
692
694
  this._rtl = rtl;
693
695
  this._setNodeElement = setNodeElement;
@@ -713,7 +715,7 @@ var TreeElement = (function () {
713
715
  * null when the folder is not rendered or has no children.
714
716
  */
715
717
  _renderChildren(node) {
716
- return !node.element || !node.hasChildren() ? null : this._createDomElements(node.element, node.children, false, node.getLevel() + 1);
718
+ return node.element && node.hasChildren() ? this._createDomElements(node.element, node.children, false, node.getLevel() + 1) : null;
717
719
  }
718
720
  _renderFromNode(node) {
719
721
  if (!node.element) {
@@ -774,10 +776,10 @@ var TreeElement = (function () {
774
776
  // li > div > [toggler], title span, [toggler]
775
777
  let div = li.firstChild;
776
778
  let titleSpan = div.childNodes[this._buttonLeft ? 1 : 0];
777
- this._fillTitleSpan(titleSpan, node.name, isSelected, level);
779
+ this._fillNode(li, titleSpan, node.name, isSelected, level);
778
780
  if (node.is_loading) {
779
781
  li.classList.add(this._classNames.loading);
780
- titleSpan.setAttribute("aria-busy", "true");
782
+ li.setAttribute("aria-busy", "true");
781
783
  }
782
784
  return li;
783
785
  }
@@ -807,11 +809,10 @@ var TreeElement = (function () {
807
809
  if (this._buttonLeft) {
808
810
  div.append(toggler);
809
811
  }
812
+ li.setAttribute("aria-expanded", getBoolString(isOpen));
810
813
 
811
814
  // title span
812
- let titleSpan = this._createTitleSpanTemplate(true);
813
- titleSpan.setAttribute("aria-expanded", getBoolString(isOpen));
814
- div.append(titleSpan);
815
+ div.append(this._createTitleSpanTemplate(true));
815
816
  if (!this._buttonLeft) {
816
817
  div.append(toggler);
817
818
  }
@@ -835,11 +836,22 @@ var TreeElement = (function () {
835
836
 
836
837
  /*
837
838
  Create the outer part of a <li> template: li > div
839
+ The li is the treeitem, so the ul of its children is inside the treeitem.
838
840
  */
839
841
  _createLiTemplate(liClasses) {
840
842
  let li = document.createElement("li");
841
843
  li.className = liClasses;
842
- li.setAttribute("role", "none");
844
+ li.setAttribute("role", "treeitem");
845
+
846
+ // Only a multiselect tree marks the nodes that are not selected
847
+ if (this._multiSelect) {
848
+ li.setAttribute("aria-selected", "false");
849
+ }
850
+
851
+ // Every node is focusable, but only the tab stop is in the tab order
852
+ if (this._tabIndex !== undefined) {
853
+ li.setAttribute("tabindex", "-1");
854
+ }
843
855
  let div = document.createElement("div");
844
856
  div.className = `${this._classNames.element} ${this._classNames.common}`;
845
857
  div.setAttribute("role", "none");
@@ -855,7 +867,7 @@ var TreeElement = (function () {
855
867
  // li > div > title span
856
868
  let div = li.firstChild;
857
869
  let titleSpan = div.firstChild;
858
- this._fillTitleSpan(titleSpan, node.name, isSelected, level);
870
+ this._fillNode(li, titleSpan, node.name, isSelected, level);
859
871
  return li;
860
872
  }
861
873
 
@@ -876,13 +888,6 @@ var TreeElement = (function () {
876
888
  }
877
889
  classes += ` ${this._buttonLeft ? this._classNames.titleButtonLeft : this._classNames.titleButtonRight}`;
878
890
  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
891
  return titleSpan;
887
892
  }
888
893
  _createUlTemplate(isRootNode) {
@@ -911,20 +916,25 @@ var TreeElement = (function () {
911
916
  if (this._ariaLabelledBy !== undefined) {
912
917
  ul.setAttribute("aria-labelledby", this._ariaLabelledBy);
913
918
  }
919
+ if (this._multiSelect) {
920
+ ul.setAttribute("aria-multiselectable", "true");
921
+ }
914
922
  }
915
923
  return ul;
916
924
  }
917
925
 
918
926
  /*
919
- Set the parts of a cloned title span that differ per node
927
+ Set the parts of a cloned li and title span that differ per node
920
928
  */
921
- _fillTitleSpan(titleSpan, nodeName, isSelected, level) {
922
- titleSpan.setAttribute("aria-level", String(level));
929
+ _fillNode(li, titleSpan, nodeName, isSelected, level) {
930
+ li.setAttribute("aria-level", String(level));
923
931
  if (isSelected) {
924
- titleSpan.setAttribute("aria-selected", "true");
932
+ li.setAttribute("aria-selected", "true");
925
933
  let tabIndex = this._tabIndex;
926
- if (tabIndex !== undefined) {
927
- titleSpan.setAttribute("tabindex", String(tabIndex));
934
+
935
+ // With multiSelect the tab stop is the active node, see updateTabStop
936
+ if (tabIndex !== undefined && !this._multiSelect) {
937
+ li.setAttribute("tabindex", String(tabIndex));
928
938
  }
929
939
  }
930
940
  if (this._autoEscape) {
@@ -933,6 +943,10 @@ var TreeElement = (function () {
933
943
  // eslint-disable-next-line no-unsanitized/property -- the user opted out of escaping with autoEscape: false
934
944
  titleSpan.innerHTML = nodeName;
935
945
  }
946
+
947
+ // The treeitem is named by its title, without the html when autoEscape
948
+ // is false. Otherwise the name would contain everything in the row.
949
+ li.setAttribute("aria-label", titleSpan.textContent);
936
950
  }
937
951
  _getButtonClasses(isOpen) {
938
952
  let classes = [this._classNames.toggler, this._classNames.common];
@@ -955,24 +969,37 @@ var TreeElement = (function () {
955
969
  }
956
970
  }
957
971
 
972
+ /* The keys move from the selected node and select the node they move to.
973
+ * With multiSelect they move from the focused node and only move the focus;
974
+ * Space adds the focused node to the selection or removes it.
975
+ */
976
+
958
977
  class KeyHandler {
959
978
  constructor({
960
979
  _closeNode: closeNode,
980
+ _focusNode: focusNode,
981
+ _getActiveNode: getActiveNode,
961
982
  _getFocusedNode: getFocusedNode,
962
983
  _getSelectedNode: getSelectedNode,
963
984
  _getTree: getTree,
964
985
  _keyboardSupport: keyboardSupport,
986
+ _multiSelect: multiSelect,
965
987
  _openNode: openNode,
966
988
  _selectNode: selectNode,
989
+ _toggleNodeInSelection: toggleNodeInSelection,
967
990
  _triggerEvent: triggerEvent
968
991
  }) {
969
992
  this._closeNode = closeNode;
993
+ this._focusNode = focusNode;
994
+ this._getActiveNode = getActiveNode;
970
995
  this._getFocusedNode = getFocusedNode;
971
996
  this._getSelectedNode = getSelectedNode;
972
997
  this._getTree = getTree;
973
998
  this._keyboardSupport = keyboardSupport;
999
+ this._multiSelect = multiSelect;
974
1000
  this._openNode = openNode;
975
1001
  this._originalSelectNode = selectNode;
1002
+ this._toggleNodeInSelection = toggleNodeInSelection;
976
1003
  this._triggerEvent = triggerEvent;
977
1004
  if (keyboardSupport) {
978
1005
  document.addEventListener("keydown", this._handleKeyDown);
@@ -984,10 +1011,10 @@ var TreeElement = (function () {
984
1011
  }
985
1012
  }
986
1013
  _moveDown() {
987
- return this._selectNode(this._getSelectedNode()?.getNextVisibleNode());
1014
+ return this._goToNode(this._getCurrentNode()?.getNextVisibleNode());
988
1015
  }
989
1016
  _moveUp() {
990
- return this._selectNode(this._getSelectedNode()?.getPreviousVisibleNode());
1017
+ return this._goToNode(this._getCurrentNode()?.getPreviousVisibleNode());
991
1018
  }
992
1019
 
993
1020
  // Activate the node, for example to open its edit page.
@@ -998,6 +1025,26 @@ var TreeElement = (function () {
998
1025
  });
999
1026
  return true;
1000
1027
  }
1028
+
1029
+ // No node is selected. These keys select the focused node, which is
1030
+ // usually the first node, see TreeElement.updateTabStop.
1031
+ _getCurrentNode() {
1032
+ return this._multiSelect ? this._getActiveNode() : this._getSelectedNode();
1033
+ }
1034
+
1035
+ // Move to the node: select it, or with multiSelect focus it.
1036
+ // Result: there was a node to move to.
1037
+ _goToNode(node) {
1038
+ if (!node) {
1039
+ return false;
1040
+ }
1041
+ if (this._multiSelect) {
1042
+ this._focusNode(node);
1043
+ } else {
1044
+ this._originalSelectNode(node);
1045
+ }
1046
+ return true;
1047
+ }
1001
1048
  _handleArrowKey(key) {
1002
1049
  switch (key) {
1003
1050
  case "ArrowDown":
@@ -1022,49 +1069,53 @@ var TreeElement = (function () {
1022
1069
  }
1023
1070
  }
1024
1071
  }
1072
+ _handleKey(key, focusedNode) {
1073
+ return this._getCurrentNode() ? this._handleArrowKey(key) : this._handleKeyWithoutSelection(key, focusedNode);
1074
+ }
1025
1075
  _handleKeyDown = e => {
1026
1076
  let focusedNode = this._getFocusedNode();
1027
1077
  if (!focusedNode) {
1028
1078
  return;
1029
1079
  }
1030
1080
  let isKeyHandled;
1031
- let selectedNode = this._getSelectedNode();
1032
1081
  switch (e.key) {
1082
+ case " ":
1083
+ {
1084
+ isKeyHandled = this._multiSelect ? this._toggle(focusedNode) : this._handleKey(e.key, focusedNode);
1085
+ break;
1086
+ }
1033
1087
  case "End":
1034
1088
  {
1035
- isKeyHandled = this._selectNode(this._getTree()?.getLastChild());
1089
+ isKeyHandled = this._goToNode(this._getTree()?.getLastChild());
1036
1090
  break;
1037
1091
  }
1038
1092
  case "Enter":
1039
1093
  {
1040
1094
  // Without a selected node, activate the focused node
1041
- isKeyHandled = this._activate(selectedNode ?? focusedNode, e);
1095
+ isKeyHandled = this._activate(this._multiSelect ? focusedNode : this._getSelectedNode() ?? focusedNode, e);
1042
1096
  break;
1043
1097
  }
1044
1098
  case "Home":
1045
1099
  {
1046
- isKeyHandled = this._selectNode(this._getTree()?.children[0]);
1100
+ isKeyHandled = this._goToNode(this._getTree()?.children[0]);
1047
1101
  break;
1048
1102
  }
1049
1103
  default:
1050
1104
  {
1051
- isKeyHandled = selectedNode ? this._handleArrowKey(e.key) : this._handleKeyWithoutSelection(e.key, focusedNode);
1105
+ isKeyHandled = this._handleKey(e.key, focusedNode);
1052
1106
  }
1053
1107
  }
1054
1108
  if (isKeyHandled) {
1055
1109
  e.preventDefault();
1056
1110
  }
1057
1111
  };
1058
-
1059
- // No node is selected. These keys select the focused node, which is
1060
- // usually the first node, see TreeElement.updateTabStop.
1061
1112
  _handleKeyWithoutSelection(key, focusedNode) {
1062
1113
  switch (key) {
1063
1114
  case " ":
1064
1115
  case "ArrowDown":
1065
1116
  case "ArrowRight":
1066
1117
  {
1067
- return this._selectNode(focusedNode);
1118
+ return this._goToNode(focusedNode);
1068
1119
  }
1069
1120
  default:
1070
1121
  {
@@ -1073,45 +1124,37 @@ var TreeElement = (function () {
1073
1124
  }
1074
1125
  }
1075
1126
  _moveLeft() {
1076
- let selectedNode = this._getSelectedNode();
1127
+ let currentNode = this._getCurrentNode();
1077
1128
 
1078
1129
  /* istanbul ignore if */
1079
- if (!selectedNode) {
1130
+ if (!currentNode) {
1080
1131
  return false;
1081
1132
  }
1082
- if (selectedNode.isFolder() && selectedNode.is_open) {
1133
+ if (currentNode.isFolder() && currentNode.is_open) {
1083
1134
  // Left on an open node closes the node
1084
- void this._closeNode(selectedNode);
1135
+ void this._closeNode(currentNode);
1085
1136
  return true;
1086
1137
  }
1087
1138
  // Left on a closed or end node moves focus to the node's parent
1088
- return this._selectNode(selectedNode.getParent());
1139
+ return this._goToNode(currentNode.getParent());
1089
1140
  }
1090
1141
  _moveRight() {
1091
- let selectedNode = this._getSelectedNode();
1092
- if (selectedNode?.isFolder()) {
1142
+ let currentNode = this._getCurrentNode();
1143
+ if (currentNode?.isFolder()) {
1093
1144
  // folder node
1094
- if (selectedNode.is_open) {
1145
+ if (currentNode.is_open) {
1095
1146
  // Right moves to the first child of an open node
1096
- return this._selectNode(selectedNode.getNextVisibleNode());
1147
+ return this._goToNode(currentNode.getNextVisibleNode());
1097
1148
  }
1098
1149
  // Right expands a closed node
1099
- void this._openNode(selectedNode);
1150
+ void this._openNode(currentNode);
1100
1151
  return true;
1101
1152
  }
1102
1153
  return false;
1103
1154
  }
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;
1155
+ _toggle(node) {
1156
+ this._toggleNodeInSelection(node);
1157
+ return true;
1115
1158
  }
1116
1159
  }
1117
1160
 
@@ -2217,12 +2260,14 @@ var TreeElement = (function () {
2217
2260
  constructor({
2218
2261
  _classNames: classNames,
2219
2262
  _getScrollLeft: getScrollLeft,
2263
+ _multiSelect: multiSelect = false,
2220
2264
  _node: node,
2221
2265
  _tabIndex: tabIndex,
2222
2266
  _treeElement: treeElement
2223
2267
  }) {
2224
2268
  this._classNames = classNames;
2225
2269
  this._getScrollLeft = getScrollLeft;
2270
+ this._multiSelect = multiSelect;
2226
2271
  this._node = node;
2227
2272
  this._tabIndex = tabIndex;
2228
2273
  this._treeElement = treeElement;
@@ -2238,35 +2283,36 @@ var TreeElement = (function () {
2238
2283
  return;
2239
2284
  }
2240
2285
  this._element.classList.remove(this._classNames.selected);
2241
- let titleSpan = this._getTitleSpan();
2286
+
2287
+ // Only a multiselect tree marks the nodes that are not selected
2288
+ if (this._multiSelect) {
2289
+ this._element.setAttribute("aria-selected", "false");
2290
+ return;
2291
+ }
2292
+ this._element.removeAttribute("aria-selected");
2242
2293
 
2243
2294
  // Check for null or undefined
2244
2295
  if (this._tabIndex != null) {
2245
- titleSpan.setAttribute("tabindex", "-1");
2296
+ this._element.setAttribute("tabindex", "-1");
2246
2297
  }
2247
- titleSpan.setAttribute("aria-selected", "false");
2248
- titleSpan.blur();
2298
+ this._element.blur();
2249
2299
  }
2250
2300
  _select(mustSetFocus) {
2251
2301
  if (!this._isRendered()) {
2252
2302
  return;
2253
2303
  }
2254
2304
  this._element.classList.add(this._classNames.selected);
2255
- let titleSpan = this._getTitleSpan();
2305
+ this._element.setAttribute("aria-selected", "true");
2256
2306
  let tabIndex = this._tabIndex;
2257
2307
 
2258
2308
  // Check for null or undefined
2259
- if (tabIndex != null) {
2260
- titleSpan.setAttribute("tabindex", tabIndex.toString());
2309
+ if (tabIndex != null && !this._multiSelect) {
2310
+ this._element.setAttribute("tabindex", tabIndex.toString());
2261
2311
  }
2262
- titleSpan.setAttribute("aria-selected", "true");
2263
2312
  if (mustSetFocus) {
2264
- titleSpan.focus();
2313
+ this._element.focus();
2265
2314
  }
2266
2315
  }
2267
- _getTitleSpan() {
2268
- return this._element.querySelector(`:scope > .${this._classNames.element} > span.${this._classNames.title}`);
2269
- }
2270
2316
  _getUl() {
2271
2317
  return this._element.querySelector(":scope > ul");
2272
2318
  }
@@ -2328,6 +2374,7 @@ var TreeElement = (function () {
2328
2374
  _classNames: classNames,
2329
2375
  _closedIconElement: closedIconElement,
2330
2376
  _getScrollLeft: getScrollLeft,
2377
+ _multiSelect: multiSelect,
2331
2378
  _node: node,
2332
2379
  _openedIconElement: openedIconElement,
2333
2380
  _renderChildren: renderChildren,
@@ -2338,6 +2385,7 @@ var TreeElement = (function () {
2338
2385
  super({
2339
2386
  _classNames: classNames,
2340
2387
  _getScrollLeft: getScrollLeft,
2388
+ _multiSelect: multiSelect,
2341
2389
  _node: node,
2342
2390
  _tabIndex: tabIndex,
2343
2391
  _treeElement: treeElement
@@ -2375,8 +2423,7 @@ var TreeElement = (function () {
2375
2423
  }
2376
2424
  }
2377
2425
  this._element.classList.add(this._classNames.closed);
2378
- let titleSpan = this._getTitleSpan();
2379
- titleSpan.setAttribute("aria-expanded", "false");
2426
+ this._element.setAttribute("aria-expanded", "false");
2380
2427
  this._triggerEvent("tree.close", {
2381
2428
  node: this._node
2382
2429
  });
@@ -2412,8 +2459,7 @@ var TreeElement = (function () {
2412
2459
  }
2413
2460
  }
2414
2461
  this._element.classList.remove(this._classNames.closed);
2415
- let titleSpan = this._getTitleSpan();
2416
- titleSpan.setAttribute("aria-expanded", "true");
2462
+ this._element.setAttribute("aria-expanded", "true");
2417
2463
  this._triggerEvent("tree.open", {
2418
2464
  node: this._node
2419
2465
  });
@@ -2921,9 +2967,9 @@ var TreeElement = (function () {
2921
2967
  } else {
2922
2968
  this._selectedNodes.delete(node.id);
2923
2969
  if (includeChildren) {
2924
- node.iterate(() => {
2925
- if (node.id != null) {
2926
- this._selectedNodes.delete(node.id);
2970
+ node.iterate(child => {
2971
+ if (child.id != null) {
2972
+ this._selectedNodes.delete(child.id);
2927
2973
  }
2928
2974
  return true;
2929
2975
  });
@@ -2942,6 +2988,9 @@ var TreeElement = (function () {
2942
2988
  * mustToggle: support deselecting the selected node
2943
2989
  */
2944
2990
  _selectSingleNode(node, optionsParam) {
2991
+ if (!this._canSelect(node)) {
2992
+ return;
2993
+ }
2945
2994
  let defaultOptions = {
2946
2995
  mustSetFocus: true,
2947
2996
  mustToggle: true
@@ -2950,31 +2999,23 @@ var TreeElement = (function () {
2950
2999
  ...defaultOptions,
2951
3000
  ...optionsParam
2952
3001
  };
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
- };
3002
+ let otherSelectedNodes = this._getSelectedNodes().filter(selectedNode => selectedNode !== node);
2967
3003
  if (this._isNodeSelected(node)) {
2968
- if (selectOptions.mustToggle) {
2969
- deselectCurrentNode(node);
3004
+ if (otherSelectedNodes.length) {
3005
+ // Part of a multiple selection: keep only this node selected
3006
+ for (let otherNode of otherSelectedNodes) {
3007
+ this._deselectNode(otherNode);
3008
+ }
3009
+ } else if (selectOptions.mustToggle) {
3010
+ this._deselectNode(node);
2970
3011
  this._triggerEvent("tree.deselect", {
2971
3012
  node
2972
3013
  });
2973
3014
  }
2974
3015
  } else {
2975
- let deselectedNode = this._getSelectedNode();
2976
- if (deselectedNode) {
2977
- deselectCurrentNode(deselectedNode);
3016
+ let deselectedNode = otherSelectedNodes[0] ?? null;
3017
+ for (let otherNode of otherSelectedNodes) {
3018
+ this._deselectNode(otherNode);
2978
3019
  }
2979
3020
  this._addToSelection(node);
2980
3021
  this._openParents(node);
@@ -2987,6 +3028,43 @@ var TreeElement = (function () {
2987
3028
  this._updateTabStop();
2988
3029
  this._saveState();
2989
3030
  }
3031
+
3032
+ /* Add a node to the selection, or remove it when it is selected, without
3033
+ * changing the other selected nodes.
3034
+ * Checks if the node is selectable and saves the state.
3035
+ */
3036
+ _toggleNodeInSelection(node, mustSetFocus = true) {
3037
+ if (!this._canSelect(node)) {
3038
+ return;
3039
+ }
3040
+ if (this._isNodeSelected(node)) {
3041
+ this._deselectNode(node);
3042
+ this._triggerEvent("tree.deselect", {
3043
+ node
3044
+ });
3045
+ } else {
3046
+ this._addToSelection(node);
3047
+ this._openParents(node);
3048
+ this._getNodeElementForNode(node)._select(mustSetFocus);
3049
+ this._triggerEvent("tree.select", {
3050
+ deselectedNode: null,
3051
+ node
3052
+ });
3053
+ }
3054
+ this._updateTabStop();
3055
+ this._saveState();
3056
+ }
3057
+ _canSelect(node) {
3058
+ if (!this._getSelectable()) {
3059
+ return false;
3060
+ }
3061
+ let onCanSelectNode = this._getOnCanSelectNode();
3062
+ return onCanSelectNode ? onCanSelectNode(node) : true;
3063
+ }
3064
+ _deselectNode(node) {
3065
+ this._removeFromSelection(node);
3066
+ this._getNodeElementForNode(node)._deselect();
3067
+ }
2990
3068
  }
2991
3069
 
2992
3070
  /* The default icons are svg instead of text: the triangle characters come
@@ -3013,6 +3091,7 @@ var TreeElement = (function () {
3013
3091
  // dataUrl: undefined,
3014
3092
  dragAndDrop: false,
3015
3093
  keyboardSupport: true,
3094
+ multiSelect: false,
3016
3095
  nodeClass: Node,
3017
3096
  // onCanMove: undefined, // Can this node be moved?
3018
3097
  // onCanMoveTo: undefined, // Can this node be moved to this position? function(moved_node, target_node, position)
@@ -3078,7 +3157,7 @@ var TreeElement = (function () {
3078
3157
  };
3079
3158
 
3080
3159
  // Generated by config/generateVersion.mjs. Do not edit.
3081
- let version = "0.3.2";
3160
+ let version = "0.4.0";
3082
3161
 
3083
3162
  // The types that appear in the public api. Consumers cannot import them from
3084
3163
  // the submodules directly, because those are not exposed in package.json.
@@ -3086,6 +3165,8 @@ var TreeElement = (function () {
3086
3165
  // as its global, instead of an object of named exports.
3087
3166
 
3088
3167
  class TreeElement {
3168
+ // With multiSelect: the node that is the tab stop, see updateTabStop
3169
+ _activeNode;
3089
3170
  _classNames;
3090
3171
  _dataLoader;
3091
3172
  _dndHandler;
@@ -3119,6 +3200,7 @@ var TreeElement = (function () {
3119
3200
  this._tree = new Node({}, true);
3120
3201
  this._nodeMap = new WeakMap();
3121
3202
  this._fallbackTabStop = null;
3203
+ this._activeNode = null;
3122
3204
  let {
3123
3205
  ariaLabel,
3124
3206
  ariaLabelledBy,
@@ -3128,6 +3210,7 @@ var TreeElement = (function () {
3128
3210
  dataFilter,
3129
3211
  dragAndDrop,
3130
3212
  keyboardSupport,
3213
+ multiSelect,
3131
3214
  onCanMove,
3132
3215
  onCanMoveTo,
3133
3216
  onCreateLi,
@@ -3179,6 +3262,17 @@ var TreeElement = (function () {
3179
3262
  let isNodeSelected = selectNodeHandler._isNodeSelected.bind(selectNodeHandler);
3180
3263
  let removeFromSelection = selectNodeHandler._removeFromSelection.bind(selectNodeHandler);
3181
3264
  let selectNode = selectNodeHandler._selectSingleNode.bind(selectNodeHandler);
3265
+ let toggleNodeInSelection = selectNodeHandler._toggleNodeInSelection.bind(selectNodeHandler);
3266
+
3267
+ // With multiSelect, a click adds a node to the selection or removes it
3268
+ let onClickTitle = node => {
3269
+ if (this._options.multiSelect) {
3270
+ toggleNodeInSelection(node, false);
3271
+ this._focusNode(node);
3272
+ } else {
3273
+ selectNode(node);
3274
+ }
3275
+ };
3182
3276
  let getMouseDelay = () => this._options.startDndDelay ?? 0;
3183
3277
  let dataLoader = new DataLoader({
3184
3278
  _classNames: classNames,
@@ -3225,12 +3319,16 @@ var TreeElement = (function () {
3225
3319
  });
3226
3320
  let keyHandler = new KeyHandler({
3227
3321
  _closeNode: closeNode,
3322
+ _focusNode: this._focusNode.bind(this),
3323
+ _getActiveNode: this._getActiveNode.bind(this),
3228
3324
  _getFocusedNode: getFocusedNode,
3229
3325
  _getSelectedNode: getSelectedNode,
3230
3326
  _getTree: getTree,
3231
3327
  _keyboardSupport: keyboardSupport,
3328
+ _multiSelect: multiSelect,
3232
3329
  _openNode: openNode,
3233
3330
  _selectNode: selectNode,
3331
+ _toggleNodeInSelection: toggleNodeInSelection,
3234
3332
  _triggerEvent: triggerEvent
3235
3333
  });
3236
3334
  let renderer = new ElementsRenderer({
@@ -3244,6 +3342,7 @@ var TreeElement = (function () {
3244
3342
  _getTree: getTree,
3245
3343
  _htmlElement: treeElement,
3246
3344
  _isNodeSelected: isNodeSelected,
3345
+ _multiSelect: multiSelect,
3247
3346
  _onCreateLi: onCreateLi,
3248
3347
  _openedIcon: openedIcon,
3249
3348
  _rtl: rtl,
@@ -3262,7 +3361,7 @@ var TreeElement = (function () {
3262
3361
  _getMouseDelay: getMouseDelay,
3263
3362
  _getNode: getNode,
3264
3363
  _onClickButton: this.toggle.bind(this),
3265
- _onClickTitle: selectNode,
3364
+ _onClickTitle: onClickTitle,
3266
3365
  _onMouseCapture: onMouseCapture,
3267
3366
  _onMouseDrag: onMouseDrag,
3268
3367
  _onMouseStart: onMouseStart,
@@ -3278,6 +3377,9 @@ var TreeElement = (function () {
3278
3377
  this._saveStateHandler = saveStateHandler;
3279
3378
  this._scrollHandler = scrollHandler;
3280
3379
  this._selectNodeHandler = selectNodeHandler;
3380
+ if (multiSelect) {
3381
+ treeElement.addEventListener("focusin", this._handleFocusIn);
3382
+ }
3281
3383
  this._initData();
3282
3384
  }
3283
3385
 
@@ -3387,6 +3489,7 @@ var TreeElement = (function () {
3387
3489
  this._dataLoader._deinit();
3388
3490
  this._keyHandler._deinit();
3389
3491
  this._mouseHandler._deinit();
3492
+ this._htmlElement.removeEventListener("focusin", this._handleFocusIn);
3390
3493
  this._tree = new Node({}, true);
3391
3494
  }
3392
3495
 
@@ -3574,7 +3677,8 @@ var TreeElement = (function () {
3574
3677
  }
3575
3678
 
3576
3679
  /**
3577
- * Selects the next visible node, like the down arrow key.
3680
+ * Selects the next visible node, like the down arrow key. With
3681
+ * `multiSelect` it moves the focus instead.
3578
3682
  *
3579
3683
  * @group Selection
3580
3684
  */
@@ -3599,7 +3703,8 @@ var TreeElement = (function () {
3599
3703
  }
3600
3704
 
3601
3705
  /**
3602
- * Selects the previous visible node, like the up arrow key.
3706
+ * Selects the previous visible node, like the up arrow key. With
3707
+ * `multiSelect` it moves the focus instead.
3603
3708
  *
3604
3709
  * @group Selection
3605
3710
  */
@@ -3729,8 +3834,8 @@ var TreeElement = (function () {
3729
3834
  */
3730
3835
  selectNode(node, options) {
3731
3836
  if (!node) {
3732
- // Called with empty node -> deselect current node
3733
- this._deselectCurrentNode();
3837
+ // Called with empty node -> deselect all nodes
3838
+ this._deselectAllNodes();
3734
3839
  this._saveStateHandler._saveState();
3735
3840
  return;
3736
3841
  }
@@ -3866,6 +3971,7 @@ var TreeElement = (function () {
3866
3971
  _classNames: classNames,
3867
3972
  _closedIconElement: closedIconElement,
3868
3973
  _getScrollLeft: getScrollLeft,
3974
+ _multiSelect: this._options.multiSelect,
3869
3975
  _node: node,
3870
3976
  _openedIconElement: openedIconElement,
3871
3977
  _renderChildren: renderChildren,
@@ -3882,6 +3988,7 @@ var TreeElement = (function () {
3882
3988
  return new NodeElement({
3883
3989
  _classNames: classNames,
3884
3990
  _getScrollLeft: getScrollLeft,
3991
+ _multiSelect: this._options.multiSelect,
3885
3992
  _node: node,
3886
3993
  _tabIndex: tabIndex,
3887
3994
  _treeElement: treeElement
@@ -3911,9 +4018,8 @@ var TreeElement = (function () {
3911
4018
  }
3912
4019
  return requestUrl;
3913
4020
  }
3914
- _deselectCurrentNode() {
3915
- let node = this.getSelectedNode();
3916
- if (node) {
4021
+ _deselectAllNodes() {
4022
+ for (let node of this.getSelectedNodes()) {
3917
4023
  this.removeFromSelection(node);
3918
4024
  }
3919
4025
  }
@@ -3926,6 +4032,20 @@ var TreeElement = (function () {
3926
4032
  }
3927
4033
  }
3928
4034
 
4035
+ // The treeitem of the first node, or null when the tree is empty
4036
+ // Move the focus to a node, without selecting it
4037
+ _focusNode(node) {
4038
+ this._activeNode = node;
4039
+ this._updateTabStop();
4040
+ node.element?.focus();
4041
+ }
4042
+
4043
+ // With multiSelect: the active node, if it is still rendered in the tree
4044
+ _getActiveNode() {
4045
+ let element = this._activeNode?.element;
4046
+ return element && this._htmlElement.contains(element) ? this._activeNode : null;
4047
+ }
4048
+
3929
4049
  // Get the maximum level for auto open
3930
4050
  _getAutoOpenMaxLevel() {
3931
4051
  if (this._options.autoOpen === true) {
@@ -3941,11 +4061,8 @@ var TreeElement = (function () {
3941
4061
  /* istanbul ignore next @preserve */
3942
4062
  return 0;
3943
4063
  }
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;
4064
+ _getFirstTreeItem() {
4065
+ return this._tree.children[0]?.element ?? null;
3949
4066
  }
3950
4067
 
3951
4068
  // The node of the HTML element that has the focus, if it is in this tree
@@ -3959,7 +4076,7 @@ var TreeElement = (function () {
3959
4076
 
3960
4077
  // The keyboard must still work for input elements.
3961
4078
  let tagName = activeElement.tagName;
3962
- if (tagName !== "A" && tagName !== "SPAN") {
4079
+ if (tagName !== "A" && tagName !== "LI" && tagName !== "SPAN") {
3963
4080
  return null;
3964
4081
  }
3965
4082
  let node = this.getNode(activeElement);
@@ -3975,6 +4092,16 @@ var TreeElement = (function () {
3975
4092
  _getNodeIdToBeSelected() {
3976
4093
  return this._saveStateHandler._getNodeIdToBeSelected();
3977
4094
  }
4095
+
4096
+ // With multiSelect, the focused node becomes the tab stop
4097
+ _handleFocusIn = () => {
4098
+ let node = this._getFocusedNode();
4099
+ if (!node || node === this._activeNode) {
4100
+ return;
4101
+ }
4102
+ this._activeNode = node;
4103
+ this._updateTabStop();
4104
+ };
3978
4105
  _hasRenderedSelectedNode() {
3979
4106
  return this.getSelectedNodes().some(node => Boolean(node.element));
3980
4107
  }
@@ -4051,7 +4178,6 @@ var TreeElement = (function () {
4051
4178
  }
4052
4179
  _mouseStart(positionInfo) {
4053
4180
  /* istanbul ignore if */
4054
- // eslint-disable-next-line unicorn/prefer-ternary
4055
4181
  if (!this._options.dragAndDrop) {
4056
4182
  return false;
4057
4183
  }
@@ -4161,12 +4287,22 @@ var TreeElement = (function () {
4161
4287
  /* Keyboard users reach the tree through the tab stop. The selected node is
4162
4288
  * the tab stop, see NodeElement.select. Without a selected node, the first
4163
4289
  * node is the tab stop, so the tree can always be reached.
4290
+ * With multiSelect the tab stop is the active node: the node that was
4291
+ * focused last. Without one, it is the first node.
4164
4292
  */
4165
4293
  _updateTabStop() {
4166
4294
  let tabIndex = this._options.tabIndex;
4295
+ let multiSelect = this._options.multiSelect;
4167
4296
  let previousTabStop = this._fallbackTabStop;
4168
- let tabStop = tabIndex == null || this._hasRenderedSelectedNode() ? null : this._getFirstTitleSpan();
4169
- if (previousTabStop && previousTabStop !== tabStop && previousTabStop.getAttribute("aria-selected") !== "true") {
4297
+ let tabStop;
4298
+ if (tabIndex == null) {
4299
+ tabStop = null;
4300
+ } else if (multiSelect) {
4301
+ tabStop = this._getActiveNode()?.element ?? this._getFirstTreeItem();
4302
+ } else {
4303
+ tabStop = this._hasRenderedSelectedNode() ? null : this._getFirstTreeItem();
4304
+ }
4305
+ if (previousTabStop && previousTabStop !== tabStop && (multiSelect || previousTabStop.getAttribute("aria-selected") !== "true")) {
4170
4306
  previousTabStop.setAttribute("tabindex", "-1");
4171
4307
  }
4172
4308
  if (tabStop && tabIndex != null) {