|
1 | 1 | --- |
2 | 2 | slug: "/dnd/overview" |
3 | 3 | title: "Overview" |
| 4 | +hide_title: true |
4 | 5 | category: draganddrop |
5 | 6 | --- |
6 | 7 |
|
7 | 8 | import { DemoBox } from "../../src/components/demo/demo-box"; |
| 9 | +import {FeaturePageHeader} from "../../src/components/docs-page/feature-page-header"; |
| 10 | + |
| 11 | +<FeaturePageHeader |
| 12 | + title="Drag and Drop" |
| 13 | + subtitle="Drag-and-drop Capabilities for tree items and external drag objects" |
| 14 | + feature="drag-and-drop" |
| 15 | +/> |
8 | 16 |
|
9 | 17 | # Overview |
10 | 18 |
|
| 19 | +<DemoBox tags={["feature/dnd"]} /> |
| 20 | + |
| 21 | +The Drag-And-Drop Feature provides drag-and-drop capabilities. It allows to drag a single tree item (or many of the |
| 22 | +[selection feature](/features/selection) is included in the tree config) and drop it somewhere else in the tree. |
| 23 | +The feature also allows you to create interactions between the tree and external drag objects, allowing you to drag |
| 24 | +tree items out of the tree, or foreign data objects from outside the tree inside. As extension of that, this can |
| 25 | +also be used to implement drag behavior between several trees. |
| 26 | + |
| 27 | +This page explains the general idea of how Dnd works in Headless Tree, and how to get started with dragging items |
| 28 | +within a tree. The guides following after will go into more depth about |
| 29 | + |
| 30 | +- TODO |
| 31 | + |
| 32 | +## Configuring Drag and Drop |
| 33 | + |
| 34 | +The gist of configuring Drag and Drop in Headless Tree, is to |
| 35 | + |
| 36 | +- add the `dragAndDropFeature` to the tree config, |
| 37 | +- define the indent of your tree items in the config variable `indent`, and |
| 38 | +- handle the `onDrop` event in the tree config. |
| 39 | + |
| 40 | +Note that the `indent` property is not required in the core tree configuration, since you can just use a custom |
| 41 | +indentation when rendering your items. For Drag and Drop it is required however to determine the drop position |
| 42 | +when the user is trying to reparent an item. (TODO link) |
| 43 | + |
| 44 | +<!-- TODO render types as interfaces in typedoc, then fix links here --> |
| 45 | +The Dnd Feature also exposes a [Dnd State](/api/core#DndState) that you can [handle yourself if you want to](/guides/state). |
| 46 | + |
11 | 47 | ```jsx typescript |
12 | 48 | import { |
13 | 49 | syncDataLoaderFeature, |
14 | | - createOnDropHandler, |
15 | 50 | dragAndDropFeature, |
16 | | - hotkeysCoreFeature, |
17 | 51 | selectionFeature, |
18 | 52 | } from "@headless-tree/core"; |
19 | 53 |
|
20 | 54 | const tree = useTree<Item>({ |
21 | 55 | indent: 20, |
22 | 56 | canReorder: true, |
23 | | - onDrop: createOnDropHandler((item, newChildren) => { |
24 | | - // set newChildren to item |
25 | | - }), |
| 57 | + onDrop: (items, target) => { |
| 58 | + // handle drop |
| 59 | + }, |
26 | 60 | features: [ |
27 | 61 | syncDataLoaderFeature, |
28 | 62 | selectionFeature, |
29 | | - hotkeysCoreFeature, |
30 | 63 | dragAndDropFeature, |
31 | 64 | ], |
32 | 65 | }); |
33 | 66 | ``` |
34 | 67 |
|
| 68 | +## The Drop Event |
| 69 | + |
| 70 | +When the user drops tree items onto a target within the tree, the [`onDrop`](/api/core/interface/dragAndDropFeatureConfig#onDrop) event is called |
| 71 | +with the list of items that were dragged, and a [drop target](/api/core#DropTarget). This drop target is an object that contains |
| 72 | +either |
| 73 | + |
| 74 | +- the `item` that is dropped to, if the user dragged on the title of an item with the intent to make it a child of that item |
| 75 | + |
| 76 | +or, if the user dragged between items while the `canReorder` config option is set to true: |
| 77 | + |
| 78 | +- the `item` that should become the new parent |
| 79 | +- the `childIndex`, describing the index within the `item` where the user dropped |
| 80 | +- the `insertionIndex`, describing the index within the `item` where the items should be placed (see details below) |
| 81 | +- `dragLineIndex` and `dragLineLevel` describing the position of the drag line that is rendered while dragging |
| 82 | + |
| 83 | +:::info Difference between `childIndex` and `insertionIndex` |
| 84 | + |
| 85 | +`childIndex` describes the visual location inside the item that is being dropped into, where the user is dragging |
| 86 | +the items to. `insertionIndex` on the other hand also respects the special case that the user might drag items within |
| 87 | +the same folder from further up in a folder, to further down in the same folder. |
| 88 | + |
| 89 | +If items are dragged from a different location into a new folder, `childIndex` and `insertionIndex` will be the same. |
| 90 | +However, if the two top-most items in a folder are dragged two items down, `childIndex` will be 4, but `insertionIndex` |
| 91 | +will be 2, since removing those two items will shift the items up by two. |
| 92 | + |
| 93 | +::: |
| 94 | + |
| 95 | +## Rendering a Tree with Drag and Drop |
| 96 | + |
| 97 | +The only difference between rendering a normal HT Tree, and rendering one with Drag and Drop capabilities, is that you |
| 98 | +need to render a drag line that shows the user where the items will be dropped. You just need to render an additional |
| 99 | +div with the style `tree.getDragLineStyle()`, and style it however you want. The computed stylings will automatically |
| 100 | +add `display: none` if the user is not currently dragging items, and will compute relative positioning based on the |
| 101 | +item rendered with `tree.getContainerProps()`. |
| 102 | + |
| 103 | +Below are examples of how to render a tree with Drag and Drop capabilities, as well as the exemplary CSS stylings |
| 104 | +used in the demo. |
| 105 | + |
35 | 106 | ```jsx |
36 | 107 | <div {...tree.getContainerProps()} className="tree"> |
37 | 108 | {tree.getItems().map((item) => ( |
38 | | - <button |
39 | | - {...item.getProps()} |
40 | | - key={item.getId()} |
41 | | - style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }} |
42 | | - > |
43 | | - <div |
44 | | - className={`folder ${item.isFolder() && "folder"}`} |
45 | | - > |
46 | | - {item.getItemName()} |
47 | | - </div> |
48 | | - </button> |
| 109 | + <MyTreeItem key={item.id} item={item} /> |
49 | 110 | ))} |
50 | 111 | <div style={tree.getDragLineStyle()} className="dragline" /> |
51 | 112 | </div> |
52 | 113 | ``` |
53 | 114 |
|
54 | | -<DemoBox tags={["feature/dnd"]} stories={["react-drag-and-drop-basic--basic"]} /> |
| 115 | +```css |
| 116 | +.dragline { |
| 117 | + position: absolute; |
| 118 | + height: 2px; |
| 119 | + width: unset; |
| 120 | + margin-top: -1px; |
| 121 | + background-color: #00aff4; |
| 122 | +} |
55 | 123 |
|
56 | | -## Updated caches in async data loaders |
| 124 | +/* Render the circle at the left of the dragline */ |
| 125 | +.dragline::before { |
| 126 | + content: ""; |
| 127 | + position: absolute; |
| 128 | + left: 0; |
| 129 | + top: -3px; |
| 130 | + height: 4px; |
| 131 | + width: 4px; |
| 132 | + background: #fff; |
| 133 | + border: 2px solid #00aff4; |
| 134 | + border-radius: 99px; |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | + |
| 139 | +## Mutating your data after the Drop |
| 140 | + |
| 141 | +When the drop event is registered with `onDrop`, you will likely want to mutate your data source to reflect |
| 142 | +the new order of items after the drag. Headless Tree provides three utility methods that make this very easy: |
| 143 | + |
| 144 | +- [`removeItemsFromParents(movedItems, onChangeChildren)`](/api/core/function/removeItemsFromParents): Calls the provided |
| 145 | + `onChangeChildren` handler to remove the items defined in the first argument from their parents. |
| 146 | +- [`insertItemsAtTarget(itemIds, target, onChangeChildren)`](/api/core/function/insertItemsAtTarget): Calls the provided |
| 147 | + `onChangeChildren` handler to insert the items defined in the first argument at the target defined in the second argument. |
| 148 | +- [`createOnDropHandler(onChangeChildren)`](/api/core/function/createOnDropHandler): Combines the two methods above to |
| 149 | + create a drop handler that removes the items from their parents and inserts them at the target. This can directly be passed to the `onDrop` config option. |
| 150 | + |
| 151 | +`itemIds` and `movedItems` are arrays of item instances, and thus the same type as what the `onDrop` config option provides. |
| 152 | +Similarly, `target` is the same type as the drop target that is provided in the `onDrop` event. |
| 153 | + |
| 154 | +In most situations, it is sufficient to call `createOnDropHandler` with a function that mutates your data source, and |
| 155 | +use that to handle drop events of items within a single tree. The other methods are useful for interactions between |
| 156 | +several trees (imagine items being removed from one tree and then inserted in another), handling cases where foreign |
| 157 | +data is dragged into a tree to create a new tree item or tree items being dragged out of the tree to external drop targets, |
| 158 | +or handling other more complicated use cases. |
| 159 | +<!-- TODO add links to other guides for those cases --> |
57 | 160 |
|
58 | 161 | ```jsx typescript |
| 162 | +import { createOnDropHandler } from "@headless-tree/core"; |
| 163 | + |
59 | 164 | const tree = useTree<Item>({ |
60 | 165 | indent: 20, |
61 | 166 | canReorder: true, |
62 | | - onDrop: createOnDropHandler((item, newChildren) => {}), |
| 167 | + onDrop: createOnDropHandler((item, newChildren) => { |
| 168 | + myData[item.getId()].children = newChildren; |
| 169 | + }), |
63 | 170 | features: [ |
64 | | - asyncDataLoaderFeature, |
| 171 | + syncDataLoaderFeature, |
65 | 172 | selectionFeature, |
66 | | - hotkeysCoreFeature, |
67 | 173 | dragAndDropFeature, |
68 | 174 | ], |
69 | 175 | }); |
70 | 176 | ``` |
71 | 177 |
|
72 | | -## Reading DragAndDrop state |
| 178 | +## Updated caches in async data loaders |
| 179 | + |
| 180 | +:::info |
| 181 | + |
| 182 | +You can ignore this when using `removeItemsFromParents`, `removeItemsFromParents` or `createOnDropHandler` to handle |
| 183 | +the drop event, as those handlers do this for you. |
| 184 | + |
| 185 | +::: |
| 186 | + |
| 187 | +When you handle the drop event yourself while using the async data loader, note that the async data loader caches |
| 188 | +tree data and does not automatically update the cache when you mutate the data source. |
| 189 | + |
| 190 | +When you update the childrens assigned to an item as response to a drop event, you can update the cache with |
| 191 | +[`item.updateCachedChildrenIds(children: string[])`](/api/core/interface/AsyncDataLoaderFeatureItemInstance#updateCachedChildrenIds): |
| 192 | + |
| 193 | +```typescript |
| 194 | +// Update your data source |
| 195 | +myData[parent.getId()].children = newChildren; |
| 196 | + |
| 197 | +// Update cache of async data loader |
| 198 | +parent.updateCachedChildrenIds(newChildren); |
| 199 | + |
| 200 | +// Trigger the recomputation of the internal tree structure |
| 201 | +tree.rebuildTree(); |
| 202 | +``` |
0 commit comments