import { DemoBox } from "../../src/components/demo/demo-box";
import {FeaturePageHeader} from "../../src/components/docs-page/feature-page-header";

<FeaturePageHeader
    title="Drag and Drop"
    subtitle="Drag-and-drop Capabilities for tree items and external drag objects"
    feature="drag-and-drop"
/>

# Overview

<!-- TODO show DND kitchensink as default sample -->
```ts jsx
import type { Meta } from "@storybook/react";
import React, { Fragment } from "react";
import {
  createOnDropHandler,
  dragAndDropFeature,
  hotkeysCoreFeature,
  insertItemsAtTarget,
  keyboardDragAndDropFeature,
  removeItemsFromParents,
  selectionFeature,
  syncDataLoaderFeature,
} from "@headless-tree/core";
import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
import cn from "classnames";
import { DemoItem, createDemoData } from "../utils/data";

const meta = {
  title: "React/Drag and Drop/Comprehensive Sample",
  tags: ["feature/dnd", "homepage"],
} satisfies Meta;

export default meta;

const { syncDataLoader, data } = createDemoData();
let newItemId = 0;
const insertNewItem = (dataTransfer: DataTransfer) => {
  const newId = `new-${newItemId++}`;
  data[newId] = {
    name: dataTransfer.getData("text/plain"),
  };
  return newId;
};

// story-start
export const ComprehensiveSample = () => {
  const tree = useTree<DemoItem>({
    initialState: {
      expandedItems: ["fruit"],
      selectedItems: ["banana", "orange"],
    },
    rootItemId: "root",
    getItemName: (item) => item.getItemData().name,
    isItemFolder: (item) => !!item.getItemData().children,
    canReorder: true,
    onDrop: createOnDropHandler((item, newChildren) => {
      data[item.getId()].children = newChildren;
    }),
    onDropForeignDragObject: (dataTransfer, target) => {
      const newId = insertNewItem(dataTransfer);
      insertItemsAtTarget([newId], target, (item, newChildrenIds) => {
        data[item.getId()].children = newChildrenIds;
      });
    },
    onCompleteForeignDrop: (items) =>
      removeItemsFromParents(items, (item, newChildren) => {
        item.getItemData().children = newChildren;
      }),
    createForeignDragObject: (items) => ({
      format: "text/plain",
      data: items.map((item) => item.getId()).join(","),
    }),
    canDropForeignDragObject: (_, target) => target.item.isFolder(),
    indent: 20,
    dataLoader: syncDataLoader,
    features: [
      syncDataLoaderFeature,
      selectionFeature,
      hotkeysCoreFeature,
      dragAndDropFeature,
      keyboardDragAndDropFeature,
    ],
  });

  return (
    <>
      <div {...tree.getContainerProps()} className="tree">
        <AssistiveTreeDescription tree={tree} />
        {tree.getItems().map((item) => (
          <button
            key={item.getId()}
            {...item.getProps()}
            style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
          >
            <div
              className={cn("treeitem", {
                focused: item.isFocused(),
                expanded: item.isExpanded(),
                selected: item.isSelected(),
                folder: item.isFolder(),
                drop: item.isDragTarget(),
              })}
            >
              {item.getItemName()}
            </div>
          </button>
        ))}
        <div style={tree.getDragLineStyle()} className="dragline" />
      </div>

      <div className="actionbar">
        <div
          className="foreign-dragsource"
          draggable
          onDragStart={(e) => {
            e.dataTransfer.setData("text/plain", "hello world");
          }}
        >
          Drag me into the tree!
        </div>
        <div
          className="foreign-dropzone"
          onDrop={(e) => {
            alert(JSON.stringify(e.dataTransfer.getData("text/plain")));
            console.log(e.dataTransfer.getData("text/plain"));
          }}
          onDragOver={(e) => e.preventDefault()}
        >
          Drop items here!
        </div>
      </div>
    </>
  );
};
```

The Drag-And-Drop Feature provides drag-and-drop capabilities. It allows to drag a single tree item (or many of the
[selection feature](https://headless-tree.lukasbach.com/llm/features/selection.md) is included in the tree config) and drop it somewhere else in the tree.
The feature also allows you to create interactions between the tree and external drag objects, allowing you to drag
tree items out of the tree, or foreign data objects from outside the tree inside. As extension of that, this can
also be used to implement drag behavior between several trees.

This page explains the general idea of how Dnd works in Headless Tree, and how to get started with dragging items
within a tree. The guides following after will go into more depth about

- How to [drag external data into your tree](https://headless-tree.lukasbach.com/llm/dnd/foreign-dnd.md),
  or [tree items out to external drop targets](https://headless-tree.lukasbach.com/llm/dnd/foreign-dnd.md)
- How to [handle drag events between multiple trees](https://headless-tree.lukasbach.com/llm/dnd/foreign-dnd.md) on the same page
- Customizing [which items can be dragged](https://headless-tree.lukasbach.com/llm/dnd/customizability.md)
  or [dropped on](https://headless-tree.lukasbach.com/llm/dnd/customizability.md)
- More details on the DnD behavior of Headless Tree

## Configuring Drag and Drop

The gist of configuring Drag and Drop in Headless Tree, is to

- add the `dragAndDropFeature` to the tree config,
- define the indent of your tree items in the config variable `indent`, and
- handle the `onDrop` event in the tree config.

Note that the `indent` property is not required in the core tree configuration, since you can just use a custom
indentation when rendering your items. For Drag and Drop it is required however to determine the drop position
when the user is trying to [reparent an item](https://headless-tree.lukasbach.com/llm/dnd/behavior.md).

The Dnd Feature also exposes a Dnd State that you can [handle yourself if you want to](https://headless-tree.lukasbach.com/llm/guides/state.md).

```jsx typescript
import {
  syncDataLoaderFeature,
  dragAndDropFeature,
  selectionFeature,
} from "@headless-tree/core";

const tree = useTree<Item>({
  indent: 20,
  canReorder: true,
  onDrop: (items, target) => {
      // handle drop
  },
  features: [
    syncDataLoaderFeature,
    selectionFeature,
    dragAndDropFeature,
  ],
});
```

:::tip

Add a minimum height to your tree container (the component receiving `tree.getProps()`) to make sure
that users can drop items there even if the tree is empty.

:::

## The Drop Event

When the user drops tree items onto a target within the tree, the `onDrop` event is called
with the list of items that were dragged, and a drop target. This drop target is an object that contains
either

- 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

or, if the user dragged between items while the `canReorder` config option is set to true:

- the `item` that should become the new parent
- the `childIndex`, describing the index within the `item` where the user dropped
- the `insertionIndex`, describing the index within the `item` where the items should be placed (see details below)
- `dragLineIndex` and `dragLineLevel` describing the position of the drag line that is rendered while dragging

You can use `isOrderedDragTarget(dragTarget)` to check which type a drag target is.

:::info Difference between `childIndex` and `insertionIndex`

`childIndex` describes the visual location inside the item that is being dropped into, where the user is dragging
the items to. `insertionIndex` on the other hand also respects the special case that the user might drag items within
the same folder from further up in a folder, to further down in the same folder.

If items are dragged from a different location into a new folder, `childIndex` and `insertionIndex` will be the same.
However, if the two top-most items in a folder are dragged two items down, `childIndex` will be 4, but `insertionIndex`
will be 2, since removing those two items will shift the items up by two.

:::

## Rendering a Tree with Drag and Drop

The only difference between rendering a normal HT Tree, and rendering one with Drag and Drop capabilities, is that you
need to render a drag line that shows the user where the items will be dropped. You just need to render an additional
div with the style `tree.getDragLineStyle()`, and style it however you want. The computed stylings will automatically
add `display: none` if the user is not currently dragging items, and will compute relative positioning based on the
item rendered with `tree.getContainerProps()`.

Below are examples of how to render a tree with Drag and Drop capabilities, as well as the exemplary CSS stylings
used in the demo.

```jsx
<div {...tree.getContainerProps()} className="tree">
  {tree.getItems().map((item) => (
    <MyTreeItem key={item.id} item={item} />
  ))}
  <div style={tree.getDragLineStyle()} className="dragline" />
</div>
```

```css
.dragline {
    height: 2px;
    margin-top: -1px;
    background-color: #00aff4;
}

/* Render the circle at the left of the dragline */
.dragline::before {
    content: "";
    position: absolute;
    left: 0;
    top: -3px;
    height: 4px;
    width: 4px;
    background: #fff;
    border: 2px solid #00aff4;
    border-radius: 99px;
}
```


## Mutating your data after the Drop

When the drop event is registered with `onDrop`, you will likely want to mutate your data source to reflect
the new order of items after the drag. Headless Tree provides three utility methods that make this very easy:

- `removeItemsFromParents(movedItems, onChangeChildren)`: Calls the provided
  `onChangeChildren` handler to remove the items defined in the first argument from their parents.
- `insertItemsAtTarget(itemIds, target, onChangeChildren)`: Calls the provided
  `onChangeChildren` handler to insert the items defined in the first argument at the target defined in the second argument.
- `createOnDropHandler(onChangeChildren)`: Combines the two methods above to
  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.

`itemIds` and `movedItems` are arrays of item instances, and thus the same type as what the `onDrop` config option provides.
Similarly, `target` is the same type as the drop target that is provided in the `onDrop` event.

In most situations, it is sufficient to call `createOnDropHandler` with a function that mutates your data source, and
use that to handle drop events of items within a single tree. The other methods are useful for interactions between
several trees (imagine items being removed from one tree and then inserted in another), handling cases where foreign
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,
or handling other more complicated use cases.
<!-- TODO add links to other guides for those cases -->

```jsx typescript
import { createOnDropHandler } from "@headless-tree/core";

const tree = useTree<Item>({
  indent: 20,
  canReorder: true,
  onDrop: createOnDropHandler((item, newChildren) => {
    myData[item.getId()].children = newChildren;
  }),
  features: [
    syncDataLoaderFeature,
    selectionFeature,
    dragAndDropFeature,
  ],
});
```

```ts jsx
import type { Meta } from "@storybook/react";
import React from "react";
import {
  createOnDropHandler,
  dragAndDropFeature,
  hotkeysCoreFeature,
  keyboardDragAndDropFeature,
  selectionFeature,
  syncDataLoaderFeature,
} from "@headless-tree/core";
import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
import cn from "classnames";

const meta = {
  title: "React/Drag and Drop/On Drop Handler",
  tags: ["feature/dnd"],
} satisfies Meta;

export default meta;

// story-start
type Item = {
  name: string;
  children?: string[];
};

const data: Record<string, Item> = {
  root: { name: "Root", children: ["lunch", "dessert"] },
  lunch: { name: "Lunch", children: ["sandwich", "salad", "soup"] },
  sandwich: { name: "Sandwich" },
  salad: { name: "Salad" },
  soup: { name: "Soup", children: ["tomato", "chicken"] },
  tomato: { name: "Tomato" },
  chicken: { name: "Chicken" },
  dessert: { name: "Dessert", children: ["icecream", "cake"] },
  icecream: { name: "Icecream" },
  cake: { name: "Cake" },
};

export const OnDropHandler = () => {
  const tree = useTree<Item>({
    initialState: {
      expandedItems: ["lunch", "dessert", "soup"],
      selectedItems: ["tomato", "chicken"],
    },
    rootItemId: "root",
    getItemName: (item) => item.getItemData().name,
    isItemFolder: (item) => !!item.getItemData().children,
    canReorder: true,
    onDrop: createOnDropHandler((item, newChildren) => {
      item.getItemData().children = newChildren;
    }),
    indent: 20,
    dataLoader: {
      getItem: (id) => data[id],
      getChildren: (id) => data[id]?.children ?? [],
    },
    features: [
      syncDataLoaderFeature,
      selectionFeature,
      hotkeysCoreFeature,
      dragAndDropFeature,
      keyboardDragAndDropFeature,
    ],
  });

  return (
    <div {...tree.getContainerProps()} className="tree">
      <AssistiveTreeDescription tree={tree} />
      {tree.getItems().map((item) => (
        <button
          {...item.getProps()}
          key={item.getId()}
          style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
        >
          <div
            className={cn("treeitem", {
              focused: item.isFocused(),
              expanded: item.isExpanded(),
              selected: item.isSelected(),
              folder: item.isFolder(),
              drop: item.isDragTarget(),
            })}
          >
            {item.getItemName()}
          </div>
        </button>
      ))}
      <div style={tree.getDragLineStyle()} className="dragline" />
    </div>
  );
};
```

:::info

In the Demo above, switch to the sample "Multiple Trees" in the selection box to see a sample
of how to use the `removeItemsFromParents` and `insertItemsAtTarget` methods instead of the
`createOnDropHandler` method.

:::

## Updated caches in async data loaders

:::info

You can ignore this when using `removeItemsFromParents`, `removeItemsFromParents` or `createOnDropHandler` to handle
the drop event, as those handlers do this for you.

:::

When you handle the drop event yourself while using the async data loader, note that the async data loader caches
tree data and does not automatically update the cache when you mutate the data source.

When you update the childrens assigned to an item as response to a drop event, you can update the cache with
`item.updateCachedChildrenIds(children: string[])`:

```typescript
// Update your data source
myData[parent.getId()].children = newChildren;

// Update cache of async data loader
parent.updateCachedChildrenIds(newChildren);

// Trigger the recomputation of the internal tree structure
tree.rebuildTree();
```

## Allowing users to drag items via keyboard

If you have implemented features that can only be triggered via drag-interactions on your tree, this can
be a limitation for users unable to use mouse interactions. Headless Tree supports full keyboard interactions
by using the [Keyboard Drag-and-Drop Feature](https://headless-tree.lukasbach.com/llm/features/kdnd.md), which adds all abilities that are usually
achieved with mouse-based drag events to keyboard hotkeys and descriptive texts visible only to screen readers.

The effort to enable this feature is very low, find out more about this on the
[Guide to Keyboard Drag-and-Drop](https://headless-tree.lukasbach.com/llm/features/kdnd.md).
