react-arborist 3.7.0 → 3.8.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/dist/main/components/cursor.js +1 -2
- package/dist/main/components/default-container.js +31 -2
- package/dist/main/components/default-cursor.js +1 -1
- package/dist/main/components/default-drag-preview.d.ts +1 -1
- package/dist/main/components/default-drag-preview.js +1 -1
- package/dist/main/components/default-row.d.ts +1 -1
- package/dist/main/components/default-row.js +1 -1
- package/dist/main/components/list-outer-element.js +1 -1
- package/dist/main/components/provider.d.ts +1 -1
- package/dist/main/components/provider.js +2 -2
- package/dist/main/components/provider.test.js +70 -0
- package/dist/main/components/row-container.d.ts +1 -1
- package/dist/main/components/row-container.js +2 -3
- package/dist/main/dnd/drag-hook.js +1 -0
- package/dist/main/hooks/use-validated-props.js +1 -2
- package/dist/main/interfaces/node-api.js +4 -1
- package/dist/main/interfaces/tree-api.d.ts +27 -5
- package/dist/main/interfaces/tree-api.js +98 -14
- package/dist/main/interfaces/tree-api.test.js +31 -0
- package/dist/main/state/drag-slice.js +1 -2
- package/dist/main/types/state.d.ts +1 -1
- package/dist/main/types/tree-props.d.ts +3 -1
- package/dist/module/components/cursor.js +1 -2
- package/dist/module/components/default-container.js +32 -3
- package/dist/module/components/default-cursor.js +1 -1
- package/dist/module/components/default-drag-preview.d.ts +1 -1
- package/dist/module/components/default-drag-preview.js +1 -1
- package/dist/module/components/default-row.d.ts +1 -1
- package/dist/module/components/default-row.js +1 -1
- package/dist/module/components/list-outer-element.js +1 -1
- package/dist/module/components/provider.d.ts +1 -1
- package/dist/module/components/provider.js +4 -4
- package/dist/module/components/provider.test.js +71 -1
- package/dist/module/components/row-container.d.ts +1 -1
- package/dist/module/components/row-container.js +2 -3
- package/dist/module/dnd/compute-drop.js +1 -1
- package/dist/module/dnd/drag-hook.js +1 -0
- package/dist/module/hooks/use-validated-props.js +1 -2
- package/dist/module/interfaces/node-api.js +4 -1
- package/dist/module/interfaces/tree-api.d.ts +27 -5
- package/dist/module/interfaces/tree-api.js +98 -14
- package/dist/module/interfaces/tree-api.test.js +31 -0
- package/dist/module/state/drag-slice.js +1 -2
- package/dist/module/types/state.d.ts +1 -1
- package/dist/module/types/tree-props.d.ts +3 -1
- package/package.json +27 -27
- package/src/components/cursor.tsx +1 -2
- package/src/components/default-container.tsx +40 -19
- package/src/components/default-cursor.tsx +1 -5
- package/src/components/default-drag-preview.tsx +3 -16
- package/src/components/default-node.tsx +0 -1
- package/src/components/default-row.tsx +2 -13
- package/src/components/drag-preview-container.tsx +1 -1
- package/src/components/list-inner-element.tsx +1 -1
- package/src/components/list-outer-element.tsx +2 -3
- package/src/components/provider.test.tsx +85 -9
- package/src/components/provider.tsx +8 -23
- package/src/components/row-container.tsx +4 -9
- package/src/components/tree.tsx +2 -6
- package/src/context.ts +2 -3
- package/src/data/create-index.ts +0 -1
- package/src/data/create-list.ts +1 -2
- package/src/data/create-root.ts +2 -9
- package/src/data/simple-tree.ts +5 -3
- package/src/dnd/compute-drop.ts +6 -15
- package/src/dnd/drag-hook.ts +1 -0
- package/src/dnd/measure-hover.ts +2 -6
- package/src/dnd/outer-drop-hook.ts +1 -1
- package/src/hooks/use-fresh-node.ts +0 -1
- package/src/hooks/use-simple-tree.ts +2 -8
- package/src/hooks/use-validated-props.ts +4 -8
- package/src/interfaces/node-api.ts +2 -2
- package/src/interfaces/tree-api.test.ts +35 -0
- package/src/interfaces/tree-api.ts +103 -36
- package/src/state/dnd-slice.ts +1 -1
- package/src/state/drag-slice.ts +2 -5
- package/src/state/edit-slice.ts +1 -4
- package/src/state/focus-slice.ts +1 -1
- package/src/state/open-slice.ts +2 -5
- package/src/state/selection-slice.ts +2 -6
- package/src/types/handlers.ts +1 -3
- package/src/types/renderers.ts +0 -1
- package/src/types/state.ts +1 -1
- package/src/types/tree-props.ts +6 -10
- package/src/types/utils.ts +2 -3
- package/src/utils.ts +5 -14
|
@@ -11,8 +11,7 @@ function Cursor() {
|
|
|
11
11
|
if (!cursor || cursor.type !== "line")
|
|
12
12
|
return null;
|
|
13
13
|
const indent = tree.indent;
|
|
14
|
-
const top = tree.
|
|
15
|
-
((_b = (_a = tree.props.padding) !== null && _a !== void 0 ? _a : tree.props.paddingTop) !== null && _b !== void 0 ? _b : 0);
|
|
14
|
+
const top = tree.rowTopPosition(cursor.index) + ((_b = (_a = tree.props.padding) !== null && _a !== void 0 ? _a : tree.props.paddingTop) !== null && _b !== void 0 ? _b : 0);
|
|
16
15
|
const left = indent * cursor.level;
|
|
17
16
|
const Cursor = tree.renderCursor;
|
|
18
17
|
return (0, jsx_runtime_1.jsx)(Cursor, { top, left, indent });
|
|
@@ -16,7 +16,6 @@ let timeoutId = null;
|
|
|
16
16
|
* the event handler. Future clean up welcome.
|
|
17
17
|
*/
|
|
18
18
|
function DefaultContainer() {
|
|
19
|
-
var _a, _b;
|
|
20
19
|
(0, context_1.useDataUpdates)();
|
|
21
20
|
const tree = (0, context_1.useTreeApi)();
|
|
22
21
|
return ((0, jsx_runtime_1.jsx)("div", { role: "tree", style: {
|
|
@@ -234,5 +233,35 @@ function DefaultContainer() {
|
|
|
234
233
|
});
|
|
235
234
|
if (node)
|
|
236
235
|
tree.focus(node.id);
|
|
237
|
-
}, children: (0, jsx_runtime_1.jsx)(
|
|
236
|
+
}, children: (0, jsx_runtime_1.jsx)(List, {}) }));
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Fixed-height trees (numeric rowHeight) render a FixedSizeList, preserving the
|
|
240
|
+
* original O(1) layout and avoiding VariableSizeList's measurement cache. Only
|
|
241
|
+
* the function form, which needs per-row heights, uses VariableSizeList.
|
|
242
|
+
*/
|
|
243
|
+
function List() {
|
|
244
|
+
var _a, _b;
|
|
245
|
+
const tree = (0, context_1.useTreeApi)();
|
|
246
|
+
const commonProps = {
|
|
247
|
+
className: tree.props.className,
|
|
248
|
+
outerRef: tree.listEl,
|
|
249
|
+
itemCount: tree.visibleNodes.length,
|
|
250
|
+
height: tree.height,
|
|
251
|
+
width: tree.width,
|
|
252
|
+
overscanCount: tree.overscanCount,
|
|
253
|
+
itemKey: (index) => { var _a; return ((_a = tree.visibleNodes[index]) === null || _a === void 0 ? void 0 : _a.id) || index; },
|
|
254
|
+
outerElementType: (_a = tree.props.outerElementType) !== null && _a !== void 0 ? _a : list_outer_element_1.ListOuterElement,
|
|
255
|
+
innerElementType: (_b = tree.props.innerElementType) !== null && _b !== void 0 ? _b : list_inner_element_1.ListInnerElement,
|
|
256
|
+
onScroll: tree.props.onScroll,
|
|
257
|
+
onItemsRendered: tree.onItemsRendered.bind(tree),
|
|
258
|
+
};
|
|
259
|
+
if (typeof tree.props.rowHeight === "function") {
|
|
260
|
+
return (
|
|
261
|
+
// @ts-ignore
|
|
262
|
+
(0, jsx_runtime_1.jsx)(react_window_1.VariableSizeList, Object.assign({}, commonProps, { itemSize: tree.rowHeightAt, ref: tree.list, children: row_container_1.RowContainer })));
|
|
263
|
+
}
|
|
264
|
+
return (
|
|
265
|
+
// @ts-ignore
|
|
266
|
+
(0, jsx_runtime_1.jsx)(react_window_1.FixedSizeList, Object.assign({}, commonProps, { itemSize: tree.rowHeight, ref: tree.list, children: row_container_1.RowContainer })));
|
|
238
267
|
}
|
|
@@ -23,7 +23,7 @@ const circleStyle = {
|
|
|
23
23
|
boxShadow: "0 0 0 3px #4B91E2",
|
|
24
24
|
borderRadius: "50%",
|
|
25
25
|
};
|
|
26
|
-
exports.DefaultCursor = react_1.default.memo(function DefaultCursor({ top, left, indent
|
|
26
|
+
exports.DefaultCursor = react_1.default.memo(function DefaultCursor({ top, left, indent }) {
|
|
27
27
|
const style = {
|
|
28
28
|
position: "absolute",
|
|
29
29
|
pointerEvents: "none",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { DragPreviewProps } from "../types/renderers";
|
|
2
|
-
export declare function DefaultDragPreview({ offset, mouse, id, dragIds, isDragging
|
|
2
|
+
export declare function DefaultDragPreview({ offset, mouse, id, dragIds, isDragging }: DragPreviewProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -25,7 +25,7 @@ const getCountStyle = (offset) => {
|
|
|
25
25
|
const { x, y } = offset;
|
|
26
26
|
return { transform: `translate(${x + 10}px, ${y + 10}px)` };
|
|
27
27
|
};
|
|
28
|
-
function DefaultDragPreview({ offset, mouse, id, dragIds, isDragging
|
|
28
|
+
function DefaultDragPreview({ offset, mouse, id, dragIds, isDragging }) {
|
|
29
29
|
return ((0, jsx_runtime_1.jsxs)(Overlay, { isDragging: isDragging, children: [(0, jsx_runtime_1.jsx)(Position, { offset: offset, children: (0, jsx_runtime_1.jsx)(PreviewNode, { id: id, dragIds: dragIds }) }), (0, jsx_runtime_1.jsx)(Count, { mouse: mouse, count: dragIds.length })] }));
|
|
30
30
|
}
|
|
31
31
|
const Overlay = (0, react_1.memo)(function Overlay(props) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { RowRendererProps } from "../types/renderers";
|
|
2
|
-
export declare function DefaultRow<T>({ node, attrs, innerRef, children
|
|
2
|
+
export declare function DefaultRow<T>({ node, attrs, innerRef, children }: RowRendererProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DefaultRow = DefaultRow;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
-
function DefaultRow({ node, attrs, innerRef, children
|
|
5
|
+
function DefaultRow({ node, attrs, innerRef, children }) {
|
|
6
6
|
return ((0, jsx_runtime_1.jsx)("div", Object.assign({}, attrs, { ref: innerRef, onFocus: (e) => e.stopPropagation(), onClick: node.handleClick, children: children })));
|
|
7
7
|
}
|
|
@@ -29,7 +29,7 @@ exports.ListOuterElement = (0, react_1.forwardRef)(function Outer(props, ref) {
|
|
|
29
29
|
const DropContainer = () => {
|
|
30
30
|
const tree = (0, context_1.useTreeApi)();
|
|
31
31
|
return ((0, jsx_runtime_1.jsx)("div", { style: {
|
|
32
|
-
height: tree.visibleNodes.length
|
|
32
|
+
height: tree.rowTopPosition(tree.visibleNodes.length),
|
|
33
33
|
width: "100%",
|
|
34
34
|
position: "absolute",
|
|
35
35
|
left: "0",
|
|
@@ -6,5 +6,5 @@ type Props<T> = {
|
|
|
6
6
|
imperativeHandle: React.Ref<TreeApi<T> | undefined>;
|
|
7
7
|
children: ReactNode;
|
|
8
8
|
};
|
|
9
|
-
export declare function TreeProvider<T>({ treeProps, imperativeHandle, children
|
|
9
|
+
export declare function TreeProvider<T>({ treeProps, imperativeHandle, children }: Props<T>): import("react/jsx-runtime").JSX.Element;
|
|
10
10
|
export {};
|
|
@@ -13,7 +13,7 @@ const react_dnd_1 = require("react-dnd");
|
|
|
13
13
|
const redux_1 = require("redux");
|
|
14
14
|
const open_slice_1 = require("../state/open-slice");
|
|
15
15
|
const SERVER_STATE = (0, initial_1.initialState)();
|
|
16
|
-
function TreeProvider({ treeProps, imperativeHandle, children
|
|
16
|
+
function TreeProvider({ treeProps, imperativeHandle, children }) {
|
|
17
17
|
const list = (0, react_1.useRef)(null);
|
|
18
18
|
const listEl = (0, react_1.useRef)(null);
|
|
19
19
|
const store = (0, react_1.useRef)(
|
|
@@ -29,7 +29,7 @@ function TreeProvider({ treeProps, imperativeHandle, children, }) {
|
|
|
29
29
|
(0, react_1.useMemo)(() => {
|
|
30
30
|
updateCount.current += 1;
|
|
31
31
|
api.update(treeProps);
|
|
32
|
-
},
|
|
32
|
+
}, Object.values(treeProps));
|
|
33
33
|
/* Rebuild visible nodes when open state changes, without clobbering
|
|
34
34
|
props set imperatively via api.update(). Bumping updateCount keeps
|
|
35
35
|
DataUpdates consumers (e.g. DefaultContainer) in sync. */
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
4
|
const react_1 = require("react");
|
|
5
5
|
const react_2 = require("@testing-library/react");
|
|
6
|
+
const react_window_1 = require("react-window");
|
|
6
7
|
const tree_1 = require("./tree");
|
|
7
8
|
const data = [
|
|
8
9
|
{
|
|
@@ -31,3 +32,72 @@ test("imperative tree.update() props survive node toggles (#228)", () => {
|
|
|
31
32
|
});
|
|
32
33
|
expect(api.rowHeight).toBe(48);
|
|
33
34
|
});
|
|
35
|
+
/* Backwards compatibility: switching FixedSizeList -> VariableSizeList must not
|
|
36
|
+
change layout for a numeric rowHeight. With openByDefault, all four nodes
|
|
37
|
+
(1 > 2, 3 > 4) are visible in DFS order. */
|
|
38
|
+
test("numeric rowHeight positions rows at index * height (#238 back-compat)", () => {
|
|
39
|
+
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, rowHeight: 24, openByDefault: true }));
|
|
40
|
+
const rows = react_2.screen.getAllByRole("treeitem");
|
|
41
|
+
expect(rows).toHaveLength(4);
|
|
42
|
+
rows.forEach((row, i) => {
|
|
43
|
+
expect(row.style.height).toBe("24px");
|
|
44
|
+
expect(row.style.top).toBe(`${i * 24}px`);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
test("function rowHeight gives each row its own height and cumulative top (#238)", () => {
|
|
48
|
+
const heights = { "1": 40, "2": 20, "3": 30, "4": 10 };
|
|
49
|
+
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, rowHeight: (node) => heights[node.id], openByDefault: true }));
|
|
50
|
+
const rows = react_2.screen.getAllByRole("treeitem");
|
|
51
|
+
expect(rows).toHaveLength(4);
|
|
52
|
+
const expected = [40, 20, 30, 10];
|
|
53
|
+
let top = 0;
|
|
54
|
+
rows.forEach((row, i) => {
|
|
55
|
+
expect(row.style.height).toBe(`${expected[i]}px`);
|
|
56
|
+
expect(row.style.top).toBe(`${top}px`);
|
|
57
|
+
top += expected[i];
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
test("mutations tell the list to recompute heights (#238)", () => {
|
|
61
|
+
const ref = (0, react_1.createRef)();
|
|
62
|
+
/* Only variable-height mode renders a VariableSizeList with a measurement
|
|
63
|
+
cache to recompute, so use a function rowHeight here. */
|
|
64
|
+
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: () => 24, openByDefault: true }));
|
|
65
|
+
const api = ref.current;
|
|
66
|
+
const reset = jest.spyOn(api.list.current, "resetAfterIndex");
|
|
67
|
+
(0, react_2.act)(() => api.close("1"));
|
|
68
|
+
expect(reset).toHaveBeenCalled();
|
|
69
|
+
reset.mockClear();
|
|
70
|
+
(0, react_2.act)(() => api.open("1"));
|
|
71
|
+
expect(reset).toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
/* react-window caches measurements by index and never invalidates them itself.
|
|
74
|
+
When data changes via props in variable-height mode, those cached sizes belong
|
|
75
|
+
to the wrong rows, so update() must drop the cache. It runs during render, so
|
|
76
|
+
it uses the shouldForceUpdate=false variant. */
|
|
77
|
+
test("changing data in variable-height mode resets the list cache (#238)", () => {
|
|
78
|
+
const ref = (0, react_1.createRef)();
|
|
79
|
+
const rowHeight = (node) => (node.isInternal ? 40 : 20);
|
|
80
|
+
const { rerender } = (0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: rowHeight, openByDefault: true }));
|
|
81
|
+
const reset = jest.spyOn(ref.current.list.current, "resetAfterIndex");
|
|
82
|
+
const nextData = [{ id: "9", name: "fresh" }, ...data];
|
|
83
|
+
(0, react_2.act)(() => {
|
|
84
|
+
rerender((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: nextData, ref: ref, rowHeight: rowHeight, openByDefault: true }));
|
|
85
|
+
});
|
|
86
|
+
expect(reset).toHaveBeenCalledWith(0, false);
|
|
87
|
+
});
|
|
88
|
+
/* The numeric path must stay on FixedSizeList: it has constant item sizes, so
|
|
89
|
+
there is no measurement cache to go stale and none of VariableSizeList's
|
|
90
|
+
overhead. A FixedSizeList has no resetAfterIndex method at all. */
|
|
91
|
+
test("numeric rowHeight renders a cache-free FixedSizeList (#238)", () => {
|
|
92
|
+
const ref = (0, react_1.createRef)();
|
|
93
|
+
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: 24, openByDefault: true }));
|
|
94
|
+
const list = ref.current.list.current;
|
|
95
|
+
expect(list).toBeInstanceOf(react_window_1.FixedSizeList);
|
|
96
|
+
expect("resetAfterIndex" in list).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
/* The function path uses VariableSizeList so per-row heights are possible. */
|
|
99
|
+
test("function rowHeight renders a VariableSizeList (#238)", () => {
|
|
100
|
+
const ref = (0, react_1.createRef)();
|
|
101
|
+
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: () => 24, openByDefault: true }));
|
|
102
|
+
expect(ref.current.list.current).toBeInstanceOf(react_window_1.VariableSizeList);
|
|
103
|
+
});
|
|
@@ -3,5 +3,5 @@ type Props = {
|
|
|
3
3
|
style: React.CSSProperties;
|
|
4
4
|
index: number;
|
|
5
5
|
};
|
|
6
|
-
export declare const RowContainer: React.MemoExoticComponent<(<T>({ index, style
|
|
6
|
+
export declare const RowContainer: React.MemoExoticComponent<(<T>({ index, style }: Props) => import("react/jsx-runtime").JSX.Element)>;
|
|
7
7
|
export {};
|
|
@@ -30,7 +30,7 @@ const context_1 = require("../context");
|
|
|
30
30
|
const drag_hook_1 = require("../dnd/drag-hook");
|
|
31
31
|
const drop_hook_1 = require("../dnd/drop-hook");
|
|
32
32
|
const use_fresh_node_1 = require("../hooks/use-fresh-node");
|
|
33
|
-
exports.RowContainer = react_1.default.memo(function RowContainer({ index, style
|
|
33
|
+
exports.RowContainer = react_1.default.memo(function RowContainer({ index, style }) {
|
|
34
34
|
/* When will the <Row> will re-render.
|
|
35
35
|
*
|
|
36
36
|
* The row component is memo'd so it will only render
|
|
@@ -61,8 +61,7 @@ exports.RowContainer = react_1.default.memo(function RowContainer({ index, style
|
|
|
61
61
|
const nodeStyle = (0, react_1.useMemo)(() => ({ paddingLeft: indent }), [indent]);
|
|
62
62
|
const rowStyle = (0, react_1.useMemo)(() => {
|
|
63
63
|
var _a, _b;
|
|
64
|
-
return (Object.assign(Object.assign({}, style), { top: parseFloat(style.top) +
|
|
65
|
-
((_b = (_a = tree.props.padding) !== null && _a !== void 0 ? _a : tree.props.paddingTop) !== null && _b !== void 0 ? _b : 0) }));
|
|
64
|
+
return (Object.assign(Object.assign({}, style), { top: parseFloat(style.top) + ((_b = (_a = tree.props.padding) !== null && _a !== void 0 ? _a : tree.props.paddingTop) !== null && _b !== void 0 ? _b : 0) }));
|
|
66
65
|
}, [style, tree.props.padding, tree.props.paddingTop]);
|
|
67
66
|
const rowAttrs = {
|
|
68
67
|
role: "treeitem",
|
|
@@ -6,8 +6,7 @@ function useValidatedProps(props) {
|
|
|
6
6
|
if (props.initialData && props.data) {
|
|
7
7
|
throw new Error(`React Arborist Tree => Provide either a data or initialData prop, but not both.`);
|
|
8
8
|
}
|
|
9
|
-
if (props.initialData &&
|
|
10
|
-
(props.onCreate || props.onDelete || props.onMove || props.onRename)) {
|
|
9
|
+
if (props.initialData && (props.onCreate || props.onDelete || props.onMove || props.onRename)) {
|
|
11
10
|
throw new Error(`React Arborist Tree => You passed the initialData prop along with a data handler.
|
|
12
11
|
Use the data prop if you want to provide your own handlers.`);
|
|
13
12
|
}
|
|
@@ -6,7 +6,10 @@ class NodeApi {
|
|
|
6
6
|
constructor(params) {
|
|
7
7
|
this.handleClick = (e) => {
|
|
8
8
|
if (e.metaKey && !this.tree.props.disableMultiSelection) {
|
|
9
|
-
|
|
9
|
+
if (this.isSelected)
|
|
10
|
+
this.deselect();
|
|
11
|
+
else
|
|
12
|
+
this.selectMulti();
|
|
10
13
|
}
|
|
11
14
|
else if (e.shiftKey && !this.tree.props.disableMultiSelection) {
|
|
12
15
|
this.selectContiguous();
|
|
@@ -2,7 +2,7 @@ import { EditResult } from "../types/handlers";
|
|
|
2
2
|
import { Identity, IdObj } from "../types/utils";
|
|
3
3
|
import { TreeProps } from "../types/tree-props";
|
|
4
4
|
import { MutableRefObject } from "react";
|
|
5
|
-
import { Align, FixedSizeList, ListOnItemsRenderedProps } from "react-window";
|
|
5
|
+
import { Align, FixedSizeList, ListOnItemsRenderedProps, VariableSizeList } from "react-window";
|
|
6
6
|
import { DefaultRow } from "../components/default-row";
|
|
7
7
|
import { DefaultNode } from "../components/default-node";
|
|
8
8
|
import { NodeApi } from "./node-api";
|
|
@@ -14,7 +14,7 @@ import { Store } from "redux";
|
|
|
14
14
|
export declare class TreeApi<T> {
|
|
15
15
|
store: Store<RootState, Actions>;
|
|
16
16
|
props: TreeProps<T>;
|
|
17
|
-
list: MutableRefObject<FixedSizeList | null>;
|
|
17
|
+
list: MutableRefObject<FixedSizeList | VariableSizeList | null>;
|
|
18
18
|
listEl: MutableRefObject<HTMLDivElement | null>;
|
|
19
19
|
static editPromise: null | ((args: EditResult) => void);
|
|
20
20
|
root: NodeApi<T>;
|
|
@@ -24,7 +24,8 @@ export declare class TreeApi<T> {
|
|
|
24
24
|
idToIndex: {
|
|
25
25
|
[id: string]: number;
|
|
26
26
|
};
|
|
27
|
-
|
|
27
|
+
private rowOffsets;
|
|
28
|
+
constructor(store: Store<RootState, Actions>, props: TreeProps<T>, list: MutableRefObject<FixedSizeList | VariableSizeList | null>, listEl: MutableRefObject<HTMLDivElement | null>);
|
|
28
29
|
update(props: TreeProps<T>): void;
|
|
29
30
|
dispatch(action: Actions): {
|
|
30
31
|
type: "FOCUS";
|
|
@@ -121,7 +122,28 @@ export declare class TreeApi<T> {
|
|
|
121
122
|
get width(): string | number;
|
|
122
123
|
get height(): number;
|
|
123
124
|
get indent(): number;
|
|
125
|
+
/**
|
|
126
|
+
* The fixed row height. When a `rowHeight` function is supplied for variable
|
|
127
|
+
* heights, this returns the default (24); use `rowHeightAt(index)` to get the
|
|
128
|
+
* height of a specific row.
|
|
129
|
+
*/
|
|
124
130
|
get rowHeight(): number;
|
|
131
|
+
/**
|
|
132
|
+
* The height of the row at `index`, evaluating the `rowHeight` function if
|
|
133
|
+
* given. Falls back to the default height for an out-of-range index so this
|
|
134
|
+
* never feeds an invalid `0` to react-window's `itemSize`.
|
|
135
|
+
*/
|
|
136
|
+
rowHeightAt: (index: number) => number;
|
|
137
|
+
/** The pixel offset of the top of the row at `index` from the top of the list. */
|
|
138
|
+
rowTopPosition: (index: number) => number;
|
|
139
|
+
/**
|
|
140
|
+
* Tell the underlying virtualized list to recompute row heights at and after
|
|
141
|
+
* `index`. Call this if a `rowHeight` function's output changes for reasons
|
|
142
|
+
* the tree can't observe (e.g. external state).
|
|
143
|
+
*/
|
|
144
|
+
redrawList: (afterIndex?: number) => void;
|
|
145
|
+
/** Lazily-built prefix sum where offsets[i] is the top of row i. */
|
|
146
|
+
private getRowOffsets;
|
|
125
147
|
get overscanCount(): number;
|
|
126
148
|
get searchTerm(): string;
|
|
127
149
|
get matchFn(): (node: NodeApi<T>) => boolean;
|
|
@@ -185,8 +207,8 @@ export declare class TreeApi<T> {
|
|
|
185
207
|
canDrop(): boolean;
|
|
186
208
|
hideCursor(): void;
|
|
187
209
|
showCursor(cursor: Cursor): void;
|
|
188
|
-
open(identity: Identity): void;
|
|
189
|
-
close(identity: Identity): void;
|
|
210
|
+
open(identity: Identity, redraw?: boolean): void;
|
|
211
|
+
close(identity: Identity, redraw?: boolean): void;
|
|
190
212
|
toggle(identity: Identity): void;
|
|
191
213
|
openParents(identity: Identity): void;
|
|
192
214
|
openSiblings(node: NodeApi<T>): void;
|
|
@@ -56,6 +56,46 @@ class TreeApi {
|
|
|
56
56
|
this.listEl = listEl;
|
|
57
57
|
this.visibleStartIndex = 0;
|
|
58
58
|
this.visibleStopIndex = 0;
|
|
59
|
+
/* Memoized prefix-sum of row heights; only used for variable heights. */
|
|
60
|
+
this.rowOffsets = null;
|
|
61
|
+
/**
|
|
62
|
+
* The height of the row at `index`, evaluating the `rowHeight` function if
|
|
63
|
+
* given. Falls back to the default height for an out-of-range index so this
|
|
64
|
+
* never feeds an invalid `0` to react-window's `itemSize`.
|
|
65
|
+
*/
|
|
66
|
+
this.rowHeightAt = (index) => {
|
|
67
|
+
const rowHeight = this.props.rowHeight;
|
|
68
|
+
if (typeof rowHeight === "function") {
|
|
69
|
+
const node = this.at(index);
|
|
70
|
+
return node ? rowHeight(node) : this.rowHeight;
|
|
71
|
+
}
|
|
72
|
+
return rowHeight !== null && rowHeight !== void 0 ? rowHeight : 24;
|
|
73
|
+
};
|
|
74
|
+
/** The pixel offset of the top of the row at `index` from the top of the list. */
|
|
75
|
+
this.rowTopPosition = (index) => {
|
|
76
|
+
/* Fixed heights: O(1). */
|
|
77
|
+
if (typeof this.props.rowHeight !== "function") {
|
|
78
|
+
return index * this.rowHeight;
|
|
79
|
+
}
|
|
80
|
+
/* Variable heights: O(1) amortized via a memoized prefix sum. */
|
|
81
|
+
const offsets = this.getRowOffsets();
|
|
82
|
+
const clamped = Math.max(0, Math.min(index, offsets.length - 1));
|
|
83
|
+
return offsets[clamped];
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Tell the underlying virtualized list to recompute row heights at and after
|
|
87
|
+
* `index`. Call this if a `rowHeight` function's output changes for reasons
|
|
88
|
+
* the tree can't observe (e.g. external state).
|
|
89
|
+
*/
|
|
90
|
+
this.redrawList = (afterIndex = 0) => {
|
|
91
|
+
this.rowOffsets = null;
|
|
92
|
+
/* Only the VariableSizeList (function rowHeight) caches measurements; a
|
|
93
|
+
FixedSizeList has constant heights and nothing to recompute. */
|
|
94
|
+
const list = this.list.current;
|
|
95
|
+
if (list && "resetAfterIndex" in list) {
|
|
96
|
+
list.resetAfterIndex(Math.max(0, afterIndex));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
59
99
|
/* Changes here must also be made in update() */
|
|
60
100
|
this.root = (0, create_root_1.createRoot)(this);
|
|
61
101
|
this.visibleNodes = (0, create_list_1.createList)(this);
|
|
@@ -67,6 +107,18 @@ class TreeApi {
|
|
|
67
107
|
this.root = (0, create_root_1.createRoot)(this);
|
|
68
108
|
this.visibleNodes = (0, create_list_1.createList)(this);
|
|
69
109
|
this.idToIndex = (0, create_index_1.createIndex)(this.visibleNodes);
|
|
110
|
+
this.rowOffsets = null;
|
|
111
|
+
/* Variable-height mode renders a VariableSizeList, which caches item
|
|
112
|
+
measurements by index and never invalidates them on its own. When the
|
|
113
|
+
visible nodes change (insert/remove/reorder), those cached sizes belong
|
|
114
|
+
to the wrong rows, so drop them. Fixed-height mode renders a
|
|
115
|
+
FixedSizeList (no cache, nothing to reset). update() runs during render,
|
|
116
|
+
so pass shouldForceUpdate=false: the in-progress render repaints the list
|
|
117
|
+
and a forceUpdate here would warn about setting state mid-render. */
|
|
118
|
+
const list = this.list.current;
|
|
119
|
+
if (list && "resetAfterIndex" in list) {
|
|
120
|
+
list.resetAfterIndex(0, false);
|
|
121
|
+
}
|
|
70
122
|
}
|
|
71
123
|
/* Store helpers */
|
|
72
124
|
dispatch(action) {
|
|
@@ -91,9 +143,24 @@ class TreeApi {
|
|
|
91
143
|
var _a;
|
|
92
144
|
return (_a = this.props.indent) !== null && _a !== void 0 ? _a : 24;
|
|
93
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* The fixed row height. When a `rowHeight` function is supplied for variable
|
|
148
|
+
* heights, this returns the default (24); use `rowHeightAt(index)` to get the
|
|
149
|
+
* height of a specific row.
|
|
150
|
+
*/
|
|
94
151
|
get rowHeight() {
|
|
95
|
-
|
|
96
|
-
|
|
152
|
+
return typeof this.props.rowHeight === "number" ? this.props.rowHeight : 24;
|
|
153
|
+
}
|
|
154
|
+
/** Lazily-built prefix sum where offsets[i] is the top of row i. */
|
|
155
|
+
getRowOffsets() {
|
|
156
|
+
if (this.rowOffsets)
|
|
157
|
+
return this.rowOffsets;
|
|
158
|
+
const offsets = [0];
|
|
159
|
+
for (let i = 0; i < this.visibleNodes.length; i++) {
|
|
160
|
+
offsets.push(offsets[i] + this.rowHeightAt(i));
|
|
161
|
+
}
|
|
162
|
+
this.rowOffsets = offsets;
|
|
163
|
+
return offsets;
|
|
97
164
|
}
|
|
98
165
|
get overscanCount() {
|
|
99
166
|
var _a;
|
|
@@ -195,9 +262,7 @@ class TreeApi {
|
|
|
195
262
|
create() {
|
|
196
263
|
return __awaiter(this, arguments, void 0, function* (opts = {}) {
|
|
197
264
|
var _a, _b;
|
|
198
|
-
const parentId = opts.parentId === undefined
|
|
199
|
-
? utils.getInsertParentId(this)
|
|
200
|
-
: opts.parentId;
|
|
265
|
+
const parentId = opts.parentId === undefined ? utils.getInsertParentId(this) : opts.parentId;
|
|
201
266
|
const index = (_a = opts.index) !== null && _a !== void 0 ? _a : utils.getInsertIndex(this);
|
|
202
267
|
const type = (_b = opts.type) !== null && _b !== void 0 ? _b : "leaf";
|
|
203
268
|
const data = yield safeRun(this.props.onCreate, {
|
|
@@ -224,20 +289,26 @@ class TreeApi {
|
|
|
224
289
|
const idents = Array.isArray(node) ? node : [node];
|
|
225
290
|
const ids = idents.map(identify);
|
|
226
291
|
const nodes = ids.map((id) => this.get(id)).filter((n) => !!n);
|
|
292
|
+
/* Guard against Math.min(...[]) === Infinity when no ids resolve to nodes. */
|
|
293
|
+
const fromIndex = nodes.length ? Math.min(...nodes.map((n) => { var _a; return (_a = n.rowIndex) !== null && _a !== void 0 ? _a : 0; })) : 0;
|
|
227
294
|
yield safeRun(this.props.onDelete, { nodes, ids });
|
|
295
|
+
this.redrawList(fromIndex);
|
|
228
296
|
});
|
|
229
297
|
}
|
|
230
298
|
edit(node) {
|
|
299
|
+
var _a, _b;
|
|
231
300
|
const id = identify(node);
|
|
232
301
|
this.resolveEdit({ cancelled: true });
|
|
233
302
|
this.scrollTo(id);
|
|
234
303
|
this.dispatch((0, edit_slice_1.edit)(id));
|
|
304
|
+
this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
|
|
235
305
|
return new Promise((resolve) => {
|
|
236
306
|
TreeApi.editPromise = resolve;
|
|
237
307
|
});
|
|
238
308
|
}
|
|
239
309
|
submit(identity, value) {
|
|
240
310
|
return __awaiter(this, void 0, void 0, function* () {
|
|
311
|
+
var _a, _b;
|
|
241
312
|
if (!identity)
|
|
242
313
|
return;
|
|
243
314
|
const id = identify(identity);
|
|
@@ -248,12 +319,14 @@ class TreeApi {
|
|
|
248
319
|
});
|
|
249
320
|
this.dispatch((0, edit_slice_1.edit)(null));
|
|
250
321
|
this.resolveEdit({ cancelled: false, value });
|
|
322
|
+
this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
|
|
251
323
|
setTimeout(() => this.onFocus()); // Return focus to element;
|
|
252
324
|
});
|
|
253
325
|
}
|
|
254
326
|
reset() {
|
|
255
327
|
this.dispatch((0, edit_slice_1.edit)(null));
|
|
256
328
|
this.resolveEdit({ cancelled: true });
|
|
329
|
+
this.redrawList();
|
|
257
330
|
setTimeout(() => this.onFocus()); // Return focus to element;
|
|
258
331
|
}
|
|
259
332
|
activate(id) {
|
|
@@ -433,9 +506,7 @@ class TreeApi {
|
|
|
433
506
|
return this.state.dnd.cursor.type === "highlight";
|
|
434
507
|
}
|
|
435
508
|
get dragNodes() {
|
|
436
|
-
return this.state.dnd.dragIds
|
|
437
|
-
.map((id) => this.get(id))
|
|
438
|
-
.filter((n) => !!n);
|
|
509
|
+
return this.state.dnd.dragIds.map((id) => this.get(id)).filter((n) => !!n);
|
|
439
510
|
}
|
|
440
511
|
get dragNode() {
|
|
441
512
|
return this.get(this.state.nodes.drag.id);
|
|
@@ -487,22 +558,28 @@ class TreeApi {
|
|
|
487
558
|
this.dispatch(dnd_slice_1.actions.cursor(cursor));
|
|
488
559
|
}
|
|
489
560
|
/* Visibility */
|
|
490
|
-
open(identity) {
|
|
561
|
+
open(identity, redraw = true) {
|
|
562
|
+
var _a, _b;
|
|
491
563
|
const id = identifyNull(identity);
|
|
492
564
|
if (!id)
|
|
493
565
|
return;
|
|
494
566
|
if (this.isOpen(id))
|
|
495
567
|
return;
|
|
496
568
|
this.dispatch(open_slice_1.actions.open(id, this.isFiltered));
|
|
569
|
+
if (redraw)
|
|
570
|
+
this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
|
|
497
571
|
safeRun(this.props.onToggle, id);
|
|
498
572
|
}
|
|
499
|
-
close(identity) {
|
|
573
|
+
close(identity, redraw = true) {
|
|
574
|
+
var _a, _b;
|
|
500
575
|
const id = identifyNull(identity);
|
|
501
576
|
if (!id)
|
|
502
577
|
return;
|
|
503
578
|
if (!this.isOpen(id))
|
|
504
579
|
return;
|
|
505
580
|
this.dispatch(open_slice_1.actions.close(id, this.isFiltered));
|
|
581
|
+
if (redraw)
|
|
582
|
+
this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
|
|
506
583
|
safeRun(this.props.onToggle, id);
|
|
507
584
|
}
|
|
508
585
|
toggle(identity) {
|
|
@@ -518,9 +595,10 @@ class TreeApi {
|
|
|
518
595
|
const node = utils.dfs(this.root, id);
|
|
519
596
|
let parent = node === null || node === void 0 ? void 0 : node.parent;
|
|
520
597
|
while (parent) {
|
|
521
|
-
this.open(parent.id);
|
|
598
|
+
this.open(parent.id, false);
|
|
522
599
|
parent = parent.parent;
|
|
523
600
|
}
|
|
601
|
+
this.redrawList();
|
|
524
602
|
}
|
|
525
603
|
openSiblings(node) {
|
|
526
604
|
const parent = node.parent;
|
|
@@ -531,23 +609,29 @@ class TreeApi {
|
|
|
531
609
|
const isOpen = node.isOpen;
|
|
532
610
|
for (let sibling of parent.children) {
|
|
533
611
|
if (sibling.isInternal) {
|
|
534
|
-
|
|
612
|
+
if (isOpen)
|
|
613
|
+
this.close(sibling.id, false);
|
|
614
|
+
else
|
|
615
|
+
this.open(sibling.id, false);
|
|
535
616
|
}
|
|
536
617
|
}
|
|
618
|
+
this.redrawList();
|
|
537
619
|
this.scrollTo(this.focusedNode);
|
|
538
620
|
}
|
|
539
621
|
}
|
|
540
622
|
openAll() {
|
|
541
623
|
utils.walk(this.root, (node) => {
|
|
542
624
|
if (node.isInternal)
|
|
543
|
-
|
|
625
|
+
this.open(node.id, false);
|
|
544
626
|
});
|
|
627
|
+
this.redrawList();
|
|
545
628
|
}
|
|
546
629
|
closeAll() {
|
|
547
630
|
utils.walk(this.root, (node) => {
|
|
548
631
|
if (node.isInternal)
|
|
549
|
-
|
|
632
|
+
this.close(node.id, false);
|
|
550
633
|
});
|
|
634
|
+
this.redrawList();
|
|
551
635
|
}
|
|
552
636
|
/* Scrolling */
|
|
553
637
|
scrollTo(identity, align = "smart") {
|
|
@@ -12,3 +12,34 @@ test("tree.canDrop()", () => {
|
|
|
12
12
|
expect(setupApi({ disableDrop: () => false }).canDrop()).toBe(true);
|
|
13
13
|
expect(setupApi({ disableDrop: false }).canDrop()).toBe(true);
|
|
14
14
|
});
|
|
15
|
+
const rowData = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
|
16
|
+
test("rowHeight defaults to 24", () => {
|
|
17
|
+
const api = setupApi({});
|
|
18
|
+
expect(api.rowHeight).toBe(24);
|
|
19
|
+
expect(api.rowHeightAt(0)).toBe(24);
|
|
20
|
+
});
|
|
21
|
+
test("fixed numeric rowHeight", () => {
|
|
22
|
+
const api = setupApi({ data: rowData, rowHeight: 30 });
|
|
23
|
+
expect(api.rowHeight).toBe(30);
|
|
24
|
+
expect(api.rowHeightAt(0)).toBe(30);
|
|
25
|
+
expect(api.rowTopPosition(0)).toBe(0);
|
|
26
|
+
expect(api.rowTopPosition(2)).toBe(60);
|
|
27
|
+
expect(api.rowTopPosition(3)).toBe(90); // total list height
|
|
28
|
+
});
|
|
29
|
+
test("variable rowHeight function", () => {
|
|
30
|
+
const heights = { a: 10, b: 20, c: 40 };
|
|
31
|
+
const api = setupApi({
|
|
32
|
+
data: rowData,
|
|
33
|
+
rowHeight: (node) => heights[node.id],
|
|
34
|
+
});
|
|
35
|
+
// The back-compat getter falls back to the default for variable heights.
|
|
36
|
+
expect(api.rowHeight).toBe(24);
|
|
37
|
+
expect(api.rowHeightAt(0)).toBe(10);
|
|
38
|
+
expect(api.rowHeightAt(1)).toBe(20);
|
|
39
|
+
expect(api.rowTopPosition(0)).toBe(0);
|
|
40
|
+
expect(api.rowTopPosition(1)).toBe(10);
|
|
41
|
+
expect(api.rowTopPosition(2)).toBe(30);
|
|
42
|
+
expect(api.rowTopPosition(3)).toBe(70); // total list height
|
|
43
|
+
// Out-of-range index falls back to the default height, never an invalid 0.
|
|
44
|
+
expect(api.rowHeightAt(99)).toBe(24);
|
|
45
|
+
});
|
|
@@ -10,8 +10,7 @@ function reducer(state = (0, initial_1.initialState)().nodes.drag, action) {
|
|
|
10
10
|
case "DND_DRAG_END":
|
|
11
11
|
return Object.assign(Object.assign({}, state), { id: null, destinationParentId: null, destinationIndex: null, selectedIds: [] });
|
|
12
12
|
case "DND_HOVERING":
|
|
13
|
-
if (action.parentId !== state.destinationParentId ||
|
|
14
|
-
action.index != state.destinationIndex) {
|
|
13
|
+
if (action.parentId !== state.destinationParentId || action.index != state.destinationIndex) {
|
|
15
14
|
return Object.assign(Object.assign({}, state), { destinationParentId: action.parentId, destinationIndex: action.index });
|
|
16
15
|
}
|
|
17
16
|
else {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { NodeApi } from "../interfaces/node-api";
|
|
2
|
-
export type NodeState = typeof NodeApi.prototype["state"];
|
|
2
|
+
export type NodeState = (typeof NodeApi.prototype)["state"];
|
|
@@ -6,6 +6,8 @@ import { ListOnScrollProps, CommonProps as ReactWindowCommonProps } from "react-
|
|
|
6
6
|
import { NodeApi } from "../interfaces/node-api";
|
|
7
7
|
import { OpenMap } from "../state/open-slice";
|
|
8
8
|
import { useDragDropManager, DndProviderProps } from "react-dnd";
|
|
9
|
+
/** Returns the height in pixels for a given node's row. */
|
|
10
|
+
export type RowHeightAccessor<T> = (node: NodeApi<T>) => number;
|
|
9
11
|
export interface TreeProps<T> {
|
|
10
12
|
data?: readonly T[];
|
|
11
13
|
initialData?: readonly T[];
|
|
@@ -18,7 +20,7 @@ export interface TreeProps<T> {
|
|
|
18
20
|
renderDragPreview?: ElementType<renderers.DragPreviewProps>;
|
|
19
21
|
renderCursor?: ElementType<renderers.CursorProps>;
|
|
20
22
|
renderContainer?: ElementType<{}>;
|
|
21
|
-
rowHeight?: number
|
|
23
|
+
rowHeight?: number | RowHeightAccessor<T>;
|
|
22
24
|
overscanCount?: number;
|
|
23
25
|
width?: number | string;
|
|
24
26
|
height?: number;
|