diff --git a/packages/react-arborist/src/hooks/use-fresh-node.ts b/packages/react-arborist/src/hooks/use-fresh-node.ts index 5ac27ad..ce50077 100644 --- a/packages/react-arborist/src/hooks/use-fresh-node.ts +++ b/packages/react-arborist/src/hooks/use-fresh-node.ts @@ -7,6 +7,7 @@ export function useFreshNode(index: number) { const original = tree.at(index); if (!original) throw new Error(`Could not find node for index: ${index}`); + console.log(original.state.willDropInAncestor, original.id); return useMemo(() => { const fresh = original.clone(); tree.visibleNodes[index] = fresh; // sneaky diff --git a/packages/react-arborist/src/interfaces/node-api.ts b/packages/react-arborist/src/interfaces/node-api.ts index cd4da42..8379963 100644 --- a/packages/react-arborist/src/interfaces/node-api.ts +++ b/packages/react-arborist/src/interfaces/node-api.ts @@ -91,6 +91,10 @@ export class NodeApi { return this.tree.willReceiveDrop(this.id); } + get willDropInAncestor() { + return this.tree.willDropInAncestor(this.id); + } + get state() { return { isClosed: this.isClosed, @@ -104,6 +108,7 @@ export class NodeApi { isSelectedEnd: this.isSelectedEnd, isSelectedStart: this.isSelectedStart, willReceiveDrop: this.willReceiveDrop, + willDropInAncestor: this.willDropInAncestor, }; } @@ -130,6 +135,16 @@ export class NodeApi { return this.parent?.children![i + 1] ?? null; } + isAncestorOf(node: NodeApi | null) { + if (!node) return false; + let ancestor: NodeApi | null = node; + while (ancestor) { + if (ancestor.id === this.id) return true; + ancestor = ancestor.parent; + } + return false; + } + select() { this.tree.select(this); } diff --git a/packages/react-arborist/src/interfaces/tree-api.ts b/packages/react-arborist/src/interfaces/tree-api.ts index b442a89..6dd98f6 100644 --- a/packages/react-arborist/src/interfaces/tree-api.ts +++ b/packages/react-arborist/src/interfaces/tree-api.ts @@ -609,6 +609,15 @@ export class TreeApi { return id === this.state.nodes.drag.idWillReceiveDrop; } + willDropInAncestor(identity: Identity) { + const id = identifyNull(identity); + if (!id) return false; + const parent = this.get(this.state.nodes.drag.dropParentId); + const node = this.get(id); + if (!parent) return false; + return parent.isAncestorOf(node); + } + /* Tree Event Handlers */ onFocus() { diff --git a/packages/react-arborist/src/state/drag-slice.ts b/packages/react-arborist/src/state/drag-slice.ts index e270bf2..59bb981 100644 --- a/packages/react-arborist/src/state/drag-slice.ts +++ b/packages/react-arborist/src/state/drag-slice.ts @@ -3,19 +3,23 @@ import { actions as dnd } from "./dnd-slice"; /* Types */ -export type DragSlice = { id: string | null; idWillReceiveDrop: string | null }; +export type DragSlice = { + id: string | null; + idWillReceiveDrop: string | null; + dropParentId: string | null; +}; /* Reducer */ export function reducer( - state: DragSlice = { id: null, idWillReceiveDrop: null }, + state: DragSlice = { id: null, idWillReceiveDrop: null, dropParentId: null }, action: ActionTypes ) { switch (action.type) { case "DND_DRAG_START": return { ...state, id: action.id }; case "DND_DRAG_END": - return { ...state, id: null }; + return { ...state, id: null, dropParentId: null }; case "DND_CURSOR": const c = action.cursor; if (c.type === "highlight" && c.id !== state.idWillReceiveDrop) { @@ -25,6 +29,12 @@ export function reducer( } else { return state; } + case "DND_HOVERING": + if (action.parentId !== state.dropParentId) { + return { ...state, dropParentId: action.parentId }; + } else { + return state; + } default: return state; } diff --git a/packages/react-arborist/src/state/initial.ts b/packages/react-arborist/src/state/initial.ts index 7c4b1fb..2f74d38 100644 --- a/packages/react-arborist/src/state/initial.ts +++ b/packages/react-arborist/src/state/initial.ts @@ -7,7 +7,7 @@ export const initialState = (props?: TreeProps): RootState => ({ open: { filtered: {}, unfiltered: props?.initialOpenState ?? {} }, focus: { id: null, treeFocused: false }, edit: { id: null }, - drag: { id: null, idWillReceiveDrop: null }, + drag: { id: null, idWillReceiveDrop: null, dropParentId: null }, selection: { ids: new Set(), anchor: null, mostRecent: null }, }, dnd: { diff --git a/packages/showcase/next.config.js b/packages/showcase/next.config.js index ae88795..3d3bc99 100644 --- a/packages/showcase/next.config.js +++ b/packages/showcase/next.config.js @@ -2,6 +2,6 @@ const nextConfig = { reactStrictMode: true, swcMinify: true, -} +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/packages/showcase/pages/cities.tsx b/packages/showcase/pages/cities.tsx index faba8fe..5e11d67 100644 --- a/packages/showcase/pages/cities.tsx +++ b/packages/showcase/pages/cities.tsx @@ -170,7 +170,7 @@ export default function Cities() { function Node({ node, style, dragHandle }: NodeRendererProps) { const Icon = node.isInternal ? BsMapFill : BsGeoFill; const indentSize = Number.parseFloat(`${style.paddingLeft || 0}`); - + return (
(id++).toString(); +const file = (name: string) => ({ name, id: nextId() }); +const folder = (name: string, ...children: Entry[]) => ({ + name, + id: nextId(), + children, +}); + +const structure = [ + folder( + "src", + file("index.ts"), + folder( + "lib", + file("index.ts"), + file("worker.ts"), + file("utils.ts"), + file("model.ts") + ), + folder( + "ui", + file("button.ts"), + file("form.ts"), + file("table.ts"), + folder( + "demo", + file("welcome.ts"), + file("example.ts"), + file("container.ts") + ) + ) + ), +]; + +function sortChildren(node: Entry): Entry { + if (!node.children) return node; + const copy = [...node.children]; + copy.sort((a, b) => { + if (!!a.children && !b.children) return -1; + if (!!b.children && !a.children) return 1; + return a.name < b.name ? -1 : 1; + }); + const children = copy.map(sortChildren); + return { ...node, children }; +} + +function useTreeSort(data: Entry[]) { + return data.map(sortChildren); +} + +function Node({ style, node, dragHandle }: NodeRendererProps) { + return ( +
+ {node.isInternal ? : } + {node.data.name} {node.id} +
+ ); +} + +export default function VSCodeDemoPage() { + const { width, height, ref } = useResizeObserver(); + + const data = useTreeSort(structure); + + return ( +
+ +
+
+ ); +} diff --git a/packages/showcase/styles/vscode.module.css b/packages/showcase/styles/vscode.module.css new file mode 100644 index 0000000..74db9c3 --- /dev/null +++ b/packages/showcase/styles/vscode.module.css @@ -0,0 +1,47 @@ +.root { + display: grid; + grid-template-columns: 360px 1fr; + grid-template-rows: 1fr; + height: 100vh; + width: 100vw; +} + +.node { + font-size: 13px; + display: grid; + grid-template-columns: auto 1fr; + gap: 10px; + cursor: default; + height: 100%; + align-items: center;; +} + +.sidebar { + background: #192226; + color: rgb(95, 122, 135); +} + +.main { + background: #253238; +} + +.node:global(.isInternal) svg { + fill: #80CBC4; +} + +.node:global(.isLeaf) svg { + width: 10px; + fill: #3865BD; +} + +.node:global(.willReceiveDrop) { + background: blue; +} + +.node:hover { + color: white; +} + +.node:global(.willDropInAncestor) { + background: lightskyblue; +} diff --git a/packages/showcase/tsconfig.json b/packages/showcase/tsconfig.json index e6bb8eb..ee832c2 100644 --- a/packages/showcase/tsconfig.json +++ b/packages/showcase/tsconfig.json @@ -17,7 +17,7 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "incremental": true + "incremental": true, }, "include": [ "next-env.d.ts", @@ -26,5 +26,5 @@ ], "exclude": [ "node_modules" - ] + ], }