new canvas playground

This commit is contained in:
Roman Pominov committed 2022-07-21 15:47:07 +03:00
1 parent cc0a41fa74
commit 4a7d2e4e7d
3 files changed
+235 -137

No files matched your search

@@ -0,0 +1,234 @@
import { ComponentMeta } from "@storybook/react";
import React, { useState, useRef } from "react";
import { Box } from "../../box";
import { useDropTarget } from "./use-drop-target";
import { useDrag } from "./use-drag";
import { usePlacement, PlacementIndicator, type Rect } from "./placement";
import { useAutoScroll } from "./use-auto-scroll";
const ROOT_ID = "root";
type ItemData = {
id: string;
style: React.CSSProperties;
acceptsChildren: boolean;
children: ItemData[];
};
const Item = ({
data,
dragItemId,
}: {
data: ItemData;
dragItemId: string | undefined;
}) => {
return (
<Box
css={{
opacity: dragItemId === data.id ? 0.3 : 1,
minWidth: 100,
minHeight: 100,
margin: 10,
padding: 10,
background: "$mint12",
border: "1px solid $mint9",
}}
style={data.style}
data-id={data.id}
title={JSON.stringify({ id: data.id, style: data.style }, null, 2)}
>
<Items data={data.children} dragItemId={dragItemId} />
</Box>
);
};
const Items = ({
data,
dragItemId,
}: {
data: ItemData[];
dragItemId: string | undefined;
}) => {
return (
<>
{data.map((child) => (
<Item data={child} dragItemId={dragItemId} key={child.id} />
))}
</>
);
};
const findItem = (data: ItemData[], id: string): ItemData | undefined => {
for (const item of data) {
if (item.id === id) {
return item;
}
const found = findItem(item.children, id);
if (found) {
return found;
}
}
return undefined;
};
const mapItems = (
data: ItemData[],
fn: (item: ItemData) => ItemData
): ItemData[] => {
return fn({
id: ROOT_ID,
style: {},
acceptsChildren: true,
children: data.map((item) => {
return fn({
...item,
children: mapItems(item.children, fn),
});
}),
}).children;
};
export const Canvas = () => {
const [data, setData] = useState<ItemData[]>([
{
id: "0",
style: {},
children: [],
acceptsChildren: true,
},
{ id: "1", style: {}, children: [], acceptsChildren: true },
{ id: "2", style: {}, children: [], acceptsChildren: true },
{ id: "3", style: {}, children: [], acceptsChildren: true },
{ id: "4", style: {}, children: [], acceptsChildren: true },
{ id: "5", style: {}, children: [], acceptsChildren: true },
]);
const [placement, setPalcement] = useState<{
index: number;
placementRect: Rect;
}>();
const [dragItemId, setDragItemId] = useState<string>();
const dropTargetId = useRef<string>();
const dropTargetHandlers = useDropTarget({
isDropTarget(element: HTMLElement) {
const id = element.dataset.id;
return (
id != null &&
(id === ROOT_ID || findItem(data, id)?.acceptsChildren === true)
);
},
onDropTargetChange(event) {
dropTargetId.current = event.target.dataset.id;
placementHandlers.handleTargetChange(event.target);
},
});
const autoScrollHandlers = useAutoScroll();
const dragProps = useDrag({
onStart(event) {
const id = event.target.dataset.id;
if (id == null) {
event.cancel();
return;
}
setDragItemId(id);
autoScrollHandlers.setEnabled(true);
},
onMove: (poiterCoordinate) => {
dropTargetHandlers.handleMove(poiterCoordinate);
autoScrollHandlers.handleMove(poiterCoordinate);
placementHandlers.handleMove(poiterCoordinate);
},
onEnd() {
const currentDropTargetId = dropTargetId.current;
if (
placement !== undefined &&
dragItemId !== undefined &&
currentDropTargetId !== undefined
) {
setData((current) => {
const dragItem = findItem(current, dragItemId);
// shouldn't happen, for TypeScript
if (dragItem === undefined) {
return current;
}
return mapItems(current, (item) => {
let children = item.children;
const oldIndex = children.findIndex(
(child) => child.id === dragItemId
);
if (oldIndex !== -1) {
children = children.slice();
children.splice(oldIndex, 1);
}
if (item.id === currentDropTargetId) {
// placement.index does not take into account the fact that the drag item will be removed.
// we need to do this to account for it.
const newIndex =
oldIndex !== -1 && oldIndex < placement.index
? placement.index - 1
: placement.index;
children = children.slice();
children.splice(newIndex, 0, dragItem);
}
return children === item.children ? item : { ...item, children };
});
});
}
dropTargetHandlers.handleEnd();
autoScrollHandlers.setEnabled(false);
placementHandlers.handleEnd();
setDragItemId(undefined);
setPalcement(undefined);
dropTargetId.current = undefined;
},
});
const placementHandlers = usePlacement({
onPalcementChange: (event) => {
setPalcement(event);
},
});
return (
<>
<Box
css={{
background: "white",
padding: 10,
width: 500,
height: 500,
overflow: "auto",
"[data-id]": { cursor: "grab" },
}}
ref={(element) => {
dropTargetHandlers.rootRef(element);
autoScrollHandlers.targetRef(element);
}}
onScroll={() => {
dropTargetHandlers.handleScroll();
placementHandlers.handleScroll();
}}
data-id={ROOT_ID}
{...dragProps}
>
<Items data={data} dragItemId={dragItemId} />
</Box>
{placement && <PlacementIndicator rect={placement.placementRect} />}
</>
);
};
export default {} as ComponentMeta<typeof Canvas>;
@@ -1,136 +0,0 @@
import { ComponentMeta } from "@storybook/react";
import React, { useRef, useState } from "react";
import { Box } from "../../box";
import { useDropTarget } from "./use-drop-target";
import { useDrag } from "./use-drag";
import { getPlacement, PlacementIndicator, type Rect } from "./placement";
import { useAutoScroll } from "./use-auto-scroll";
export const Playground = () => {
const [dropTargetRect, setDropTargetRect] = useState<Rect>();
const [placementIndicatorRect, setPlacementIndicatorRect] = useState<Rect>();
const holdRef = useRef<HTMLElement>();
const dragItemRef = useRef<HTMLElement>();
const handleHoldEnd = () => {
holdRef.current?.style.removeProperty("background");
};
const dropTargetHandlers = useDropTarget({
isDropTarget(element: HTMLElement) {
return element.dataset.draggable === "true";
},
onDropTargetChange({ rect, target }) {
setDropTargetRect(rect);
const placementRect = getPlacement({ target });
setPlacementIndicatorRect(placementRect);
},
onHold({ target }: { target: HTMLElement }) {
handleHoldEnd();
holdRef.current = target;
target.style.background = "red";
},
});
const autoScrollHandlers = useAutoScroll();
const dragProps = useDrag({
onStart(event) {
if (event.target.dataset.draggable === "false") {
event.cancel();
return;
}
dragItemRef.current = event.target;
autoScrollHandlers.setEnabled(true);
},
onMove: (poiterCoordinate) => {
dropTargetHandlers.handleMove(poiterCoordinate);
autoScrollHandlers.handleMove(poiterCoordinate);
},
onEnd() {
dropTargetHandlers.handleEnd();
setDropTargetRect(undefined);
setPlacementIndicatorRect(undefined);
handleHoldEnd();
autoScrollHandlers.setEnabled(false);
},
onShiftChange({ shifts }) {
if (dragItemRef.current) {
dragItemRef.current.textContent = `shifted ${shifts}`;
}
},
});
return (
<Box
{...dragProps}
ref={(el) => {
dropTargetHandlers.rootRef(el);
autoScrollHandlers.targetRef(el);
}}
css={{ display: "block", overflow: "auto", height: 500 }}
onScroll={dropTargetHandlers.handleScroll}
>
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$cyanA9" />
<Item background="$slateA9" />
<Item background="$blueA9" draggable={false}>
Not Draggable
</Item>
<Outline rect={dropTargetRect} />
<PlacementIndicator rect={placementIndicatorRect} />
</Box>
);
};
export default {} as ComponentMeta<typeof Playground>;
const Item = ({
background,
draggable = true,
children,
}: {
background: string;
draggable?: boolean;
children?: React.ReactNode;
}) => (
<Box
css={{ height: 100, width: 100, background, margin: 10 }}
data-draggable={draggable}
>
{children}
</Box>
);
const Outline = ({ rect }: { rect?: Rect }) => {
if (rect === undefined) return null;
const style = {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
};
return (
<Box
style={style}
css={{
position: "absolute",
pointerEvents: "none",
outline: "1px solid red",
outlineOffset: -1,
}}
/>
);
};
@@ -102,7 +102,7 @@ export const SortableList = ({
let newIndex = placement.index;
// placement.index does not take into account the fact that the drag item will be removed.
// we need to do this to account for that
// we need to do this to account for it.
if (oldIndex < newIndex) {
newIndex = Math.max(0, newIndex - 1);
}