diff --git a/packages/react-arborist/src/dnd/compute-drop.ts b/packages/react-arborist/src/dnd/compute-drop.ts index 5f00dbc..c492aa8 100644 --- a/packages/react-arborist/src/dnd/compute-drop.ts +++ b/packages/react-arborist/src/dnd/compute-drop.ts @@ -26,7 +26,7 @@ function getNodesAroundCursor( hover: HoverData ): [NodeApi | null, NodeApi | null] { if (!node) { - // We're hoving over the empty part of the list, not over an item, + // We're hovering over the empty part of the list, not over an item, // Put the cursor below the last item which is "prev" return [prev, null]; } @@ -83,7 +83,10 @@ export type ComputedDrop = { cursor: Cursor | null; }; -function dropAt(parentId: string | undefined, index: number): DropResult { +function dropAt( + parentId: string | undefined, + index: number | null +): DropResult { return { parentId: parentId || null, index }; } @@ -135,7 +138,7 @@ export function computeDrop(args: Args): ComputedDrop { /* Hovering over the middle of a folder */ if (node && node.isInternal && hover.inMiddle) { return { - drop: dropAt(node.id, 0), + drop: dropAt(node.id, null), cursor: highlightCursor(node.id), }; } diff --git a/packages/react-arborist/src/dnd/drag-hook.ts b/packages/react-arborist/src/dnd/drag-hook.ts index 8e8bb7c..22ac564 100644 --- a/packages/react-arborist/src/dnd/drag-hook.ts +++ b/packages/react-arborist/src/dnd/drag-hook.ts @@ -31,7 +31,7 @@ export function useDragHook(node: NodeApi): ConnectDragSource { safeRun(tree.props.onMove, { dragIds, parentId: parentId === ROOT_ID ? null : parentId, - index, + index: index === null ? 0 : index, // When it's null it was dropped over a folder dragNodes: tree.dragNodes, parentNode: tree.get(parentId), }); diff --git a/packages/react-arborist/src/dnd/drop-hook.ts b/packages/react-arborist/src/dnd/drop-hook.ts index 0f52c01..6be259d 100644 --- a/packages/react-arborist/src/dnd/drop-hook.ts +++ b/packages/react-arborist/src/dnd/drop-hook.ts @@ -8,7 +8,7 @@ import { actions as dnd } from "../state/dnd-slice"; export type DropResult = { parentId: string | null; - index: number; + index: number | null; }; export function useDropHook( diff --git a/packages/react-arborist/src/interfaces/node-api.ts b/packages/react-arborist/src/interfaces/node-api.ts index cd4da42..34d7138 100644 --- a/packages/react-arborist/src/interfaces/node-api.ts +++ b/packages/react-arborist/src/interfaces/node-api.ts @@ -130,6 +130,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..9ba5868 100644 --- a/packages/react-arborist/src/interfaces/tree-api.ts +++ b/packages/react-arborist/src/interfaces/tree-api.ts @@ -419,6 +419,18 @@ export class TreeApi { .filter((n) => !!n) as NodeApi[]; } + get dragNode() { + return this.get(this.state.nodes.drag.id); + } + + get dragDestinationParent() { + return this.get(this.state.nodes.drag.destinationParentId); + } + + get dragDestinationIndex() { + return this.state.nodes.drag.destinationIndex; + } + canDrop() { if (this.isFiltered) return false; const parentNode = this.get(this.state.dnd.parentId) ?? this.root; @@ -428,7 +440,7 @@ export class TreeApi { for (const drag of dragNodes) { if (!drag) return false; if (!parentNode) return false; - if (drag.isInternal && utils.isDecendent(parentNode, drag)) return false; + if (drag.isInternal && utils.isDescendant(parentNode, drag)) return false; } // Allow the user to insert their own logic @@ -436,7 +448,7 @@ export class TreeApi { return !isDisabled({ parentNode, dragNodes: this.dragNodes, - index: this.state.dnd.index, + index: this.state.dnd.index || 0, }); } else if (typeof isDisabled == "string") { // @ts-ignore @@ -606,7 +618,8 @@ export class TreeApi { willReceiveDrop(node: string | IdObj | null) { const id = identifyNull(node); if (!id) return false; - return id === this.state.nodes.drag.idWillReceiveDrop; + const { destinationParentId, destinationIndex } = this.state.nodes.drag; + return id === destinationParentId && destinationIndex === null; } /* Tree Event Handlers */ diff --git a/packages/react-arborist/src/state/dnd-slice.ts b/packages/react-arborist/src/state/dnd-slice.ts index da5b048..afe0fb7 100644 --- a/packages/react-arborist/src/state/dnd-slice.ts +++ b/packages/react-arborist/src/state/dnd-slice.ts @@ -8,7 +8,7 @@ export type DndState = { cursor: Cursor; dragIds: string[]; parentId: null | string; - index: number; + index: number | null; }; /* Actions */ @@ -22,7 +22,7 @@ export const actions = { dragEnd() { return { type: "DND_DRAG_END" as const }; }, - hovering(parentId: string | null, index: number) { + hovering(parentId: string | null, index: number | null) { return { type: "DND_HOVERING" as const, parentId, index }; }, }; diff --git a/packages/react-arborist/src/state/drag-slice.ts b/packages/react-arborist/src/state/drag-slice.ts index e270bf2..517adc6 100644 --- a/packages/react-arborist/src/state/drag-slice.ts +++ b/packages/react-arborist/src/state/drag-slice.ts @@ -1,27 +1,43 @@ import { ActionTypes } from "../types/utils"; import { actions as dnd } from "./dnd-slice"; +import { initialState } from "./initial"; /* Types */ -export type DragSlice = { id: string | null; idWillReceiveDrop: string | null }; +export type DragSlice = { + id: string | null; + selectedIds: string[]; + destinationParentId: string | null; + destinationIndex: number | null; +}; /* Reducer */ export function reducer( - state: DragSlice = { id: null, idWillReceiveDrop: null }, + state: DragSlice = initialState().nodes.drag, action: ActionTypes -) { +): DragSlice { switch (action.type) { case "DND_DRAG_START": - return { ...state, id: action.id }; + return { ...state, id: action.id, selectedIds: action.dragIds }; case "DND_DRAG_END": - return { ...state, id: null }; - case "DND_CURSOR": - const c = action.cursor; - if (c.type === "highlight" && c.id !== state.idWillReceiveDrop) { - return { ...state, idWillReceiveDrop: c.id }; - } else if (c.type !== "highlight" && state.idWillReceiveDrop !== null) { - return { ...state, idWillReceiveDrop: null }; + return { + ...state, + id: null, + destinationParentId: null, + destinationIndex: null, + selectedIds: [], + }; + case "DND_HOVERING": + if ( + action.parentId !== state.destinationParentId || + action.index != state.destinationIndex + ) { + return { + ...state, + destinationParentId: action.parentId, + destinationIndex: action.index, + }; } else { return state; } diff --git a/packages/react-arborist/src/state/initial.ts b/packages/react-arborist/src/state/initial.ts index 7c4b1fb..7a2e027 100644 --- a/packages/react-arborist/src/state/initial.ts +++ b/packages/react-arborist/src/state/initial.ts @@ -7,7 +7,12 @@ 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, + selectedIds: [], + destinationParentId: null, + destinationIndex: null, + }, selection: { ids: new Set(), anchor: null, mostRecent: null }, }, dnd: { diff --git a/packages/react-arborist/src/utils.ts b/packages/react-arborist/src/utils.ts index 8fa2175..aeec8e0 100644 --- a/packages/react-arborist/src/utils.ts +++ b/packages/react-arborist/src/utils.ts @@ -15,9 +15,9 @@ export function isClosed(node: NodeApi | null) { } /** - * Is first param a decendent of the second param + * Is first param a descendant of the second param */ -export const isDecendent = (a: NodeApi, b: NodeApi) => { +export const isDescendant = (a: NodeApi, b: NodeApi) => { let n: NodeApi | null = a; while (n) { if (n.id === b.id) return true; 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, tree }: 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..5b2e9d5 --- /dev/null +++ b/packages/showcase/styles/vscode.module.css @@ -0,0 +1,43 @@ +.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:hover { + color: white; +} + +.highlight { + background: #062F4A; +} 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" - ] + ], }