Skip to content

Commit b0423d8

Browse files
committed
comprehensive sandbox
1 parent 0b45d6b commit b0423d8

File tree

9 files changed

+553
-0
lines changed

9 files changed

+553
-0
lines changed

examples/comprehensive/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React + TS</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "@headless-tree/example-comprehensive",
3+
"description": "Integration of Headless Tree with React with most HT features enabled",
4+
"private": true,
5+
"version": "0.0.0",
6+
"type": "module",
7+
"scripts": {
8+
"dev": "vite",
9+
"start": "vite",
10+
"build": "tsc -b && vite build"
11+
},
12+
"dependencies": {
13+
"@headless-tree/core": "^1.0.0",
14+
"@headless-tree/react": "^1.0.0",
15+
"classnames": "^2.3.2",
16+
"react": "^19.0.0",
17+
"react-dom": "^19.0.0"
18+
},
19+
"devDependencies": {
20+
"@types/react": "^19.0.10",
21+
"@types/react-dom": "^19.0.4",
22+
"@vitejs/plugin-react": "^4.3.4",
23+
"globals": "^16.0.0",
24+
"typescript": "~5.7.2",
25+
"vite": "^6.3.1"
26+
}
27+
}

examples/comprehensive/readme.MD

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Integration of Headless Tree with React with most HT features enabled
2+
3+
To run this example:
4+
5+
- `npm install` to install dependencies
6+
- `npm start` to start the dev server
7+
8+
You can run this sample from [Stackblitz](https://stackblitz.com/github/lukasbach/headless-tree/tree/main/examples/comprehensive?preset=node&file=src/main.tsx) or [CodeSandbox](https://codesandbox.io/p/devbox/github/lukasbach/headless-tree/tree/main/examples/comprehensive?file=src/main.tsx). The source code is available on [GitHub](https://github.com/lukasbach/headless-tree/tree/main/examples/comprehensive).
9+

examples/comprehensive/src/data.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
export type DemoItem = {
2+
name: string;
3+
children?: string[];
4+
};
5+
6+
export const data: Record<string, DemoItem> = {
7+
root: {
8+
name: "Root",
9+
children: ["fruit", "vegetables", "meals", "dessert", "drinks"],
10+
},
11+
fruit: {
12+
name: "Fruit",
13+
children: ["apple", "banana", "orange", "berries", "lemon"],
14+
},
15+
apple: { name: "Apple" },
16+
banana: { name: "Banana" },
17+
orange: { name: "Orange" },
18+
lemon: { name: "Lemon" },
19+
berries: { name: "Berries", children: ["red", "blue", "black"] },
20+
red: { name: "Red", children: ["strawberry", "raspberry"] },
21+
strawberry: { name: "Strawberry" },
22+
raspberry: { name: "Raspberry" },
23+
blue: { name: "Blue", children: ["blueberry"] },
24+
blueberry: { name: "Blueberry" },
25+
black: { name: "Black", children: ["blackberry"] },
26+
blackberry: { name: "Blackberry" },
27+
vegetables: {
28+
name: "Vegetables",
29+
children: ["tomato", "carrot", "cucumber", "potato"],
30+
},
31+
tomato: { name: "Tomato" },
32+
carrot: { name: "Carrot" },
33+
cucumber: { name: "Cucumber" },
34+
potato: { name: "Potato" },
35+
meals: {
36+
name: "Meals",
37+
children: ["america", "europe", "asia", "australia"],
38+
},
39+
america: { name: "America", children: ["burger", "hotdog", "pizza"] },
40+
burger: { name: "Burger" },
41+
hotdog: { name: "Hotdog" },
42+
pizza: { name: "Pizza" },
43+
europe: {
44+
name: "Europe",
45+
children: ["pasta", "paella", "schnitzel", "risotto", "weisswurst"],
46+
},
47+
pasta: { name: "Pasta" },
48+
paella: { name: "Paella" },
49+
schnitzel: { name: "Schnitzel" },
50+
risotto: { name: "Risotto" },
51+
weisswurst: { name: "Weisswurst" },
52+
asia: { name: "Asia", children: ["sushi", "ramen", "curry", "noodles"] },
53+
sushi: { name: "Sushi" },
54+
ramen: { name: "Ramen" },
55+
curry: { name: "Curry" },
56+
noodles: { name: "Noodles" },
57+
australia: {
58+
name: "Australia",
59+
children: ["potatowedges", "pokebowl", "lemoncurd", "kumarafries"],
60+
},
61+
potatowedges: { name: "Potato Wedges" },
62+
pokebowl: { name: "Poke Bowl" },
63+
lemoncurd: { name: "Lemon Curd" },
64+
kumarafries: { name: "Kumara Fries" },
65+
dessert: {
66+
name: "Dessert",
67+
children: ["icecream", "cake", "pudding", "cookies"],
68+
},
69+
icecream: { name: "Icecream" },
70+
cake: { name: "Cake" },
71+
pudding: { name: "Pudding" },
72+
cookies: { name: "Cookies" },
73+
drinks: { name: "Drinks", children: ["water", "juice", "beer", "wine"] },
74+
water: { name: "Water" },
75+
juice: { name: "Juice" },
76+
beer: { name: "Beer" },
77+
wine: { name: "Wine" },
78+
};
79+
80+
const wait = (ms: number) =>
81+
new Promise((resolve) => {
82+
setTimeout(resolve, ms);
83+
});
84+
85+
export const syncDataLoader = {
86+
getItem: (id: string) => data[id],
87+
getChildren: (id: string) => data[id]?.children ?? [],
88+
};
89+
90+
export const asyncDataLoader = {
91+
getItem: (itemId: string) => wait(500).then(() => data[itemId]),
92+
getChildren: (itemId: string) =>
93+
wait(800).then(() => data[itemId]?.children ?? []),
94+
};
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { Fragment, StrictMode } from "react";
2+
import { createRoot } from "react-dom/client";
3+
import "./style.css";
4+
import {
5+
DragTarget,
6+
ItemInstance,
7+
asyncDataLoaderFeature,
8+
createOnDropHandler,
9+
dragAndDropFeature,
10+
hotkeysCoreFeature,
11+
insertItemsAtTarget,
12+
keyboardDragAndDropFeature,
13+
removeItemsFromParents,
14+
renamingFeature,
15+
searchFeature,
16+
selectionFeature,
17+
} from "@headless-tree/core";
18+
import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
19+
import cx from "classnames";
20+
import { DemoItem, asyncDataLoader, data } from "./data";
21+
22+
let newItemId = 0;
23+
const insertNewItem = (dataTransfer: DataTransfer) => {
24+
const newId = `new-${newItemId++}`;
25+
data[newId] = {
26+
name: dataTransfer.getData("text/plain"),
27+
};
28+
return newId;
29+
};
30+
31+
const onDropForeignDragObject = (
32+
dataTransfer: DataTransfer,
33+
target: DragTarget<DemoItem>,
34+
) => {
35+
const newId = insertNewItem(dataTransfer);
36+
insertItemsAtTarget([newId], target, (item, newChildrenIds) => {
37+
data[item.getId()].children = newChildrenIds;
38+
});
39+
};
40+
const onCompleteForeignDrop = (items: ItemInstance<DemoItem>[]) =>
41+
removeItemsFromParents(items, (item, newChildren) => {
42+
item.getItemData().children = newChildren;
43+
});
44+
const onRename = (item: ItemInstance<DemoItem>, value: string) => {
45+
data[item.getId()].name = value;
46+
};
47+
const getCssClass = (item: ItemInstance<DemoItem>) =>
48+
cx("treeitem", {
49+
focused: item.isFocused(),
50+
expanded: item.isExpanded(),
51+
selected: item.isSelected(),
52+
folder: item.isFolder(),
53+
drop: item.isDragTarget(),
54+
searchmatch: item.isMatchingSearch(),
55+
});
56+
57+
export const Tree = () => {
58+
const tree = useTree<DemoItem>({
59+
initialState: {
60+
expandedItems: ["fruit"],
61+
selectedItems: ["banana", "orange"],
62+
},
63+
rootItemId: "root",
64+
getItemName: (item) => item.getItemData()?.name,
65+
isItemFolder: (item) => !!item.getItemData()?.children,
66+
canReorder: true,
67+
onDrop: createOnDropHandler((item, newChildren) => {
68+
data[item.getId()].children = newChildren;
69+
}),
70+
onRename,
71+
onDropForeignDragObject,
72+
onCompleteForeignDrop,
73+
createForeignDragObject: (items) => ({
74+
format: "text/plain",
75+
data: items.map((item) => item.getId()).join(","),
76+
}),
77+
canDropForeignDragObject: (_, target) => target.item.isFolder(),
78+
indent: 20,
79+
dataLoader: asyncDataLoader,
80+
features: [
81+
asyncDataLoaderFeature,
82+
selectionFeature,
83+
hotkeysCoreFeature,
84+
dragAndDropFeature,
85+
keyboardDragAndDropFeature,
86+
renamingFeature,
87+
searchFeature,
88+
],
89+
});
90+
91+
return (
92+
<>
93+
{tree.isSearchOpen() && (
94+
<div className="searchbox">
95+
<input {...tree.getSearchInputElementProps()} />
96+
<span>({tree.getSearchMatchingItems().length} matches)</span>
97+
</div>
98+
)}
99+
<div {...tree.getContainerProps()} className="tree">
100+
<AssistiveTreeDescription tree={tree} />
101+
{tree.getItems().map((item) => (
102+
<Fragment key={item.getId()}>
103+
{item.isRenaming() ? (
104+
<div
105+
className="renaming-item"
106+
style={{ marginLeft: `${item.getItemMeta().level * 20}px` }}
107+
>
108+
<input {...item.getRenameInputProps()} />
109+
</div>
110+
) : (
111+
<button
112+
{...item.getProps()}
113+
style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
114+
>
115+
<div className={getCssClass(item)}>{item.getItemName()}</div>
116+
</button>
117+
)}
118+
</Fragment>
119+
))}
120+
<div style={tree.getDragLineStyle()} className="dragline" />
121+
</div>
122+
123+
<div className="actionbar">
124+
<div
125+
className="foreign-dragsource"
126+
draggable
127+
onDragStart={(e) => {
128+
e.dataTransfer.setData("text/plain", "hello world");
129+
}}
130+
>
131+
Drag me into the tree!
132+
</div>
133+
<div
134+
className="foreign-dropzone"
135+
onDrop={(e) => {
136+
alert(JSON.stringify(e.dataTransfer.getData("text/plain")));
137+
console.log(e.dataTransfer.getData("text/plain"));
138+
}}
139+
onDragOver={(e) => e.preventDefault()}
140+
>
141+
Drop items here!
142+
</div>
143+
<button className="actionbtn" onClick={() => tree.openSearch()}>
144+
Search items
145+
</button>
146+
<button
147+
className="actionbtn"
148+
onClick={() => tree.getItemInstance("fruit").startRenaming()}
149+
>
150+
Rename Fruit
151+
</button>
152+
</div>
153+
</>
154+
);
155+
};
156+
157+
createRoot(document.getElementById("root")!).render(
158+
<StrictMode>
159+
<div style={{ maxWidth: "300px" }}>
160+
<Tree />
161+
</div>
162+
</StrictMode>,
163+
);

0 commit comments

Comments
 (0)