react-arborist 3.4.3 → 3.6.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 +50 -6
- package/dist/main/components/provider.js +8 -1
- package/dist/main/index.d.ts +2 -0
- package/dist/main/index.js +3 -1
- package/dist/main/interfaces/node-api.d.ts +1 -0
- package/dist/main/interfaces/node-api.js +3 -0
- package/dist/main/interfaces/tree-api.d.ts +14 -8
- package/dist/main/interfaces/tree-api.js +47 -24
- package/dist/main/types/tree-props.d.ts +5 -1
- package/dist/main/utils.d.ts +54 -0
- package/dist/main/utils.js +68 -0
- package/dist/module/components/provider.js +8 -1
- package/dist/module/index.d.ts +2 -0
- package/dist/module/index.js +1 -0
- package/dist/module/interfaces/node-api.d.ts +1 -0
- package/dist/module/interfaces/node-api.js +3 -0
- package/dist/module/interfaces/tree-api.d.ts +14 -8
- package/dist/module/interfaces/tree-api.js +47 -24
- package/dist/module/types/tree-props.d.ts +5 -1
- package/dist/module/utils.d.ts +54 -0
- package/dist/module/utils.js +67 -0
- package/package.json +4 -4
- package/src/components/provider.tsx +8 -3
- package/src/index.ts +2 -0
- package/src/interfaces/node-api.ts +4 -0
- package/src/interfaces/tree-api.ts +61 -30
- package/src/types/tree-props.ts +6 -1
- package/src/utils.ts +81 -0
package/README.md
CHANGED
|
@@ -268,6 +268,8 @@ const { ref, width, height } = useResizeObserver();
|
|
|
268
268
|
- Interfaces
|
|
269
269
|
- [Node API](#node-api-reference)
|
|
270
270
|
- [Tree API](#tree-api-reference)
|
|
271
|
+
- Utilities
|
|
272
|
+
- [getTreeLinePrefix](#gettreelineprefix)
|
|
271
273
|
|
|
272
274
|
## Tree Component Props
|
|
273
275
|
|
|
@@ -303,11 +305,12 @@ interface TreeProps<T> {
|
|
|
303
305
|
padding?: number;
|
|
304
306
|
|
|
305
307
|
/* Config */
|
|
306
|
-
childrenAccessor?: string | ((d: T) => T[] | null);
|
|
308
|
+
childrenAccessor?: string | ((d: T) => readonly T[] | null);
|
|
307
309
|
idAccessor?: string | ((d: T) => string);
|
|
308
310
|
openByDefault?: boolean;
|
|
309
311
|
selectionFollowsFocus?: boolean;
|
|
310
312
|
disableMultiSelection?: boolean;
|
|
313
|
+
disableSelect?: string | boolean | BoolFunc<T>;
|
|
311
314
|
disableEdit?: string | boolean | BoolFunc<T>;
|
|
312
315
|
disableDrag?: string | boolean | BoolFunc<T>;
|
|
313
316
|
disableDrop?:
|
|
@@ -343,7 +346,11 @@ interface TreeProps<T> {
|
|
|
343
346
|
dndRootElement?: globalThis.Node | null;
|
|
344
347
|
onClick?: MouseEventHandler;
|
|
345
348
|
onContextMenu?: MouseEventHandler;
|
|
346
|
-
|
|
349
|
+
dndBackend?: Extract<
|
|
350
|
+
DndProviderProps<unknown, unknown>,
|
|
351
|
+
{ backend: unknown }
|
|
352
|
+
>["backend"];
|
|
353
|
+
dndManager?: ReturnType<typeof useDragDropManager>;
|
|
347
354
|
}
|
|
348
355
|
```
|
|
349
356
|
|
|
@@ -648,17 +655,17 @@ _tree_.**isSelected**(_id_) : _boolean_
|
|
|
648
655
|
|
|
649
656
|
Returns true if the node with _id_ is selected.
|
|
650
657
|
|
|
651
|
-
_tree_.**select**(_id_)
|
|
658
|
+
_tree_.**select**(_id_, _[opts]_)
|
|
652
659
|
|
|
653
|
-
Select only the node with _id_.
|
|
660
|
+
Select only the node with _id_. Accepts an optional options object: `{ align?: "auto" | "smart" | "center" | "end" | "start"; focus?: boolean }`. `align` is forwarded to the scroll behavior; passing `focus: false` suppresses the focus change and the `onFocus` callback.
|
|
654
661
|
|
|
655
662
|
_tree_.**deselect**(_id_)
|
|
656
663
|
|
|
657
664
|
Deselect the node with _id_.
|
|
658
665
|
|
|
659
|
-
_tree_.**selectMulti**(_id_)
|
|
666
|
+
_tree_.**selectMulti**(_id_, _[opts]_)
|
|
660
667
|
|
|
661
|
-
Add to the selection the node with _id_.
|
|
668
|
+
Add to the selection the node with _id_. Accepts the same options object as `select`.
|
|
662
669
|
|
|
663
670
|
_tree_.**selectContiguous**(_id_)
|
|
664
671
|
|
|
@@ -740,6 +747,43 @@ _tree_.**root** : _NodeApi_
|
|
|
740
747
|
|
|
741
748
|
Returns the root _NodeApi_ instance. Its children are the Node representations of the _data_ prop array.
|
|
742
749
|
|
|
750
|
+
## Utilities
|
|
751
|
+
|
|
752
|
+
### getTreeLinePrefix
|
|
753
|
+
|
|
754
|
+
Generates a tree-line prefix string (using Unix `tree`-style box-drawing characters like `├`, `└`, `│`) for a given node. Useful when you want ASCII/Unicode connector lines in a custom node renderer.
|
|
755
|
+
|
|
756
|
+
```ts
|
|
757
|
+
function getTreeLinePrefix(
|
|
758
|
+
node: NodeApi<any>,
|
|
759
|
+
chars?: Partial<TreeLineChars>
|
|
760
|
+
): string;
|
|
761
|
+
|
|
762
|
+
type TreeLineChars = {
|
|
763
|
+
last: string; // default: "└ "
|
|
764
|
+
middle: string; // default: "├ "
|
|
765
|
+
pipe: string; // default: "│ "
|
|
766
|
+
blank: string; // default: "\u3000 "
|
|
767
|
+
};
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
Wrap the prefix in a monospace span so the connectors line up:
|
|
771
|
+
|
|
772
|
+
```tsx
|
|
773
|
+
import { Tree, getTreeLinePrefix } from "react-arborist";
|
|
774
|
+
|
|
775
|
+
function Node({ node, style }) {
|
|
776
|
+
return (
|
|
777
|
+
<div style={style}>
|
|
778
|
+
<span style={{ fontFamily: "monospace" }}>{getTreeLinePrefix(node)}</span>
|
|
779
|
+
{node.data.name}
|
|
780
|
+
</div>
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
Pass a partial `chars` object to override any of the default characters (e.g. for an ASCII-only style).
|
|
786
|
+
|
|
743
787
|
## Author
|
|
744
788
|
|
|
745
789
|
[James Kerr](https://twitter.com/specialCaseDev) at [Brim Data](https://brimdata.io) for the [Zui desktop app](https://www.youtube.com/watch?v=I2y663n8d2A).
|
|
@@ -47,5 +47,12 @@ function TreeProvider({ treeProps, imperativeHandle, children, }) {
|
|
|
47
47
|
store.current.dispatch(open_slice_1.actions.clear(true));
|
|
48
48
|
}
|
|
49
49
|
}, [api.props.searchTerm]);
|
|
50
|
-
return ((0, jsx_runtime_1.jsx)(context_1.TreeApiContext.Provider, { value: api, children: (0, jsx_runtime_1.jsx)(context_1.DataUpdatesContext.Provider, { value: updateCount.current, children: (0, jsx_runtime_1.jsx)(context_1.NodesContext.Provider, { value: state.nodes, children: (0, jsx_runtime_1.jsx)(context_1.DndContext.Provider, { value: state.dnd, children: (0, jsx_runtime_1.jsx)(react_dnd_1.DndProvider, Object.assign({
|
|
50
|
+
return ((0, jsx_runtime_1.jsx)(context_1.TreeApiContext.Provider, { value: api, children: (0, jsx_runtime_1.jsx)(context_1.DataUpdatesContext.Provider, { value: updateCount.current, children: (0, jsx_runtime_1.jsx)(context_1.NodesContext.Provider, { value: state.nodes, children: (0, jsx_runtime_1.jsx)(context_1.DndContext.Provider, { value: state.dnd, children: (0, jsx_runtime_1.jsx)(react_dnd_1.DndProvider, Object.assign({}, (treeProps.dndManager
|
|
51
|
+
? { manager: treeProps.dndManager }
|
|
52
|
+
: {
|
|
53
|
+
backend: treeProps.dndBackend || react_dnd_html5_backend_1.HTML5Backend,
|
|
54
|
+
options: {
|
|
55
|
+
rootElement: api.props.dndRootElement || undefined,
|
|
56
|
+
},
|
|
57
|
+
}), { children: children })) }) }) }) }));
|
|
51
58
|
}
|
package/dist/main/index.d.ts
CHANGED
package/dist/main/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.Tree = void 0;
|
|
17
|
+
exports.getTreeLinePrefix = exports.Tree = void 0;
|
|
18
18
|
/* The Public Api */
|
|
19
19
|
var tree_1 = require("./components/tree");
|
|
20
20
|
Object.defineProperty(exports, "Tree", { enumerable: true, get: function () { return tree_1.Tree; } });
|
|
@@ -25,3 +25,5 @@ __exportStar(require("./interfaces/node-api"), exports);
|
|
|
25
25
|
__exportStar(require("./interfaces/tree-api"), exports);
|
|
26
26
|
__exportStar(require("./data/simple-tree"), exports);
|
|
27
27
|
__exportStar(require("./hooks/use-simple-tree"), exports);
|
|
28
|
+
var utils_1 = require("./utils");
|
|
29
|
+
Object.defineProperty(exports, "getTreeLinePrefix", { enumerable: true, get: function () { return utils_1.getTreeLinePrefix; } });
|
|
@@ -136,7 +136,7 @@ export declare class TreeApi<T> {
|
|
|
136
136
|
get(id: string | null): NodeApi<T> | null;
|
|
137
137
|
at(index: number): NodeApi<T> | null;
|
|
138
138
|
nodesBetween(startId: string | null, endId: string | null): NodeApi<T>[];
|
|
139
|
-
indexOf(id:
|
|
139
|
+
indexOf(id: Identity): number | null;
|
|
140
140
|
get editingId(): string | null;
|
|
141
141
|
createInternal(): Promise<void>;
|
|
142
142
|
createLeaf(): Promise<void>;
|
|
@@ -145,11 +145,11 @@ export declare class TreeApi<T> {
|
|
|
145
145
|
parentId?: null | string;
|
|
146
146
|
index?: null | number;
|
|
147
147
|
}): Promise<void>;
|
|
148
|
-
delete(node:
|
|
148
|
+
delete(node: Identity | string[] | IdObj[]): Promise<void>;
|
|
149
149
|
edit(node: string | IdObj): Promise<EditResult>;
|
|
150
150
|
submit(identity: Identity, value: string): Promise<void>;
|
|
151
151
|
reset(): void;
|
|
152
|
-
activate(id:
|
|
152
|
+
activate(id: Identity): void;
|
|
153
153
|
private resolveEdit;
|
|
154
154
|
get selectedIds(): Set<string>;
|
|
155
155
|
get selectedNodes(): NodeApi<T>[];
|
|
@@ -163,14 +163,18 @@ export declare class TreeApi<T> {
|
|
|
163
163
|
focus?: boolean;
|
|
164
164
|
}): void;
|
|
165
165
|
deselect(node: Identity): void;
|
|
166
|
-
selectMulti(identity: Identity
|
|
166
|
+
selectMulti(identity: Identity, opts?: {
|
|
167
|
+
align?: Align;
|
|
168
|
+
focus?: boolean;
|
|
169
|
+
}): void;
|
|
167
170
|
selectContiguous(identity: Identity): void;
|
|
168
171
|
deselectAll(): void;
|
|
169
172
|
selectAll(): void;
|
|
173
|
+
private filterSelectableNodes;
|
|
170
174
|
setSelection(args: {
|
|
171
175
|
ids: (IdObj | string)[] | null;
|
|
172
|
-
anchor:
|
|
173
|
-
mostRecent:
|
|
176
|
+
anchor: Identity;
|
|
177
|
+
mostRecent: Identity;
|
|
174
178
|
}): void;
|
|
175
179
|
get cursorParentId(): string | null;
|
|
176
180
|
get cursorOverFolder(): boolean;
|
|
@@ -199,10 +203,12 @@ export declare class TreeApi<T> {
|
|
|
199
203
|
isOpen(id?: string): boolean;
|
|
200
204
|
isEditable(data: T): boolean;
|
|
201
205
|
isDraggable(data: T): boolean;
|
|
202
|
-
|
|
206
|
+
isSelectable(data: T): boolean;
|
|
207
|
+
private isActionPossible;
|
|
208
|
+
isDragging(node: Identity): boolean;
|
|
203
209
|
isFocused(id: string): boolean;
|
|
204
210
|
isMatch(node: NodeApi<T>): boolean;
|
|
205
|
-
willReceiveDrop(node:
|
|
211
|
+
willReceiveDrop(node: Identity): boolean;
|
|
206
212
|
onFocus(): void;
|
|
207
213
|
onBlur(): void;
|
|
208
214
|
onItemsRendered(args: ListOnItemsRenderedProps): void;
|
|
@@ -327,20 +327,24 @@ class TreeApi {
|
|
|
327
327
|
this.focus(this.at(index));
|
|
328
328
|
}
|
|
329
329
|
select(node, opts = {}) {
|
|
330
|
+
var _a;
|
|
330
331
|
if (!node)
|
|
331
332
|
return;
|
|
332
333
|
const changeFocus = opts.focus !== false;
|
|
333
334
|
const id = identify(node);
|
|
334
335
|
if (changeFocus)
|
|
335
336
|
this.dispatch((0, focus_slice_1.focus)(id));
|
|
336
|
-
this.
|
|
337
|
-
|
|
338
|
-
|
|
337
|
+
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
|
338
|
+
this.setSelection({
|
|
339
|
+
ids: [id],
|
|
340
|
+
anchor: id,
|
|
341
|
+
mostRecent: id,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
339
344
|
this.scrollTo(id, opts.align);
|
|
340
345
|
if (this.focusedNode && changeFocus) {
|
|
341
346
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
342
347
|
}
|
|
343
|
-
safeRun(this.props.onSelect, this.selectedNodes);
|
|
344
348
|
}
|
|
345
349
|
deselect(node) {
|
|
346
350
|
if (!node)
|
|
@@ -349,28 +353,37 @@ class TreeApi {
|
|
|
349
353
|
this.dispatch(selection_slice_1.actions.remove(id));
|
|
350
354
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
351
355
|
}
|
|
352
|
-
selectMulti(identity) {
|
|
356
|
+
selectMulti(identity, opts = {}) {
|
|
353
357
|
const node = this.get(identifyNull(identity));
|
|
354
358
|
if (!node)
|
|
355
359
|
return;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
360
|
+
const changeFocus = opts.focus !== false;
|
|
361
|
+
if (changeFocus)
|
|
362
|
+
this.dispatch((0, focus_slice_1.focus)(node.id));
|
|
363
|
+
if (node.isSelectable) {
|
|
364
|
+
this.dispatch(selection_slice_1.actions.add(node.id));
|
|
365
|
+
this.dispatch(selection_slice_1.actions.anchor(node.id));
|
|
366
|
+
this.dispatch(selection_slice_1.actions.mostRecent(node.id));
|
|
367
|
+
}
|
|
368
|
+
this.scrollTo(node, opts.align);
|
|
369
|
+
if (this.focusedNode && changeFocus) {
|
|
362
370
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
371
|
+
}
|
|
363
372
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
364
373
|
}
|
|
365
374
|
selectContiguous(identity) {
|
|
375
|
+
var _a;
|
|
366
376
|
if (!identity)
|
|
367
377
|
return;
|
|
368
378
|
const id = identify(identity);
|
|
369
|
-
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
370
379
|
this.dispatch((0, focus_slice_1.focus)(id));
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
380
|
+
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
|
381
|
+
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
382
|
+
const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, identifyNull(id)));
|
|
383
|
+
this.dispatch(selection_slice_1.actions.remove(this.nodesBetween(anchor, mostRecent)));
|
|
384
|
+
this.dispatch(selection_slice_1.actions.add(selectableNodes));
|
|
385
|
+
this.dispatch(selection_slice_1.actions.mostRecent(id));
|
|
386
|
+
}
|
|
374
387
|
this.scrollTo(id);
|
|
375
388
|
if (this.focusedNode)
|
|
376
389
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
@@ -381,17 +394,23 @@ class TreeApi {
|
|
|
381
394
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
382
395
|
}
|
|
383
396
|
selectAll() {
|
|
384
|
-
var _a;
|
|
397
|
+
var _a, _b, _c;
|
|
398
|
+
const allSelectableNodes = this.filterSelectableNodes(Object.keys(this.idToIndex));
|
|
385
399
|
this.setSelection({
|
|
386
|
-
ids:
|
|
387
|
-
anchor:
|
|
388
|
-
mostRecent:
|
|
400
|
+
ids: allSelectableNodes,
|
|
401
|
+
anchor: (_a = allSelectableNodes[0]) !== null && _a !== void 0 ? _a : null,
|
|
402
|
+
mostRecent: (_b = allSelectableNodes[allSelectableNodes.length - 1]) !== null && _b !== void 0 ? _b : null,
|
|
389
403
|
});
|
|
390
|
-
this.dispatch((0, focus_slice_1.focus)((
|
|
404
|
+
this.dispatch((0, focus_slice_1.focus)((_c = this.lastNode) === null || _c === void 0 ? void 0 : _c.id));
|
|
391
405
|
if (this.focusedNode)
|
|
392
406
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
393
407
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
394
408
|
}
|
|
409
|
+
filterSelectableNodes(nodes) {
|
|
410
|
+
return nodes
|
|
411
|
+
.map((n) => this.get(identify(n)))
|
|
412
|
+
.filter((n) => !!n && n.isSelectable);
|
|
413
|
+
}
|
|
395
414
|
setSelection(args) {
|
|
396
415
|
var _a;
|
|
397
416
|
const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map(identify));
|
|
@@ -589,12 +608,16 @@ class TreeApi {
|
|
|
589
608
|
}
|
|
590
609
|
}
|
|
591
610
|
isEditable(data) {
|
|
592
|
-
|
|
593
|
-
return !utils.access(data, check);
|
|
611
|
+
return this.isActionPossible(data, this.props.disableEdit);
|
|
594
612
|
}
|
|
595
613
|
isDraggable(data) {
|
|
596
|
-
|
|
597
|
-
|
|
614
|
+
return this.isActionPossible(data, this.props.disableDrag);
|
|
615
|
+
}
|
|
616
|
+
isSelectable(data) {
|
|
617
|
+
return this.isActionPossible(data, this.props.disableSelect);
|
|
618
|
+
}
|
|
619
|
+
isActionPossible(data, disabler = () => false) {
|
|
620
|
+
return !utils.access(data, disabler);
|
|
598
621
|
}
|
|
599
622
|
isDragging(node) {
|
|
600
623
|
const id = identifyNull(node);
|
|
@@ -5,7 +5,7 @@ import { ElementType, MouseEventHandler } from "react";
|
|
|
5
5
|
import { ListOnScrollProps } from "react-window";
|
|
6
6
|
import { NodeApi } from "../interfaces/node-api";
|
|
7
7
|
import { OpenMap } from "../state/open-slice";
|
|
8
|
-
import { useDragDropManager } from "react-dnd";
|
|
8
|
+
import { useDragDropManager, DndProviderProps } from "react-dnd";
|
|
9
9
|
export interface TreeProps<T> {
|
|
10
10
|
data?: readonly T[];
|
|
11
11
|
initialData?: readonly T[];
|
|
@@ -31,6 +31,7 @@ export interface TreeProps<T> {
|
|
|
31
31
|
openByDefault?: boolean;
|
|
32
32
|
selectionFollowsFocus?: boolean;
|
|
33
33
|
disableMultiSelection?: boolean;
|
|
34
|
+
disableSelect?: string | boolean | BoolFunc<T>;
|
|
34
35
|
disableEdit?: string | boolean | BoolFunc<T>;
|
|
35
36
|
disableDrag?: string | boolean | BoolFunc<T>;
|
|
36
37
|
disableDrop?: string | boolean | ((args: {
|
|
@@ -52,5 +53,8 @@ export interface TreeProps<T> {
|
|
|
52
53
|
dndRootElement?: globalThis.Node | null;
|
|
53
54
|
onClick?: MouseEventHandler;
|
|
54
55
|
onContextMenu?: MouseEventHandler;
|
|
56
|
+
dndBackend?: Extract<DndProviderProps<unknown, unknown>, {
|
|
57
|
+
backend: unknown;
|
|
58
|
+
}>["backend"];
|
|
55
59
|
dndManager?: ReturnType<typeof useDragDropManager>;
|
|
56
60
|
}
|
package/dist/main/utils.d.ts
CHANGED
|
@@ -22,4 +22,58 @@ export declare function mergeRefs(...refs: any): (instance: any) => void;
|
|
|
22
22
|
export declare function safeRun<T extends (...args: any[]) => any>(fn: T | undefined, ...args: Parameters<T>): any;
|
|
23
23
|
export declare function waitFor(fn: () => boolean): Promise<void>;
|
|
24
24
|
export declare function getInsertIndex(tree: TreeApi<any>): number;
|
|
25
|
+
export type TreeLineChars = {
|
|
26
|
+
last: string;
|
|
27
|
+
middle: string;
|
|
28
|
+
pipe: string;
|
|
29
|
+
blank: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Generate a tree-line prefix string for a node.
|
|
33
|
+
*
|
|
34
|
+
* Returns characters like `├ `, `└ `, `│` that visually connect
|
|
35
|
+
* parent and child nodes, similar to the Unix `tree` command.
|
|
36
|
+
*
|
|
37
|
+
* **Styling note:** The prefix uses Box Drawing characters (`│`, `├`, `└`)
|
|
38
|
+
* which require a monospace font for correct alignment. Wrap the prefix
|
|
39
|
+
* in a `<span>` with `fontFamily: "monospace"` and use a consistent
|
|
40
|
+
* `fontSize` (e.g. 14–16px). Inherited `line-height` or `font-size`
|
|
41
|
+
* from parent elements can cause misalignment.
|
|
42
|
+
*
|
|
43
|
+
* @example Basic usage
|
|
44
|
+
* ```tsx
|
|
45
|
+
* function MyNode({ node, style }: NodeRendererProps<MyData>) {
|
|
46
|
+
* return (
|
|
47
|
+
* <div style={style}>
|
|
48
|
+
* <span style={{ fontFamily: "monospace", fontSize: 14 }}>
|
|
49
|
+
* {getTreeLinePrefix(node)}
|
|
50
|
+
* </span>
|
|
51
|
+
* {node.data.name}
|
|
52
|
+
* </div>
|
|
53
|
+
* );
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @example With folder/file icons
|
|
58
|
+
* ```tsx
|
|
59
|
+
* function MyNode({ node, style }: NodeRendererProps<MyData>) {
|
|
60
|
+
* const icon = node.isLeaf ? "📄" : node.isOpen ? "📂" : "📁";
|
|
61
|
+
* return (
|
|
62
|
+
* <div style={style}>
|
|
63
|
+
* <span style={{ fontFamily: "monospace", fontSize: 16 }}>
|
|
64
|
+
* {getTreeLinePrefix(node)}
|
|
65
|
+
* </span>
|
|
66
|
+
* {icon} {node.data.name}
|
|
67
|
+
* </div>
|
|
68
|
+
* );
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example Custom characters
|
|
73
|
+
* ```tsx
|
|
74
|
+
* // ASCII-only style
|
|
75
|
+
* getTreeLinePrefix(node, { last: "`- ", middle: "|- ", pipe: "|", blank: " " })
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare function getTreeLinePrefix(node: NodeApi<any>, chars?: Partial<TreeLineChars>): string;
|
|
25
79
|
export declare function getInsertParentId(tree: TreeApi<any>): string | null;
|
package/dist/main/utils.js
CHANGED
|
@@ -17,6 +17,7 @@ exports.mergeRefs = mergeRefs;
|
|
|
17
17
|
exports.safeRun = safeRun;
|
|
18
18
|
exports.waitFor = waitFor;
|
|
19
19
|
exports.getInsertIndex = getInsertIndex;
|
|
20
|
+
exports.getTreeLinePrefix = getTreeLinePrefix;
|
|
20
21
|
exports.getInsertParentId = getInsertParentId;
|
|
21
22
|
function bound(n, min, max) {
|
|
22
23
|
return Math.max(Math.min(n, max), min);
|
|
@@ -176,6 +177,73 @@ function getInsertIndex(tree) {
|
|
|
176
177
|
return focus.childIndex + 1;
|
|
177
178
|
return 0;
|
|
178
179
|
}
|
|
180
|
+
const defaultTreeLineChars = {
|
|
181
|
+
last: "└ ",
|
|
182
|
+
middle: "├ ",
|
|
183
|
+
pipe: "│ ",
|
|
184
|
+
blank: "\u3000 ",
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Generate a tree-line prefix string for a node.
|
|
188
|
+
*
|
|
189
|
+
* Returns characters like `├ `, `└ `, `│` that visually connect
|
|
190
|
+
* parent and child nodes, similar to the Unix `tree` command.
|
|
191
|
+
*
|
|
192
|
+
* **Styling note:** The prefix uses Box Drawing characters (`│`, `├`, `└`)
|
|
193
|
+
* which require a monospace font for correct alignment. Wrap the prefix
|
|
194
|
+
* in a `<span>` with `fontFamily: "monospace"` and use a consistent
|
|
195
|
+
* `fontSize` (e.g. 14–16px). Inherited `line-height` or `font-size`
|
|
196
|
+
* from parent elements can cause misalignment.
|
|
197
|
+
*
|
|
198
|
+
* @example Basic usage
|
|
199
|
+
* ```tsx
|
|
200
|
+
* function MyNode({ node, style }: NodeRendererProps<MyData>) {
|
|
201
|
+
* return (
|
|
202
|
+
* <div style={style}>
|
|
203
|
+
* <span style={{ fontFamily: "monospace", fontSize: 14 }}>
|
|
204
|
+
* {getTreeLinePrefix(node)}
|
|
205
|
+
* </span>
|
|
206
|
+
* {node.data.name}
|
|
207
|
+
* </div>
|
|
208
|
+
* );
|
|
209
|
+
* }
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* @example With folder/file icons
|
|
213
|
+
* ```tsx
|
|
214
|
+
* function MyNode({ node, style }: NodeRendererProps<MyData>) {
|
|
215
|
+
* const icon = node.isLeaf ? "📄" : node.isOpen ? "📂" : "📁";
|
|
216
|
+
* return (
|
|
217
|
+
* <div style={style}>
|
|
218
|
+
* <span style={{ fontFamily: "monospace", fontSize: 16 }}>
|
|
219
|
+
* {getTreeLinePrefix(node)}
|
|
220
|
+
* </span>
|
|
221
|
+
* {icon} {node.data.name}
|
|
222
|
+
* </div>
|
|
223
|
+
* );
|
|
224
|
+
* }
|
|
225
|
+
* ```
|
|
226
|
+
*
|
|
227
|
+
* @example Custom characters
|
|
228
|
+
* ```tsx
|
|
229
|
+
* // ASCII-only style
|
|
230
|
+
* getTreeLinePrefix(node, { last: "`- ", middle: "|- ", pipe: "|", blank: " " })
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
function getTreeLinePrefix(node, chars = {}) {
|
|
234
|
+
const c = Object.assign(Object.assign({}, defaultTreeLineChars), chars);
|
|
235
|
+
if (node.level === 0)
|
|
236
|
+
return "";
|
|
237
|
+
const isLast = node.nextSibling === null;
|
|
238
|
+
let prefix = isLast ? c.last : c.middle;
|
|
239
|
+
let ancestor = node.parent;
|
|
240
|
+
while (ancestor && ancestor.level > 0) {
|
|
241
|
+
const isAncestorLast = ancestor.nextSibling === null;
|
|
242
|
+
prefix = (isAncestorLast ? c.blank : c.pipe) + prefix;
|
|
243
|
+
ancestor = ancestor.parent;
|
|
244
|
+
}
|
|
245
|
+
return prefix;
|
|
246
|
+
}
|
|
179
247
|
function getInsertParentId(tree) {
|
|
180
248
|
const focus = tree.focusedNode;
|
|
181
249
|
if (!focus)
|
|
@@ -44,5 +44,12 @@ export function TreeProvider({ treeProps, imperativeHandle, children, }) {
|
|
|
44
44
|
store.current.dispatch(visibility.clear(true));
|
|
45
45
|
}
|
|
46
46
|
}, [api.props.searchTerm]);
|
|
47
|
-
return (_jsx(TreeApiContext.Provider, { value: api, children: _jsx(DataUpdatesContext.Provider, { value: updateCount.current, children: _jsx(NodesContext.Provider, { value: state.nodes, children: _jsx(DndContext.Provider, { value: state.dnd, children: _jsx(DndProvider, Object.assign({
|
|
47
|
+
return (_jsx(TreeApiContext.Provider, { value: api, children: _jsx(DataUpdatesContext.Provider, { value: updateCount.current, children: _jsx(NodesContext.Provider, { value: state.nodes, children: _jsx(DndContext.Provider, { value: state.dnd, children: _jsx(DndProvider, Object.assign({}, (treeProps.dndManager
|
|
48
|
+
? { manager: treeProps.dndManager }
|
|
49
|
+
: {
|
|
50
|
+
backend: treeProps.dndBackend || HTML5Backend,
|
|
51
|
+
options: {
|
|
52
|
+
rootElement: api.props.dndRootElement || undefined,
|
|
53
|
+
},
|
|
54
|
+
}), { children: children })) }) }) }) }));
|
|
48
55
|
}
|
package/dist/module/index.d.ts
CHANGED
package/dist/module/index.js
CHANGED
|
@@ -136,7 +136,7 @@ export declare class TreeApi<T> {
|
|
|
136
136
|
get(id: string | null): NodeApi<T> | null;
|
|
137
137
|
at(index: number): NodeApi<T> | null;
|
|
138
138
|
nodesBetween(startId: string | null, endId: string | null): NodeApi<T>[];
|
|
139
|
-
indexOf(id:
|
|
139
|
+
indexOf(id: Identity): number | null;
|
|
140
140
|
get editingId(): string | null;
|
|
141
141
|
createInternal(): Promise<void>;
|
|
142
142
|
createLeaf(): Promise<void>;
|
|
@@ -145,11 +145,11 @@ export declare class TreeApi<T> {
|
|
|
145
145
|
parentId?: null | string;
|
|
146
146
|
index?: null | number;
|
|
147
147
|
}): Promise<void>;
|
|
148
|
-
delete(node:
|
|
148
|
+
delete(node: Identity | string[] | IdObj[]): Promise<void>;
|
|
149
149
|
edit(node: string | IdObj): Promise<EditResult>;
|
|
150
150
|
submit(identity: Identity, value: string): Promise<void>;
|
|
151
151
|
reset(): void;
|
|
152
|
-
activate(id:
|
|
152
|
+
activate(id: Identity): void;
|
|
153
153
|
private resolveEdit;
|
|
154
154
|
get selectedIds(): Set<string>;
|
|
155
155
|
get selectedNodes(): NodeApi<T>[];
|
|
@@ -163,14 +163,18 @@ export declare class TreeApi<T> {
|
|
|
163
163
|
focus?: boolean;
|
|
164
164
|
}): void;
|
|
165
165
|
deselect(node: Identity): void;
|
|
166
|
-
selectMulti(identity: Identity
|
|
166
|
+
selectMulti(identity: Identity, opts?: {
|
|
167
|
+
align?: Align;
|
|
168
|
+
focus?: boolean;
|
|
169
|
+
}): void;
|
|
167
170
|
selectContiguous(identity: Identity): void;
|
|
168
171
|
deselectAll(): void;
|
|
169
172
|
selectAll(): void;
|
|
173
|
+
private filterSelectableNodes;
|
|
170
174
|
setSelection(args: {
|
|
171
175
|
ids: (IdObj | string)[] | null;
|
|
172
|
-
anchor:
|
|
173
|
-
mostRecent:
|
|
176
|
+
anchor: Identity;
|
|
177
|
+
mostRecent: Identity;
|
|
174
178
|
}): void;
|
|
175
179
|
get cursorParentId(): string | null;
|
|
176
180
|
get cursorOverFolder(): boolean;
|
|
@@ -199,10 +203,12 @@ export declare class TreeApi<T> {
|
|
|
199
203
|
isOpen(id?: string): boolean;
|
|
200
204
|
isEditable(data: T): boolean;
|
|
201
205
|
isDraggable(data: T): boolean;
|
|
202
|
-
|
|
206
|
+
isSelectable(data: T): boolean;
|
|
207
|
+
private isActionPossible;
|
|
208
|
+
isDragging(node: Identity): boolean;
|
|
203
209
|
isFocused(id: string): boolean;
|
|
204
210
|
isMatch(node: NodeApi<T>): boolean;
|
|
205
|
-
willReceiveDrop(node:
|
|
211
|
+
willReceiveDrop(node: Identity): boolean;
|
|
206
212
|
onFocus(): void;
|
|
207
213
|
onBlur(): void;
|
|
208
214
|
onItemsRendered(args: ListOnItemsRenderedProps): void;
|
|
@@ -301,20 +301,24 @@ export class TreeApi {
|
|
|
301
301
|
this.focus(this.at(index));
|
|
302
302
|
}
|
|
303
303
|
select(node, opts = {}) {
|
|
304
|
+
var _a;
|
|
304
305
|
if (!node)
|
|
305
306
|
return;
|
|
306
307
|
const changeFocus = opts.focus !== false;
|
|
307
308
|
const id = identify(node);
|
|
308
309
|
if (changeFocus)
|
|
309
310
|
this.dispatch(focus(id));
|
|
310
|
-
this.
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
|
312
|
+
this.setSelection({
|
|
313
|
+
ids: [id],
|
|
314
|
+
anchor: id,
|
|
315
|
+
mostRecent: id,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
313
318
|
this.scrollTo(id, opts.align);
|
|
314
319
|
if (this.focusedNode && changeFocus) {
|
|
315
320
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
316
321
|
}
|
|
317
|
-
safeRun(this.props.onSelect, this.selectedNodes);
|
|
318
322
|
}
|
|
319
323
|
deselect(node) {
|
|
320
324
|
if (!node)
|
|
@@ -323,28 +327,37 @@ export class TreeApi {
|
|
|
323
327
|
this.dispatch(selection.remove(id));
|
|
324
328
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
325
329
|
}
|
|
326
|
-
selectMulti(identity) {
|
|
330
|
+
selectMulti(identity, opts = {}) {
|
|
327
331
|
const node = this.get(identifyNull(identity));
|
|
328
332
|
if (!node)
|
|
329
333
|
return;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
334
|
+
const changeFocus = opts.focus !== false;
|
|
335
|
+
if (changeFocus)
|
|
336
|
+
this.dispatch(focus(node.id));
|
|
337
|
+
if (node.isSelectable) {
|
|
338
|
+
this.dispatch(selection.add(node.id));
|
|
339
|
+
this.dispatch(selection.anchor(node.id));
|
|
340
|
+
this.dispatch(selection.mostRecent(node.id));
|
|
341
|
+
}
|
|
342
|
+
this.scrollTo(node, opts.align);
|
|
343
|
+
if (this.focusedNode && changeFocus) {
|
|
336
344
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
345
|
+
}
|
|
337
346
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
338
347
|
}
|
|
339
348
|
selectContiguous(identity) {
|
|
349
|
+
var _a;
|
|
340
350
|
if (!identity)
|
|
341
351
|
return;
|
|
342
352
|
const id = identify(identity);
|
|
343
|
-
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
344
353
|
this.dispatch(focus(id));
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
354
|
+
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
|
355
|
+
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
356
|
+
const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, identifyNull(id)));
|
|
357
|
+
this.dispatch(selection.remove(this.nodesBetween(anchor, mostRecent)));
|
|
358
|
+
this.dispatch(selection.add(selectableNodes));
|
|
359
|
+
this.dispatch(selection.mostRecent(id));
|
|
360
|
+
}
|
|
348
361
|
this.scrollTo(id);
|
|
349
362
|
if (this.focusedNode)
|
|
350
363
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
@@ -355,17 +368,23 @@ export class TreeApi {
|
|
|
355
368
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
356
369
|
}
|
|
357
370
|
selectAll() {
|
|
358
|
-
var _a;
|
|
371
|
+
var _a, _b, _c;
|
|
372
|
+
const allSelectableNodes = this.filterSelectableNodes(Object.keys(this.idToIndex));
|
|
359
373
|
this.setSelection({
|
|
360
|
-
ids:
|
|
361
|
-
anchor:
|
|
362
|
-
mostRecent:
|
|
374
|
+
ids: allSelectableNodes,
|
|
375
|
+
anchor: (_a = allSelectableNodes[0]) !== null && _a !== void 0 ? _a : null,
|
|
376
|
+
mostRecent: (_b = allSelectableNodes[allSelectableNodes.length - 1]) !== null && _b !== void 0 ? _b : null,
|
|
363
377
|
});
|
|
364
|
-
this.dispatch(focus((
|
|
378
|
+
this.dispatch(focus((_c = this.lastNode) === null || _c === void 0 ? void 0 : _c.id));
|
|
365
379
|
if (this.focusedNode)
|
|
366
380
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
367
381
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
368
382
|
}
|
|
383
|
+
filterSelectableNodes(nodes) {
|
|
384
|
+
return nodes
|
|
385
|
+
.map((n) => this.get(identify(n)))
|
|
386
|
+
.filter((n) => !!n && n.isSelectable);
|
|
387
|
+
}
|
|
369
388
|
setSelection(args) {
|
|
370
389
|
var _a;
|
|
371
390
|
const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map(identify));
|
|
@@ -563,12 +582,16 @@ export class TreeApi {
|
|
|
563
582
|
}
|
|
564
583
|
}
|
|
565
584
|
isEditable(data) {
|
|
566
|
-
|
|
567
|
-
return !utils.access(data, check);
|
|
585
|
+
return this.isActionPossible(data, this.props.disableEdit);
|
|
568
586
|
}
|
|
569
587
|
isDraggable(data) {
|
|
570
|
-
|
|
571
|
-
|
|
588
|
+
return this.isActionPossible(data, this.props.disableDrag);
|
|
589
|
+
}
|
|
590
|
+
isSelectable(data) {
|
|
591
|
+
return this.isActionPossible(data, this.props.disableSelect);
|
|
592
|
+
}
|
|
593
|
+
isActionPossible(data, disabler = () => false) {
|
|
594
|
+
return !utils.access(data, disabler);
|
|
572
595
|
}
|
|
573
596
|
isDragging(node) {
|
|
574
597
|
const id = identifyNull(node);
|
|
@@ -5,7 +5,7 @@ import { ElementType, MouseEventHandler } from "react";
|
|
|
5
5
|
import { ListOnScrollProps } from "react-window";
|
|
6
6
|
import { NodeApi } from "../interfaces/node-api";
|
|
7
7
|
import { OpenMap } from "../state/open-slice";
|
|
8
|
-
import { useDragDropManager } from "react-dnd";
|
|
8
|
+
import { useDragDropManager, DndProviderProps } from "react-dnd";
|
|
9
9
|
export interface TreeProps<T> {
|
|
10
10
|
data?: readonly T[];
|
|
11
11
|
initialData?: readonly T[];
|
|
@@ -31,6 +31,7 @@ export interface TreeProps<T> {
|
|
|
31
31
|
openByDefault?: boolean;
|
|
32
32
|
selectionFollowsFocus?: boolean;
|
|
33
33
|
disableMultiSelection?: boolean;
|
|
34
|
+
disableSelect?: string | boolean | BoolFunc<T>;
|
|
34
35
|
disableEdit?: string | boolean | BoolFunc<T>;
|
|
35
36
|
disableDrag?: string | boolean | BoolFunc<T>;
|
|
36
37
|
disableDrop?: string | boolean | ((args: {
|
|
@@ -52,5 +53,8 @@ export interface TreeProps<T> {
|
|
|
52
53
|
dndRootElement?: globalThis.Node | null;
|
|
53
54
|
onClick?: MouseEventHandler;
|
|
54
55
|
onContextMenu?: MouseEventHandler;
|
|
56
|
+
dndBackend?: Extract<DndProviderProps<unknown, unknown>, {
|
|
57
|
+
backend: unknown;
|
|
58
|
+
}>["backend"];
|
|
55
59
|
dndManager?: ReturnType<typeof useDragDropManager>;
|
|
56
60
|
}
|
package/dist/module/utils.d.ts
CHANGED
|
@@ -22,4 +22,58 @@ export declare function mergeRefs(...refs: any): (instance: any) => void;
|
|
|
22
22
|
export declare function safeRun<T extends (...args: any[]) => any>(fn: T | undefined, ...args: Parameters<T>): any;
|
|
23
23
|
export declare function waitFor(fn: () => boolean): Promise<void>;
|
|
24
24
|
export declare function getInsertIndex(tree: TreeApi<any>): number;
|
|
25
|
+
export type TreeLineChars = {
|
|
26
|
+
last: string;
|
|
27
|
+
middle: string;
|
|
28
|
+
pipe: string;
|
|
29
|
+
blank: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Generate a tree-line prefix string for a node.
|
|
33
|
+
*
|
|
34
|
+
* Returns characters like `├ `, `└ `, `│` that visually connect
|
|
35
|
+
* parent and child nodes, similar to the Unix `tree` command.
|
|
36
|
+
*
|
|
37
|
+
* **Styling note:** The prefix uses Box Drawing characters (`│`, `├`, `└`)
|
|
38
|
+
* which require a monospace font for correct alignment. Wrap the prefix
|
|
39
|
+
* in a `<span>` with `fontFamily: "monospace"` and use a consistent
|
|
40
|
+
* `fontSize` (e.g. 14–16px). Inherited `line-height` or `font-size`
|
|
41
|
+
* from parent elements can cause misalignment.
|
|
42
|
+
*
|
|
43
|
+
* @example Basic usage
|
|
44
|
+
* ```tsx
|
|
45
|
+
* function MyNode({ node, style }: NodeRendererProps<MyData>) {
|
|
46
|
+
* return (
|
|
47
|
+
* <div style={style}>
|
|
48
|
+
* <span style={{ fontFamily: "monospace", fontSize: 14 }}>
|
|
49
|
+
* {getTreeLinePrefix(node)}
|
|
50
|
+
* </span>
|
|
51
|
+
* {node.data.name}
|
|
52
|
+
* </div>
|
|
53
|
+
* );
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @example With folder/file icons
|
|
58
|
+
* ```tsx
|
|
59
|
+
* function MyNode({ node, style }: NodeRendererProps<MyData>) {
|
|
60
|
+
* const icon = node.isLeaf ? "📄" : node.isOpen ? "📂" : "📁";
|
|
61
|
+
* return (
|
|
62
|
+
* <div style={style}>
|
|
63
|
+
* <span style={{ fontFamily: "monospace", fontSize: 16 }}>
|
|
64
|
+
* {getTreeLinePrefix(node)}
|
|
65
|
+
* </span>
|
|
66
|
+
* {icon} {node.data.name}
|
|
67
|
+
* </div>
|
|
68
|
+
* );
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example Custom characters
|
|
73
|
+
* ```tsx
|
|
74
|
+
* // ASCII-only style
|
|
75
|
+
* getTreeLinePrefix(node, { last: "`- ", middle: "|- ", pipe: "|", blank: " " })
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare function getTreeLinePrefix(node: NodeApi<any>, chars?: Partial<TreeLineChars>): string;
|
|
25
79
|
export declare function getInsertParentId(tree: TreeApi<any>): string | null;
|
package/dist/module/utils.js
CHANGED
|
@@ -154,6 +154,73 @@ export function getInsertIndex(tree) {
|
|
|
154
154
|
return focus.childIndex + 1;
|
|
155
155
|
return 0;
|
|
156
156
|
}
|
|
157
|
+
const defaultTreeLineChars = {
|
|
158
|
+
last: "└ ",
|
|
159
|
+
middle: "├ ",
|
|
160
|
+
pipe: "│ ",
|
|
161
|
+
blank: "\u3000 ",
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Generate a tree-line prefix string for a node.
|
|
165
|
+
*
|
|
166
|
+
* Returns characters like `├ `, `└ `, `│` that visually connect
|
|
167
|
+
* parent and child nodes, similar to the Unix `tree` command.
|
|
168
|
+
*
|
|
169
|
+
* **Styling note:** The prefix uses Box Drawing characters (`│`, `├`, `└`)
|
|
170
|
+
* which require a monospace font for correct alignment. Wrap the prefix
|
|
171
|
+
* in a `<span>` with `fontFamily: "monospace"` and use a consistent
|
|
172
|
+
* `fontSize` (e.g. 14–16px). Inherited `line-height` or `font-size`
|
|
173
|
+
* from parent elements can cause misalignment.
|
|
174
|
+
*
|
|
175
|
+
* @example Basic usage
|
|
176
|
+
* ```tsx
|
|
177
|
+
* function MyNode({ node, style }: NodeRendererProps<MyData>) {
|
|
178
|
+
* return (
|
|
179
|
+
* <div style={style}>
|
|
180
|
+
* <span style={{ fontFamily: "monospace", fontSize: 14 }}>
|
|
181
|
+
* {getTreeLinePrefix(node)}
|
|
182
|
+
* </span>
|
|
183
|
+
* {node.data.name}
|
|
184
|
+
* </div>
|
|
185
|
+
* );
|
|
186
|
+
* }
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* @example With folder/file icons
|
|
190
|
+
* ```tsx
|
|
191
|
+
* function MyNode({ node, style }: NodeRendererProps<MyData>) {
|
|
192
|
+
* const icon = node.isLeaf ? "📄" : node.isOpen ? "📂" : "📁";
|
|
193
|
+
* return (
|
|
194
|
+
* <div style={style}>
|
|
195
|
+
* <span style={{ fontFamily: "monospace", fontSize: 16 }}>
|
|
196
|
+
* {getTreeLinePrefix(node)}
|
|
197
|
+
* </span>
|
|
198
|
+
* {icon} {node.data.name}
|
|
199
|
+
* </div>
|
|
200
|
+
* );
|
|
201
|
+
* }
|
|
202
|
+
* ```
|
|
203
|
+
*
|
|
204
|
+
* @example Custom characters
|
|
205
|
+
* ```tsx
|
|
206
|
+
* // ASCII-only style
|
|
207
|
+
* getTreeLinePrefix(node, { last: "`- ", middle: "|- ", pipe: "|", blank: " " })
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
export function getTreeLinePrefix(node, chars = {}) {
|
|
211
|
+
const c = Object.assign(Object.assign({}, defaultTreeLineChars), chars);
|
|
212
|
+
if (node.level === 0)
|
|
213
|
+
return "";
|
|
214
|
+
const isLast = node.nextSibling === null;
|
|
215
|
+
let prefix = isLast ? c.last : c.middle;
|
|
216
|
+
let ancestor = node.parent;
|
|
217
|
+
while (ancestor && ancestor.level > 0) {
|
|
218
|
+
const isAncestorLast = ancestor.nextSibling === null;
|
|
219
|
+
prefix = (isAncestorLast ? c.blank : c.pipe) + prefix;
|
|
220
|
+
ancestor = ancestor.parent;
|
|
221
|
+
}
|
|
222
|
+
return prefix;
|
|
223
|
+
}
|
|
157
224
|
export function getInsertParentId(tree) {
|
|
158
225
|
const focus = tree.focusedNode;
|
|
159
226
|
if (!focus)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-arborist",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"source": "src/index.ts",
|
|
6
6
|
"main": "dist/main/index.js",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
],
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
25
|
-
"url": "https://github.com/
|
|
25
|
+
"url": "https://github.com/jameskerr/react-arborist.git"
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://react-arborist.netlify.app",
|
|
28
|
-
"bugs": "https://github.com/
|
|
28
|
+
"bugs": "https://github.com/jameskerr/react-arborist/issues",
|
|
29
29
|
"keywords": [
|
|
30
30
|
"react",
|
|
31
31
|
"arborist",
|
|
@@ -59,4 +59,4 @@
|
|
|
59
59
|
"ts-jest": "^29.1.1",
|
|
60
60
|
"typescript": "^5.6.0"
|
|
61
61
|
}
|
|
62
|
-
}
|
|
62
|
+
}
|
|
@@ -84,9 +84,14 @@ export function TreeProvider<T>({
|
|
|
84
84
|
<NodesContext.Provider value={state.nodes}>
|
|
85
85
|
<DndContext.Provider value={state.dnd}>
|
|
86
86
|
<DndProvider
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
{...(treeProps.dndManager
|
|
88
|
+
? { manager: treeProps.dndManager }
|
|
89
|
+
: {
|
|
90
|
+
backend: treeProps.dndBackend || HTML5Backend,
|
|
91
|
+
options: {
|
|
92
|
+
rootElement: api.props.dndRootElement || undefined,
|
|
93
|
+
},
|
|
94
|
+
})}
|
|
90
95
|
>
|
|
91
96
|
{children}
|
|
92
97
|
</DndProvider>
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EditResult } from "../types/handlers";
|
|
2
|
-
import { Identity, IdObj } from "../types/utils";
|
|
2
|
+
import { BoolFunc, Identity, IdObj } from "../types/utils";
|
|
3
3
|
import { TreeProps } from "../types/tree-props";
|
|
4
4
|
import { MutableRefObject } from "react";
|
|
5
5
|
import { Align, FixedSizeList, ListOnItemsRenderedProps } from "react-window";
|
|
@@ -169,7 +169,7 @@ export class TreeApi<T> {
|
|
|
169
169
|
return this.visibleNodes.slice(start, end + 1);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
indexOf(id:
|
|
172
|
+
indexOf(id: Identity) {
|
|
173
173
|
const key = utils.identifyNull(id);
|
|
174
174
|
if (!key) return null;
|
|
175
175
|
return this.idToIndex[key];
|
|
@@ -219,7 +219,7 @@ export class TreeApi<T> {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
async delete(node:
|
|
222
|
+
async delete(node: Identity | string[] | IdObj[]) {
|
|
223
223
|
if (!node) return;
|
|
224
224
|
const idents = Array.isArray(node) ? node : [node];
|
|
225
225
|
const ids = idents.map(identify);
|
|
@@ -256,7 +256,7 @@ export class TreeApi<T> {
|
|
|
256
256
|
setTimeout(() => this.onFocus()); // Return focus to element;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
activate(id:
|
|
259
|
+
activate(id: Identity) {
|
|
260
260
|
const node = this.get(identifyNull(id));
|
|
261
261
|
if (!node) return;
|
|
262
262
|
safeRun(this.props.onActivate, node);
|
|
@@ -328,14 +328,17 @@ export class TreeApi<T> {
|
|
|
328
328
|
const changeFocus = opts.focus !== false;
|
|
329
329
|
const id = identify(node);
|
|
330
330
|
if (changeFocus) this.dispatch(focus(id));
|
|
331
|
-
this.
|
|
332
|
-
|
|
333
|
-
|
|
331
|
+
if (this.get(id)?.isSelectable) {
|
|
332
|
+
this.setSelection({
|
|
333
|
+
ids: [id],
|
|
334
|
+
anchor: id,
|
|
335
|
+
mostRecent: id,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
334
338
|
this.scrollTo(id, opts.align);
|
|
335
339
|
if (this.focusedNode && changeFocus) {
|
|
336
340
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
337
341
|
}
|
|
338
|
-
safeRun(this.props.onSelect, this.selectedNodes);
|
|
339
342
|
}
|
|
340
343
|
|
|
341
344
|
deselect(node: Identity) {
|
|
@@ -345,26 +348,36 @@ export class TreeApi<T> {
|
|
|
345
348
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
346
349
|
}
|
|
347
350
|
|
|
348
|
-
selectMulti(identity: Identity) {
|
|
351
|
+
selectMulti(identity: Identity, opts: { align?: Align; focus?: boolean } = {}) {
|
|
349
352
|
const node = this.get(identifyNull(identity));
|
|
350
353
|
if (!node) return;
|
|
351
|
-
|
|
352
|
-
this.dispatch(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
354
|
+
const changeFocus = opts.focus !== false;
|
|
355
|
+
if (changeFocus) this.dispatch(focus(node.id));
|
|
356
|
+
if (node.isSelectable) {
|
|
357
|
+
this.dispatch(selection.add(node.id));
|
|
358
|
+
this.dispatch(selection.anchor(node.id));
|
|
359
|
+
this.dispatch(selection.mostRecent(node.id));
|
|
360
|
+
}
|
|
361
|
+
this.scrollTo(node, opts.align);
|
|
362
|
+
if (this.focusedNode && changeFocus) {
|
|
363
|
+
safeRun(this.props.onFocus, this.focusedNode);
|
|
364
|
+
}
|
|
357
365
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
358
366
|
}
|
|
359
367
|
|
|
360
368
|
selectContiguous(identity: Identity) {
|
|
361
369
|
if (!identity) return;
|
|
362
370
|
const id = identify(identity);
|
|
363
|
-
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
364
371
|
this.dispatch(focus(id));
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
372
|
+
if (this.get(id)?.isSelectable) {
|
|
373
|
+
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
374
|
+
const selectableNodes = this.filterSelectableNodes(
|
|
375
|
+
this.nodesBetween(anchor, identifyNull(id)),
|
|
376
|
+
);
|
|
377
|
+
this.dispatch(selection.remove(this.nodesBetween(anchor, mostRecent)));
|
|
378
|
+
this.dispatch(selection.add(selectableNodes));
|
|
379
|
+
this.dispatch(selection.mostRecent(id));
|
|
380
|
+
}
|
|
368
381
|
this.scrollTo(id);
|
|
369
382
|
if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
|
|
370
383
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
@@ -376,20 +389,29 @@ export class TreeApi<T> {
|
|
|
376
389
|
}
|
|
377
390
|
|
|
378
391
|
selectAll() {
|
|
392
|
+
const allSelectableNodes = this.filterSelectableNodes(
|
|
393
|
+
Object.keys(this.idToIndex),
|
|
394
|
+
);
|
|
379
395
|
this.setSelection({
|
|
380
|
-
ids:
|
|
381
|
-
anchor:
|
|
382
|
-
mostRecent:
|
|
396
|
+
ids: allSelectableNodes,
|
|
397
|
+
anchor: allSelectableNodes[0] ?? null,
|
|
398
|
+
mostRecent: allSelectableNodes[allSelectableNodes.length - 1] ?? null,
|
|
383
399
|
});
|
|
384
400
|
this.dispatch(focus(this.lastNode?.id));
|
|
385
401
|
if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
|
|
386
402
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
387
403
|
}
|
|
388
404
|
|
|
405
|
+
private filterSelectableNodes(nodes: (IdObj | string)[]) {
|
|
406
|
+
return nodes
|
|
407
|
+
.map((n) => this.get(identify(n)))
|
|
408
|
+
.filter((n): n is NodeApi<T> => !!n && n.isSelectable);
|
|
409
|
+
}
|
|
410
|
+
|
|
389
411
|
setSelection(args: {
|
|
390
412
|
ids: (IdObj | string)[] | null;
|
|
391
|
-
anchor:
|
|
392
|
-
mostRecent:
|
|
413
|
+
anchor: Identity;
|
|
414
|
+
mostRecent: Identity;
|
|
393
415
|
}) {
|
|
394
416
|
const ids = new Set(args.ids?.map(identify));
|
|
395
417
|
const anchor = identifyNull(args.anchor);
|
|
@@ -593,16 +615,25 @@ export class TreeApi<T> {
|
|
|
593
615
|
}
|
|
594
616
|
|
|
595
617
|
isEditable(data: T) {
|
|
596
|
-
|
|
597
|
-
return !utils.access(data, check);
|
|
618
|
+
return this.isActionPossible(data, this.props.disableEdit);
|
|
598
619
|
}
|
|
599
620
|
|
|
600
621
|
isDraggable(data: T) {
|
|
601
|
-
|
|
602
|
-
|
|
622
|
+
return this.isActionPossible(data, this.props.disableDrag);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
isSelectable(data: T) {
|
|
626
|
+
return this.isActionPossible(data, this.props.disableSelect);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private isActionPossible(
|
|
630
|
+
data: T,
|
|
631
|
+
disabler: string | boolean | BoolFunc<T> = () => false,
|
|
632
|
+
) {
|
|
633
|
+
return !utils.access(data, disabler);
|
|
603
634
|
}
|
|
604
635
|
|
|
605
|
-
isDragging(node:
|
|
636
|
+
isDragging(node: Identity) {
|
|
606
637
|
const id = identifyNull(node);
|
|
607
638
|
if (!id) return false;
|
|
608
639
|
return this.state.nodes.drag.id === id;
|
|
@@ -616,7 +647,7 @@ export class TreeApi<T> {
|
|
|
616
647
|
return this.matchFn(node);
|
|
617
648
|
}
|
|
618
649
|
|
|
619
|
-
willReceiveDrop(node:
|
|
650
|
+
willReceiveDrop(node: Identity) {
|
|
620
651
|
const id = identifyNull(node);
|
|
621
652
|
if (!id) return false;
|
|
622
653
|
const { destinationParentId, destinationIndex } = this.state.nodes.drag;
|
package/src/types/tree-props.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { ElementType, MouseEventHandler } from "react";
|
|
|
5
5
|
import { ListOnScrollProps } from "react-window";
|
|
6
6
|
import { NodeApi } from "../interfaces/node-api";
|
|
7
7
|
import { OpenMap } from "../state/open-slice";
|
|
8
|
-
import { useDragDropManager } from "react-dnd";
|
|
8
|
+
import { useDragDropManager, DndProviderProps } from "react-dnd";
|
|
9
9
|
|
|
10
10
|
export interface TreeProps<T> {
|
|
11
11
|
/* Data Options */
|
|
@@ -41,6 +41,7 @@ export interface TreeProps<T> {
|
|
|
41
41
|
openByDefault?: boolean;
|
|
42
42
|
selectionFollowsFocus?: boolean;
|
|
43
43
|
disableMultiSelection?: boolean;
|
|
44
|
+
disableSelect?: string | boolean | BoolFunc<T>;
|
|
44
45
|
disableEdit?: string | boolean | BoolFunc<T>;
|
|
45
46
|
disableDrag?: string | boolean | BoolFunc<T>;
|
|
46
47
|
disableDrop?:
|
|
@@ -76,5 +77,9 @@ export interface TreeProps<T> {
|
|
|
76
77
|
dndRootElement?: globalThis.Node | null;
|
|
77
78
|
onClick?: MouseEventHandler;
|
|
78
79
|
onContextMenu?: MouseEventHandler;
|
|
80
|
+
dndBackend?: Extract<
|
|
81
|
+
DndProviderProps<unknown, unknown>,
|
|
82
|
+
{ backend: unknown }
|
|
83
|
+
>["backend"];
|
|
79
84
|
dndManager?: ReturnType<typeof useDragDropManager>;
|
|
80
85
|
}
|
package/src/utils.ts
CHANGED
|
@@ -173,6 +173,87 @@ export function getInsertIndex(tree: TreeApi<any>) {
|
|
|
173
173
|
return 0;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
export type TreeLineChars = {
|
|
177
|
+
last: string;
|
|
178
|
+
middle: string;
|
|
179
|
+
pipe: string;
|
|
180
|
+
blank: string;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const defaultTreeLineChars: TreeLineChars = {
|
|
184
|
+
last: "└ ",
|
|
185
|
+
middle: "├ ",
|
|
186
|
+
pipe: "│ ",
|
|
187
|
+
blank: "\u3000 ",
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Generate a tree-line prefix string for a node.
|
|
192
|
+
*
|
|
193
|
+
* Returns characters like `├ `, `└ `, `│` that visually connect
|
|
194
|
+
* parent and child nodes, similar to the Unix `tree` command.
|
|
195
|
+
*
|
|
196
|
+
* **Styling note:** The prefix uses Box Drawing characters (`│`, `├`, `└`)
|
|
197
|
+
* which require a monospace font for correct alignment. Wrap the prefix
|
|
198
|
+
* in a `<span>` with `fontFamily: "monospace"` and use a consistent
|
|
199
|
+
* `fontSize` (e.g. 14–16px). Inherited `line-height` or `font-size`
|
|
200
|
+
* from parent elements can cause misalignment.
|
|
201
|
+
*
|
|
202
|
+
* @example Basic usage
|
|
203
|
+
* ```tsx
|
|
204
|
+
* function MyNode({ node, style }: NodeRendererProps<MyData>) {
|
|
205
|
+
* return (
|
|
206
|
+
* <div style={style}>
|
|
207
|
+
* <span style={{ fontFamily: "monospace", fontSize: 14 }}>
|
|
208
|
+
* {getTreeLinePrefix(node)}
|
|
209
|
+
* </span>
|
|
210
|
+
* {node.data.name}
|
|
211
|
+
* </div>
|
|
212
|
+
* );
|
|
213
|
+
* }
|
|
214
|
+
* ```
|
|
215
|
+
*
|
|
216
|
+
* @example With folder/file icons
|
|
217
|
+
* ```tsx
|
|
218
|
+
* function MyNode({ node, style }: NodeRendererProps<MyData>) {
|
|
219
|
+
* const icon = node.isLeaf ? "📄" : node.isOpen ? "📂" : "📁";
|
|
220
|
+
* return (
|
|
221
|
+
* <div style={style}>
|
|
222
|
+
* <span style={{ fontFamily: "monospace", fontSize: 16 }}>
|
|
223
|
+
* {getTreeLinePrefix(node)}
|
|
224
|
+
* </span>
|
|
225
|
+
* {icon} {node.data.name}
|
|
226
|
+
* </div>
|
|
227
|
+
* );
|
|
228
|
+
* }
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* @example Custom characters
|
|
232
|
+
* ```tsx
|
|
233
|
+
* // ASCII-only style
|
|
234
|
+
* getTreeLinePrefix(node, { last: "`- ", middle: "|- ", pipe: "|", blank: " " })
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export function getTreeLinePrefix(
|
|
238
|
+
node: NodeApi<any>,
|
|
239
|
+
chars: Partial<TreeLineChars> = {}
|
|
240
|
+
): string {
|
|
241
|
+
const c = { ...defaultTreeLineChars, ...chars };
|
|
242
|
+
if (node.level === 0) return "";
|
|
243
|
+
|
|
244
|
+
const isLast = node.nextSibling === null;
|
|
245
|
+
let prefix = isLast ? c.last : c.middle;
|
|
246
|
+
|
|
247
|
+
let ancestor = node.parent;
|
|
248
|
+
while (ancestor && ancestor.level > 0) {
|
|
249
|
+
const isAncestorLast = ancestor.nextSibling === null;
|
|
250
|
+
prefix = (isAncestorLast ? c.blank : c.pipe) + prefix;
|
|
251
|
+
ancestor = ancestor.parent;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return prefix;
|
|
255
|
+
}
|
|
256
|
+
|
|
176
257
|
export function getInsertParentId(tree: TreeApi<any>) {
|
|
177
258
|
const focus = tree.focusedNode;
|
|
178
259
|
if (!focus) return null;
|