diff --git a/modules/react-arborist/src/nodes/accessor.ts b/modules/react-arborist/src/nodes/accessor.ts new file mode 100644 index 0000000..8790656 --- /dev/null +++ b/modules/react-arborist/src/nodes/accessor.ts @@ -0,0 +1,37 @@ +export type NodeDataAccessors = { + id: (d: T) => string; + children: (d: T) => T[]; + isLeaf: (d: T) => boolean; +}; + +export class Accessor { + constructor(public accessors: Partial> = {}) {} + + getId(d: T): string { + if (this.accessors.id) { + return this.accessors.id(d); + } else if (d && typeof d === "object" && "id" in d) { + return d.id as string; + } else { + throw new Error("No id found for node data. Specify an id accessor."); + } + } + + getChildren(d: T): null | T[] { + if (this.accessors.children) { + return this.accessors.children(d); + } else if (d && typeof d === "object" && "children" in d) { + return d.children as T[]; + } else { + return null; + } + } + + getIsLeaf(d: T): boolean { + if (this.accessors.isLeaf) { + return this.accessors.isLeaf(d); + } else { + return !this.getChildren(d); + } + } +} diff --git a/modules/react-arborist/src/nodes/arborist.ts b/modules/react-arborist/src/nodes/arborist.ts new file mode 100644 index 0000000..9e101a9 --- /dev/null +++ b/modules/react-arborist/src/nodes/arborist.ts @@ -0,0 +1,3 @@ +export class Arborist { + constructor(root: NodeStruct, accessors: Accessor) +} diff --git a/modules/react-arborist/src/nodes/create-tree.ts b/modules/react-arborist/src/nodes/create-tree.ts new file mode 100644 index 0000000..864f18f --- /dev/null +++ b/modules/react-arborist/src/nodes/create-tree.ts @@ -0,0 +1,16 @@ +import { useEffect, useState } from "react"; +import { Accessor, NodeDataAccessors } from "./accessor"; +import { createRootNodeStruct } from "./root-node-struct"; +import { TreeStruct } from "./tree-struct"; + +type Options = NodeDataAccessors; + +export function createTree(data: T[], options: Partial> = {}) { + const accessor = new Accessor(options); + const root = createRootNodeStruct(data, accessor)); + const tree = new TreeStruct(root, accessor) + + return { + nodes: tree.nodes + } +} diff --git a/modules/react-arborist/src/nodes/flatten.ts b/modules/react-arborist/src/nodes/flatten.ts new file mode 100644 index 0000000..9a07be7 --- /dev/null +++ b/modules/react-arborist/src/nodes/flatten.ts @@ -0,0 +1,25 @@ +import { TreeApi } from "../interfaces/tree-api"; +import { NodeStruct } from "./node-struct"; + +export type RowStruct = { + node: NodeStruct; + index: number; +}; + +export function flatten(tree: TreeApi): RowStruct[] { + const list: RowStruct[] = []; + const queue = [...tree.root.children!]; + let node = queue.shift(); + let index = 0; + while (node) { + list.push({ + node, + index: index++, + }); + if (!node.isLeaf && tree.isOpen(node.id)) { + queue.unshift(...node.children!); + } + node = queue.shift(); + } + return list; +} diff --git a/modules/react-arborist/src/nodes/node-struct.ts b/modules/react-arborist/src/nodes/node-struct.ts new file mode 100644 index 0000000..c870cb4 --- /dev/null +++ b/modules/react-arborist/src/nodes/node-struct.ts @@ -0,0 +1,57 @@ +import { Accessor } from "./accessor"; + +export type NodeStruct = { + id: string; + data: T; + children: NodeStruct[] | null; + parent: NodeStruct | null; + isLeaf: boolean; + level: number; +}; + +export function createNodeStruct(args: { + data: T; + level: number; + parent: NodeStruct | null; + accessor: Accessor; +}) { + const { data, accessor, level, parent } = args; + const id = accessor.getId(data); + const isLeaf = accessor.getIsLeaf(data); + const children = isLeaf ? null : accessor.getChildren(data); + const node: NodeStruct = { + id, + isLeaf, + data, + level, + parent, + children: null, + }; + if (children) { + node.children = children.map((child) => + createNodeStruct({ + data: child, + parent: node, + level: level + 1, + accessor, + }), + ); + } + return node; +} + +export function find( + id: string, + current?: NodeStruct, +): NodeStruct | null { + if (!current) return null; + if (current.id === id) return current; + if (current.children) { + for (let child of current.children) { + const found = find(id, child); + if (found) return found; + } + return null; + } + return null; +} diff --git a/modules/react-arborist/src/nodes/root-node-struct.ts b/modules/react-arborist/src/nodes/root-node-struct.ts new file mode 100644 index 0000000..e7241fc --- /dev/null +++ b/modules/react-arborist/src/nodes/root-node-struct.ts @@ -0,0 +1,19 @@ +import { Accessor } from "./accessor"; +import { NodeStruct, createNodeStruct } from "./node-struct"; + +export const ROOT_ID = "__REACT_ARBORIST_INTERNAL_ROOT__"; + +export function createRootNodeStruct(data: T[], accessor: Accessor) { + const root: NodeStruct = { + id: ROOT_ID, + data: null as unknown as T, + level: -1, + isLeaf: false, + parent: null, + children: null, + }; + root.children = data.map((child) => + createNodeStruct({ data: child, parent: root, level: 0, accessor }), + ); + return root; +} diff --git a/modules/react-arborist/src/nodes/tree-struct.ts b/modules/react-arborist/src/nodes/tree-struct.ts new file mode 100644 index 0000000..02eec73 --- /dev/null +++ b/modules/react-arborist/src/nodes/tree-struct.ts @@ -0,0 +1,55 @@ +import { Accessor } from "./accessor"; +import { NodeStruct, find } from "./node-struct"; + +/* We wrap and mutate the data provided */ +export class TreeStruct { + constructor( + public root: NodeStruct, + public accessor: Accessor, + ) {} + + get data() { + return this.root.children?.map((node) => node.data); + } + + get nodes() { + return this.root; // maybe this returns an array + } + + create(args: { parentId: string | null; index: number; data: T }) { + const { parentId, index, data } = args; + const parent = parentId ? find(parentId, this.root) : this.root; + if (!parent) return null; + const siblings = this.accessor.getChildren(parent.data)!; + + siblings.splice(index, 0, data); + } + + update(args: { id: string; changes: Partial }) { + const { id, changes } = args; + const node = find(id, this.root); + + if (node) node.data = { ...node.data, ...changes }; + } + + move(args: { id: string; parentId: string | null; index: number }) { + const { id, parentId, index } = args; + const node = find(id, this.root); + const parent = parentId ? find(parentId, this.root) : this.root; + if (!node || !parent) return; + const newSiblings = this.accessor.getChildren(parent.data)!; + const oldSiblings = this.accessor.getChildren(node.parent!.data)!; + const oldIndex = oldSiblings.indexOf(node.data); + + newSiblings.splice(index, 0, node.data); // Add to new parent + oldSiblings.splice(oldIndex, 1); // Remove from old parent + } + + destroy(args: { id: string }) { + const node = find(args.id, this.root); + if (!node) return; + const siblings = this.accessor.getChildren(node.parent!.data)!; + const index = siblings.indexOf(node.data); + siblings.splice(index); + } +} diff --git a/modules/react-arborist/src/nodes/use-nodes.ts b/modules/react-arborist/src/nodes/use-nodes.ts new file mode 100644 index 0000000..0bed6a1 --- /dev/null +++ b/modules/react-arborist/src/nodes/use-nodes.ts @@ -0,0 +1,19 @@ +import { useEffect, useState } from "react"; +import { Accessor, NodeDataAccessors } from "./accessor"; +import { createRootNodeStruct } from "./root-node-struct"; + +type Options = NodeDataAccessors; + +export function useNodes(data: T[], options: Partial> = {}) { + const accessor = new Accessor(options); + const [value, set] = useState(() => createRootNodeStruct(data, accessor)); + + useEffect(() => { + set(createRootNodeStruct(data, accessor)); + }, [data]); + + return { + value, + set, + }; +}