@asup/tree-of-nodes 0.9.1 → 1.0.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 +134 -1
- package/dist/__mocks__/mockNodes.d.ts +4 -0
- package/dist/__mocks__/styleMock.d.ts +2 -0
- package/dist/__tests__/ContextMenu.d.ts +1 -0
- package/dist/__tests__/TreeNode.test.d.ts +1 -0
- package/dist/__tests__/TreeOfNodes.test.d.ts +1 -0
- package/dist/__tests__/WordEntry.test.d.ts +1 -0
- package/dist/cjs/__mocks__/mockNodes.d.ts +4 -0
- package/dist/cjs/__mocks__/styleMock.d.ts +2 -0
- package/dist/cjs/__tests__/ContextMenu.d.ts +1 -0
- package/dist/cjs/__tests__/TreeNode.test.d.ts +1 -0
- package/dist/cjs/__tests__/TreeOfNodes.test.d.ts +1 -0
- package/dist/cjs/__tests__/WordEntry.test.d.ts +1 -0
- package/dist/cjs/components/TreeOfNodes.d.ts +3 -27
- package/dist/cjs/components/TreeOfNodesContext.d.ts +28 -0
- package/dist/cjs/components/WordEntry.d.ts +2 -2
- package/dist/cjs/components/index.d.ts +2 -1
- package/dist/cjs/main.js +62 -65
- package/dist/components/TreeOfNodes.d.ts +3 -27
- package/dist/components/TreeOfNodesContext.d.ts +28 -0
- package/dist/components/WordEntry.d.ts +2 -2
- package/dist/components/index.d.ts +2 -1
- package/dist/main.js +62 -65
- package/dist/tree-of-nodes.d.ts +12 -10
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,5 +1,138 @@
|
|
|
1
|
-
|
|
1
|
+
[npm]: https://img.shields.io/npm/v/@asup/tree-of-nodes
|
|
2
|
+
[npm-url]: https://www.npmjs.com/package/@asup/tree-of-nodes
|
|
3
|
+
[size]: https://packagephobia.now.sh/badge?p=@asup/tree-of-nodes
|
|
4
|
+
[size-url]: https://packagephobia.now.sh/result?p=@asup/tree-of-nodes
|
|
5
|
+
|
|
6
|
+
[![npm][npm]][npm-url]
|
|
7
|
+
[![size][size]][size-url]
|
|
8
|
+

|
|
9
|
+
[](https://raw.githubusercontent.com/PaulDThomas/tree-of-nodes/master/LICENCE)
|
|
10
|
+
|
|
11
|
+
# @asup/tree-of-notes
|
|
2
12
|
|
|
3
13
|
REACT treeview, because I couldn't quite find what I wanted.
|
|
4
14
|
|
|
5
15
|
Works with/needs bootstrap, react-bootstrap, react-bootstrap-icons.
|
|
16
|
+
Uses a context menu to facilitate Add/Rename/Remove actions.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
# with npm
|
|
22
|
+
npm install @asup/tree-of-nodes
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Tree-of-nodes Usage
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
import { iNodeUpdate, TreeNodeData, TreeOfNodes } from '@asup/tree-of-nodes';
|
|
29
|
+
import '@asup/tree-of-nodes/dist/style.css';
|
|
30
|
+
|
|
31
|
+
... inside REACT component
|
|
32
|
+
|
|
33
|
+
<TreeOfNodes<T>
|
|
34
|
+
id: string;
|
|
35
|
+
nodeList: TreeNodeData<T>[];
|
|
36
|
+
roots: Key[];
|
|
37
|
+
selectedId?: Key;
|
|
38
|
+
handleSelect?: (ret: Key) => void;
|
|
39
|
+
onAdd?: (parentId: Key, newName: string) => Promise<iNodeUpdate>;
|
|
40
|
+
onRemove?: (childId: Key) => Promise<iNodeUpdate>;
|
|
41
|
+
onRename?: (childId: Key, newName: string) => Promise<iNodeUpdate>;
|
|
42
|
+
canAddRoot?: boolean;
|
|
43
|
+
canRemoveRoot?: boolean;
|
|
44
|
+
canRenameRoot?: boolean;
|
|
45
|
+
canAddChildren?: boolean;
|
|
46
|
+
canRemoveChildren?: boolean;
|
|
47
|
+
canRenameChildren?: boolean;
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The component expects a list of nodes of type `TreeNodeData<T>` with unique `Key`s.
|
|
52
|
+
|
|
53
|
+
## Properties
|
|
54
|
+
|
|
55
|
+
| Prop | Description | Default |
|
|
56
|
+
| :---------------- | :---------------------------------------------------------------------------------------------- | :---------------: |
|
|
57
|
+
| id | HTML id attribute | |
|
|
58
|
+
| nodeList | Array of node data | |
|
|
59
|
+
| roots | One or more node Keys to use as the root of the tree |
|
|
60
|
+
| selectedId | Currently selectedId | |
|
|
61
|
+
| handleSelect | Function called when a node is clicked (selected), this should be used to update the selectedId | `() => {return;}` |
|
|
62
|
+
| onAdd | Function called when a new node is added, this should be used to update the nodeList | |
|
|
63
|
+
| onRemove | Function called when a node is removed, this should be used to update the nodeList | |
|
|
64
|
+
| onRename | Function called when a node is renamed, this should be used to update the nodeList | |
|
|
65
|
+
| ~~canAddRoot~~ | Not currently implemented | |
|
|
66
|
+
| canRemoveRoot | Allows removal of a root node in combination with specification of onRemove function | `false` |
|
|
67
|
+
| canRenameRoot | Allows renaming of a root node in combination with specification of onRename function | `false` |
|
|
68
|
+
| canAddChildren | Allows addition of children in combination with specification of onAdd function | `false` |
|
|
69
|
+
| canRemoveChildren | Allows renaming of non-root nodes in combination with specification of onRemove function | `false` |
|
|
70
|
+
| canRenameChildren | Allows renaming of non-root nodes in combination with specification of onRename function | `false` |
|
|
71
|
+
|
|
72
|
+
### TreeNodeData
|
|
73
|
+
|
|
74
|
+
Required format for list of nodes, includes a holder for any type of data, which is there specified in the `TreeOfNodes` specification.
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
export type TreeNodeData<T> = {
|
|
78
|
+
id: Key;
|
|
79
|
+
label: string;
|
|
80
|
+
parentId?: Key;
|
|
81
|
+
data: T;
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### iNodeUpdate
|
|
86
|
+
|
|
87
|
+
Use this format to return from update functions.
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
export interface iNodeUpdate {
|
|
91
|
+
success: boolean;
|
|
92
|
+
ErrorText?: string;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Context Menu Usage
|
|
97
|
+
|
|
98
|
+
Inbuilt context menu provider, takes a list of available actions and renders a context menu on appropriate click.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
import { ContextMenuProvider, iMenuItem } from '@asup/tree-of-nodes';
|
|
102
|
+
import '@asup/tree-of-nodes/dist/style.css';
|
|
103
|
+
|
|
104
|
+
... inside REACT component
|
|
105
|
+
|
|
106
|
+
<ContextMenuProvider>
|
|
107
|
+
|
|
108
|
+
<SomeChild
|
|
109
|
+
|
|
110
|
+
const showMenu () = useCallback(
|
|
111
|
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
e.stopPropagation();
|
|
114
|
+
const menuItems: iMenuItem[] = [
|
|
115
|
+
{ label: 'Item 1', action: item1Function },
|
|
116
|
+
{ label: 'Item 2', action: item2Function },
|
|
117
|
+
...
|
|
118
|
+
];
|
|
119
|
+
menuContext.set && menuContext.set({
|
|
120
|
+
visible: true,
|
|
121
|
+
y: e.pageY,
|
|
122
|
+
x: e.pageX,
|
|
123
|
+
menuItems: menuItems,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
[...]);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div onContextMenu={showMenu}>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
>
|
|
134
|
+
|
|
135
|
+
</ContextMenuProvider>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Add an `onContextMenu` action to an element inside the `ContextMenuProvider`, and create a corresponding function that loads the menuItems array and then sets it to visible.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,20 +1,7 @@
|
|
|
1
1
|
import { Key } from 'react';
|
|
2
|
-
|
|
3
|
-
id: Key;
|
|
4
|
-
label: string;
|
|
5
|
-
parentId?: Key;
|
|
6
|
-
};
|
|
7
|
-
export declare type TreeNodeData<T> = {
|
|
8
|
-
id: Key;
|
|
9
|
-
label: string;
|
|
10
|
-
parentId?: Key;
|
|
11
|
-
data: T;
|
|
12
|
-
};
|
|
13
|
-
export interface iNodeUpdate {
|
|
14
|
-
success: boolean;
|
|
15
|
-
ErrorText?: string;
|
|
16
|
-
}
|
|
2
|
+
import { iNodeUpdate, TreeNodeData } from './TreeOfNodesContext';
|
|
17
3
|
interface iTreeOfNodes<T> {
|
|
4
|
+
id: string;
|
|
18
5
|
nodeList: TreeNodeData<T>[];
|
|
19
6
|
roots: Key[];
|
|
20
7
|
selectedId?: Key;
|
|
@@ -29,16 +16,5 @@ interface iTreeOfNodes<T> {
|
|
|
29
16
|
canRemoveChildren?: boolean;
|
|
30
17
|
canRenameChildren?: boolean;
|
|
31
18
|
}
|
|
32
|
-
declare
|
|
33
|
-
nodeList: TreeNodeData<T>[];
|
|
34
|
-
selectedId?: Key;
|
|
35
|
-
handleSelect?: (ret: Key) => void;
|
|
36
|
-
expandedNodes?: Key[];
|
|
37
|
-
handleExpandClick?: (ret: Key, force?: boolean) => void;
|
|
38
|
-
onAddChild?: (parentId: Key, newName: string) => Promise<iNodeUpdate>;
|
|
39
|
-
onRename?: (id: Key, newName: string) => Promise<iNodeUpdate>;
|
|
40
|
-
onRemove?: (id: Key) => Promise<iNodeUpdate>;
|
|
41
|
-
};
|
|
42
|
-
export declare const TreeOfNodesContext: import("react").Context<TreeOfNodesContextData<unknown>>;
|
|
43
|
-
export declare const TreeOfNodes: <T extends unknown>({ nodeList, roots, canRemoveRoot, canRenameRoot, canAddChildren, canRemoveChildren, canRenameChildren, selectedId, handleSelect, onAdd, onRename, onRemove, }: iTreeOfNodes<T>) => JSX.Element;
|
|
19
|
+
export declare const TreeOfNodes: <T extends unknown>({ id, nodeList, roots, canRemoveRoot, canRenameRoot, canAddChildren, canRemoveChildren, canRenameChildren, selectedId, handleSelect, onAdd, onRename, onRemove, }: iTreeOfNodes<T>) => JSX.Element;
|
|
44
20
|
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Key } from 'react';
|
|
2
|
+
export interface iNodeUpdate {
|
|
3
|
+
success: boolean;
|
|
4
|
+
ErrorText?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TreeNodeBase {
|
|
7
|
+
id: Key;
|
|
8
|
+
label: string;
|
|
9
|
+
parentId?: Key;
|
|
10
|
+
}
|
|
11
|
+
export interface TreeNodeData<T> {
|
|
12
|
+
id: Key;
|
|
13
|
+
label: string;
|
|
14
|
+
parentId?: Key;
|
|
15
|
+
data: T;
|
|
16
|
+
}
|
|
17
|
+
export interface TreeOfNodesContextData<T> {
|
|
18
|
+
id: string;
|
|
19
|
+
nodeList: TreeNodeData<T>[];
|
|
20
|
+
selectedId?: Key;
|
|
21
|
+
handleSelect?: (ret: Key) => void;
|
|
22
|
+
expandedNodes?: Key[];
|
|
23
|
+
handleExpandClick?: (ret: Key, force?: boolean) => void;
|
|
24
|
+
onAddChild?: (parentId: Key, newName: string) => Promise<iNodeUpdate>;
|
|
25
|
+
onRename?: (id: Key, newName: string) => Promise<iNodeUpdate>;
|
|
26
|
+
onRemove?: (id: Key) => Promise<iNodeUpdate>;
|
|
27
|
+
}
|
|
28
|
+
export declare const TreeOfNodesContext: import("react").Context<TreeOfNodesContextData<unknown>>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ContextMenu } from './ContextMenu';
|
|
2
2
|
import { ContextMenuProvider } from './ContextMenuProvider';
|
|
3
3
|
import { TreeNode } from './TreeNode';
|
|
4
|
-
import {
|
|
4
|
+
import { TreeOfNodes } from './TreeOfNodes';
|
|
5
|
+
import { iNodeUpdate, TreeNodeBase, TreeNodeData } from './TreeOfNodesContext';
|
|
5
6
|
import { WordEntry } from './WordEntry';
|
|
6
7
|
export { ContextMenu, ContextMenuProvider, TreeNode, TreeOfNodes, WordEntry };
|
|
7
8
|
export type { TreeNodeBase, TreeNodeData, iNodeUpdate };
|
package/dist/cjs/main.js
CHANGED
|
@@ -28,8 +28,7 @@ const ContextMenu = React.forwardRef(({ entries, visible, xPos, yPos }, ref) =>
|
|
|
28
28
|
top: `${yPos}px`,
|
|
29
29
|
left: `${xPos}px`,
|
|
30
30
|
}, children: entries.map((e, i) => (jsxRuntime.jsx("div", { className: 'context-menu-item', onClick: () => {
|
|
31
|
-
|
|
32
|
-
e.action();
|
|
31
|
+
e.action && e.action();
|
|
33
32
|
}, children: e.label ?? '' }, i))) }));
|
|
34
33
|
});
|
|
35
34
|
|
|
@@ -1342,7 +1341,7 @@ if (process.env.NODE_ENV !== 'production') {
|
|
|
1342
1341
|
const Spinner = /*#__PURE__*/React__namespace.forwardRef(({
|
|
1343
1342
|
bsPrefix,
|
|
1344
1343
|
variant,
|
|
1345
|
-
animation,
|
|
1344
|
+
animation = 'border',
|
|
1346
1345
|
size,
|
|
1347
1346
|
// Need to define the default "as" during prop destructuring to be compatible with styled-components github.com/react-bootstrap/react-bootstrap/issues/3595
|
|
1348
1347
|
as: Component = 'div',
|
|
@@ -1505,79 +1504,46 @@ ExclamationCircleFill.defaultProps = {
|
|
|
1505
1504
|
var ExclamationCircleFill$1 = ExclamationCircleFill;
|
|
1506
1505
|
|
|
1507
1506
|
const TreeOfNodesContext = React.createContext({
|
|
1507
|
+
id: 'tree-of-nodes',
|
|
1508
1508
|
nodeList: [],
|
|
1509
|
-
});
|
|
1510
|
-
const TreeOfNodes = ({ nodeList, roots,
|
|
1511
|
-
// canAddRoot = false,
|
|
1512
|
-
canRemoveRoot = false, canRenameRoot = false, canAddChildren = false, canRemoveChildren = false, canRenameChildren = false, selectedId, handleSelect = () => {
|
|
1513
|
-
return;
|
|
1514
|
-
}, onAdd, onRename, onRemove, }) => {
|
|
1515
|
-
const [expandedNodes, setExpandedNodes] = React.useState([]);
|
|
1516
|
-
// Change expansion and update context
|
|
1517
|
-
const changeExpand = React.useCallback((eKey, force) => {
|
|
1518
|
-
const newExpandedNodes = [...expandedNodes];
|
|
1519
|
-
const ix = newExpandedNodes.findIndex((nKey) => nKey === eKey);
|
|
1520
|
-
if ((ix === -1 && force === undefined) || force === true)
|
|
1521
|
-
newExpandedNodes.push(eKey);
|
|
1522
|
-
else
|
|
1523
|
-
newExpandedNodes.splice(ix, 1);
|
|
1524
|
-
setExpandedNodes(newExpandedNodes);
|
|
1525
|
-
}, [expandedNodes]);
|
|
1526
|
-
return (jsxRuntime.jsx(ContextMenuProvider, { children: jsxRuntime.jsx(TreeOfNodesContext.Provider, { value: {
|
|
1527
|
-
nodeList,
|
|
1528
|
-
selectedId,
|
|
1529
|
-
handleSelect,
|
|
1530
|
-
expandedNodes,
|
|
1531
|
-
handleExpandClick: (r, f) => changeExpand(r, f),
|
|
1532
|
-
onAddChild: onAdd,
|
|
1533
|
-
onRename,
|
|
1534
|
-
onRemove,
|
|
1535
|
-
}, children: roots.map((r) => (jsxRuntime.jsx(TreeNode, { id: r, canRemove: canRemoveRoot, canAddChildren: canAddChildren, canRename: canRenameRoot, canRemoveChildren: canRemoveChildren, canRenameChildren: canRenameChildren }, r))) }) }));
|
|
1536
|
-
};
|
|
1509
|
+
});
|
|
1537
1510
|
|
|
1538
1511
|
const WordEntry = React.forwardRef(({ id, value, editing, saving, setValue, sendEscape, style }, ref) => {
|
|
1539
|
-
const [currentStyle, setCurrentStyle] = React.useState({
|
|
1540
|
-
display: 'inline-block',
|
|
1541
|
-
});
|
|
1542
1512
|
WordEntry.displayName = 'WordEntry';
|
|
1513
|
+
const currentStyle = React.useMemo(() => {
|
|
1514
|
+
return {
|
|
1515
|
+
display: 'inline-block',
|
|
1516
|
+
padding: '1px',
|
|
1517
|
+
paddingLeft: '3px',
|
|
1518
|
+
paddingRight: '3px',
|
|
1519
|
+
border: '1px',
|
|
1520
|
+
borderRadius: '3px',
|
|
1521
|
+
...style,
|
|
1522
|
+
};
|
|
1523
|
+
}, [style]);
|
|
1543
1524
|
const [currentValue, setCurrentValue] = React.useState(value ?? '');
|
|
1544
1525
|
React.useEffect(() => {
|
|
1545
1526
|
setCurrentValue(value ?? '');
|
|
1546
1527
|
}, [value]);
|
|
1547
1528
|
const returnData = React.useCallback(() => {
|
|
1548
|
-
|
|
1549
|
-
return;
|
|
1550
|
-
setValue(currentValue);
|
|
1529
|
+
setValue && setValue(currentValue);
|
|
1551
1530
|
}, [currentValue, setValue]);
|
|
1552
1531
|
const keyPress = React.useCallback((e) => {
|
|
1553
1532
|
if (e.code === 'Escape') {
|
|
1554
1533
|
setCurrentValue(value ?? '');
|
|
1555
|
-
|
|
1556
|
-
sendEscape();
|
|
1534
|
+
sendEscape && sendEscape();
|
|
1557
1535
|
}
|
|
1558
1536
|
else if (e.code === 'Enter') {
|
|
1559
1537
|
returnData();
|
|
1560
1538
|
}
|
|
1561
1539
|
}, [returnData, sendEscape, value]);
|
|
1562
|
-
React.useEffect(() => {
|
|
1563
|
-
setCurrentStyle({
|
|
1564
|
-
padding: '1px',
|
|
1565
|
-
paddingLeft: '3px',
|
|
1566
|
-
paddingRight: '3px',
|
|
1567
|
-
border: '1px',
|
|
1568
|
-
borderRadius: '3px',
|
|
1569
|
-
display: 'inline-block',
|
|
1570
|
-
...style,
|
|
1571
|
-
});
|
|
1572
|
-
}, [style]);
|
|
1573
1540
|
// Simple if not editing
|
|
1574
1541
|
if (!editing) {
|
|
1575
|
-
return jsxRuntime.jsx("span", { style: currentStyle, children: value });
|
|
1542
|
+
return (jsxRuntime.jsx("span", { style: currentStyle, id: String(id), children: value }));
|
|
1576
1543
|
}
|
|
1577
|
-
return (jsxRuntime.jsxs("div", { style: currentStyle, id: id
|
|
1544
|
+
return (jsxRuntime.jsxs("div", { style: currentStyle, id: `${id}-holder`, children: [saving && (jsxRuntime.jsx(Spinner$1, { id: `${id}-spinner`, size: 'sm', animation: 'border' })), jsxRuntime.jsx("input", { id: String(id), ref: ref, type: 'text', style: {
|
|
1578
1545
|
lineHeight: '0.75rem',
|
|
1579
|
-
border:
|
|
1580
|
-
margin: !saving && !editing ? '1px' : '',
|
|
1546
|
+
border: '1px dotted black',
|
|
1581
1547
|
width: 'auto',
|
|
1582
1548
|
minWidth: '0',
|
|
1583
1549
|
}, value: currentValue, disabled: saving, onChange: (e) => setCurrentValue(e.currentTarget.value), onBlur: returnData, onKeyDownCapture: keyPress })] }));
|
|
@@ -1590,7 +1556,7 @@ const TreeNode = ({ id, canRemove, canRename, canAddChildren, canRemoveChildren,
|
|
|
1590
1556
|
// Node information
|
|
1591
1557
|
const thisNode = React.useMemo(() => treeContext.nodeList.find((n) => n.id === id), [id, treeContext.nodeList]);
|
|
1592
1558
|
const childNodes = React.useMemo(() => treeContext.nodeList.filter((n) => n.parentId === id), [id, treeContext.nodeList]);
|
|
1593
|
-
//
|
|
1559
|
+
// State
|
|
1594
1560
|
const loading = React.useMemo(() => treeContext.nodeList.findIndex((n) => n.id === id) === -1, [id, treeContext.nodeList]);
|
|
1595
1561
|
const [error, setError] = React.useState(false);
|
|
1596
1562
|
const [errorText, setErrotText] = React.useState(null);
|
|
@@ -1638,14 +1604,12 @@ const TreeNode = ({ id, canRemove, canRename, canAddChildren, canRemoveChildren,
|
|
|
1638
1604
|
}, [handleReturn, id, treeContext]);
|
|
1639
1605
|
const deleteThis = React.useCallback(async () => {
|
|
1640
1606
|
setUpdatingNode(true);
|
|
1641
|
-
if (thisNode === undefined)
|
|
1642
|
-
return;
|
|
1643
1607
|
if (treeContext.onRemove) {
|
|
1644
1608
|
handleReturn(await treeContext.onRemove(id));
|
|
1645
1609
|
}
|
|
1646
1610
|
setUpdatingNode(false);
|
|
1647
|
-
}, [handleReturn, id,
|
|
1648
|
-
//
|
|
1611
|
+
}, [handleReturn, id, treeContext]);
|
|
1612
|
+
// Context actions
|
|
1649
1613
|
const addChild = React.useCallback(() => {
|
|
1650
1614
|
if (menuContext.set) {
|
|
1651
1615
|
menuContext.set({ visible: false });
|
|
@@ -1678,12 +1642,15 @@ const TreeNode = ({ id, canRemove, canRename, canAddChildren, canRemoveChildren,
|
|
|
1678
1642
|
e.preventDefault();
|
|
1679
1643
|
e.stopPropagation();
|
|
1680
1644
|
const menuItems = [];
|
|
1681
|
-
if (canAddChildren)
|
|
1645
|
+
if (canAddChildren) {
|
|
1682
1646
|
menuItems.push({ label: 'Add', action: addChild });
|
|
1683
|
-
|
|
1647
|
+
}
|
|
1648
|
+
if (canRename) {
|
|
1684
1649
|
menuItems.push({ label: 'Rename', action: renameThis });
|
|
1685
|
-
|
|
1650
|
+
}
|
|
1651
|
+
if (canRemove && (childNodes === undefined || childNodes.length === 0)) {
|
|
1686
1652
|
menuItems.push({ label: 'Delete', action: removeThis });
|
|
1653
|
+
}
|
|
1687
1654
|
if (typeof menuContext.set === 'function') {
|
|
1688
1655
|
menuContext.set({
|
|
1689
1656
|
visible: true,
|
|
@@ -1703,11 +1670,12 @@ const TreeNode = ({ id, canRemove, canRename, canAddChildren, canRemoveChildren,
|
|
|
1703
1670
|
renameThis,
|
|
1704
1671
|
]);
|
|
1705
1672
|
// Return node
|
|
1706
|
-
return (jsxRuntime.jsxs("div", { className: '
|
|
1673
|
+
return (jsxRuntime.jsxs("div", { id: `${treeContext.id}-treenode-${id}`, className: 'treenode', onClickCapture: () => treeContext.handleSelect && treeContext.handleSelect(id), onContextMenuCapture: () => treeContext.handleSelect && treeContext.handleSelect(id), onFocusCapture: () => treeContext.handleSelect && treeContext.handleSelect(id), children: [error ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(ExclamationCircleFill$1, { color: 'var(--bs-danger)' }), ' ', errorText && errorText !== '' ? errorText : 'An unknown error has occured'] })) : loading ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Spinner$1, { size: 'sm', animation: 'border' }), ' ', "Loading..."] })) : (jsxRuntime.jsxs("span", { onContextMenu: showMenu, children: [childNodes !== undefined && childNodes.length > 0 ? (!expanded ? (jsxRuntime.jsx(CaretRightFill$1, { role: 'button', "aria-expanded": false, id: `${treeContext.id}-treenode-caret-${id}`, onClick: () => treeContext.handleExpandClick && treeContext.handleExpandClick(id) })) : (jsxRuntime.jsx(CaretDownFill$1, { role: 'button', "aria-expanded": true, id: `${treeContext.id}-treenode-caret-${id}`, onClick: () => treeContext.handleExpandClick && treeContext.handleExpandClick(id) }))) : (jsxRuntime.jsx(CaretRight$1, { role: 'button', "aria-expanded": false, "aria-disabled": true })), thisNode && (jsxRuntime.jsx(WordEntry, { id: `${treeContext.id}-treenode-entry-${id}`, ref: currentNameRef, value: thisNode.label, editing: renaming, saving: updatingNode, setValue: (ret) => renameNode(ret), sendEscape: () => setRenaming(false), style: {
|
|
1707
1674
|
border: currentBorder,
|
|
1708
1675
|
margin: currentBorder === '' ? '1px' : '',
|
|
1709
1676
|
backgroundColor: currentBorder === '' ? '' : 'lightgrey',
|
|
1710
|
-
} }))
|
|
1677
|
+
} }))] })), childNodes !== undefined &&
|
|
1678
|
+
childNodes.map((h, i) => (jsxRuntime.jsx("span", { id: `${treeContext.id}-treenode-child-${h.id}`, style: {
|
|
1711
1679
|
marginLeft: '16px',
|
|
1712
1680
|
display: 'block',
|
|
1713
1681
|
maxHeight: expanded ? '99999px' : '0px',
|
|
@@ -1716,13 +1684,42 @@ const TreeNode = ({ id, canRemove, canRename, canAddChildren, canRemoveChildren,
|
|
|
1716
1684
|
visibility: expanded ? 'inherit' : 'hidden',
|
|
1717
1685
|
opacity: expanded ? 1 : 0,
|
|
1718
1686
|
transition: 'visibility .3s, opacity .3s ease-in-out, max-height 0.3s ease-in-out',
|
|
1719
|
-
}, children: jsxRuntime.jsx(TreeNode, { id: h.id, canRemove: canRemoveChildren, canRename: canRenameChildren, canAddChildren: canAddChildren, canRemoveChildren: canRemoveChildren, canRenameChildren: canRenameChildren }) }, h.id)))
|
|
1687
|
+
}, children: jsxRuntime.jsx(TreeNode, { id: h.id, canRemove: canRemoveChildren, canRename: canRenameChildren, canAddChildren: canAddChildren, canRemoveChildren: canRemoveChildren, canRenameChildren: canRenameChildren }) }, h.id))), showNewNode && (jsxRuntime.jsx("div", { children: jsxRuntime.jsx(WordEntry, { style: { marginLeft: '14px' }, id: `treenode-new-${id}`, ref: newNameRef, editing: true, setValue: (ret) => {
|
|
1720
1688
|
confirmNewNode(ret);
|
|
1721
1689
|
}, saving: savingNewNode, sendEscape: () => {
|
|
1722
1690
|
setShowNewNode(false);
|
|
1723
1691
|
} }) }))] }));
|
|
1724
1692
|
};
|
|
1725
1693
|
|
|
1694
|
+
const TreeOfNodes = ({ id, nodeList, roots,
|
|
1695
|
+
// canAddRoot = false,
|
|
1696
|
+
canRemoveRoot = false, canRenameRoot = false, canAddChildren = false, canRemoveChildren = false, canRenameChildren = false, selectedId, handleSelect = () => {
|
|
1697
|
+
return;
|
|
1698
|
+
}, onAdd, onRename, onRemove, }) => {
|
|
1699
|
+
const [expandedNodes, setExpandedNodes] = React.useState([]);
|
|
1700
|
+
// Change expansion and update context
|
|
1701
|
+
const changeExpand = React.useCallback((eKey, force) => {
|
|
1702
|
+
const newExpandedNodes = [...expandedNodes];
|
|
1703
|
+
const ix = newExpandedNodes.findIndex((nKey) => nKey === eKey);
|
|
1704
|
+
if ((ix === -1 && force === undefined) || force === true)
|
|
1705
|
+
newExpandedNodes.push(eKey);
|
|
1706
|
+
else
|
|
1707
|
+
newExpandedNodes.splice(ix, 1);
|
|
1708
|
+
setExpandedNodes(newExpandedNodes);
|
|
1709
|
+
}, [expandedNodes]);
|
|
1710
|
+
return (jsxRuntime.jsx(ContextMenuProvider, { children: jsxRuntime.jsx(TreeOfNodesContext.Provider, { value: {
|
|
1711
|
+
id: id,
|
|
1712
|
+
nodeList,
|
|
1713
|
+
selectedId,
|
|
1714
|
+
handleSelect,
|
|
1715
|
+
expandedNodes,
|
|
1716
|
+
handleExpandClick: (r, f) => changeExpand(r, f),
|
|
1717
|
+
onAddChild: onAdd,
|
|
1718
|
+
onRename,
|
|
1719
|
+
onRemove,
|
|
1720
|
+
}, children: roots.map((r) => (jsxRuntime.jsx(TreeNode, { id: r, canRemove: canRemoveRoot, canAddChildren: canAddChildren, canRename: canRenameRoot, canRemoveChildren: canRemoveChildren, canRenameChildren: canRenameChildren }, r))) }) }));
|
|
1721
|
+
};
|
|
1722
|
+
|
|
1726
1723
|
exports.ContextMenu = ContextMenu;
|
|
1727
1724
|
exports.ContextMenuProvider = ContextMenuProvider;
|
|
1728
1725
|
exports.TreeNode = TreeNode;
|
|
@@ -1,20 +1,7 @@
|
|
|
1
1
|
import { Key } from 'react';
|
|
2
|
-
|
|
3
|
-
id: Key;
|
|
4
|
-
label: string;
|
|
5
|
-
parentId?: Key;
|
|
6
|
-
};
|
|
7
|
-
export declare type TreeNodeData<T> = {
|
|
8
|
-
id: Key;
|
|
9
|
-
label: string;
|
|
10
|
-
parentId?: Key;
|
|
11
|
-
data: T;
|
|
12
|
-
};
|
|
13
|
-
export interface iNodeUpdate {
|
|
14
|
-
success: boolean;
|
|
15
|
-
ErrorText?: string;
|
|
16
|
-
}
|
|
2
|
+
import { iNodeUpdate, TreeNodeData } from './TreeOfNodesContext';
|
|
17
3
|
interface iTreeOfNodes<T> {
|
|
4
|
+
id: string;
|
|
18
5
|
nodeList: TreeNodeData<T>[];
|
|
19
6
|
roots: Key[];
|
|
20
7
|
selectedId?: Key;
|
|
@@ -29,16 +16,5 @@ interface iTreeOfNodes<T> {
|
|
|
29
16
|
canRemoveChildren?: boolean;
|
|
30
17
|
canRenameChildren?: boolean;
|
|
31
18
|
}
|
|
32
|
-
declare
|
|
33
|
-
nodeList: TreeNodeData<T>[];
|
|
34
|
-
selectedId?: Key;
|
|
35
|
-
handleSelect?: (ret: Key) => void;
|
|
36
|
-
expandedNodes?: Key[];
|
|
37
|
-
handleExpandClick?: (ret: Key, force?: boolean) => void;
|
|
38
|
-
onAddChild?: (parentId: Key, newName: string) => Promise<iNodeUpdate>;
|
|
39
|
-
onRename?: (id: Key, newName: string) => Promise<iNodeUpdate>;
|
|
40
|
-
onRemove?: (id: Key) => Promise<iNodeUpdate>;
|
|
41
|
-
};
|
|
42
|
-
export declare const TreeOfNodesContext: import("react").Context<TreeOfNodesContextData<unknown>>;
|
|
43
|
-
export declare const TreeOfNodes: <T extends unknown>({ nodeList, roots, canRemoveRoot, canRenameRoot, canAddChildren, canRemoveChildren, canRenameChildren, selectedId, handleSelect, onAdd, onRename, onRemove, }: iTreeOfNodes<T>) => JSX.Element;
|
|
19
|
+
export declare const TreeOfNodes: <T extends unknown>({ id, nodeList, roots, canRemoveRoot, canRenameRoot, canAddChildren, canRemoveChildren, canRenameChildren, selectedId, handleSelect, onAdd, onRename, onRemove, }: iTreeOfNodes<T>) => JSX.Element;
|
|
44
20
|
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Key } from 'react';
|
|
2
|
+
export interface iNodeUpdate {
|
|
3
|
+
success: boolean;
|
|
4
|
+
ErrorText?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TreeNodeBase {
|
|
7
|
+
id: Key;
|
|
8
|
+
label: string;
|
|
9
|
+
parentId?: Key;
|
|
10
|
+
}
|
|
11
|
+
export interface TreeNodeData<T> {
|
|
12
|
+
id: Key;
|
|
13
|
+
label: string;
|
|
14
|
+
parentId?: Key;
|
|
15
|
+
data: T;
|
|
16
|
+
}
|
|
17
|
+
export interface TreeOfNodesContextData<T> {
|
|
18
|
+
id: string;
|
|
19
|
+
nodeList: TreeNodeData<T>[];
|
|
20
|
+
selectedId?: Key;
|
|
21
|
+
handleSelect?: (ret: Key) => void;
|
|
22
|
+
expandedNodes?: Key[];
|
|
23
|
+
handleExpandClick?: (ret: Key, force?: boolean) => void;
|
|
24
|
+
onAddChild?: (parentId: Key, newName: string) => Promise<iNodeUpdate>;
|
|
25
|
+
onRename?: (id: Key, newName: string) => Promise<iNodeUpdate>;
|
|
26
|
+
onRemove?: (id: Key) => Promise<iNodeUpdate>;
|
|
27
|
+
}
|
|
28
|
+
export declare const TreeOfNodesContext: import("react").Context<TreeOfNodesContextData<unknown>>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ContextMenu } from './ContextMenu';
|
|
2
2
|
import { ContextMenuProvider } from './ContextMenuProvider';
|
|
3
3
|
import { TreeNode } from './TreeNode';
|
|
4
|
-
import {
|
|
4
|
+
import { TreeOfNodes } from './TreeOfNodes';
|
|
5
|
+
import { iNodeUpdate, TreeNodeBase, TreeNodeData } from './TreeOfNodesContext';
|
|
5
6
|
import { WordEntry } from './WordEntry';
|
|
6
7
|
export { ContextMenu, ContextMenuProvider, TreeNode, TreeOfNodes, WordEntry };
|
|
7
8
|
export type { TreeNodeBase, TreeNodeData, iNodeUpdate };
|
package/dist/main.js
CHANGED
|
@@ -8,8 +8,7 @@ const ContextMenu = React__default.forwardRef(({ entries, visible, xPos, yPos },
|
|
|
8
8
|
top: `${yPos}px`,
|
|
9
9
|
left: `${xPos}px`,
|
|
10
10
|
}, children: entries.map((e, i) => (jsx("div", { className: 'context-menu-item', onClick: () => {
|
|
11
|
-
|
|
12
|
-
e.action();
|
|
11
|
+
e.action && e.action();
|
|
13
12
|
}, children: e.label ?? '' }, i))) }));
|
|
14
13
|
});
|
|
15
14
|
|
|
@@ -1322,7 +1321,7 @@ if (process.env.NODE_ENV !== 'production') {
|
|
|
1322
1321
|
const Spinner = /*#__PURE__*/React.forwardRef(({
|
|
1323
1322
|
bsPrefix,
|
|
1324
1323
|
variant,
|
|
1325
|
-
animation,
|
|
1324
|
+
animation = 'border',
|
|
1326
1325
|
size,
|
|
1327
1326
|
// Need to define the default "as" during prop destructuring to be compatible with styled-components github.com/react-bootstrap/react-bootstrap/issues/3595
|
|
1328
1327
|
as: Component = 'div',
|
|
@@ -1485,79 +1484,46 @@ ExclamationCircleFill.defaultProps = {
|
|
|
1485
1484
|
var ExclamationCircleFill$1 = ExclamationCircleFill;
|
|
1486
1485
|
|
|
1487
1486
|
const TreeOfNodesContext = createContext({
|
|
1487
|
+
id: 'tree-of-nodes',
|
|
1488
1488
|
nodeList: [],
|
|
1489
|
-
});
|
|
1490
|
-
const TreeOfNodes = ({ nodeList, roots,
|
|
1491
|
-
// canAddRoot = false,
|
|
1492
|
-
canRemoveRoot = false, canRenameRoot = false, canAddChildren = false, canRemoveChildren = false, canRenameChildren = false, selectedId, handleSelect = () => {
|
|
1493
|
-
return;
|
|
1494
|
-
}, onAdd, onRename, onRemove, }) => {
|
|
1495
|
-
const [expandedNodes, setExpandedNodes] = useState([]);
|
|
1496
|
-
// Change expansion and update context
|
|
1497
|
-
const changeExpand = useCallback((eKey, force) => {
|
|
1498
|
-
const newExpandedNodes = [...expandedNodes];
|
|
1499
|
-
const ix = newExpandedNodes.findIndex((nKey) => nKey === eKey);
|
|
1500
|
-
if ((ix === -1 && force === undefined) || force === true)
|
|
1501
|
-
newExpandedNodes.push(eKey);
|
|
1502
|
-
else
|
|
1503
|
-
newExpandedNodes.splice(ix, 1);
|
|
1504
|
-
setExpandedNodes(newExpandedNodes);
|
|
1505
|
-
}, [expandedNodes]);
|
|
1506
|
-
return (jsx(ContextMenuProvider, { children: jsx(TreeOfNodesContext.Provider, { value: {
|
|
1507
|
-
nodeList,
|
|
1508
|
-
selectedId,
|
|
1509
|
-
handleSelect,
|
|
1510
|
-
expandedNodes,
|
|
1511
|
-
handleExpandClick: (r, f) => changeExpand(r, f),
|
|
1512
|
-
onAddChild: onAdd,
|
|
1513
|
-
onRename,
|
|
1514
|
-
onRemove,
|
|
1515
|
-
}, children: roots.map((r) => (jsx(TreeNode, { id: r, canRemove: canRemoveRoot, canAddChildren: canAddChildren, canRename: canRenameRoot, canRemoveChildren: canRemoveChildren, canRenameChildren: canRenameChildren }, r))) }) }));
|
|
1516
|
-
};
|
|
1489
|
+
});
|
|
1517
1490
|
|
|
1518
1491
|
const WordEntry = React__default.forwardRef(({ id, value, editing, saving, setValue, sendEscape, style }, ref) => {
|
|
1519
|
-
const [currentStyle, setCurrentStyle] = useState({
|
|
1520
|
-
display: 'inline-block',
|
|
1521
|
-
});
|
|
1522
1492
|
WordEntry.displayName = 'WordEntry';
|
|
1493
|
+
const currentStyle = useMemo(() => {
|
|
1494
|
+
return {
|
|
1495
|
+
display: 'inline-block',
|
|
1496
|
+
padding: '1px',
|
|
1497
|
+
paddingLeft: '3px',
|
|
1498
|
+
paddingRight: '3px',
|
|
1499
|
+
border: '1px',
|
|
1500
|
+
borderRadius: '3px',
|
|
1501
|
+
...style,
|
|
1502
|
+
};
|
|
1503
|
+
}, [style]);
|
|
1523
1504
|
const [currentValue, setCurrentValue] = useState(value ?? '');
|
|
1524
1505
|
useEffect(() => {
|
|
1525
1506
|
setCurrentValue(value ?? '');
|
|
1526
1507
|
}, [value]);
|
|
1527
1508
|
const returnData = useCallback(() => {
|
|
1528
|
-
|
|
1529
|
-
return;
|
|
1530
|
-
setValue(currentValue);
|
|
1509
|
+
setValue && setValue(currentValue);
|
|
1531
1510
|
}, [currentValue, setValue]);
|
|
1532
1511
|
const keyPress = useCallback((e) => {
|
|
1533
1512
|
if (e.code === 'Escape') {
|
|
1534
1513
|
setCurrentValue(value ?? '');
|
|
1535
|
-
|
|
1536
|
-
sendEscape();
|
|
1514
|
+
sendEscape && sendEscape();
|
|
1537
1515
|
}
|
|
1538
1516
|
else if (e.code === 'Enter') {
|
|
1539
1517
|
returnData();
|
|
1540
1518
|
}
|
|
1541
1519
|
}, [returnData, sendEscape, value]);
|
|
1542
|
-
useEffect(() => {
|
|
1543
|
-
setCurrentStyle({
|
|
1544
|
-
padding: '1px',
|
|
1545
|
-
paddingLeft: '3px',
|
|
1546
|
-
paddingRight: '3px',
|
|
1547
|
-
border: '1px',
|
|
1548
|
-
borderRadius: '3px',
|
|
1549
|
-
display: 'inline-block',
|
|
1550
|
-
...style,
|
|
1551
|
-
});
|
|
1552
|
-
}, [style]);
|
|
1553
1520
|
// Simple if not editing
|
|
1554
1521
|
if (!editing) {
|
|
1555
|
-
return jsx("span", { style: currentStyle, children: value });
|
|
1522
|
+
return (jsx("span", { style: currentStyle, id: String(id), children: value }));
|
|
1556
1523
|
}
|
|
1557
|
-
return (jsxs("div", { style: currentStyle, id: id
|
|
1524
|
+
return (jsxs("div", { style: currentStyle, id: `${id}-holder`, children: [saving && (jsx(Spinner$1, { id: `${id}-spinner`, size: 'sm', animation: 'border' })), jsx("input", { id: String(id), ref: ref, type: 'text', style: {
|
|
1558
1525
|
lineHeight: '0.75rem',
|
|
1559
|
-
border:
|
|
1560
|
-
margin: !saving && !editing ? '1px' : '',
|
|
1526
|
+
border: '1px dotted black',
|
|
1561
1527
|
width: 'auto',
|
|
1562
1528
|
minWidth: '0',
|
|
1563
1529
|
}, value: currentValue, disabled: saving, onChange: (e) => setCurrentValue(e.currentTarget.value), onBlur: returnData, onKeyDownCapture: keyPress })] }));
|
|
@@ -1570,7 +1536,7 @@ const TreeNode = ({ id, canRemove, canRename, canAddChildren, canRemoveChildren,
|
|
|
1570
1536
|
// Node information
|
|
1571
1537
|
const thisNode = useMemo(() => treeContext.nodeList.find((n) => n.id === id), [id, treeContext.nodeList]);
|
|
1572
1538
|
const childNodes = useMemo(() => treeContext.nodeList.filter((n) => n.parentId === id), [id, treeContext.nodeList]);
|
|
1573
|
-
//
|
|
1539
|
+
// State
|
|
1574
1540
|
const loading = useMemo(() => treeContext.nodeList.findIndex((n) => n.id === id) === -1, [id, treeContext.nodeList]);
|
|
1575
1541
|
const [error, setError] = useState(false);
|
|
1576
1542
|
const [errorText, setErrotText] = useState(null);
|
|
@@ -1618,14 +1584,12 @@ const TreeNode = ({ id, canRemove, canRename, canAddChildren, canRemoveChildren,
|
|
|
1618
1584
|
}, [handleReturn, id, treeContext]);
|
|
1619
1585
|
const deleteThis = useCallback(async () => {
|
|
1620
1586
|
setUpdatingNode(true);
|
|
1621
|
-
if (thisNode === undefined)
|
|
1622
|
-
return;
|
|
1623
1587
|
if (treeContext.onRemove) {
|
|
1624
1588
|
handleReturn(await treeContext.onRemove(id));
|
|
1625
1589
|
}
|
|
1626
1590
|
setUpdatingNode(false);
|
|
1627
|
-
}, [handleReturn, id,
|
|
1628
|
-
//
|
|
1591
|
+
}, [handleReturn, id, treeContext]);
|
|
1592
|
+
// Context actions
|
|
1629
1593
|
const addChild = useCallback(() => {
|
|
1630
1594
|
if (menuContext.set) {
|
|
1631
1595
|
menuContext.set({ visible: false });
|
|
@@ -1658,12 +1622,15 @@ const TreeNode = ({ id, canRemove, canRename, canAddChildren, canRemoveChildren,
|
|
|
1658
1622
|
e.preventDefault();
|
|
1659
1623
|
e.stopPropagation();
|
|
1660
1624
|
const menuItems = [];
|
|
1661
|
-
if (canAddChildren)
|
|
1625
|
+
if (canAddChildren) {
|
|
1662
1626
|
menuItems.push({ label: 'Add', action: addChild });
|
|
1663
|
-
|
|
1627
|
+
}
|
|
1628
|
+
if (canRename) {
|
|
1664
1629
|
menuItems.push({ label: 'Rename', action: renameThis });
|
|
1665
|
-
|
|
1630
|
+
}
|
|
1631
|
+
if (canRemove && (childNodes === undefined || childNodes.length === 0)) {
|
|
1666
1632
|
menuItems.push({ label: 'Delete', action: removeThis });
|
|
1633
|
+
}
|
|
1667
1634
|
if (typeof menuContext.set === 'function') {
|
|
1668
1635
|
menuContext.set({
|
|
1669
1636
|
visible: true,
|
|
@@ -1683,11 +1650,12 @@ const TreeNode = ({ id, canRemove, canRename, canAddChildren, canRemoveChildren,
|
|
|
1683
1650
|
renameThis,
|
|
1684
1651
|
]);
|
|
1685
1652
|
// Return node
|
|
1686
|
-
return (jsxs("div", { className: '
|
|
1653
|
+
return (jsxs("div", { id: `${treeContext.id}-treenode-${id}`, className: 'treenode', onClickCapture: () => treeContext.handleSelect && treeContext.handleSelect(id), onContextMenuCapture: () => treeContext.handleSelect && treeContext.handleSelect(id), onFocusCapture: () => treeContext.handleSelect && treeContext.handleSelect(id), children: [error ? (jsxs(Fragment, { children: [jsx(ExclamationCircleFill$1, { color: 'var(--bs-danger)' }), ' ', errorText && errorText !== '' ? errorText : 'An unknown error has occured'] })) : loading ? (jsxs(Fragment, { children: [jsx(Spinner$1, { size: 'sm', animation: 'border' }), ' ', "Loading..."] })) : (jsxs("span", { onContextMenu: showMenu, children: [childNodes !== undefined && childNodes.length > 0 ? (!expanded ? (jsx(CaretRightFill$1, { role: 'button', "aria-expanded": false, id: `${treeContext.id}-treenode-caret-${id}`, onClick: () => treeContext.handleExpandClick && treeContext.handleExpandClick(id) })) : (jsx(CaretDownFill$1, { role: 'button', "aria-expanded": true, id: `${treeContext.id}-treenode-caret-${id}`, onClick: () => treeContext.handleExpandClick && treeContext.handleExpandClick(id) }))) : (jsx(CaretRight$1, { role: 'button', "aria-expanded": false, "aria-disabled": true })), thisNode && (jsx(WordEntry, { id: `${treeContext.id}-treenode-entry-${id}`, ref: currentNameRef, value: thisNode.label, editing: renaming, saving: updatingNode, setValue: (ret) => renameNode(ret), sendEscape: () => setRenaming(false), style: {
|
|
1687
1654
|
border: currentBorder,
|
|
1688
1655
|
margin: currentBorder === '' ? '1px' : '',
|
|
1689
1656
|
backgroundColor: currentBorder === '' ? '' : 'lightgrey',
|
|
1690
|
-
} }))
|
|
1657
|
+
} }))] })), childNodes !== undefined &&
|
|
1658
|
+
childNodes.map((h, i) => (jsx("span", { id: `${treeContext.id}-treenode-child-${h.id}`, style: {
|
|
1691
1659
|
marginLeft: '16px',
|
|
1692
1660
|
display: 'block',
|
|
1693
1661
|
maxHeight: expanded ? '99999px' : '0px',
|
|
@@ -1696,11 +1664,40 @@ const TreeNode = ({ id, canRemove, canRename, canAddChildren, canRemoveChildren,
|
|
|
1696
1664
|
visibility: expanded ? 'inherit' : 'hidden',
|
|
1697
1665
|
opacity: expanded ? 1 : 0,
|
|
1698
1666
|
transition: 'visibility .3s, opacity .3s ease-in-out, max-height 0.3s ease-in-out',
|
|
1699
|
-
}, children: jsx(TreeNode, { id: h.id, canRemove: canRemoveChildren, canRename: canRenameChildren, canAddChildren: canAddChildren, canRemoveChildren: canRemoveChildren, canRenameChildren: canRenameChildren }) }, h.id)))
|
|
1667
|
+
}, children: jsx(TreeNode, { id: h.id, canRemove: canRemoveChildren, canRename: canRenameChildren, canAddChildren: canAddChildren, canRemoveChildren: canRemoveChildren, canRenameChildren: canRenameChildren }) }, h.id))), showNewNode && (jsx("div", { children: jsx(WordEntry, { style: { marginLeft: '14px' }, id: `treenode-new-${id}`, ref: newNameRef, editing: true, setValue: (ret) => {
|
|
1700
1668
|
confirmNewNode(ret);
|
|
1701
1669
|
}, saving: savingNewNode, sendEscape: () => {
|
|
1702
1670
|
setShowNewNode(false);
|
|
1703
1671
|
} }) }))] }));
|
|
1704
1672
|
};
|
|
1705
1673
|
|
|
1674
|
+
const TreeOfNodes = ({ id, nodeList, roots,
|
|
1675
|
+
// canAddRoot = false,
|
|
1676
|
+
canRemoveRoot = false, canRenameRoot = false, canAddChildren = false, canRemoveChildren = false, canRenameChildren = false, selectedId, handleSelect = () => {
|
|
1677
|
+
return;
|
|
1678
|
+
}, onAdd, onRename, onRemove, }) => {
|
|
1679
|
+
const [expandedNodes, setExpandedNodes] = useState([]);
|
|
1680
|
+
// Change expansion and update context
|
|
1681
|
+
const changeExpand = useCallback((eKey, force) => {
|
|
1682
|
+
const newExpandedNodes = [...expandedNodes];
|
|
1683
|
+
const ix = newExpandedNodes.findIndex((nKey) => nKey === eKey);
|
|
1684
|
+
if ((ix === -1 && force === undefined) || force === true)
|
|
1685
|
+
newExpandedNodes.push(eKey);
|
|
1686
|
+
else
|
|
1687
|
+
newExpandedNodes.splice(ix, 1);
|
|
1688
|
+
setExpandedNodes(newExpandedNodes);
|
|
1689
|
+
}, [expandedNodes]);
|
|
1690
|
+
return (jsx(ContextMenuProvider, { children: jsx(TreeOfNodesContext.Provider, { value: {
|
|
1691
|
+
id: id,
|
|
1692
|
+
nodeList,
|
|
1693
|
+
selectedId,
|
|
1694
|
+
handleSelect,
|
|
1695
|
+
expandedNodes,
|
|
1696
|
+
handleExpandClick: (r, f) => changeExpand(r, f),
|
|
1697
|
+
onAddChild: onAdd,
|
|
1698
|
+
onRename,
|
|
1699
|
+
onRemove,
|
|
1700
|
+
}, children: roots.map((r) => (jsx(TreeNode, { id: r, canRemove: canRemoveRoot, canAddChildren: canAddChildren, canRename: canRenameRoot, canRemoveChildren: canRemoveChildren, canRenameChildren: canRenameChildren }, r))) }) }));
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1706
1703
|
export { ContextMenu, ContextMenuProvider, TreeNode, TreeOfNodes, WordEntry };
|
package/dist/tree-of-nodes.d.ts
CHANGED
|
@@ -29,22 +29,24 @@ interface TreeNodeProps {
|
|
|
29
29
|
}
|
|
30
30
|
declare const TreeNode: ({ id, canRemove, canRename, canAddChildren, canRemoveChildren, canRenameChildren, }: TreeNodeProps) => JSX.Element;
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
interface iNodeUpdate {
|
|
33
|
+
success: boolean;
|
|
34
|
+
ErrorText?: string;
|
|
35
|
+
}
|
|
36
|
+
interface TreeNodeBase {
|
|
33
37
|
id: Key;
|
|
34
38
|
label: string;
|
|
35
39
|
parentId?: Key;
|
|
36
|
-
}
|
|
37
|
-
|
|
40
|
+
}
|
|
41
|
+
interface TreeNodeData<T> {
|
|
38
42
|
id: Key;
|
|
39
43
|
label: string;
|
|
40
44
|
parentId?: Key;
|
|
41
45
|
data: T;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
success: boolean;
|
|
45
|
-
ErrorText?: string;
|
|
46
|
-
}
|
|
46
|
+
}
|
|
47
|
+
|
|
47
48
|
interface iTreeOfNodes<T> {
|
|
49
|
+
id: string;
|
|
48
50
|
nodeList: TreeNodeData<T>[];
|
|
49
51
|
roots: Key[];
|
|
50
52
|
selectedId?: Key;
|
|
@@ -59,10 +61,10 @@ interface iTreeOfNodes<T> {
|
|
|
59
61
|
canRemoveChildren?: boolean;
|
|
60
62
|
canRenameChildren?: boolean;
|
|
61
63
|
}
|
|
62
|
-
declare const TreeOfNodes: <T extends unknown>({ nodeList, roots, canRemoveRoot, canRenameRoot, canAddChildren, canRemoveChildren, canRenameChildren, selectedId, handleSelect, onAdd, onRename, onRemove, }: iTreeOfNodes<T>) => JSX.Element;
|
|
64
|
+
declare const TreeOfNodes: <T extends unknown>({ id, nodeList, roots, canRemoveRoot, canRenameRoot, canAddChildren, canRemoveChildren, canRenameChildren, selectedId, handleSelect, onAdd, onRename, onRemove, }: iTreeOfNodes<T>) => JSX.Element;
|
|
63
65
|
|
|
64
66
|
interface WordEntryProps {
|
|
65
|
-
id
|
|
67
|
+
id: Key;
|
|
66
68
|
value?: string;
|
|
67
69
|
editing?: boolean;
|
|
68
70
|
saving?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asup/tree-of-nodes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "REACT Typescript Treeview component",
|
|
5
5
|
"author": "Paul Thomas <@PaulDThomas>",
|
|
6
6
|
"private": false,
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
52
52
|
"husky": "^8.0.1",
|
|
53
53
|
"jest": "^29.2.2",
|
|
54
|
+
"jest-environment-jsdom": "^29.3.1",
|
|
54
55
|
"lint-staged": "^13.0.3",
|
|
55
56
|
"parcel": "^2.7.0",
|
|
56
57
|
"postcss": "^8.4.18",
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
"rollup-plugin-terser": "^7.0.2",
|
|
64
65
|
"ts-jest": "^29.0.3",
|
|
65
66
|
"ts-node": "^10.9.1",
|
|
66
|
-
"typescript": "^4.
|
|
67
|
+
"typescript": "^4.9.3"
|
|
67
68
|
},
|
|
68
69
|
"lint-staged": {
|
|
69
70
|
"**/*": "prettier --write --ignore-unknown"
|
|
@@ -95,6 +96,6 @@
|
|
|
95
96
|
"scripts": {
|
|
96
97
|
"build": "rollup -c --configPlugin typescript",
|
|
97
98
|
"start": "parcel demo/index.html --dist-dir demo/dist",
|
|
98
|
-
"test": "jest"
|
|
99
|
+
"test": "jest --watch"
|
|
99
100
|
}
|
|
100
101
|
}
|