tree-element 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +23 -20
  2. package/lib/classNames.js +4 -4
  3. package/lib/classNames.js.map +1 -1
  4. package/lib/dataLoader.d.ts +0 -1
  5. package/lib/dataLoader.d.ts.map +1 -1
  6. package/lib/dataLoader.js +10 -7
  7. package/lib/dataLoader.js.map +1 -1
  8. package/lib/dragAndDropHandler/generateHitAreas.d.ts.map +1 -1
  9. package/lib/dragAndDropHandler/generateHitAreas.js +3 -2
  10. package/lib/dragAndDropHandler/generateHitAreas.js.map +1 -1
  11. package/lib/dragAndDropHandler/index.d.ts +2 -4
  12. package/lib/dragAndDropHandler/index.d.ts.map +1 -1
  13. package/lib/dragAndDropHandler/index.js +2 -9
  14. package/lib/dragAndDropHandler/index.js.map +1 -1
  15. package/lib/elementsRenderer.d.ts +11 -4
  16. package/lib/elementsRenderer.d.ts.map +1 -1
  17. package/lib/elementsRenderer.js +104 -43
  18. package/lib/elementsRenderer.js.map +1 -1
  19. package/lib/events.d.ts +13 -7
  20. package/lib/events.d.ts.map +1 -1
  21. package/lib/folderOpenState.d.ts +8 -0
  22. package/lib/folderOpenState.d.ts.map +1 -0
  23. package/lib/folderOpenState.js +22 -0
  24. package/lib/folderOpenState.js.map +1 -0
  25. package/lib/index.d.ts +27 -20
  26. package/lib/index.d.ts.map +1 -1
  27. package/lib/index.js +166 -65
  28. package/lib/index.js.map +1 -1
  29. package/lib/keyHandler.d.ts +16 -3
  30. package/lib/keyHandler.d.ts.map +1 -1
  31. package/lib/keyHandler.js +79 -32
  32. package/lib/keyHandler.js.map +1 -1
  33. package/lib/methodTypes.d.ts +3 -0
  34. package/lib/methodTypes.d.ts.map +1 -1
  35. package/lib/mouseHandler.d.ts.map +1 -1
  36. package/lib/mouseHandler.js +12 -12
  37. package/lib/mouseHandler.js.map +1 -1
  38. package/lib/node.d.ts +12 -10
  39. package/lib/node.d.ts.map +1 -1
  40. package/lib/node.js +12 -10
  41. package/lib/node.js.map +1 -1
  42. package/lib/nodeElement/folderElement.d.ts +7 -5
  43. package/lib/nodeElement/folderElement.d.ts.map +1 -1
  44. package/lib/nodeElement/folderElement.js +35 -55
  45. package/lib/nodeElement/folderElement.js.map +1 -1
  46. package/lib/nodeElement/index.d.ts +3 -2
  47. package/lib/nodeElement/index.d.ts.map +1 -1
  48. package/lib/nodeElement/index.js +15 -12
  49. package/lib/nodeElement/index.js.map +1 -1
  50. package/lib/options.d.ts +29 -14
  51. package/lib/options.d.ts.map +1 -1
  52. package/lib/options.js +12 -0
  53. package/lib/options.js.map +1 -0
  54. package/lib/saveStateHandler.d.ts.map +1 -1
  55. package/lib/saveStateHandler.js +6 -6
  56. package/lib/saveStateHandler.js.map +1 -1
  57. package/lib/selectNodeHandler.d.ts +3 -0
  58. package/lib/selectNodeHandler.d.ts.map +1 -1
  59. package/lib/selectNodeHandler.js +66 -22
  60. package/lib/selectNodeHandler.js.map +1 -1
  61. package/lib/setDefaultOptions.d.ts.map +1 -1
  62. package/lib/setDefaultOptions.js +1 -0
  63. package/lib/setDefaultOptions.js.map +1 -1
  64. package/lib/version.d.ts +1 -1
  65. package/lib/version.js +1 -1
  66. package/lib/version.js.map +1 -1
  67. package/package.json +38 -32
  68. package/src/classNames.ts +4 -4
  69. package/src/dataLoader.ts +9 -10
  70. package/src/dragAndDropHandler/generateHitAreas.ts +3 -2
  71. package/src/dragAndDropHandler/index.ts +3 -17
  72. package/src/elementsRenderer.ts +124 -55
  73. package/src/events.ts +13 -7
  74. package/src/folderOpenState.ts +38 -0
  75. package/src/index.ts +191 -82
  76. package/src/keyHandler.ts +104 -34
  77. package/src/methodTypes.ts +6 -0
  78. package/src/mouseHandler.ts +15 -13
  79. package/src/node.ts +12 -10
  80. package/src/nodeElement/folderElement.ts +47 -72
  81. package/src/nodeElement/index.ts +20 -17
  82. package/src/options.ts +46 -14
  83. package/src/saveStateHandler.ts +5 -8
  84. package/src/selectNodeHandler.ts +69 -25
  85. package/src/setDefaultOptions.ts +1 -0
  86. package/src/version.ts +1 -1
  87. package/tree_element.css +48 -5
  88. package/tree_element.debug.js +540 -278
  89. package/tree_element.debug.js.map +1 -1
  90. package/tree_element.js +2 -2
  91. package/tree_element.js.map +1 -1
package/src/keyHandler.ts CHANGED
@@ -1,53 +1,77 @@
1
1
  import type {
2
2
  CloseNode,
3
+ FocusNode,
4
+ GetActiveNode,
3
5
  GetFocusedNode,
4
6
  GetSelectedNode,
5
7
  GetTree,
6
8
  OpenNode,
7
9
  SelectNode,
10
+ ToggleNodeInSelection,
8
11
  TriggerEvent,
9
12
  } from "./methodTypes";
10
13
  import type { Node } from "./node";
11
14
 
12
15
  interface KeyHandlerParams {
13
16
  closeNode: CloseNode;
17
+ focusNode: FocusNode;
18
+ getActiveNode: GetActiveNode;
14
19
  getFocusedNode: GetFocusedNode;
15
20
  getSelectedNode: GetSelectedNode;
16
21
  getTree: GetTree;
17
22
  keyboardSupport: boolean;
23
+ multiSelect: boolean;
18
24
  openNode: OpenNode;
19
25
  selectNode: SelectNode;
26
+ toggleNodeInSelection: ToggleNodeInSelection;
20
27
  triggerEvent: TriggerEvent;
21
28
  }
22
29
 
30
+ /* The keys move from the selected node and select the node they move to.
31
+ * With multiSelect they move from the focused node and only move the focus;
32
+ * Space adds the focused node to the selection or removes it.
33
+ */
34
+
23
35
  export default class KeyHandler {
24
36
  private closeNode: CloseNode;
37
+ private focusNode: FocusNode;
38
+ private getActiveNode: GetActiveNode;
25
39
  private getFocusedNode: GetFocusedNode;
26
40
  private getSelectedNode: GetSelectedNode;
27
41
  private getTree: GetTree;
28
42
 
29
43
  private keyboardSupport: boolean;
44
+ private multiSelect: boolean;
30
45
  private openNode: OpenNode;
31
46
  private originalSelectNode: SelectNode;
47
+ private toggleNodeInSelection: ToggleNodeInSelection;
32
48
  private triggerEvent: TriggerEvent;
33
49
 
34
50
  constructor({
35
51
  closeNode,
52
+ focusNode,
53
+ getActiveNode,
36
54
  getFocusedNode,
37
55
  getSelectedNode,
38
56
  getTree,
39
57
  keyboardSupport,
58
+ multiSelect,
40
59
  openNode,
41
60
  selectNode,
61
+ toggleNodeInSelection,
42
62
  triggerEvent,
43
63
  }: KeyHandlerParams) {
44
64
  this.closeNode = closeNode;
65
+ this.focusNode = focusNode;
66
+ this.getActiveNode = getActiveNode;
45
67
  this.getFocusedNode = getFocusedNode;
46
68
  this.getSelectedNode = getSelectedNode;
47
69
  this.getTree = getTree;
48
70
  this.keyboardSupport = keyboardSupport;
71
+ this.multiSelect = multiSelect;
49
72
  this.openNode = openNode;
50
73
  this.originalSelectNode = selectNode;
74
+ this.toggleNodeInSelection = toggleNodeInSelection;
51
75
  this.triggerEvent = triggerEvent;
52
76
 
53
77
  if (keyboardSupport) {
@@ -62,11 +86,31 @@ export default class KeyHandler {
62
86
  }
63
87
 
64
88
  public moveDown(): boolean {
65
- return this.selectNode(this.getSelectedNode()?.getNextVisibleNode());
89
+ return this.goToNode(this.getCurrentNode()?.getNextVisibleNode());
66
90
  }
67
91
 
68
92
  public moveUp(): boolean {
69
- return this.selectNode(this.getSelectedNode()?.getPreviousVisibleNode());
93
+ return this.goToNode(this.getCurrentNode()?.getPreviousVisibleNode());
94
+ }
95
+
96
+ // Change keyboardSupport after construction
97
+ public setKeyboardSupport(keyboardSupport: boolean): void {
98
+ if (keyboardSupport === this.keyboardSupport) {
99
+ return;
100
+ }
101
+
102
+ this.keyboardSupport = keyboardSupport;
103
+
104
+ if (keyboardSupport) {
105
+ document.addEventListener("keydown", this.handleKeyDown);
106
+ } else {
107
+ document.removeEventListener("keydown", this.handleKeyDown);
108
+ }
109
+ }
110
+
111
+ // Change multiSelect after construction
112
+ public setMultiSelect(multiSelect: boolean): void {
113
+ this.multiSelect = multiSelect;
70
114
  }
71
115
 
72
116
  // Activate the node, for example to open its edit page.
@@ -75,6 +119,28 @@ export default class KeyHandler {
75
119
  return true;
76
120
  }
77
121
 
122
+ // No node is selected. These keys select the focused node, which is
123
+ // usually the first node, see TreeElement.updateTabStop.
124
+ private getCurrentNode(): Node | null {
125
+ return this.multiSelect ? this.getActiveNode() : this.getSelectedNode();
126
+ }
127
+
128
+ // Move to the node: select it, or with multiSelect focus it.
129
+ // Result: there was a node to move to.
130
+ private goToNode(node?: Node | null): boolean {
131
+ if (!node) {
132
+ return false;
133
+ }
134
+
135
+ if (this.multiSelect) {
136
+ this.focusNode(node);
137
+ } else {
138
+ this.originalSelectNode(node);
139
+ }
140
+
141
+ return true;
142
+ }
143
+
78
144
  private handleArrowKey(key: string): boolean {
79
145
  switch (key) {
80
146
  case "ArrowDown": {
@@ -99,6 +165,12 @@ export default class KeyHandler {
99
165
  }
100
166
  }
101
167
 
168
+ private handleKey(key: string, focusedNode: Node): boolean {
169
+ return this.getCurrentNode()
170
+ ? this.handleArrowKey(key)
171
+ : this.handleKeyWithoutSelection(key, focusedNode);
172
+ }
173
+
102
174
  private handleKeyDown = (e: KeyboardEvent): void => {
103
175
  const focusedNode = this.getFocusedNode();
104
176
 
@@ -108,29 +180,37 @@ export default class KeyHandler {
108
180
 
109
181
  let isKeyHandled: boolean;
110
182
 
111
- const selectedNode = this.getSelectedNode();
112
-
113
183
  switch (e.key) {
184
+ case " ": {
185
+ isKeyHandled = this.multiSelect
186
+ ? this.toggle(focusedNode)
187
+ : this.handleKey(e.key, focusedNode);
188
+ break;
189
+ }
190
+
114
191
  case "End": {
115
- isKeyHandled = this.selectNode(this.getTree()?.getLastChild());
192
+ isKeyHandled = this.goToNode(this.getTree()?.getLastChild());
116
193
  break;
117
194
  }
118
195
 
119
196
  case "Enter": {
120
197
  // Without a selected node, activate the focused node
121
- isKeyHandled = this.activate(selectedNode ?? focusedNode, e);
198
+ isKeyHandled = this.activate(
199
+ this.multiSelect
200
+ ? focusedNode
201
+ : (this.getSelectedNode() ?? focusedNode),
202
+ e,
203
+ );
122
204
  break;
123
205
  }
124
206
 
125
207
  case "Home": {
126
- isKeyHandled = this.selectNode(this.getTree()?.children[0]);
208
+ isKeyHandled = this.goToNode(this.getTree()?.children[0]);
127
209
  break;
128
210
  }
129
211
 
130
212
  default: {
131
- isKeyHandled = selectedNode
132
- ? this.handleArrowKey(e.key)
133
- : this.handleKeyWithoutSelection(e.key, focusedNode);
213
+ isKeyHandled = this.handleKey(e.key, focusedNode);
134
214
  }
135
215
  }
136
216
 
@@ -139,14 +219,12 @@ export default class KeyHandler {
139
219
  }
140
220
  };
141
221
 
142
- // No node is selected. These keys select the focused node, which is
143
- // usually the first node, see TreeElement.updateTabStop.
144
222
  private handleKeyWithoutSelection(key: string, focusedNode: Node): boolean {
145
223
  switch (key) {
146
224
  case " ":
147
225
  case "ArrowDown":
148
226
  case "ArrowRight": {
149
- return this.selectNode(focusedNode);
227
+ return this.goToNode(focusedNode);
150
228
  }
151
229
 
152
230
  default: {
@@ -156,48 +234,40 @@ export default class KeyHandler {
156
234
  }
157
235
 
158
236
  private moveLeft(): boolean {
159
- const selectedNode = this.getSelectedNode();
237
+ const currentNode = this.getCurrentNode();
160
238
 
161
239
  /* istanbul ignore if */
162
- if (!selectedNode) {
240
+ if (!currentNode) {
163
241
  return false;
164
242
  }
165
243
 
166
- if (selectedNode.isFolder() && selectedNode.is_open) {
244
+ if (currentNode.isFolder() && currentNode.is_open) {
167
245
  // Left on an open node closes the node
168
- void this.closeNode(selectedNode);
246
+ void this.closeNode(currentNode);
169
247
  return true;
170
248
  }
171
249
  // Left on a closed or end node moves focus to the node's parent
172
- return this.selectNode(selectedNode.getParent());
250
+ return this.goToNode(currentNode.getParent());
173
251
  }
174
252
 
175
253
  private moveRight(): boolean {
176
- const selectedNode = this.getSelectedNode();
254
+ const currentNode = this.getCurrentNode();
177
255
 
178
- if (selectedNode?.isFolder()) {
256
+ if (currentNode?.isFolder()) {
179
257
  // folder node
180
- if (selectedNode.is_open) {
258
+ if (currentNode.is_open) {
181
259
  // Right moves to the first child of an open node
182
- return this.selectNode(selectedNode.getNextVisibleNode());
260
+ return this.goToNode(currentNode.getNextVisibleNode());
183
261
  }
184
262
  // Right expands a closed node
185
- void this.openNode(selectedNode);
263
+ void this.openNode(currentNode);
186
264
  return true;
187
265
  }
188
266
  return false;
189
267
  }
190
268
 
191
- /* Select the node.
192
- * Don't do anything if the node is null.
193
- * Result: a different node was selected.
194
- */
195
- private selectNode(node?: Node | null): boolean {
196
- if (node) {
197
- this.originalSelectNode(node);
198
-
199
- return true;
200
- }
201
- return false;
269
+ private toggle(node: Node): boolean {
270
+ this.toggleNodeInSelection(node);
271
+ return true;
202
272
  }
203
273
  }
@@ -6,6 +6,10 @@ export type AddToSelection = (node: Node) => void;
6
6
 
7
7
  export type CloseNode = (node: Node) => Promise<void>;
8
8
 
9
+ export type FocusNode = (node: Node) => void;
10
+
11
+ export type GetActiveNode = () => Node | null;
12
+
9
13
  export type GetFocusedNode = () => Node | null;
10
14
 
11
15
  export type GetNodeById = (nodeId: NodeId) => Node | null;
@@ -34,6 +38,8 @@ export type RemoveFromSelection = (node: Node) => void;
34
38
 
35
39
  export type SelectNode = (node: Node) => void;
36
40
 
41
+ export type ToggleNodeInSelection = (node: Node) => void;
42
+
37
43
  // Trigger an event. Return if the event is processed (true) or cancelled (false).
38
44
  export type TriggerEvent = <Name extends TreeEventName>(
39
45
  eventName: Name,
@@ -180,7 +180,7 @@ class MouseHandler {
180
180
  }
181
181
  };
182
182
 
183
- private handleContextmenu = (e: MouseEvent) => {
183
+ private handleContextmenu = (e: MouseEvent): void => {
184
184
  if (!e.target) {
185
185
  return;
186
186
  }
@@ -189,21 +189,23 @@ class MouseHandler {
189
189
  `ul.${this.classNames.tree} .${this.classNames.element}`,
190
190
  );
191
191
 
192
- if (div) {
193
- const node = this.getNode(div);
194
- if (node) {
195
- e.preventDefault();
196
- e.stopPropagation();
192
+ if (!div) {
193
+ return;
194
+ }
197
195
 
198
- this.triggerEvent("tree.contextmenu", {
199
- node,
200
- originalEvent: e,
201
- });
202
- return false;
203
- }
196
+ const node = this.getNode(div);
197
+
198
+ if (!node) {
199
+ return;
204
200
  }
205
201
 
206
- return null;
202
+ e.preventDefault();
203
+ e.stopPropagation();
204
+
205
+ this.triggerEvent("tree.contextmenu", {
206
+ node,
207
+ originalEvent: e,
208
+ });
207
209
  };
208
210
 
209
211
  private handleDblclick = (e: MouseEvent): void => {
package/src/node.ts CHANGED
@@ -28,14 +28,14 @@ type IterateCallback = (node: Node, level: number) => boolean;
28
28
  *
29
29
  * @groupDescription Searching
30
30
  * These work on any node, and search that node's subtree. Called on the root
31
- * node, they search the whole tree — which is what the tree's own methods of
31
+ * node, they search the whole tree—which is what the tree's own methods of
32
32
  * the same name do.
33
33
  *
34
34
  * @groupDescription Changing the tree
35
35
  * ::: warning
36
36
  * These methods change the data without re-rendering. The tree's
37
- * [methods](/reference/methods) — `appendNode`, `removeNode`, `moveNode` and
38
- * friends — do the same and refresh the display, so prefer those. If you do
37
+ * [methods](/reference/methods)—`appendNode`, `removeNode`, `moveNode` and
38
+ * friends—do the same and refresh the display, so prefer those. If you do
39
39
  * use these, call `tree.refresh()` afterwards.
40
40
  * :::
41
41
  */
@@ -85,7 +85,7 @@ export class Node {
85
85
  */
86
86
  public nodeClass?: typeof Node;
87
87
  /**
88
- The parent; `null` for the root node.
88
+ The parent, or `null` for the root node.
89
89
  */
90
90
  public parent: Node | null;
91
91
  /**
@@ -191,7 +191,9 @@ export class Node {
191
191
  }
192
192
 
193
193
  /**
194
- * Inserts a new parent between this node and its current parent.
194
+ * Inserts a new parent between this node and its current parent. Its
195
+ * siblings move along: the new node takes all children of the current
196
+ * parent, and becomes its only child.
195
197
  *
196
198
  * @returns The new node, or `null` when the node has no parent.
197
199
  * @group Changing the tree
@@ -268,7 +270,7 @@ export class Node {
268
270
  * Returns the subtree as plain data, ready for `JSON.stringify`.
269
271
  * Internal properties (`parent`, `children`, `element`, `tree`,
270
272
  * `idMapping`, `nodeClass`, `load_on_demand`, `isEmptyFolder`) are left
271
- * out; your own properties are kept.
273
+ * out. Your own properties are kept.
272
274
  *
273
275
  * ```js
274
276
  * tree.getTree().getData();
@@ -384,7 +386,7 @@ export class Node {
384
386
  }
385
387
 
386
388
  /**
387
- * Like `getNextNode`, but skipping nodes inside closed folders — this is
389
+ * Like `getNextNode`, but skipping nodes inside closed folders. This is
388
390
  * what the arrow keys use.
389
391
  *
390
392
  * @group Moving around the tree
@@ -472,7 +474,7 @@ export class Node {
472
474
  }
473
475
 
474
476
  /**
475
- * Returns the parent: `null` for a top-level node — the root node is not
477
+ * Returns the parent, or `null` for a top-level node: the root node is not
476
478
  * returned.
477
479
  *
478
480
  * @group Moving around the tree
@@ -525,8 +527,8 @@ export class Node {
525
527
  }
526
528
 
527
529
  /**
528
- * Like `getPreviousNode`, but skipping nodes inside closed folders —
529
- * this is what the arrow keys use.
530
+ * Like `getPreviousNode`, but skipping nodes inside closed folders.
531
+ * This is what the arrow keys use.
530
532
  *
531
533
  * @group Moving around the tree
532
534
  */
@@ -1,32 +1,32 @@
1
1
  import type { AnimationSpeed } from "../animation";
2
+ import type { FolderIcons } from "../folderOpenState";
2
3
  import type { TriggerEvent } from "../methodTypes";
3
4
  import type { Node, Position } from "../node";
4
5
  import type { NodeElementParams } from "./index";
5
6
 
6
7
  import { slideDown, slideUp } from "../animation";
8
+ import { setFolderLiOpenState, setTogglerOpenState } from "../folderOpenState";
7
9
  import NodeElement from "./index";
8
10
 
9
11
  export type RenderChildren = (node: Node) => HTMLUListElement | null;
10
12
 
11
13
  interface FolderElementParams extends NodeElementParams {
12
- closedIconElement?: globalThis.Node;
13
- openedIconElement?: globalThis.Node;
14
+ icons: FolderIcons;
14
15
  renderChildren: RenderChildren;
15
16
  triggerEvent: TriggerEvent;
16
17
  }
17
18
 
18
19
  class FolderElement extends NodeElement {
19
- private closedIconElement?: globalThis.Node;
20
- private openedIconElement?: globalThis.Node;
20
+ private icons: FolderIcons;
21
21
  private renderChildren: RenderChildren;
22
22
  private triggerEvent: TriggerEvent;
23
23
 
24
24
  constructor({
25
25
  classNames,
26
- closedIconElement,
27
26
  getScrollLeft,
27
+ icons,
28
+ multiSelect,
28
29
  node,
29
- openedIconElement,
30
30
  renderChildren,
31
31
  tabIndex,
32
32
  treeElement,
@@ -35,13 +35,13 @@ class FolderElement extends NodeElement {
35
35
  super({
36
36
  classNames,
37
37
  getScrollLeft,
38
+ multiSelect,
38
39
  node,
39
40
  tabIndex,
40
41
  treeElement,
41
42
  });
42
43
 
43
- this.closedIconElement = closedIconElement;
44
- this.openedIconElement = openedIconElement;
44
+ this.icons = icons;
45
45
  this.renderChildren = renderChildren;
46
46
  this.triggerEvent = triggerEvent;
47
47
  }
@@ -56,40 +56,13 @@ class FolderElement extends NodeElement {
56
56
 
57
57
  this.node.is_open = false;
58
58
 
59
- if (!this.isRendered()) {
60
- this.triggerEvent("tree.close", { node: this.node });
61
- return;
62
- }
63
-
64
- const button = this.getButton();
65
- button.classList.add(this.classNames.closed);
66
- button.replaceChildren();
67
-
68
- const closedIconElement = this.closedIconElement;
69
-
70
- if (closedIconElement) {
71
- const icon = closedIconElement.cloneNode(true);
72
- button.append(icon);
59
+ if (this.isRendered()) {
60
+ this.setButtonState(false);
61
+ await this.setUlState(this.getUl(), false, slide, animationSpeed);
62
+ this.setElementState(false);
73
63
  }
74
64
 
75
- const ul = this.getUl();
76
-
77
- if (ul) {
78
- if (slide) {
79
- await slideUp(ul, animationSpeed);
80
- } else {
81
- ul.style.display = "none";
82
- }
83
- }
84
-
85
- this.element.classList.add(this.classNames.closed);
86
-
87
- const titleSpan = this.getTitleSpan();
88
- titleSpan.setAttribute("aria-expanded", "false");
89
-
90
- this.triggerEvent("tree.close", {
91
- node: this.node,
92
- });
65
+ this.triggerEvent("tree.close", { node: this.node });
93
66
  }
94
67
 
95
68
  public async open(
@@ -102,42 +75,19 @@ class FolderElement extends NodeElement {
102
75
 
103
76
  this.node.is_open = true;
104
77
 
105
- if (!this.isRendered()) {
106
- // The folder is rendered open when its parent is opened
107
- this.triggerEvent("tree.open", { node: this.node });
108
- return;
109
- }
110
-
111
- const button = this.getButton();
112
- button.classList.remove(this.classNames.closed);
113
- button.replaceChildren();
114
-
115
- const openedIconElement = this.openedIconElement;
116
-
117
- if (openedIconElement) {
118
- const icon = openedIconElement.cloneNode(true);
119
- button.append(icon);
120
- }
78
+ // A folder that is not rendered is rendered open when its parent is
79
+ // opened
80
+ if (this.isRendered()) {
81
+ this.setButtonState(true);
121
82
 
122
- // The children are rendered the first time the folder is opened
123
- const ul = this.getUl() ?? this.renderChildren(this.node);
83
+ // The children are rendered the first time the folder is opened
84
+ const ul = this.getUl() ?? this.renderChildren(this.node);
85
+ await this.setUlState(ul, true, slide, animationSpeed);
124
86
 
125
- if (ul) {
126
- if (slide) {
127
- await slideDown(ul, animationSpeed);
128
- } else {
129
- ul.style.display = "block";
130
- }
87
+ this.setElementState(true);
131
88
  }
132
89
 
133
- this.element.classList.remove(this.classNames.closed);
134
-
135
- const titleSpan = this.getTitleSpan();
136
- titleSpan.setAttribute("aria-expanded", "true");
137
-
138
- this.triggerEvent("tree.open", {
139
- node: this.node,
140
- });
90
+ this.triggerEvent("tree.open", { node: this.node });
141
91
  }
142
92
 
143
93
  protected mustShowBorderDropHint(position: Position): boolean {
@@ -149,6 +99,31 @@ class FolderElement extends NodeElement {
149
99
  `:scope > .${this.classNames.element} > .${this.classNames.toggler}`,
150
100
  ) as HTMLElement;
151
101
  }
102
+
103
+ private setButtonState(isOpen: boolean): void {
104
+ setTogglerOpenState(this.getButton(), isOpen, this.classNames, this.icons);
105
+ }
106
+
107
+ private setElementState(isOpen: boolean): void {
108
+ setFolderLiOpenState(this.element, isOpen, this.classNames);
109
+ }
110
+
111
+ private async setUlState(
112
+ ul: HTMLUListElement | null,
113
+ isOpen: boolean,
114
+ slide: boolean,
115
+ animationSpeed: AnimationSpeed,
116
+ ): Promise<void> {
117
+ if (!ul) {
118
+ return;
119
+ }
120
+
121
+ if (slide) {
122
+ await (isOpen ? slideDown : slideUp)(ul, animationSpeed);
123
+ } else {
124
+ ul.style.display = isOpen ? "block" : "none";
125
+ }
126
+ }
152
127
  }
153
128
 
154
129
  export default FolderElement;
@@ -9,6 +9,10 @@ import GhostDropHint from "./ghostDropHint";
9
9
  export interface NodeElementParams {
10
10
  classNames: ClassNames;
11
11
  getScrollLeft: GetScrollLeft;
12
+ /* With multiSelect the focus moves independently of the selection, so
13
+ * selecting doesn't change the tabindex or the focus.
14
+ */
15
+ multiSelect?: boolean;
12
16
  node: Node;
13
17
  tabIndex?: number;
14
18
  treeElement: HTMLElement;
@@ -19,18 +23,21 @@ class NodeElement {
19
23
  public node: Node;
20
24
  protected classNames: ClassNames;
21
25
  private getScrollLeft: GetScrollLeft;
26
+ private multiSelect: boolean;
22
27
  private tabIndex?: number;
23
28
  private treeElement: HTMLElement;
24
29
 
25
30
  constructor({
26
31
  classNames,
27
32
  getScrollLeft,
33
+ multiSelect = false,
28
34
  node,
29
35
  tabIndex,
30
36
  treeElement,
31
37
  }: NodeElementParams) {
32
38
  this.classNames = classNames;
33
39
  this.getScrollLeft = getScrollLeft;
40
+ this.multiSelect = multiSelect;
34
41
  this.node = node;
35
42
  this.tabIndex = tabIndex;
36
43
  this.treeElement = treeElement;
@@ -52,16 +59,20 @@ class NodeElement {
52
59
 
53
60
  this.element.classList.remove(this.classNames.selected);
54
61
 
55
- const titleSpan = this.getTitleSpan();
62
+ // Only a multiselect tree marks the nodes that are not selected
63
+ if (this.multiSelect) {
64
+ this.element.setAttribute("aria-selected", "false");
65
+ return;
66
+ }
67
+
68
+ this.element.removeAttribute("aria-selected");
56
69
 
57
70
  // Check for null or undefined
58
71
  if (this.tabIndex != null) {
59
- titleSpan.setAttribute("tabindex", "-1");
72
+ this.element.setAttribute("tabindex", "-1");
60
73
  }
61
74
 
62
- titleSpan.setAttribute("aria-selected", "false");
63
-
64
- titleSpan.blur();
75
+ this.element.blur();
65
76
  }
66
77
 
67
78
  public select(mustSetFocus: boolean): void {
@@ -70,28 +81,20 @@ class NodeElement {
70
81
  }
71
82
 
72
83
  this.element.classList.add(this.classNames.selected);
84
+ this.element.setAttribute("aria-selected", "true");
73
85
 
74
- const titleSpan = this.getTitleSpan();
75
86
  const tabIndex = this.tabIndex;
76
87
 
77
88
  // Check for null or undefined
78
- if (tabIndex != null) {
79
- titleSpan.setAttribute("tabindex", tabIndex.toString());
89
+ if (tabIndex != null && !this.multiSelect) {
90
+ this.element.setAttribute("tabindex", tabIndex.toString());
80
91
  }
81
92
 
82
- titleSpan.setAttribute("aria-selected", "true");
83
-
84
93
  if (mustSetFocus) {
85
- titleSpan.focus();
94
+ this.element.focus();
86
95
  }
87
96
  }
88
97
 
89
- protected getTitleSpan(): HTMLSpanElement {
90
- return this.element.querySelector(
91
- `:scope > .${this.classNames.element} > span.${this.classNames.title}`,
92
- ) as HTMLSpanElement;
93
- }
94
-
95
98
  protected getUl(): HTMLUListElement | null {
96
99
  return this.element.querySelector(":scope > ul");
97
100
  }