@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.
Files changed (46) hide show
  1. package/README.md +137 -3
  2. package/dist/cjs/main.js +76 -79
  3. package/dist/cjs/src/__mocks__/mockNodes.d.ts +4 -0
  4. package/dist/cjs/src/__mocks__/styleMock.d.ts +2 -0
  5. package/dist/cjs/src/__tests__/TreeNode.test.d.ts +1 -0
  6. package/dist/cjs/src/__tests__/TreeOfNodes.test.d.ts +1 -0
  7. package/dist/cjs/src/__tests__/WordEntry.test.d.ts +1 -0
  8. package/dist/cjs/{components → src/components}/ContextMenu.d.ts +1 -0
  9. package/dist/cjs/{components → src/components}/ContextMenuProvider.d.ts +0 -0
  10. package/dist/cjs/{components → src/components}/TreeNode.d.ts +0 -0
  11. package/dist/cjs/src/components/TreeOfNodes.d.ts +20 -0
  12. package/dist/cjs/src/components/TreeOfNodesContext.d.ts +28 -0
  13. package/dist/cjs/{components → src/components}/WordEntry.d.ts +2 -2
  14. package/dist/cjs/{components → src/components}/index.d.ts +3 -2
  15. package/dist/cjs/{main.d.ts → src/main.d.ts} +0 -0
  16. package/dist/cjs/style.css +1 -0
  17. package/dist/{esm/main.js → main.js} +63 -66
  18. package/dist/src/__mocks__/mockNodes.d.ts +4 -0
  19. package/dist/src/__mocks__/styleMock.d.ts +2 -0
  20. package/dist/src/__tests__/TreeNode.test.d.ts +1 -0
  21. package/dist/src/__tests__/TreeOfNodes.test.d.ts +1 -0
  22. package/dist/src/__tests__/WordEntry.test.d.ts +1 -0
  23. package/dist/{esm → src}/components/ContextMenu.d.ts +1 -0
  24. package/dist/{esm → src}/components/ContextMenuProvider.d.ts +0 -0
  25. package/dist/{esm → src}/components/TreeNode.d.ts +0 -0
  26. package/dist/src/components/TreeOfNodes.d.ts +20 -0
  27. package/dist/src/components/TreeOfNodesContext.d.ts +28 -0
  28. package/dist/{esm → src}/components/WordEntry.d.ts +2 -2
  29. package/dist/{esm → src}/components/index.d.ts +3 -2
  30. package/dist/{esm → src}/main.d.ts +0 -0
  31. package/dist/style.css +1 -0
  32. package/dist/tree-of-nodes.d.ts +30 -68
  33. package/package.json +9 -6
  34. package/dist/cjs/components/TreeOfNodes.d.ts +0 -44
  35. package/dist/esm/components/TreeOfNodes.d.ts +0 -44
  36. package/dist/favicon.80afec6c.ico +0 -0
  37. package/dist/index.8d438033.css +0 -12012
  38. package/dist/index.8d438033.css.map +0 -1
  39. package/dist/index.8ffabb7b.js +0 -188309
  40. package/dist/index.8ffabb7b.js.map +0 -1
  41. package/dist/index.c9e3402a.css +0 -19
  42. package/dist/index.c9e3402a.css.map +0 -1
  43. package/dist/index.d92d26d8.css +0 -12048
  44. package/dist/index.d92d26d8.css.map +0 -1
  45. package/dist/index.html +0 -17
  46. package/dist/public/manifest.webmanifest +0 -1
package/README.md CHANGED
@@ -1,4 +1,138 @@
1
- # Tree of nodes for REACT
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
- REACT treeview, because I couldn't quite find what I wanted
4
- Works with bootstrap
6
+ [![npm][npm]][npm-url]
7
+ [![size][size]][size-url]
8
+ ![npm bundle size](https://img.shields.io/bundlephobia/min/@asup/simple-table)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- 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]; }
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
- n.default = e;
20
- return Object.freeze(n);
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
- if (typeof e.action === 'function')
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
- if (typeof setValue !== 'function')
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
- if (typeof sendEscape === 'function')
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, children: [saving && (jsxRuntime.jsx(Spinner$1, { id: id ? `${id}-spinner` : undefined, size: 'sm', animation: 'border' })), jsxRuntime.jsx("input", { id: id ? `${id}-input` : undefined, ref: ref, type: 'text', style: {
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: editing ? '1px dotted black' : saving ? '' : '1px dotted grey',
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
- // Axios state
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, thisNode, treeContext]);
1648
- // // Context actions
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 directory', action: addChild });
1683
- if (canRename)
1645
+ if (canAddChildren) {
1646
+ menuItems.push({ label: 'Add', action: addChild });
1647
+ }
1648
+ if (canRename) {
1684
1649
  menuItems.push({ label: 'Rename', action: renameThis });
1685
- if (canRemove && (childNodes === undefined || childNodes.length === 0))
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: 'hierarchy-node', 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, { id: `hierarchynode-caret-${id}`, onClick: () => treeContext.handleExpandClick && treeContext.handleExpandClick(id) })) : (jsxRuntime.jsx(CaretDownFill$1, { id: `hierarchynode-caret-${id}`, onClick: () => treeContext.handleExpandClick && treeContext.handleExpandClick(id) }))) : (jsxRuntime.jsx(CaretRight$1, {})), thisNode !== undefined ? (jsxRuntime.jsx(WordEntry, { id: id ? `treenode-word-${id}` : undefined, ref: currentNameRef, value: thisNode.label, editing: renaming, saving: updatingNode, setValue: (ret) => renameNode(ret), style: {
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
- } })) : (jsxRuntime.jsx("i", { children: "No name" }))] })), childNodes !== undefined && (jsxRuntime.jsx(jsxRuntime.Fragment, { children: childNodes.map((h) => (jsxRuntime.jsx("span", { style: {
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))) })), showNewNode && (jsxRuntime.jsx("div", { children: jsxRuntime.jsx(WordEntry, { id: id ? `hierarchynode-new-${id}` : undefined, ref: newNameRef, editing: true, setValue: (ret) => {
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,4 @@
1
+ import { TreeNodeData } from 'components/TreeOfNodesContext';
2
+ export declare const mockNodes: TreeNodeData<{
3
+ value: number;
4
+ } | number | undefined>[];
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,4 @@
1
1
  import React from 'react';
2
2
  import { contextMenuProps } from './ContextMenuProvider';
3
+ import './ContextMenu.css';
3
4
  export declare const ContextMenu: React.ForwardRefExoticComponent<contextMenuProps & React.RefAttributes<HTMLDivElement>>;
@@ -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,6 +1,6 @@
1
- import React from 'react';
1
+ import React, { Key } from 'react';
2
2
  interface WordEntryProps {
3
- id?: string;
3
+ id: Key;
4
4
  value?: string;
5
5
  editing?: boolean;
6
6
  saving?: boolean;
@@ -1,7 +1,8 @@
1
1
  import { ContextMenu } from './ContextMenu';
2
2
  import { ContextMenuProvider } from './ContextMenuProvider';
3
3
  import { TreeNode } from './TreeNode';
4
- import { iNodeUpdate, TreeNodeData, TreeOfNodes } from './TreeOfNodes';
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}