mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-07 20:44:05 +09:00
new canvas playground
This commit is contained in:
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>;
|
||||
-136
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+1
-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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user