|
| 1 | +import type { Meta } from "@storybook/react"; |
| 2 | +import React from "react"; |
| 3 | +import { |
| 4 | + dragAndDropFeature, |
| 5 | + hotkeysCoreFeature, |
| 6 | + insertItemsAtTarget, |
| 7 | + keyboardDragAndDropFeature, |
| 8 | + removeItemsFromParents, |
| 9 | + selectionFeature, |
| 10 | + syncDataLoaderFeature, |
| 11 | +} from "@headless-tree/core"; |
| 12 | +import { useTree } from "@headless-tree/react"; |
| 13 | +import cn from "classnames"; |
| 14 | + |
| 15 | +const meta = { |
| 16 | + title: "React/Guides/Multiple Trees Drop Restriction", |
| 17 | + tags: ["feature/dnd"], |
| 18 | +} satisfies Meta; |
| 19 | + |
| 20 | +export default meta; |
| 21 | + |
| 22 | +// story-start |
| 23 | +type Item = { |
| 24 | + name: string; |
| 25 | + children?: string[]; |
| 26 | +}; |
| 27 | + |
| 28 | +const data: Record<string, Item> = { |
| 29 | + root1: { name: "Root", children: ["lunch", "dessert"] }, |
| 30 | + root2: { |
| 31 | + name: "Root", |
| 32 | + children: ["solar", "centauri", "cannotdrop1", "cannotdrop2"], |
| 33 | + }, |
| 34 | + lunch: { name: "Lunch", children: ["sandwich", "salad", "soup"] }, |
| 35 | + sandwich: { name: "Sandwich" }, |
| 36 | + salad: { name: "Salad" }, |
| 37 | + soup: { name: "Soup", children: ["tomato", "chicken"] }, |
| 38 | + tomato: { name: "Tomato" }, |
| 39 | + chicken: { name: "Chicken" }, |
| 40 | + dessert: { name: "Dessert", children: ["icecream", "cake"] }, |
| 41 | + icecream: { name: "Icecream" }, |
| 42 | + cake: { name: "Cake" }, |
| 43 | + solar: { |
| 44 | + name: "Solar System", |
| 45 | + children: ["jupiter", "earth", "mars", "venus"], |
| 46 | + }, |
| 47 | + jupiter: { name: "Jupiter", children: ["io", "europa", "ganymede"] }, |
| 48 | + io: { name: "Io" }, |
| 49 | + europa: { name: "Europa" }, |
| 50 | + ganymede: { name: "Ganymede" }, |
| 51 | + earth: { name: "Earth", children: ["moon"] }, |
| 52 | + moon: { name: "Moon" }, |
| 53 | + mars: { name: "Mars" }, |
| 54 | + venus: { name: "Venus" }, |
| 55 | + centauri: { |
| 56 | + name: "Alpha Centauri", |
| 57 | + children: ["rigilkent", "toliman", "proxima"], |
| 58 | + }, |
| 59 | + rigilkent: { name: "Rigel Kentaurus" }, |
| 60 | + toliman: { name: "Toliman" }, |
| 61 | + proxima: { name: "Proxima Centauri" }, |
| 62 | + cannotdrop1: { name: "Cannot Drop in other Tree 1" }, |
| 63 | + cannotdrop2: { name: "Cannot Drop in other Tree 2" }, |
| 64 | +}; |
| 65 | + |
| 66 | +const Tree = (props: { root: string; prefix: string }) => { |
| 67 | + const tree = useTree<Item>({ |
| 68 | + rootItemId: props.root, |
| 69 | + dataLoader: { |
| 70 | + getItem: (id) => data[id], |
| 71 | + getChildren: (id) => data[id]?.children ?? [], |
| 72 | + }, |
| 73 | + getItemName: (item) => item.getItemData().name, |
| 74 | + isItemFolder: (item) => item.getItemData().children !== undefined, |
| 75 | + canReorder: true, |
| 76 | + indent: 20, |
| 77 | + |
| 78 | + // When moving items out of the tree, this is used to serialize the |
| 79 | + // dragged items as foreign drag object |
| 80 | + createForeignDragObject: (items) => ({ |
| 81 | + format: "text/plain", |
| 82 | + data: JSON.stringify(items.map((item) => item.getId())), |
| 83 | + effectAllowed: items.some((item) => item.getId().includes("cannotdrop")) |
| 84 | + ? "none" |
| 85 | + : "copy", |
| 86 | + }), |
| 87 | + |
| 88 | + // Called before dropping to check if foreign drag object can be dropped |
| 89 | + canDropForeignDragObject: (dataTransfer) => |
| 90 | + !dataTransfer.getData("text/plain").includes("cannotdrop"), |
| 91 | + |
| 92 | + // Called on every drag over to determine if a draggable visualization should be shown |
| 93 | + canDragForeignDragObjectOver: (dataTransfer) => |
| 94 | + dataTransfer.effectAllowed !== "none", |
| 95 | + |
| 96 | + // Remaining drop logic. This is the same as in "Multiple Trees", see this example for more |
| 97 | + // details on that logic. |
| 98 | + onDrop: async (items, target) => { |
| 99 | + const itemIds = items.map((item) => item.getId()); |
| 100 | + await removeItemsFromParents(items, (item, newChildren) => { |
| 101 | + item.getItemData().children = newChildren; |
| 102 | + }); |
| 103 | + await insertItemsAtTarget(itemIds, target, (item, newChildren) => { |
| 104 | + item.getItemData().children = newChildren; |
| 105 | + }); |
| 106 | + }, |
| 107 | + onDropForeignDragObject: (dataTransfer, target) => { |
| 108 | + const newChildrenIds = JSON.parse(dataTransfer.getData("text/plain")); |
| 109 | + insertItemsAtTarget(newChildrenIds, target, (item, newChildren) => { |
| 110 | + item.getItemData().children = newChildren; |
| 111 | + }); |
| 112 | + }, |
| 113 | + onCompleteForeignDrop: (items) => { |
| 114 | + removeItemsFromParents(items, (item, newChildren) => { |
| 115 | + item.getItemData().children = newChildren; |
| 116 | + }); |
| 117 | + }, |
| 118 | + features: [ |
| 119 | + syncDataLoaderFeature, |
| 120 | + selectionFeature, |
| 121 | + hotkeysCoreFeature, |
| 122 | + dragAndDropFeature, |
| 123 | + keyboardDragAndDropFeature, |
| 124 | + ], |
| 125 | + }); |
| 126 | + |
| 127 | + return ( |
| 128 | + <div {...tree.getContainerProps()} className="tree"> |
| 129 | + {tree.getItems().map((item) => ( |
| 130 | + <button |
| 131 | + {...item.getProps()} |
| 132 | + key={item.getId()} |
| 133 | + style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }} |
| 134 | + > |
| 135 | + <div |
| 136 | + className={cn("treeitem", { |
| 137 | + focused: item.isFocused(), |
| 138 | + expanded: item.isExpanded(), |
| 139 | + selected: item.isSelected(), |
| 140 | + folder: item.isFolder(), |
| 141 | + drop: item.isDragTarget(), |
| 142 | + })} |
| 143 | + > |
| 144 | + {item.getItemName()} |
| 145 | + </div> |
| 146 | + </button> |
| 147 | + ))} |
| 148 | + <div style={tree.getDragLineStyle()} className="dragline" /> |
| 149 | + </div> |
| 150 | + ); |
| 151 | +}; |
| 152 | + |
| 153 | +export const MultipleTreesDropRestriction = () => { |
| 154 | + return ( |
| 155 | + <> |
| 156 | + <p className="description"> |
| 157 | + In this case, "canDragForeignDragObjectOver" is used to determine if a |
| 158 | + foreign drag object can be dragged over a target. |
| 159 | + </p> |
| 160 | + <div style={{ display: "flex" }}> |
| 161 | + <div style={{ width: "200px", marginRight: "20px" }}> |
| 162 | + <Tree root="root1" prefix="a" /> |
| 163 | + </div> |
| 164 | + <div style={{ width: "200px", marginRight: "20px" }}> |
| 165 | + <Tree root="root2" prefix="b" /> |
| 166 | + </div> |
| 167 | + </div> |
| 168 | + </> |
| 169 | + ); |
| 170 | +}; |
0 commit comments