import {DemoBox} from "../../src/components/demo/demo-box";

# Dragging in or out of tree

With the `createOnDropHandler` method, it is fairly easy to set up drag-and-drop to
allow users to drag items within a tree. With some additional effort, it is also possible
to implement the ability to drag items out of the tree, optionally removing them from the
tree on the drop event, or dragging foreign objects from outside into the tree, inserting
them as newly created tree items.

This can also be combined to make the tree interact with other instances of Headless Tree
on the same page or different locations in the application.

Headless Tree introduces a concept called *foreign drag objects*, which are is data that can
be transferred using the [DataTransfer Browser API](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer)
to attach information to drag events. Dragging data from outside the tree inside works by
attaching your data to the drag event, and letting Headless Tree know how to interpret whether
it can handle this data as a tree item, and how.

Similarly, dragging data out works by letting Headless Tree know how to attach selected items
into the drag event, and then handling the drag event elsewhere in your app.


## Dragging tree items out of tree

The gist of allowing users to drag tree items out of the tree, is to just implement the
`createForeignDragObject`
method in the tree config. When users will start dragging items, this will be called with the items
to attach arbitrary information to the [dataTransfer object](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer)
of the drag event, that you can then handle elsewhere.

Optionally, you can also implement the `onCompleteForeignDrop`
to handle the event that the user has dropped the item outside of the tree. If a drop target has accepted the
dropped data, this method will be called, making it easy to handle the finalization of the drag within Headless
Tree, e.g. by removing the items that where dragged out of the tree. This can easily be achieved
using the `removeItemsFromParents(movedItems, onChangeChildren)` method introduced in the previous guide.

```ts jsx
import type { Meta } from "@storybook/react";
import React, { useState } from "react";
import {
  createOnDropHandler,
  dragAndDropFeature,
  hotkeysCoreFeature,
  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/Drag Outside",
  tags: ["feature/dnd"],
} satisfies Meta;

export default meta;

const { data, syncDataLoader } = createDemoData();

// story-start
export const DragOutside = () => {
  const [state, setState] = useState({});
  const tree = useTree<DemoItem>({
    state,
    setState,
    rootItemId: "root",
    getItemName: (item) => item.getItemData().name,
    isItemFolder: (item) => !!item.getItemData().children,
    canReorder: true,
    onDrop: createOnDropHandler((item, newChildren) => {
      data[item.getId()].children = newChildren;
    }),
    createForeignDragObject: (items) => ({
      format: "text/plain",
      data: `custom foreign drag object: ${items
        .map((item) => item.getId())
        .join(",")}`,
    }),
    indent: 20,
    dataLoader: syncDataLoader,
    onCompleteForeignDrop: (items) => {
      removeItemsFromParents(items, (item, newChildren) => {
        item.getItemData().children = newChildren;
      });
    },
    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>
      <div
        style={{ marginTop: "40px" }}
        onDrop={(e) =>
          alert(
            `Drop, dataTransfer payload is ${JSON.stringify(
              e.dataTransfer.getData("text/plain"),
            )}`,
          )
        }
        onDragOver={(e) => e.preventDefault()}
      >
        Drop items here!
      </div>
    </>
  );
};
```

To support this use case with keyboard navigation, you can use the [Keyboard Drag and Drop feature](https://headless-tree.lukasbach.com/llm/features/kdnd.md)
and follow the [guide on dragging items out of trees with keyboard controls](https://headless-tree.lukasbach.com/llm/features/kdnd.md).

## Dragging foreign objects inside of tree

Allowing users to drag data into the tree from outside consists of two steps:

- Implement the `canDropForeignDragObject`
  method in the tree config, to determine whether the tree can handle the data that is being dragged. It will be called
  on every viable drop target location with the `dataTransfer` object of the drag event and the target drop location,
  and should return a boolean indicating whether the data can be dropped there.
- Implement the `onDropForeignDragObject`
  method in the tree config, to handle the drop event when the user has dropped the data. This method will be called
  with the `dataTransfer` object of the drag event and the target drop location.

The `onDropForeignDragObject` method can be used to create new tree items from the data that was dragged in.
Again, the previously introduced `insertItemsAtTarget` method can be used
to insert items at the target location after you created them based on the data from the `dataTransfer` object.

In the sample below, the `onDropForeignDragObject` method is implemented to create a single new tree item, and then
insert it at the target location with the `insertItemsAtTarget` method.

```ts jsx
import type { Meta } from "@storybook/react";
import React, { useState } from "react";
import {
  createOnDropHandler,
  dragAndDropFeature,
  hotkeysCoreFeature,
  insertItemsAtTarget,
  keyboardDragAndDropFeature,
  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/Drag Inside",
  tags: ["feature/dnd"],
} satisfies Meta;

export default meta;

const { syncDataLoader, data } = createDemoData();
let newItemId = 0;

// story-start
export const DragInside = () => {
  const [state, setState] = useState({});
  const tree = useTree<DemoItem>({
    state,
    setState,
    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 = `new-${newItemId++}`;
      data[newId] = {
        name: dataTransfer.getData("text/plain"),
      };
      insertItemsAtTarget([newId], target, (item, newChildrenIds) => {
        data[item.getId()].children = newChildrenIds;
      });
      alert(
        `Dropped external data with payload "${JSON.stringify(
          dataTransfer.getData("text/plain"),
        )}" on ${JSON.stringify(target)}`,
      );
    },
    canDropForeignDragObject: (_, target) =>
      target.item.isFolder() && target.item.getId() !== "drinks",
    indent: 20,
    dataLoader: syncDataLoader,
    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>
      <div
        style={{ marginTop: "10px" }}
        draggable
        onDragStart={(e) => {
          e.dataTransfer.setData("text/plain", "hello world");
        }}
      >
        Drag me into the tree! (but not in Drinks)
      </div>
    </>
  );
};
```

To support this use case with keyboard navigation, you can use the [Keyboard Drag and Drop feature](https://headless-tree.lukasbach.com/llm/features/kdnd.md)
and follow the [guide on starting keyboard-controlled drags from outside with custom drag data](https://headless-tree.lukasbach.com/llm/features/kdnd.md).

## Interactions between multiple instances of Headless Tree

The concepts introduced above can be combined to allow interactions between multiple instances of Headless Tree.
The following sample demonstrates how to set up two trees that can interact with each other, allowing users to drag
items from one tree to the other.

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

const meta = {
  title: "React/Guides/Multiple Trees",
  tags: ["feature/dnd", "homepage"],
} satisfies Meta;

export default meta;

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

const data: Record<string, Item> = {
  root1: { name: "Root", children: ["lunch", "dessert"] },
  root2: { name: "Root", children: ["solar", "centauri"] },
  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" },
  solar: {
    name: "Solar System",
    children: ["jupiter", "earth", "mars", "venus"],
  },
  jupiter: { name: "Jupiter", children: ["io", "europa", "ganymede"] },
  io: { name: "Io" },
  europa: { name: "Europa" },
  ganymede: { name: "Ganymede" },
  earth: { name: "Earth", children: ["moon"] },
  moon: { name: "Moon" },
  mars: { name: "Mars" },
  venus: { name: "Venus" },
  centauri: {
    name: "Alpha Centauri",
    children: ["rigilkent", "toliman", "proxima"],
  },
  rigilkent: { name: "Rigel Kentaurus" },
  toliman: { name: "Toliman" },
  proxima: { name: "Proxima Centauri" },
};

const Tree = (props: { root: string; prefix: string }) => {
  const tree = useTree<Item>({
    rootItemId: props.root,
    dataLoader: {
      getItem: (id) => data[id],
      getChildren: (id) => data[id]?.children ?? [],
    },
    getItemName: (item) => item.getItemData().name,
    isItemFolder: (item) => item.getItemData().children !== undefined,
    canReorder: true,
    indent: 20,

    // onDrop is only called when moving items WITHIN one tree.
    // This handles the entire move operation.
    // Normally you can use `createOnDropHandler` for that, the following just
    // demonstrates how to do with the individual handlers.
    onDrop: async (items, target) => {
      const itemIds = items.map((item) => item.getId());
      await removeItemsFromParents(items, (item, newChildren) => {
        item.getItemData().children = newChildren;
      });
      await insertItemsAtTarget(itemIds, target, (item, newChildren) => {
        item.getItemData().children = newChildren;
      });
    },

    // When moving items out of the tree, this is used to serialize the
    // dragged items as foreign drag object
    createForeignDragObject: (items) => ({
      format: "text/plain",
      data: JSON.stringify(items.map((item) => item.getId())),
    }),

    // This is called in the target tree to verify if foreign drag objects
    // are permitted to be dropped there.
    canDropForeignDragObject: () => true,

    // This is called in the target tree when the foreign drag object is
    // dropped. This handler inserts the moved items
    onDropForeignDragObject: (dataTransfer, target) => {
      const newChildrenIds = JSON.parse(dataTransfer.getData("text/plain"));
      insertItemsAtTarget(newChildrenIds, target, (item, newChildren) => {
        item.getItemData().children = newChildren;
      });
    },

    // This is called in the source tree when the foreign drag is completed.
    // This handler removes the moved items from the source tree.
    onCompleteForeignDrop: (items) => {
      removeItemsFromParents(items, (item, newChildren) => {
        item.getItemData().children = newChildren;
      });
    },
    features: [
      syncDataLoaderFeature,
      selectionFeature,
      hotkeysCoreFeature,
      dragAndDropFeature,
      keyboardDragAndDropFeature,
    ],
  });

  return (
    <div {...tree.getContainerProps()} className="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>
  );
};

export const MultipleTrees = () => {
  return (
    <>
      <p className="description">
        In this sample, both trees share a datasource and have different items
        defined as roots, which makes dragging easy to implement since moving
        items just means updating children IDs. The Story "Advanced Multiple
        Trees" shows how to implement multiple trees with different datasources.
      </p>
      <div style={{ display: "flex" }}>
        <div style={{ width: "200px", marginRight: "20px" }}>
          <Tree root="root1" prefix="a" />
        </div>
        <div style={{ width: "200px", marginRight: "20px" }}>
          <Tree root="root2" prefix="b" />
        </div>
      </div>
    </>
  );
};
```

The sample above incorporates a simple use case where both trees have the same data source, meaning that
it is sufficient to update child IDs of the parents of the moved items to reflect the data after the
mutation. In some use cases, several trees make use of distinct data sources, so you will need to make
sure to also add the target data source to create the newly moved items in there:

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

const meta = {
  title: "React/Guides/Advanced Multiple Trees",
  tags: ["feature/dnd", "homepage"],
} satisfies Meta;

export default meta;

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

const data1: 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" },
};

const data2: Record<string, Item> = {
  root: { name: "Root", children: ["solar", "centauri"] },
  solar: {
    name: "Solar System",
    children: ["jupiter", "earth", "mars", "venus"],
  },
  jupiter: { name: "Jupiter", children: ["io", "europa", "ganymede"] },
  io: { name: "Io" },
  europa: { name: "Europa" },
  ganymede: { name: "Ganymede" },
  earth: { name: "Earth", children: ["moon"] },
  moon: { name: "Moon" },
  mars: { name: "Mars" },
  venus: { name: "Venus" },
  centauri: {
    name: "Alpha Centauri",
    children: ["rigilkent", "toliman", "proxima"],
  },
  rigilkent: { name: "Rigel Kentaurus" },
  toliman: { name: "Toliman" },
  proxima: { name: "Proxima Centauri" },
};

/* Return all IDs of items within the given items, even deeply nested ones */
const resolveNestedItems = (
  tree: TreeInstance<Item>,
  items: string[],
): string[] => {
  if (items.length === 0) return [];
  const immediateChildren = items
    .map(tree.getConfig().dataLoader.getChildren)
    .flat() as string[];
  const nestedChildren = resolveNestedItems(tree, immediateChildren);
  return [...items, ...nestedChildren];
};

const Tree = (props: { data: Record<string, Item>; prefix: string }) => {
  const [data, setData] = useState(props.data);
  const tree: TreeInstance<Item> = useTree<Item>({
    rootItemId: "root",
    dataLoader: {
      getItem: (id) => data[id],
      getChildren: (id) => data[id]?.children ?? [],
    },
    getItemName: (item) => item.getItemData().name,
    isItemFolder: (item) => item.getItemData().children !== undefined,
    canReorder: true,
    indent: 20,

    // onDrop is only called when moving items WITHIN one tree.
    // This handles the entire move operation.
    // Normally you can use `createOnDropHandler` for that, the following just
    // demonstrates how to do with the individual handlers.
    onDrop: async (items, target) => {
      const itemIds = items.map((item) => item.getId());
      await removeItemsFromParents(items, (item, newChildren) => {
        item.getItemData().children = newChildren;
      });
      await insertItemsAtTarget(itemIds, target, (item, newChildren) => {
        item.getItemData().children = newChildren;
      });
    },

    // When moving items out of the tree, this is used to serialize the
    // dragged items as foreign drag object.
    // Since the trees have distinct data sources, we provide the necessary
    // information to move the items to the new data source as well.
    createForeignDragObject: (items) => {
      const nestedItems = resolveNestedItems(
        tree,
        items.map((item) => item.getId()),
      );
      return {
        format: "text/plain",
        data: JSON.stringify({
          items: items.map((item) => item.getId()),
          nestedItems: Object.fromEntries(
            nestedItems.map((id) => [id, props.data[id]]),
          ),
        }),
      };
    },

    // This is called in the target tree to verify if foreign drag objects
    // are permitted to be dropped there.
    canDropForeignDragObject: () => true,

    // This is called in the target tree when the foreign drag object is
    // dropped. This handler inserts the moved items, and also injects
    // the item data into the new data source.
    onDropForeignDragObject: (dataTransfer, target) => {
      const { items, nestedItems } = JSON.parse(
        dataTransfer.getData("text/plain"),
      );
      setData({ ...data, ...nestedItems });
      insertItemsAtTarget(items, target, (item, newChildren) => {
        item.getItemData().children = newChildren;
      });
    },

    // This is called in the source tree when the foreign drag is completed.
    // This handler removes the moved items from the source tree.
    onCompleteForeignDrop: (items) => {
      removeItemsFromParents(items, (item, newChildren) => {
        item.getItemData().children = newChildren;
      });
    },
    features: [
      syncDataLoaderFeature,
      selectionFeature,
      hotkeysCoreFeature,
      dragAndDropFeature,
      keyboardDragAndDropFeature,
    ],
  });

  return (
    <div {...tree.getContainerProps()} className="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>
  );
};

export const AdvancedMultipleTrees = () => {
  return (
    <>
      <p className="description">
        This is a more complicated use case than the normal "Multiple Trees"
        story. In this case, several trees each have their own dedicated data
        source. When items are dragged from one tree to the other, all data of
        every nested item that is part of the selection is sent in the drag
        event, and attached to the new data source in the target tree.
      </p>
      <div style={{ display: "flex" }}>
        <div style={{ width: "200px", marginRight: "20px" }}>
          <Tree data={data1} prefix="a" />
        </div>
        <div style={{ width: "200px", marginRight: "20px" }}>
          <Tree data={data2} prefix="b" />
        </div>
      </div>
    </>
  );
};
```
