react-arborist 3.4.2 → 3.5.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 +49 -6
- package/dist/main/components/provider.js +8 -1
- package/dist/main/components/row-container.js +1 -0
- package/dist/main/index.d.ts +2 -0
- package/dist/main/index.js +3 -1
- package/dist/main/interfaces/tree-api.d.ts +4 -1
- package/dist/main/interfaces/tree-api.js +7 -4
- package/dist/main/types/tree-props.d.ts +4 -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/components/row-container.js +1 -0
- package/dist/module/index.d.ts +2 -0
- package/dist/module/index.js +1 -0
- package/dist/module/interfaces/tree-api.d.ts +4 -1
- package/dist/module/interfaces/tree-api.js +7 -4
- package/dist/module/types/tree-props.d.ts +4 -1
- package/dist/module/utils.d.ts +54 -0
- package/dist/module/utils.js +67 -0
- package/package.json +5 -5
- package/src/components/provider.tsx +8 -3
- package/src/components/row-container.tsx +1 -0
- package/src/index.ts +2 -0
- package/src/interfaces/tree-api.ts +7 -4
- package/src/types/tree-props.ts +5 -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,7 +305,7 @@ 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;
|
|
@@ -343,7 +345,11 @@ interface TreeProps<T> {
|
|
|
343
345
|
dndRootElement?: globalThis.Node | null;
|
|
344
346
|
onClick?: MouseEventHandler;
|
|
345
347
|
onContextMenu?: MouseEventHandler;
|
|
346
|
-
|
|
348
|
+
dndBackend?: Extract<
|
|
349
|
+
DndProviderProps<unknown, unknown>,
|
|
350
|
+
{ backend: unknown }
|
|
351
|
+
>["backend"];
|
|
352
|
+
dndManager?: ReturnType<typeof useDragDropManager>;
|
|
347
353
|
}
|
|
348
354
|
```
|
|
349
355
|
|
|
@@ -648,17 +654,17 @@ _tree_.**isSelected**(_id_) : _boolean_
|
|
|
648
654
|
|
|
649
655
|
Returns true if the node with _id_ is selected.
|
|
650
656
|
|
|
651
|
-
_tree_.**select**(_id_)
|
|
657
|
+
_tree_.**select**(_id_, _[opts]_)
|
|
652
658
|
|
|
653
|
-
Select only the node with _id_.
|
|
659
|
+
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
660
|
|
|
655
661
|
_tree_.**deselect**(_id_)
|
|
656
662
|
|
|
657
663
|
Deselect the node with _id_.
|
|
658
664
|
|
|
659
|
-
_tree_.**selectMulti**(_id_)
|
|
665
|
+
_tree_.**selectMulti**(_id_, _[opts]_)
|
|
660
666
|
|
|
661
|
-
Add to the selection the node with _id_.
|
|
667
|
+
Add to the selection the node with _id_. Accepts the same options object as `select`.
|
|
662
668
|
|
|
663
669
|
_tree_.**selectContiguous**(_id_)
|
|
664
670
|
|
|
@@ -740,6 +746,43 @@ _tree_.**root** : _NodeApi_
|
|
|
740
746
|
|
|
741
747
|
Returns the root _NodeApi_ instance. Its children are the Node representations of the _data_ prop array.
|
|
742
748
|
|
|
749
|
+
## Utilities
|
|
750
|
+
|
|
751
|
+
### getTreeLinePrefix
|
|
752
|
+
|
|
753
|
+
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.
|
|
754
|
+
|
|
755
|
+
```ts
|
|
756
|
+
function getTreeLinePrefix(
|
|
757
|
+
node: NodeApi<any>,
|
|
758
|
+
chars?: Partial<TreeLineChars>
|
|
759
|
+
): string;
|
|
760
|
+
|
|
761
|
+
type TreeLineChars = {
|
|
762
|
+
last: string; // default: "└ "
|
|
763
|
+
middle: string; // default: "├ "
|
|
764
|
+
pipe: string; // default: "│ "
|
|
765
|
+
blank: string; // default: "\u3000 "
|
|
766
|
+
};
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
Wrap the prefix in a monospace span so the connectors line up:
|
|
770
|
+
|
|
771
|
+
```tsx
|
|
772
|
+
import { Tree, getTreeLinePrefix } from "react-arborist";
|
|
773
|
+
|
|
774
|
+
function Node({ node, style }) {
|
|
775
|
+
return (
|
|
776
|
+
<div style={style}>
|
|
777
|
+
<span style={{ fontFamily: "monospace" }}>{getTreeLinePrefix(node)}</span>
|
|
778
|
+
{node.data.name}
|
|
779
|
+
</div>
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
Pass a partial `chars` object to override any of the default characters (e.g. for an ASCII-only style).
|
|
785
|
+
|
|
743
786
|
## Author
|
|
744
787
|
|
|
745
788
|
[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
|
}
|
|
@@ -68,6 +68,7 @@ exports.RowContainer = react_1.default.memo(function RowContainer({ index, style
|
|
|
68
68
|
role: "treeitem",
|
|
69
69
|
"aria-level": node.level + 1,
|
|
70
70
|
"aria-selected": node.isSelected,
|
|
71
|
+
"aria-expanded": node.isOpen,
|
|
71
72
|
style: rowStyle,
|
|
72
73
|
tabIndex: -1,
|
|
73
74
|
className: tree.props.rowClassName,
|
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; } });
|
|
@@ -163,7 +163,10 @@ 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;
|
|
@@ -349,17 +349,20 @@ class TreeApi {
|
|
|
349
349
|
this.dispatch(selection_slice_1.actions.remove(id));
|
|
350
350
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
351
351
|
}
|
|
352
|
-
selectMulti(identity) {
|
|
352
|
+
selectMulti(identity, opts = {}) {
|
|
353
353
|
const node = this.get(identifyNull(identity));
|
|
354
354
|
if (!node)
|
|
355
355
|
return;
|
|
356
|
-
|
|
356
|
+
const changeFocus = opts.focus !== false;
|
|
357
|
+
if (changeFocus)
|
|
358
|
+
this.dispatch((0, focus_slice_1.focus)(node.id));
|
|
357
359
|
this.dispatch(selection_slice_1.actions.add(node.id));
|
|
358
360
|
this.dispatch(selection_slice_1.actions.anchor(node.id));
|
|
359
361
|
this.dispatch(selection_slice_1.actions.mostRecent(node.id));
|
|
360
|
-
this.scrollTo(node);
|
|
361
|
-
if (this.focusedNode)
|
|
362
|
+
this.scrollTo(node, opts.align);
|
|
363
|
+
if (this.focusedNode && changeFocus) {
|
|
362
364
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
365
|
+
}
|
|
363
366
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
364
367
|
}
|
|
365
368
|
selectContiguous(identity) {
|
|
@@ -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[];
|
|
@@ -52,5 +52,8 @@ export interface TreeProps<T> {
|
|
|
52
52
|
dndRootElement?: globalThis.Node | null;
|
|
53
53
|
onClick?: MouseEventHandler;
|
|
54
54
|
onContextMenu?: MouseEventHandler;
|
|
55
|
+
dndBackend?: Extract<DndProviderProps<unknown, unknown>, {
|
|
56
|
+
backend: unknown;
|
|
57
|
+
}>["backend"];
|
|
55
58
|
dndManager?: ReturnType<typeof useDragDropManager>;
|
|
56
59
|
}
|
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
|
}
|
|
@@ -42,6 +42,7 @@ export const RowContainer = React.memo(function RowContainer({ index, style, })
|
|
|
42
42
|
role: "treeitem",
|
|
43
43
|
"aria-level": node.level + 1,
|
|
44
44
|
"aria-selected": node.isSelected,
|
|
45
|
+
"aria-expanded": node.isOpen,
|
|
45
46
|
style: rowStyle,
|
|
46
47
|
tabIndex: -1,
|
|
47
48
|
className: tree.props.rowClassName,
|
package/dist/module/index.d.ts
CHANGED
package/dist/module/index.js
CHANGED
|
@@ -163,7 +163,10 @@ 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;
|
|
@@ -323,17 +323,20 @@ export class TreeApi {
|
|
|
323
323
|
this.dispatch(selection.remove(id));
|
|
324
324
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
325
325
|
}
|
|
326
|
-
selectMulti(identity) {
|
|
326
|
+
selectMulti(identity, opts = {}) {
|
|
327
327
|
const node = this.get(identifyNull(identity));
|
|
328
328
|
if (!node)
|
|
329
329
|
return;
|
|
330
|
-
|
|
330
|
+
const changeFocus = opts.focus !== false;
|
|
331
|
+
if (changeFocus)
|
|
332
|
+
this.dispatch(focus(node.id));
|
|
331
333
|
this.dispatch(selection.add(node.id));
|
|
332
334
|
this.dispatch(selection.anchor(node.id));
|
|
333
335
|
this.dispatch(selection.mostRecent(node.id));
|
|
334
|
-
this.scrollTo(node);
|
|
335
|
-
if (this.focusedNode)
|
|
336
|
+
this.scrollTo(node, opts.align);
|
|
337
|
+
if (this.focusedNode && changeFocus) {
|
|
336
338
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
339
|
+
}
|
|
337
340
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
338
341
|
}
|
|
339
342
|
selectContiguous(identity) {
|
|
@@ -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[];
|
|
@@ -52,5 +52,8 @@ export interface TreeProps<T> {
|
|
|
52
52
|
dndRootElement?: globalThis.Node | null;
|
|
53
53
|
onClick?: MouseEventHandler;
|
|
54
54
|
onContextMenu?: MouseEventHandler;
|
|
55
|
+
dndBackend?: Extract<DndProviderProps<unknown, unknown>, {
|
|
56
|
+
backend: unknown;
|
|
57
|
+
}>["backend"];
|
|
55
58
|
dndManager?: ReturnType<typeof useDragDropManager>;
|
|
56
59
|
}
|
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.5.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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"react-dnd": "^14.0.3",
|
|
42
42
|
"react-dnd-html5-backend": "^14.0.3",
|
|
43
|
-
"react-window": "^1.8.
|
|
43
|
+
"react-window": "^1.8.11",
|
|
44
44
|
"redux": "^5.0.0",
|
|
45
45
|
"use-sync-external-store": "^1.2.0"
|
|
46
46
|
},
|
|
@@ -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>
|
|
@@ -60,6 +60,7 @@ export const RowContainer = React.memo(function RowContainer<T>({
|
|
|
60
60
|
role: "treeitem",
|
|
61
61
|
"aria-level": node.level + 1,
|
|
62
62
|
"aria-selected": node.isSelected,
|
|
63
|
+
"aria-expanded": node.isOpen,
|
|
63
64
|
style: rowStyle,
|
|
64
65
|
tabIndex: -1,
|
|
65
66
|
className: tree.props.rowClassName,
|
package/src/index.ts
CHANGED
|
@@ -345,15 +345,18 @@ export class TreeApi<T> {
|
|
|
345
345
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
-
selectMulti(identity: Identity) {
|
|
348
|
+
selectMulti(identity: Identity, opts: { align?: Align; focus?: boolean } = {}) {
|
|
349
349
|
const node = this.get(identifyNull(identity));
|
|
350
350
|
if (!node) return;
|
|
351
|
-
|
|
351
|
+
const changeFocus = opts.focus !== false;
|
|
352
|
+
if (changeFocus) this.dispatch(focus(node.id));
|
|
352
353
|
this.dispatch(selection.add(node.id));
|
|
353
354
|
this.dispatch(selection.anchor(node.id));
|
|
354
355
|
this.dispatch(selection.mostRecent(node.id));
|
|
355
|
-
this.scrollTo(node);
|
|
356
|
-
if (this.focusedNode
|
|
356
|
+
this.scrollTo(node, opts.align);
|
|
357
|
+
if (this.focusedNode && changeFocus) {
|
|
358
|
+
safeRun(this.props.onFocus, this.focusedNode);
|
|
359
|
+
}
|
|
357
360
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
358
361
|
}
|
|
359
362
|
|
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 */
|
|
@@ -76,5 +76,9 @@ export interface TreeProps<T> {
|
|
|
76
76
|
dndRootElement?: globalThis.Node | null;
|
|
77
77
|
onClick?: MouseEventHandler;
|
|
78
78
|
onContextMenu?: MouseEventHandler;
|
|
79
|
+
dndBackend?: Extract<
|
|
80
|
+
DndProviderProps<unknown, unknown>,
|
|
81
|
+
{ backend: unknown }
|
|
82
|
+
>["backend"];
|
|
79
83
|
dndManager?: ReturnType<typeof useDragDropManager>;
|
|
80
84
|
}
|
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;
|