prasi-lib/comps/list/Tree.tsx

169 lines
4.3 KiB
TypeScript
Executable File

import { useLocal } from "lib/utils/use-local";
import { ElementType, FC, ReactElement, ReactNode, useEffect } from "react";
import { NodeRendererProps, Tree as Arborist, NodeApi } from "react-arborist";
export type TreeItem = {
id: string;
text: string;
id_parent?: string;
sort_idx?: number;
children?: TreeItem[];
};
export const Tree = <T extends TreeItem>({
tree,
renderRow,
className,
rowHeight,
onChange,
drag,
}: {
className: string;
tree: T[];
drag?: boolean;
onChange?: (rows: T[]) => Promise<void> | void;
rowHeight?: number;
renderRow: (arg: { row: T; style: any; dragHandle: any }) => ReactNode;
}) => {
const local = useLocal({
update: {} as Record<string, T>,
timeout: null as any,
children: generateTreeChildren({
renderRow,
updateNode: (data, arg) => {
for (const [k, v] of Object.entries(arg)) {
(data as any)[k] = v;
}
local.update[data.id] = { ...data };
clearTimeout(local.timeout);
local.timeout = setTimeout(async () => {
if (onChange) {
await onChange(Object.values(local.update));
local.update = {};
}
}, 300);
},
}),
tree: formatTree(tree),
width: 0,
height: 0,
rob: new ResizeObserver(([el]) => {
local.width = el.contentRect.width;
local.height = el.contentRect.height;
local.render();
}),
});
useEffect(() => {
return () => {
local.rob.disconnect();
};
}, []);
return (
<div
className={cx(
className,
css`
position: relative;
> div {
position: absolute;
left: 0;
bottom: 0;
right: 0;
top: 0;
}
`
)}
ref={(el) => {
if (el) {
local.rob.observe(el);
const bound = el.getBoundingClientRect();
if (local.height === 0) {
local.width = bound.width;
local.height = bound.height;
local.render();
}
}
}}
>
{local.width > 0 && local.height > 0 && (
<Arborist
initialData={local.tree}
width={local.width}
indent={10}
rowHeight={rowHeight}
height={local.height}
disableDrag={drag === false}
children={local.children}
/>
)}
</div>
);
};
const formatTree = <T extends TreeItem>(tree: T[]) => {
const res: T[] = [];
const ftree = tree
.filter((e) => e.id)
.sort((a, b) => (a.sort_idx || 0) - (b.sort_idx || 0))
.map((e) => ({
...e,
id: e.id,
text: e.text,
children: e.children,
id_parent: e.id_parent || null,
sort_idx: e.sort_idx || "0",
})) as TreeItem[];
const scan = (id_parent: null | string, parent: T) => {
for (const s of ftree) {
if (s.id_parent === id_parent) {
if (!parent.children) {
parent.children = [];
}
if (!parent.children.find((e) => e.id === s.id)) {
parent.children.push(s);
scan(s.id, s as T);
}
}
}
};
scan(null, {
id: "root",
id_parent: "",
sort_idx: 0,
text: "",
children: res as TreeItem[],
} as T);
return res;
};
const generateTreeChildren = <T extends TreeItem>(arg: {
renderRow: (arg: { row: T; style: any; dragHandle: any }) => ReactNode;
updateNode: (data: T, new_data: Partial<T>) => void;
}) => {
const { renderRow, updateNode } = arg;
return (({ node, style, dragHandle }) => {
let sort_idx = node.data.sort_idx;
if (typeof sort_idx === "string") sort_idx = parseInt(sort_idx);
let parent_id: any = node.parent?.id;
if (parent_id === "__REACT_ARBORIST_INTERNAL_ROOT__") parent_id = null;
if (!node.isDragging) {
if (sort_idx !== node.rowIndex) {
updateNode(node.data, { sort_idx: node.rowIndex } as Partial<T>);
}
if (parent_id !== node.data.id_parent) {
updateNode(node.data, { id_parent: parent_id } as Partial<T>);
}
}
return renderRow({ row: node.data, style, dragHandle });
}) as ElementType<NodeRendererProps<T>>;
};