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.
- package/README.md +1 -0
- package/lib/classNames.js +4 -4
- package/lib/classNames.js.map +1 -1
- package/lib/dataLoader.d.ts +0 -1
- package/lib/dataLoader.d.ts.map +1 -1
- package/lib/dataLoader.js +6 -6
- package/lib/dataLoader.js.map +1 -1
- package/lib/elementsRenderer.d.ts +4 -2
- package/lib/elementsRenderer.d.ts.map +1 -1
- package/lib/elementsRenderer.js +35 -21
- package/lib/elementsRenderer.js.map +1 -1
- package/lib/index.d.ts +10 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +75 -18
- package/lib/index.js.map +1 -1
- package/lib/keyHandler.d.ts +14 -3
- package/lib/keyHandler.d.ts.map +1 -1
- package/lib/keyHandler.js +61 -32
- package/lib/keyHandler.js.map +1 -1
- package/lib/methodTypes.d.ts +3 -0
- package/lib/methodTypes.d.ts.map +1 -1
- package/lib/nodeElement/folderElement.d.ts +1 -1
- package/lib/nodeElement/folderElement.d.ts.map +1 -1
- package/lib/nodeElement/folderElement.js +4 -4
- package/lib/nodeElement/folderElement.js.map +1 -1
- package/lib/nodeElement/index.d.ts +3 -2
- package/lib/nodeElement/index.d.ts.map +1 -1
- package/lib/nodeElement/index.js +15 -12
- package/lib/nodeElement/index.js.map +1 -1
- package/lib/options.d.ts +13 -0
- package/lib/options.d.ts.map +1 -1
- package/lib/selectNodeHandler.d.ts +3 -0
- package/lib/selectNodeHandler.d.ts.map +1 -1
- package/lib/selectNodeHandler.js +54 -22
- package/lib/selectNodeHandler.js.map +1 -1
- package/lib/setDefaultOptions.d.ts.map +1 -1
- package/lib/setDefaultOptions.js +1 -0
- package/lib/setDefaultOptions.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- package/package.json +19 -19
- package/src/classNames.ts +4 -4
- package/src/dataLoader.ts +6 -9
- package/src/elementsRenderer.ts +42 -24
- package/src/index.ts +83 -24
- package/src/keyHandler.ts +84 -34
- package/src/methodTypes.ts +6 -0
- package/src/nodeElement/folderElement.ts +4 -4
- package/src/nodeElement/index.ts +20 -17
- package/src/options.ts +13 -0
- package/src/selectNodeHandler.ts +58 -25
- package/src/setDefaultOptions.ts +1 -0
- package/src/version.ts +1 -1
- package/tree_element.css +16 -0
- package/tree_element.debug.js +256 -120
- package/tree_element.debug.js.map +1 -1
- package/tree_element.js +2 -2
- package/tree_element.js.map +1 -1
package/src/index.ts
CHANGED
|
@@ -47,6 +47,8 @@ interface TreeElementParams extends Partial<TreeElementOptions> {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export default class TreeElement {
|
|
50
|
+
// With multiSelect: the node that is the tab stop, see updateTabStop
|
|
51
|
+
private activeNode: Node | null;
|
|
50
52
|
private classNames: ClassNames;
|
|
51
53
|
private dataLoader: DataLoader;
|
|
52
54
|
private dndHandler: DragAndDropHandler;
|
|
@@ -82,6 +84,7 @@ export default class TreeElement {
|
|
|
82
84
|
this.tree = new Node({}, true);
|
|
83
85
|
this.nodeMap = new WeakMap();
|
|
84
86
|
this.fallbackTabStop = null;
|
|
87
|
+
this.activeNode = null;
|
|
85
88
|
|
|
86
89
|
const {
|
|
87
90
|
ariaLabel,
|
|
@@ -92,6 +95,7 @@ export default class TreeElement {
|
|
|
92
95
|
dataFilter,
|
|
93
96
|
dragAndDrop,
|
|
94
97
|
keyboardSupport,
|
|
98
|
+
multiSelect,
|
|
95
99
|
onCanMove,
|
|
96
100
|
onCanMoveTo,
|
|
97
101
|
onCreateLi,
|
|
@@ -153,6 +157,19 @@ export default class TreeElement {
|
|
|
153
157
|
const selectNode =
|
|
154
158
|
selectNodeHandler.selectSingleNode.bind(selectNodeHandler);
|
|
155
159
|
|
|
160
|
+
const toggleNodeInSelection =
|
|
161
|
+
selectNodeHandler.toggleNodeInSelection.bind(selectNodeHandler);
|
|
162
|
+
|
|
163
|
+
// With multiSelect, a click adds a node to the selection or removes it
|
|
164
|
+
const onClickTitle = (node: Node) => {
|
|
165
|
+
if (this.options.multiSelect) {
|
|
166
|
+
toggleNodeInSelection(node, false);
|
|
167
|
+
this.focusNode(node);
|
|
168
|
+
} else {
|
|
169
|
+
selectNode(node);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
156
173
|
const getMouseDelay = () => this.options.startDndDelay ?? 0;
|
|
157
174
|
|
|
158
175
|
const dataLoader = new DataLoader({
|
|
@@ -205,12 +222,16 @@ export default class TreeElement {
|
|
|
205
222
|
|
|
206
223
|
const keyHandler = new KeyHandler({
|
|
207
224
|
closeNode,
|
|
225
|
+
focusNode: this.focusNode.bind(this),
|
|
226
|
+
getActiveNode: this.getActiveNode.bind(this),
|
|
208
227
|
getFocusedNode,
|
|
209
228
|
getSelectedNode,
|
|
210
229
|
getTree,
|
|
211
230
|
keyboardSupport,
|
|
231
|
+
multiSelect,
|
|
212
232
|
openNode,
|
|
213
233
|
selectNode,
|
|
234
|
+
toggleNodeInSelection,
|
|
214
235
|
triggerEvent,
|
|
215
236
|
});
|
|
216
237
|
|
|
@@ -225,6 +246,7 @@ export default class TreeElement {
|
|
|
225
246
|
getTree,
|
|
226
247
|
htmlElement: treeElement,
|
|
227
248
|
isNodeSelected,
|
|
249
|
+
multiSelect,
|
|
228
250
|
onCreateLi,
|
|
229
251
|
openedIcon,
|
|
230
252
|
rtl,
|
|
@@ -245,7 +267,7 @@ export default class TreeElement {
|
|
|
245
267
|
getMouseDelay,
|
|
246
268
|
getNode,
|
|
247
269
|
onClickButton: this.toggle.bind(this),
|
|
248
|
-
onClickTitle
|
|
270
|
+
onClickTitle,
|
|
249
271
|
onMouseCapture,
|
|
250
272
|
onMouseDrag,
|
|
251
273
|
onMouseStart,
|
|
@@ -263,6 +285,10 @@ export default class TreeElement {
|
|
|
263
285
|
this.scrollHandler = scrollHandler;
|
|
264
286
|
this.selectNodeHandler = selectNodeHandler;
|
|
265
287
|
|
|
288
|
+
if (multiSelect) {
|
|
289
|
+
treeElement.addEventListener("focusin", this.handleFocusIn);
|
|
290
|
+
}
|
|
291
|
+
|
|
266
292
|
this.initData();
|
|
267
293
|
}
|
|
268
294
|
|
|
@@ -388,6 +414,7 @@ export default class TreeElement {
|
|
|
388
414
|
this.dataLoader.deinit();
|
|
389
415
|
this.keyHandler.deinit();
|
|
390
416
|
this.mouseHandler.deinit();
|
|
417
|
+
this.htmlElement.removeEventListener("focusin", this.handleFocusIn);
|
|
391
418
|
|
|
392
419
|
this.tree = new Node({}, true);
|
|
393
420
|
}
|
|
@@ -584,7 +611,8 @@ export default class TreeElement {
|
|
|
584
611
|
}
|
|
585
612
|
|
|
586
613
|
/**
|
|
587
|
-
* Selects the next visible node, like the down arrow key.
|
|
614
|
+
* Selects the next visible node, like the down arrow key. With
|
|
615
|
+
* `multiSelect` it moves the focus instead.
|
|
588
616
|
*
|
|
589
617
|
* @group Selection
|
|
590
618
|
*/
|
|
@@ -609,7 +637,8 @@ export default class TreeElement {
|
|
|
609
637
|
}
|
|
610
638
|
|
|
611
639
|
/**
|
|
612
|
-
* Selects the previous visible node, like the up arrow key.
|
|
640
|
+
* Selects the previous visible node, like the up arrow key. With
|
|
641
|
+
* `multiSelect` it moves the focus instead.
|
|
613
642
|
*
|
|
614
643
|
* @group Selection
|
|
615
644
|
*/
|
|
@@ -754,8 +783,8 @@ export default class TreeElement {
|
|
|
754
783
|
*/
|
|
755
784
|
public selectNode(node: Node | null, options?: SelectNodeOptions): void {
|
|
756
785
|
if (!node) {
|
|
757
|
-
// Called with empty node -> deselect
|
|
758
|
-
this.
|
|
786
|
+
// Called with empty node -> deselect all nodes
|
|
787
|
+
this.deselectAllNodes();
|
|
759
788
|
this.saveStateHandler.saveState();
|
|
760
789
|
return;
|
|
761
790
|
}
|
|
@@ -913,6 +942,7 @@ export default class TreeElement {
|
|
|
913
942
|
classNames,
|
|
914
943
|
closedIconElement,
|
|
915
944
|
getScrollLeft,
|
|
945
|
+
multiSelect: this.options.multiSelect,
|
|
916
946
|
node,
|
|
917
947
|
openedIconElement,
|
|
918
948
|
renderChildren,
|
|
@@ -933,6 +963,7 @@ export default class TreeElement {
|
|
|
933
963
|
return new NodeElement({
|
|
934
964
|
classNames,
|
|
935
965
|
getScrollLeft,
|
|
966
|
+
multiSelect: this.options.multiSelect,
|
|
936
967
|
node,
|
|
937
968
|
tabIndex,
|
|
938
969
|
treeElement,
|
|
@@ -968,9 +999,8 @@ export default class TreeElement {
|
|
|
968
999
|
return requestUrl;
|
|
969
1000
|
}
|
|
970
1001
|
|
|
971
|
-
private
|
|
972
|
-
const node
|
|
973
|
-
if (node) {
|
|
1002
|
+
private deselectAllNodes(): void {
|
|
1003
|
+
for (const node of this.getSelectedNodes()) {
|
|
974
1004
|
this.removeFromSelection(node);
|
|
975
1005
|
}
|
|
976
1006
|
}
|
|
@@ -984,6 +1014,23 @@ export default class TreeElement {
|
|
|
984
1014
|
}
|
|
985
1015
|
}
|
|
986
1016
|
|
|
1017
|
+
// The treeitem of the first node, or null when the tree is empty
|
|
1018
|
+
// Move the focus to a node, without selecting it
|
|
1019
|
+
private focusNode(node: Node): void {
|
|
1020
|
+
this.activeNode = node;
|
|
1021
|
+
this.updateTabStop();
|
|
1022
|
+
node.element?.focus();
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// With multiSelect: the active node, if it is still rendered in the tree
|
|
1026
|
+
private getActiveNode(): Node | null {
|
|
1027
|
+
const element = this.activeNode?.element;
|
|
1028
|
+
|
|
1029
|
+
return element && this.htmlElement.contains(element)
|
|
1030
|
+
? this.activeNode
|
|
1031
|
+
: null;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
987
1034
|
// Get the maximum level for auto open
|
|
988
1035
|
private getAutoOpenMaxLevel(): number {
|
|
989
1036
|
if (this.options.autoOpen === true) {
|
|
@@ -1000,15 +1047,8 @@ export default class TreeElement {
|
|
|
1000
1047
|
return 0;
|
|
1001
1048
|
}
|
|
1002
1049
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
const firstElement = this.tree.children[0]?.element;
|
|
1006
|
-
|
|
1007
|
-
return firstElement
|
|
1008
|
-
? firstElement.querySelector<HTMLElement>(
|
|
1009
|
-
`:scope > .${this.classNames.element} > span.${this.classNames.title}`,
|
|
1010
|
-
)
|
|
1011
|
-
: null;
|
|
1050
|
+
private getFirstTreeItem(): HTMLElement | null {
|
|
1051
|
+
return this.tree.children[0]?.element ?? null;
|
|
1012
1052
|
}
|
|
1013
1053
|
|
|
1014
1054
|
// The node of the HTML element that has the focus, if it is in this tree
|
|
@@ -1022,7 +1062,7 @@ export default class TreeElement {
|
|
|
1022
1062
|
|
|
1023
1063
|
// The keyboard must still work for input elements.
|
|
1024
1064
|
const tagName = activeElement.tagName;
|
|
1025
|
-
if (tagName !== "A" && tagName !== "SPAN") {
|
|
1065
|
+
if (tagName !== "A" && tagName !== "LI" && tagName !== "SPAN") {
|
|
1026
1066
|
return null;
|
|
1027
1067
|
}
|
|
1028
1068
|
|
|
@@ -1045,6 +1085,18 @@ export default class TreeElement {
|
|
|
1045
1085
|
return this.saveStateHandler.getNodeIdToBeSelected();
|
|
1046
1086
|
}
|
|
1047
1087
|
|
|
1088
|
+
// With multiSelect, the focused node becomes the tab stop
|
|
1089
|
+
private handleFocusIn = (): void => {
|
|
1090
|
+
const node = this.getFocusedNode();
|
|
1091
|
+
|
|
1092
|
+
if (!node || node === this.activeNode) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
this.activeNode = node;
|
|
1097
|
+
this.updateTabStop();
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1048
1100
|
private hasRenderedSelectedNode(): boolean {
|
|
1049
1101
|
return this.getSelectedNodes().some((node) => Boolean(node.element));
|
|
1050
1102
|
}
|
|
@@ -1147,7 +1199,6 @@ export default class TreeElement {
|
|
|
1147
1199
|
|
|
1148
1200
|
private mouseStart(positionInfo: PositionInfo): boolean {
|
|
1149
1201
|
/* istanbul ignore if */
|
|
1150
|
-
// eslint-disable-next-line unicorn/prefer-ternary
|
|
1151
1202
|
if (!this.options.dragAndDrop) {
|
|
1152
1203
|
return false;
|
|
1153
1204
|
}
|
|
@@ -1283,19 +1334,27 @@ export default class TreeElement {
|
|
|
1283
1334
|
/* Keyboard users reach the tree through the tab stop. The selected node is
|
|
1284
1335
|
* the tab stop, see NodeElement.select. Without a selected node, the first
|
|
1285
1336
|
* node is the tab stop, so the tree can always be reached.
|
|
1337
|
+
* With multiSelect the tab stop is the active node: the node that was
|
|
1338
|
+
* focused last. Without one, it is the first node.
|
|
1286
1339
|
*/
|
|
1287
1340
|
private updateTabStop(): void {
|
|
1288
1341
|
const tabIndex = this.options.tabIndex;
|
|
1342
|
+
const multiSelect = this.options.multiSelect;
|
|
1289
1343
|
const previousTabStop = this.fallbackTabStop;
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1344
|
+
let tabStop: HTMLElement | null;
|
|
1345
|
+
|
|
1346
|
+
if (tabIndex == null) {
|
|
1347
|
+
tabStop = null;
|
|
1348
|
+
} else if (multiSelect) {
|
|
1349
|
+
tabStop = this.getActiveNode()?.element ?? this.getFirstTreeItem();
|
|
1350
|
+
} else {
|
|
1351
|
+
tabStop = this.hasRenderedSelectedNode() ? null : this.getFirstTreeItem();
|
|
1352
|
+
}
|
|
1294
1353
|
|
|
1295
1354
|
if (
|
|
1296
1355
|
previousTabStop &&
|
|
1297
1356
|
previousTabStop !== tabStop &&
|
|
1298
|
-
previousTabStop.getAttribute("aria-selected") !== "true"
|
|
1357
|
+
(multiSelect || previousTabStop.getAttribute("aria-selected") !== "true")
|
|
1299
1358
|
) {
|
|
1300
1359
|
previousTabStop.setAttribute("tabindex", "-1");
|
|
1301
1360
|
}
|
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,11 @@ export default class KeyHandler {
|
|
|
62
86
|
}
|
|
63
87
|
|
|
64
88
|
public moveDown(): boolean {
|
|
65
|
-
return this.
|
|
89
|
+
return this.goToNode(this.getCurrentNode()?.getNextVisibleNode());
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
public moveUp(): boolean {
|
|
69
|
-
return this.
|
|
93
|
+
return this.goToNode(this.getCurrentNode()?.getPreviousVisibleNode());
|
|
70
94
|
}
|
|
71
95
|
|
|
72
96
|
// Activate the node, for example to open its edit page.
|
|
@@ -75,6 +99,28 @@ export default class KeyHandler {
|
|
|
75
99
|
return true;
|
|
76
100
|
}
|
|
77
101
|
|
|
102
|
+
// No node is selected. These keys select the focused node, which is
|
|
103
|
+
// usually the first node, see TreeElement.updateTabStop.
|
|
104
|
+
private getCurrentNode(): Node | null {
|
|
105
|
+
return this.multiSelect ? this.getActiveNode() : this.getSelectedNode();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Move to the node: select it, or with multiSelect focus it.
|
|
109
|
+
// Result: there was a node to move to.
|
|
110
|
+
private goToNode(node?: Node | null): boolean {
|
|
111
|
+
if (!node) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.multiSelect) {
|
|
116
|
+
this.focusNode(node);
|
|
117
|
+
} else {
|
|
118
|
+
this.originalSelectNode(node);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
78
124
|
private handleArrowKey(key: string): boolean {
|
|
79
125
|
switch (key) {
|
|
80
126
|
case "ArrowDown": {
|
|
@@ -99,6 +145,12 @@ export default class KeyHandler {
|
|
|
99
145
|
}
|
|
100
146
|
}
|
|
101
147
|
|
|
148
|
+
private handleKey(key: string, focusedNode: Node): boolean {
|
|
149
|
+
return this.getCurrentNode()
|
|
150
|
+
? this.handleArrowKey(key)
|
|
151
|
+
: this.handleKeyWithoutSelection(key, focusedNode);
|
|
152
|
+
}
|
|
153
|
+
|
|
102
154
|
private handleKeyDown = (e: KeyboardEvent): void => {
|
|
103
155
|
const focusedNode = this.getFocusedNode();
|
|
104
156
|
|
|
@@ -108,29 +160,37 @@ export default class KeyHandler {
|
|
|
108
160
|
|
|
109
161
|
let isKeyHandled: boolean;
|
|
110
162
|
|
|
111
|
-
const selectedNode = this.getSelectedNode();
|
|
112
|
-
|
|
113
163
|
switch (e.key) {
|
|
164
|
+
case " ": {
|
|
165
|
+
isKeyHandled = this.multiSelect
|
|
166
|
+
? this.toggle(focusedNode)
|
|
167
|
+
: this.handleKey(e.key, focusedNode);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
114
171
|
case "End": {
|
|
115
|
-
isKeyHandled = this.
|
|
172
|
+
isKeyHandled = this.goToNode(this.getTree()?.getLastChild());
|
|
116
173
|
break;
|
|
117
174
|
}
|
|
118
175
|
|
|
119
176
|
case "Enter": {
|
|
120
177
|
// Without a selected node, activate the focused node
|
|
121
|
-
isKeyHandled = this.activate(
|
|
178
|
+
isKeyHandled = this.activate(
|
|
179
|
+
this.multiSelect
|
|
180
|
+
? focusedNode
|
|
181
|
+
: (this.getSelectedNode() ?? focusedNode),
|
|
182
|
+
e,
|
|
183
|
+
);
|
|
122
184
|
break;
|
|
123
185
|
}
|
|
124
186
|
|
|
125
187
|
case "Home": {
|
|
126
|
-
isKeyHandled = this.
|
|
188
|
+
isKeyHandled = this.goToNode(this.getTree()?.children[0]);
|
|
127
189
|
break;
|
|
128
190
|
}
|
|
129
191
|
|
|
130
192
|
default: {
|
|
131
|
-
isKeyHandled =
|
|
132
|
-
? this.handleArrowKey(e.key)
|
|
133
|
-
: this.handleKeyWithoutSelection(e.key, focusedNode);
|
|
193
|
+
isKeyHandled = this.handleKey(e.key, focusedNode);
|
|
134
194
|
}
|
|
135
195
|
}
|
|
136
196
|
|
|
@@ -139,14 +199,12 @@ export default class KeyHandler {
|
|
|
139
199
|
}
|
|
140
200
|
};
|
|
141
201
|
|
|
142
|
-
// No node is selected. These keys select the focused node, which is
|
|
143
|
-
// usually the first node, see TreeElement.updateTabStop.
|
|
144
202
|
private handleKeyWithoutSelection(key: string, focusedNode: Node): boolean {
|
|
145
203
|
switch (key) {
|
|
146
204
|
case " ":
|
|
147
205
|
case "ArrowDown":
|
|
148
206
|
case "ArrowRight": {
|
|
149
|
-
return this.
|
|
207
|
+
return this.goToNode(focusedNode);
|
|
150
208
|
}
|
|
151
209
|
|
|
152
210
|
default: {
|
|
@@ -156,48 +214,40 @@ export default class KeyHandler {
|
|
|
156
214
|
}
|
|
157
215
|
|
|
158
216
|
private moveLeft(): boolean {
|
|
159
|
-
const
|
|
217
|
+
const currentNode = this.getCurrentNode();
|
|
160
218
|
|
|
161
219
|
/* istanbul ignore if */
|
|
162
|
-
if (!
|
|
220
|
+
if (!currentNode) {
|
|
163
221
|
return false;
|
|
164
222
|
}
|
|
165
223
|
|
|
166
|
-
if (
|
|
224
|
+
if (currentNode.isFolder() && currentNode.is_open) {
|
|
167
225
|
// Left on an open node closes the node
|
|
168
|
-
void this.closeNode(
|
|
226
|
+
void this.closeNode(currentNode);
|
|
169
227
|
return true;
|
|
170
228
|
}
|
|
171
229
|
// Left on a closed or end node moves focus to the node's parent
|
|
172
|
-
return this.
|
|
230
|
+
return this.goToNode(currentNode.getParent());
|
|
173
231
|
}
|
|
174
232
|
|
|
175
233
|
private moveRight(): boolean {
|
|
176
|
-
const
|
|
234
|
+
const currentNode = this.getCurrentNode();
|
|
177
235
|
|
|
178
|
-
if (
|
|
236
|
+
if (currentNode?.isFolder()) {
|
|
179
237
|
// folder node
|
|
180
|
-
if (
|
|
238
|
+
if (currentNode.is_open) {
|
|
181
239
|
// Right moves to the first child of an open node
|
|
182
|
-
return this.
|
|
240
|
+
return this.goToNode(currentNode.getNextVisibleNode());
|
|
183
241
|
}
|
|
184
242
|
// Right expands a closed node
|
|
185
|
-
void this.openNode(
|
|
243
|
+
void this.openNode(currentNode);
|
|
186
244
|
return true;
|
|
187
245
|
}
|
|
188
246
|
return false;
|
|
189
247
|
}
|
|
190
248
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
*/
|
|
195
|
-
private selectNode(node?: Node | null): boolean {
|
|
196
|
-
if (node) {
|
|
197
|
-
this.originalSelectNode(node);
|
|
198
|
-
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
return false;
|
|
249
|
+
private toggle(node: Node): boolean {
|
|
250
|
+
this.toggleNodeInSelection(node);
|
|
251
|
+
return true;
|
|
202
252
|
}
|
|
203
253
|
}
|
package/src/methodTypes.ts
CHANGED
|
@@ -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,
|
|
@@ -25,6 +25,7 @@ class FolderElement extends NodeElement {
|
|
|
25
25
|
classNames,
|
|
26
26
|
closedIconElement,
|
|
27
27
|
getScrollLeft,
|
|
28
|
+
multiSelect,
|
|
28
29
|
node,
|
|
29
30
|
openedIconElement,
|
|
30
31
|
renderChildren,
|
|
@@ -35,6 +36,7 @@ class FolderElement extends NodeElement {
|
|
|
35
36
|
super({
|
|
36
37
|
classNames,
|
|
37
38
|
getScrollLeft,
|
|
39
|
+
multiSelect,
|
|
38
40
|
node,
|
|
39
41
|
tabIndex,
|
|
40
42
|
treeElement,
|
|
@@ -84,8 +86,7 @@ class FolderElement extends NodeElement {
|
|
|
84
86
|
|
|
85
87
|
this.element.classList.add(this.classNames.closed);
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
titleSpan.setAttribute("aria-expanded", "false");
|
|
89
|
+
this.element.setAttribute("aria-expanded", "false");
|
|
89
90
|
|
|
90
91
|
this.triggerEvent("tree.close", {
|
|
91
92
|
node: this.node,
|
|
@@ -132,8 +133,7 @@ class FolderElement extends NodeElement {
|
|
|
132
133
|
|
|
133
134
|
this.element.classList.remove(this.classNames.closed);
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
titleSpan.setAttribute("aria-expanded", "true");
|
|
136
|
+
this.element.setAttribute("aria-expanded", "true");
|
|
137
137
|
|
|
138
138
|
this.triggerEvent("tree.open", {
|
|
139
139
|
node: this.node,
|
package/src/nodeElement/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
72
|
+
this.element.setAttribute("tabindex", "-1");
|
|
60
73
|
}
|
|
61
74
|
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/options.ts
CHANGED
|
@@ -151,6 +151,19 @@ export interface TreeElementOptions {
|
|
|
151
151
|
* @group Selection
|
|
152
152
|
*/
|
|
153
153
|
keyboardSupport: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Let the user select multiple nodes: a click adds a node to the
|
|
156
|
+
* selection, or removes it when it is selected. The arrow keys move the
|
|
157
|
+
* focus instead of the selection, and `Space` adds the focused node or
|
|
158
|
+
* removes it. See [Selection](/guide/selection#multiple-nodes).
|
|
159
|
+
*
|
|
160
|
+
* Set it in the constructor; changing it with `setOption` is not
|
|
161
|
+
* supported.
|
|
162
|
+
*
|
|
163
|
+
* @defaultValue `false`
|
|
164
|
+
* @group Selection
|
|
165
|
+
*/
|
|
166
|
+
multiSelect: boolean;
|
|
154
167
|
/**
|
|
155
168
|
* The class used to create nodes. Subclass [`Node`](/reference/node) to
|
|
156
169
|
* add your own methods. The `Node` class is not exposed on the global
|