Skip to content

Commit

Permalink
Create a VSCode Demo (#179)
Browse files Browse the repository at this point in the history
get dragNode() {}
get dragDestinationParent() {}
get dragDestinationIndex() {}
  • Loading branch information
jameskerr committed Nov 6, 2023
1 parent fbf0290 commit 83d5e55
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 29 deletions.
9 changes: 6 additions & 3 deletions packages/react-arborist/src/dnd/compute-drop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down Expand Up @@ -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 };
}

Expand Down Expand Up @@ -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),
};
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react-arborist/src/dnd/drag-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function useDragHook<T>(node: NodeApi<T>): 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),
});
Expand Down
2 changes: 1 addition & 1 deletion packages/react-arborist/src/dnd/drop-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 10 additions & 0 deletions packages/react-arborist/src/interfaces/node-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ export class NodeApi<T = any> {
return this.parent?.children![i + 1] ?? null;
}

isAncestorOf(node: NodeApi<T> | null) {
if (!node) return false;
let ancestor: NodeApi<T> | null = node;
while (ancestor) {
if (ancestor.id === this.id) return true;
ancestor = ancestor.parent;
}
return false;
}

select() {
this.tree.select(this);
}
Expand Down
19 changes: 16 additions & 3 deletions packages/react-arborist/src/interfaces/tree-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,18 @@ export class TreeApi<T> {
.filter((n) => !!n) as NodeApi<T>[];
}

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;
Expand All @@ -428,15 +440,15 @@ export class TreeApi<T> {
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
if (typeof isDisabled == "function") {
return !isDisabled({
parentNode,
dragNodes: this.dragNodes,
index: this.state.dnd.index,
index: this.state.dnd.index || 0,
});
} else if (typeof isDisabled == "string") {
// @ts-ignore
Expand Down Expand Up @@ -606,7 +618,8 @@ export class TreeApi<T> {
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 */
Expand Down
4 changes: 2 additions & 2 deletions packages/react-arborist/src/state/dnd-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type DndState = {
cursor: Cursor;
dragIds: string[];
parentId: null | string;
index: number;
index: number | null;
};

/* Actions */
Expand All @@ -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 };
},
};
Expand Down
38 changes: 27 additions & 11 deletions packages/react-arborist/src/state/drag-slice.ts
Original file line number Diff line number Diff line change
@@ -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<typeof dnd>
) {
): 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;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/react-arborist/src/state/initial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ export const initialState = (props?: TreeProps<any>): 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: {
Expand Down
4 changes: 2 additions & 2 deletions packages/react-arborist/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ export function isClosed(node: NodeApi<any> | null) {
}

/**
* Is first param a decendent of the second param
* Is first param a descendant of the second param
*/
export const isDecendent = (a: NodeApi<any>, b: NodeApi<any>) => {
export const isDescendant = (a: NodeApi<any>, b: NodeApi<any>) => {
let n: NodeApi<any> | null = a;
while (n) {
if (n.id === b.id) return true;
Expand Down
4 changes: 2 additions & 2 deletions packages/showcase/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
};

module.exports = nextConfig
module.exports = nextConfig;
2 changes: 1 addition & 1 deletion packages/showcase/pages/cities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export default function Cities() {
function Node({ node, style, dragHandle }: NodeRendererProps<Data>) {
const Icon = node.isInternal ? BsMapFill : BsGeoFill;
const indentSize = Number.parseFloat(`${style.paddingLeft || 0}`);

return (
<div
ref={dragHandle}
Expand Down
101 changes: 101 additions & 0 deletions packages/showcase/pages/vscode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import useResizeObserver from "use-resize-observer";
import styles from "../styles/vscode.module.css";
import { NodeRendererProps, Tree } from "react-arborist";
import { SiTypescript } from "react-icons/si";
import { MdFolder } from "react-icons/md";
import clsx from "clsx";

let id = 1;

type Entry = { name: string; id: string; children?: Entry[] };

const nextId = () => (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<Entry>) {
return (
<div
style={style}
className={clsx(styles.node, node.state, {
[styles.highlight]:
tree.dragDestinationParent?.isAncestorOf(node) &&
tree.dragDestinationParent?.id !== tree.dragNode?.parent?.id,
})}
ref={dragHandle}
>
{node.isInternal ? <MdFolder /> : <SiTypescript />}
{node.data.name} {node.id}
</div>
);
}

export default function VSCodeDemoPage() {
const { width, height, ref } = useResizeObserver();

const data = useTreeSort(structure);

return (
<div className={styles.root}>
<aside className={styles.sidebar} ref={ref}>
<Tree
data={data}
width={width}
height={height}
rowHeight={22}
onMove={() => {}}
renderCursor={() => null}
>
{Node}
</Tree>
</aside>
<main className={styles.main}></main>
</div>
);
}
43 changes: 43 additions & 0 deletions packages/showcase/styles/vscode.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 2 additions & 2 deletions packages/showcase/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
},
"include": [
"next-env.d.ts",
Expand All @@ -26,5 +26,5 @@
],
"exclude": [
"node_modules"
]
],
}

0 comments on commit 83d5e55

Please sign in to comment.