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.
- package/README.md +23 -20
- 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 +10 -7
- package/lib/dataLoader.js.map +1 -1
- package/lib/dragAndDropHandler/generateHitAreas.d.ts.map +1 -1
- package/lib/dragAndDropHandler/generateHitAreas.js +3 -2
- package/lib/dragAndDropHandler/generateHitAreas.js.map +1 -1
- package/lib/dragAndDropHandler/index.d.ts +2 -4
- package/lib/dragAndDropHandler/index.d.ts.map +1 -1
- package/lib/dragAndDropHandler/index.js +2 -9
- package/lib/dragAndDropHandler/index.js.map +1 -1
- package/lib/elementsRenderer.d.ts +11 -4
- package/lib/elementsRenderer.d.ts.map +1 -1
- package/lib/elementsRenderer.js +104 -43
- package/lib/elementsRenderer.js.map +1 -1
- package/lib/events.d.ts +13 -7
- package/lib/events.d.ts.map +1 -1
- package/lib/folderOpenState.d.ts +8 -0
- package/lib/folderOpenState.d.ts.map +1 -0
- package/lib/folderOpenState.js +22 -0
- package/lib/folderOpenState.js.map +1 -0
- package/lib/index.d.ts +27 -20
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +166 -65
- package/lib/index.js.map +1 -1
- package/lib/keyHandler.d.ts +16 -3
- package/lib/keyHandler.d.ts.map +1 -1
- package/lib/keyHandler.js +79 -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/mouseHandler.d.ts.map +1 -1
- package/lib/mouseHandler.js +12 -12
- package/lib/mouseHandler.js.map +1 -1
- package/lib/node.d.ts +12 -10
- package/lib/node.d.ts.map +1 -1
- package/lib/node.js +12 -10
- package/lib/node.js.map +1 -1
- package/lib/nodeElement/folderElement.d.ts +7 -5
- package/lib/nodeElement/folderElement.d.ts.map +1 -1
- package/lib/nodeElement/folderElement.js +35 -55
- 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 +29 -14
- package/lib/options.d.ts.map +1 -1
- package/lib/options.js +12 -0
- package/lib/options.js.map +1 -0
- package/lib/saveStateHandler.d.ts.map +1 -1
- package/lib/saveStateHandler.js +6 -6
- package/lib/saveStateHandler.js.map +1 -1
- package/lib/selectNodeHandler.d.ts +3 -0
- package/lib/selectNodeHandler.d.ts.map +1 -1
- package/lib/selectNodeHandler.js +66 -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 +38 -32
- package/src/classNames.ts +4 -4
- package/src/dataLoader.ts +9 -10
- package/src/dragAndDropHandler/generateHitAreas.ts +3 -2
- package/src/dragAndDropHandler/index.ts +3 -17
- package/src/elementsRenderer.ts +124 -55
- package/src/events.ts +13 -7
- package/src/folderOpenState.ts +38 -0
- package/src/index.ts +191 -82
- package/src/keyHandler.ts +104 -34
- package/src/methodTypes.ts +6 -0
- package/src/mouseHandler.ts +15 -13
- package/src/node.ts +12 -10
- package/src/nodeElement/folderElement.ts +47 -72
- package/src/nodeElement/index.ts +20 -17
- package/src/options.ts +46 -14
- package/src/saveStateHandler.ts +5 -8
- package/src/selectNodeHandler.ts +69 -25
- package/src/setDefaultOptions.ts +1 -0
- package/src/version.ts +1 -1
- package/tree_element.css +48 -5
- package/tree_element.debug.js +540 -278
- package/tree_element.debug.js.map +1 -1
- package/tree_element.js +2 -2
- 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.
|
|
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());
|
|
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.
|
|
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(
|
|
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.
|
|
208
|
+
isKeyHandled = this.goToNode(this.getTree()?.children[0]);
|
|
127
209
|
break;
|
|
128
210
|
}
|
|
129
211
|
|
|
130
212
|
default: {
|
|
131
|
-
isKeyHandled =
|
|
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.
|
|
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
|
|
237
|
+
const currentNode = this.getCurrentNode();
|
|
160
238
|
|
|
161
239
|
/* istanbul ignore if */
|
|
162
|
-
if (!
|
|
240
|
+
if (!currentNode) {
|
|
163
241
|
return false;
|
|
164
242
|
}
|
|
165
243
|
|
|
166
|
-
if (
|
|
244
|
+
if (currentNode.isFolder() && currentNode.is_open) {
|
|
167
245
|
// Left on an open node closes the node
|
|
168
|
-
void this.closeNode(
|
|
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.
|
|
250
|
+
return this.goToNode(currentNode.getParent());
|
|
173
251
|
}
|
|
174
252
|
|
|
175
253
|
private moveRight(): boolean {
|
|
176
|
-
const
|
|
254
|
+
const currentNode = this.getCurrentNode();
|
|
177
255
|
|
|
178
|
-
if (
|
|
256
|
+
if (currentNode?.isFolder()) {
|
|
179
257
|
// folder node
|
|
180
|
-
if (
|
|
258
|
+
if (currentNode.is_open) {
|
|
181
259
|
// Right moves to the first child of an open node
|
|
182
|
-
return this.
|
|
260
|
+
return this.goToNode(currentNode.getNextVisibleNode());
|
|
183
261
|
}
|
|
184
262
|
// Right expands a closed node
|
|
185
|
-
void this.openNode(
|
|
263
|
+
void this.openNode(currentNode);
|
|
186
264
|
return true;
|
|
187
265
|
}
|
|
188
266
|
return false;
|
|
189
267
|
}
|
|
190
268
|
|
|
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;
|
|
269
|
+
private toggle(node: Node): boolean {
|
|
270
|
+
this.toggleNodeInSelection(node);
|
|
271
|
+
return true;
|
|
202
272
|
}
|
|
203
273
|
}
|
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,
|
package/src/mouseHandler.ts
CHANGED
|
@@ -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
|
-
|
|
194
|
-
|
|
195
|
-
e.preventDefault();
|
|
196
|
-
e.stopPropagation();
|
|
192
|
+
if (!div) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
197
195
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
196
|
+
const node = this.getNode(div);
|
|
197
|
+
|
|
198
|
+
if (!node) {
|
|
199
|
+
return;
|
|
204
200
|
}
|
|
205
201
|
|
|
206
|
-
|
|
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
|
|
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)
|
|
38
|
-
* friends
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 (
|
|
60
|
-
this.
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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.
|
|
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;
|
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
|
}
|