@asup/tree-of-nodes 0.9.0 → 1.0.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 +137 -3
- package/dist/cjs/main.js +76 -79
- package/dist/cjs/src/__mocks__/mockNodes.d.ts +4 -0
- package/dist/cjs/src/__mocks__/styleMock.d.ts +2 -0
- package/dist/cjs/src/__tests__/TreeNode.test.d.ts +1 -0
- package/dist/cjs/src/__tests__/TreeOfNodes.test.d.ts +1 -0
- package/dist/cjs/src/__tests__/WordEntry.test.d.ts +1 -0
- package/dist/cjs/{components → src/components}/ContextMenu.d.ts +1 -0
- package/dist/cjs/{components → src/components}/ContextMenuProvider.d.ts +0 -0
- package/dist/cjs/{components → src/components}/TreeNode.d.ts +0 -0
- package/dist/cjs/src/components/TreeOfNodes.d.ts +20 -0
- package/dist/cjs/src/components/TreeOfNodesContext.d.ts +28 -0
- package/dist/cjs/{components → src/components}/WordEntry.d.ts +2 -2
- package/dist/cjs/{components → src/components}/index.d.ts +3 -2
- package/dist/cjs/{main.d.ts → src/main.d.ts} +0 -0
- package/dist/cjs/style.css +1 -0
- package/dist/{esm/main.js → main.js} +63 -66
- package/dist/src/__mocks__/mockNodes.d.ts +4 -0
- package/dist/src/__mocks__/styleMock.d.ts +2 -0
- package/dist/src/__tests__/TreeNode.test.d.ts +1 -0
- package/dist/src/__tests__/TreeOfNodes.test.d.ts +1 -0
- package/dist/src/__tests__/WordEntry.test.d.ts +1 -0
- package/dist/{esm → src}/components/ContextMenu.d.ts +1 -0
- package/dist/{esm → src}/components/ContextMenuProvider.d.ts +0 -0
- package/dist/{esm → src}/components/TreeNode.d.ts +0 -0
- package/dist/src/components/TreeOfNodes.d.ts +20 -0
- package/dist/src/components/TreeOfNodesContext.d.ts +28 -0
- package/dist/{esm → src}/components/WordEntry.d.ts +2 -2
- package/dist/{esm → src}/components/index.d.ts +3 -2
- package/dist/{esm → src}/main.d.ts +0 -0
- package/dist/style.css +1 -0
- package/dist/tree-of-nodes.d.ts +30 -68
- package/package.json +9 -6
- package/dist/cjs/components/TreeOfNodes.d.ts +0 -44
- package/dist/esm/components/TreeOfNodes.d.ts +0 -44
- package/dist/favicon.80afec6c.ico +0 -0
- package/dist/index.8d438033.css +0 -12012
- package/dist/index.8d438033.css.map +0 -1
- package/dist/index.8ffabb7b.js +0 -188309
- package/dist/index.8ffabb7b.js.map +0 -1
- package/dist/index.c9e3402a.css +0 -19
- package/dist/index.c9e3402a.css.map +0 -1
- package/dist/index.d92d26d8.css +0 -12048
- package/dist/index.d92d26d8.css.map +0 -1
- package/dist/index.html +0 -17
- package/dist/public/manifest.webmanifest +0 -1
package/README.md
CHANGED
|
@@ -1,4 +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
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
12
|
+
|
|
13
|
+
REACT treeview, because I couldn't quite find what I wanted.
|
|
14
|
+
|
|
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.
|
package/dist/cjs/main.js
CHANGED
|
@@ -4,20 +4,20 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
4
4
|
var React = require('react');
|
|
5
5
|
|
|
6
6
|
function _interopNamespaceDefault(e) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
});
|
|
16
|
-
}
|
|
7
|
+
var n = Object.create(null);
|
|
8
|
+
if (e) {
|
|
9
|
+
Object.keys(e).forEach(function (k) {
|
|
10
|
+
if (k !== 'default') {
|
|
11
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
12
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () { return e[k]; }
|
|
17
15
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
n.default = e;
|
|
20
|
+
return Object.freeze(n);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
|
|
@@ -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
|
-
...style,
|
|
1570
|
-
display: 'inline-block',
|
|
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)
|
|
1682
|
-
menuItems.push({ label: 'Add
|
|
1683
|
-
|
|
1645
|
+
if (canAddChildren) {
|
|
1646
|
+
menuItems.push({ label: 'Add', action: addChild });
|
|
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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Key } from 'react';
|
|
2
|
+
import { iNodeUpdate, TreeNodeData } from './TreeOfNodesContext';
|
|
3
|
+
interface iTreeOfNodes<T> {
|
|
4
|
+
id: string;
|
|
5
|
+
nodeList: TreeNodeData<T>[];
|
|
6
|
+
roots: Key[];
|
|
7
|
+
selectedId?: Key;
|
|
8
|
+
handleSelect?: (ret: Key) => void;
|
|
9
|
+
onAdd?: (parentId: Key, newName: string) => Promise<iNodeUpdate>;
|
|
10
|
+
onRename?: (childId: Key, newName: string) => Promise<iNodeUpdate>;
|
|
11
|
+
onRemove?: (childId: Key) => Promise<iNodeUpdate>;
|
|
12
|
+
canAddRoot?: boolean;
|
|
13
|
+
canRemoveRoot?: boolean;
|
|
14
|
+
canRenameRoot?: boolean;
|
|
15
|
+
canAddChildren?: boolean;
|
|
16
|
+
canRemoveChildren?: boolean;
|
|
17
|
+
canRenameChildren?: boolean;
|
|
18
|
+
}
|
|
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;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Key } from 'react';
|
|
2
|
+
export interface iNodeUpdate {
|
|
3
|
+
success: boolean;
|
|
4
|
+
ErrorText?: string;
|
|
5
|
+
}
|
|
6
|
+
export type TreeNodeBase = {
|
|
7
|
+
id: Key;
|
|
8
|
+
label: string;
|
|
9
|
+
parentId?: Key;
|
|
10
|
+
};
|
|
11
|
+
export type TreeNodeData<T> = {
|
|
12
|
+
id: Key;
|
|
13
|
+
label: string;
|
|
14
|
+
parentId?: Key;
|
|
15
|
+
data: T;
|
|
16
|
+
};
|
|
17
|
+
export type 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
|
-
export type { TreeNodeData, iNodeUpdate };
|
|
8
|
+
export type { TreeNodeBase, TreeNodeData, iNodeUpdate };
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.context-menu{background-color:#fbfdf6;border:1px solid #111418;border-radius:8px;opacity:1;position:absolute;visibility:hidden}.context-menu.visible{visibility:"inherit"}.context-menu-item{color:#111418;cursor:pointer;min-width:80px;padding:0 4px}.context-menu-item:first-child{padding-top:2px}.context-menu-item:last-child{padding-bottom:2px}.context-menu-item:hover{background-color:#fbe9e6}.context-menu-item:hover:first-child{border-radius:6px 6px 0 0}.context-menu-item:hover:last-child{border-radius:0 0 6px 6px}
|