drag&drop preview on the right sidebar #2

This commit is contained in:
Oleg Isonen committed 2022-03-24 16:31:13 +00:00
1 parent 85f1ec79fb
commit a279b2d302
23 files changed
+292 -198

No files matched your search

@@ -113,17 +113,15 @@ const createPlaceholder = (
};
export const useContentEditable = ({
id,
component,
instance,
children,
onChangeChildren,
}: {
id: Instance["id"];
component: Instance["component"];
instance: Instance;
children: Array<JSX.Element | string>;
onChangeChildren?: OnChangeChildren;
}) => {
const { isContentEditable } = primitives[component];
const { isContentEditable } = primitives[instance.component];
const [isDisabled, setIsDisabled] = useState<boolean>(true);
const ref = useRef<HTMLDivElement>();
const [selectedInstance] = useSelectedInstance();
@@ -136,7 +134,7 @@ export const useContentEditable = ({
const updates = toUpdates(items);
if (onChangeChildren) {
onChangeChildren({
instanceId: id,
instanceId: instance.id,
updates,
});
}
@@ -177,12 +175,12 @@ export const useContentEditable = ({
(event) => {
// We only want to do this if the component is the one that was clicked
// @todo this logic shouldn't be necessary
if (selectedInstance?.id !== id) return;
if (selectedInstance?.id !== instance.id) return;
// Prevent inserting a newline when you want to start editing mode
event.preventDefault();
toggleDisable(false);
},
[toggleDisable, id, selectedInstance]
[toggleDisable, instance.id, selectedInstance]
);
useHotkeys(
@@ -195,10 +193,10 @@ export const useContentEditable = ({
);
useEffect(() => {
if (selectedInstance?.id !== id) {
if (selectedInstance?.id !== instance.id) {
toggleDisable(true);
}
}, [selectedInstance, id, toggleDisable]);
}, [selectedInstance, instance.id, toggleDisable]);
const toolbar = useToolbar({
editable: isContentEditable && isDisabled === false ? editable : undefined,
@@ -220,7 +218,7 @@ export const useContentEditable = ({
onBlur() {
// We only want to do this if the component is the one that was clicked
// @todo this logic shouldn't be necessary
if (selectedInstance?.id !== id) return;
if (selectedInstance?.id !== instance.id) return;
// When toolbar is open we don't want to disable editable
// even though we lost focus
@@ -230,13 +228,13 @@ export const useContentEditable = ({
onDoubleClick() {
// We only want to do this if the component is the one that was clicked
// @todo this logic shouldn't be necessary
if (selectedInstance?.id !== id) return;
if (selectedInstance?.id !== instance.id) return;
toggleDisable(false);
},
onInput: () => {
// We only want to do this if the component is the one that was clicked
// @todo this logic shouldn't be necessary
if (selectedInstance?.id !== id) return;
if (selectedInstance?.id !== instance.id) return;
if (isDisabled) return;
// Makes sure we call it right away the first time because user can disable editable before we updated
@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { type Instance, css as createCss, type CSS } from "@webstudio-is/sdk";
import { useDragData, useSelectedInstance } from "~/canvas/shared/nano-values";
import { useDropData, useSelectedInstance } from "~/canvas/shared/nano-values";
import { primitives } from "~/shared/component";
// @todo this doesn't work with the node at the top edge of the iframe, tag gets hidden.
@@ -51,32 +51,31 @@ const dragOverOutlineStyle = {
};
type UseCssProps = {
id: Instance["id"];
component: Instance["component"];
instance: Instance;
css: CSS;
};
export const useCss = ({ id, component, css }: UseCssProps): string => {
const [dragData] = useDragData();
export const useCss = ({ instance, css }: UseCssProps): string => {
const [dropData] = useDropData();
const [selectedInstance] = useSelectedInstance();
return useMemo(() => {
const primitive = primitives[component];
const primitive = primitives[instance.component];
let overrides: CSS = hoverOutlineStyle;
if (primitive.canAcceptChild()) {
overrides = emptyOutlineStyle;
}
if (selectedInstance?.id === id) {
if (selectedInstance?.id === instance.id) {
overrides = selectedOutlineStyle;
}
if (dragData !== undefined) {
if (dragData.id === id && primitive.canAcceptChild()) {
if (dropData !== undefined) {
if (dropData.instance.id === instance.id && primitive.canAcceptChild()) {
overrides = dragOverOutlineStyle;
}
}
return createCss(css)({ css: overrides });
}, [dragData, selectedInstance, css, component, id]);
}, [dropData, selectedInstance, css, instance]);
};
@@ -1,21 +1,20 @@
import { useCallback, useEffect } from "react";
import { useDrag } from "react-dnd";
import { type Instance } from "@webstudio-is/sdk";
import { type InitialDragData } from "~/shared/component";
import { type DragData } from "~/shared/component";
import { publish } from "../../shared/pubsub";
//import { usePointerOutline } from "~/canvas/hooks";
type UseDraggable = {
id: Instance["id"];
component: Instance["component"];
instance: Instance;
isDisabled: boolean;
};
export const useDraggable = ({ id, component, isDisabled }: UseDraggable) => {
export const useDraggable = ({ instance, isDisabled }: UseDraggable) => {
//const updatePointerOutline = usePointerOutline();
const [{ isDragging, currentOffset }, dragRefCallback] = useDrag(
() => ({
type: component,
type: instance.component,
collect(monitor) {
return {
isDragging: monitor.isDragging(),
@@ -32,27 +31,24 @@ export const useDraggable = ({ id, component, isDisabled }: UseDraggable) => {
useEffect(() => {
// Started
if (isDragging === true) {
publish<"dragStartComponent">({ type: "dragStartComponent" });
publish<"dragStartInstance">({ type: "dragStartInstance" });
} else {
// Ended
publish<"dragEndInstance", Instance["id"]>({
type: "dragEndInstance",
payload: id,
});
publish<"dragEndInstance", Instance["id"]>({ type: "dragEndInstance" });
}
}, [isDragging, id]);
}, [isDragging, instance.id]);
useEffect(() => {
if (currentOffset === null || isDragging === false) return;
// updatePointerOutline(currentOffset);
publish<"dragComponent", InitialDragData>({
type: "dragComponent",
publish<"dragInstance", DragData>({
type: "dragInstance",
payload: {
component,
instance,
currentOffset,
},
});
}, [component, currentOffset, isDragging]);
}, [instance, currentOffset, isDragging]);
const handleMouseDown = useCallback(
(event) => {
@@ -12,22 +12,20 @@ import { useCss } from "./use-css";
import { useDraggable } from "./use-draggable";
type WrapperComponentDevProps = {
id: Instance["id"];
component: Instance["component"];
instance: Instance;
css: CSS;
children: Array<JSX.Element | string>;
onChangeChildren?: OnChangeChildren;
};
export const WrapperComponentDev = ({
id,
component,
instance,
css,
children,
onChangeChildren,
...rest
}: WrapperComponentDevProps) => {
const className = useCss({ id, css, component });
const className = useCss({ instance, css });
const {
ref: contentEditableRef,
@@ -35,15 +33,13 @@ export const WrapperComponentDev = ({
toolbar: editableToolbar,
...contentEditableProps
} = useContentEditable({
id,
component,
instance,
children,
onChangeChildren,
});
const { dragRefCallback, ...draggableProps } = useDraggable({
id,
component,
instance,
isDisabled: isContentEditableDisabled === false,
});
@@ -56,10 +52,11 @@ export const WrapperComponentDev = ({
[contentEditableRef, dragRefCallback]
);
const userProps = useUserProps(id);
const readonly = component === "Input" ? { readOnly: true } : undefined;
const userProps = useUserProps(instance.id);
const readonly =
instance.component === "Input" ? { readOnly: true } : undefined;
const { Component } = primitives[component];
const { Component } = primitives[instance.component];
return (
<>
{editableToolbar}
@@ -70,14 +67,14 @@ export const WrapperComponentDev = ({
{...draggableProps}
{...readonly}
className={className}
id={id}
id={instance.id}
tabIndex={0}
data-component={component}
data-label={primitives[component].label}
data-id={id}
data-component={instance.component}
data-label={primitives[instance.component].label}
data-id={instance.id}
ref={refCallback}
onClick={(event: MouseEvent) => {
if (component === "Link") {
if (instance.component === "Link") {
event.preventDefault();
}
}}
+3 -3
View File
@@ -1,9 +1,9 @@
import { createValueContainer, useValue } from "react-nano-state";
import { type Instance } from "@webstudio-is/sdk";
import { type DragData } from "~/shared/component";
import { type DropData } from "~/shared/component";
const dragDataContainer = createValueContainer<DragData | undefined>();
export const useDragData = () => useValue(dragDataContainer);
const dropDataContainer = createValueContainer<DropData | undefined>();
export const useDropData = () => useValue(dropDataContainer);
const selectedInstanceContainer = createValueContainer<Instance | undefined>();
export const useSelectedInstance = () => useValue(selectedInstanceContainer);
+76 -73
View File
@@ -1,9 +1,9 @@
import { useState } from "react";
import ObjectId from "bson-objectid";
import memoize from "lodash.memoize";
import { type Instance } from "@webstudio-is/sdk";
import { type InitialDragData, type DragData } from "~/shared/component";
import { type DragData, type DropData } from "~/shared/component";
import {
findInstanceById,
type InstanceInsertionSpec,
type InstanceReparentingSpec,
} from "~/shared/tree-utils";
@@ -12,7 +12,7 @@ import {
findInsertionIndex,
getDragOverInfo,
} from "~/shared/dom-utils";
import { useDragData, useSelectedInstance } from "./nano-values";
import { useDropData, useSelectedInstance } from "./nano-values";
import { publish, useSubscribe } from "./pubsub";
//import {usePointerOutline} from './use-pointer-outline'
@@ -36,22 +36,20 @@ export const useDragDropHandlers = ({
useState<InstanceInsertionSpec>();
const [instanceReparentingSpec, setInstanceReparentingSpec] =
useState<InstanceReparentingSpec>();
const [dragData, setDragData] = useDragData();
const [dropData, setDropData] = useDropData();
const [dragData, setDragData] = useState<DragData>();
const insert = ({
parentId,
component,
position,
instance,
dropData,
}: {
parentId: Instance["id"];
component: Instance["component"];
position: DragData["position"];
instance: Instance;
dropData: DropData;
}) => {
const instanceInsertionSpec = {
parentId,
component,
position,
id: ObjectId().toString(),
instance,
parentId: dropData.instance.id,
position: dropData.position,
} as const;
setInstanceInsertionSpec(instanceInsertionSpec);
publish<"syncInstanceInsertion", InstanceInsertionSpec>({
@@ -60,44 +58,43 @@ export const useDragDropHandlers = ({
});
};
const clearCaches = () => {
if (getBoundingClientRect.cache?.clear) getBoundingClientRect.cache.clear();
if (getComputedStyle.cache?.clear) getComputedStyle.cache.clear();
};
useSubscribe<"dragStartComponent">("dragStartComponent", () => {
useSubscribe<"dragStartInstance">("dragStartInstance", () => {
setSelectedInstance(undefined);
});
useSubscribe<"dragEndComponent">("dragEndComponent", () => {
clearCaches();
if (dragData === undefined) return;
insert({
parentId: dragData.id,
component: dragData.component,
position: dragData.position,
});
useSubscribe<"dragEndInstance">("dragEndInstance", () => {
// Cleanup
if (getBoundingClientRect.cache?.clear) getBoundingClientRect.cache.clear();
if (getComputedStyle.cache?.clear) getComputedStyle.cache.clear();
setDropData(undefined);
setDragData(undefined);
});
useSubscribe<"dragEndInstance", Instance["id"]>("dragEndInstance", (id) => {
clearCaches();
if (
dropData === undefined ||
dragData === undefined ||
// Can't reparent an instance inside itself
dragData.id === id
dropData.instance.id === dragData.instance.id
) {
return;
}
const isNew =
findInstanceById(rootInstance, dragData.instance.id) === undefined;
if (isNew) {
insert({
instance: dragData.instance,
dropData,
});
return;
}
const instanceReparentingSpec = {
parentId: dragData.id,
position: dragData.position,
id,
parentId: dropData.instance.id,
position: dropData.position,
id: dragData.instance.id,
};
setInstanceReparentingSpec(instanceReparentingSpec);
setDragData(undefined);
publish<"syncInstanceReparenting", InstanceReparentingSpec>({
type: "syncInstanceReparenting",
payload: instanceReparentingSpec,
@@ -105,50 +102,56 @@ export const useDragDropHandlers = ({
});
//const updatePointerOutline = usePointerOutline();
useSubscribe<"dragComponent", InitialDragData>(
"dragComponent",
({ currentOffset, component }) => {
// updatePointerOutline(currentOffset)
const dragOver = getDragOverInfo(currentOffset, getBoundingClientRect);
useSubscribe<"dragInstance", DragData>("dragInstance", (dragData) => {
const { currentOffset } = dragData;
// updatePointerOutline(currentOffset)
const dragOver = getDragOverInfo(currentOffset, getBoundingClientRect);
if (dragOver.element === undefined) return;
if (dragOver.element === undefined) return;
const closestChild = findClosestChild(
dragOver.element,
currentOffset,
getBoundingClientRect,
getComputedStyle
);
const dropInstance = findInstanceById(rootInstance, dragOver.element.id);
let insertionIndex = 0;
if (dropInstance === undefined) return;
// When element has children.
if (dragOver.element !== undefined && closestChild !== undefined) {
insertionIndex = findInsertionIndex(dragOver, closestChild);
}
const closestChild = findClosestChild(
dragOver.element,
currentOffset,
getBoundingClientRect,
getComputedStyle
);
setDragData({
id: dragOver.element.id,
component,
currentOffset,
position: insertionIndex,
});
let insertionIndex = 0;
// When element has children.
if (dragOver.element !== undefined && closestChild !== undefined) {
insertionIndex = findInsertionIndex(dragOver, closestChild);
}
);
useSubscribe<"insertComponent", { component: Instance["component"] }>(
"insertComponent",
({ component }) => {
if (selectedInstance === undefined) {
setSelectedInstance(rootInstance);
}
insert({
component,
const dropData = {
instance: dropInstance,
position: insertionIndex,
};
setDragData(dragData);
setDropData(dropData);
publish<"dropPreview", { dropData: DropData; dragData: DragData }>({
type: "dropPreview",
payload: { dropData, dragData },
});
});
useSubscribe<"insertInstance", Instance>("insertInstance", (instance) => {
if (selectedInstance === undefined) {
setSelectedInstance(rootInstance);
}
insert({
instance,
dropData: {
position: "end",
parentId: selectedInstance?.id ?? rootInstance.id,
});
}
);
instance: selectedInstance ?? rootInstance,
},
});
});
return { instanceInsertionSpec, instanceReparentingSpec };
};
+11 -3
View File
@@ -7,7 +7,7 @@ import type { SelectedInstanceData } from "~/shared/component";
import { Box, Flex, Grid, type CSS } from "~/shared/design-system";
import interStyles from "~/shared/font-faces/inter.css";
import { SidebarLeft } from "./features/sidebar-left";
import { SidebarRight } from "./features/sidebar-right";
import { Inspector } from "./features/inspector";
import {
CanvasIframe,
useSubscribe,
@@ -22,6 +22,7 @@ import { Topbar } from "./features/topbar";
import designerStyles from "./designer.css";
import { useSync } from "./features/sync";
import { Breadcrumbs } from "./features/breadcrumbs";
import { TreePrevew } from "./features/tree-preview";
export const links = () => {
return [
@@ -44,7 +45,7 @@ const useSubscribeSelectedInstanceData = () => {
};
type SidePanelProps = {
children: JSX.Element;
children: JSX.Element | Array<JSX.Element>;
isPreviewMode: boolean;
css?: CSS;
gridArea: "inspector" | "sidebar";
@@ -139,6 +140,13 @@ export const Designer = ({ config, project }: DesignerProps) => {
const [publish, iframeRef] = usePublish();
const [isPreviewMode] = useIsPreviewMode();
useSubscribe<"dragStartInstance">("dragStartInstance", () => {
setIsDragging(true);
});
useSubscribe<"dragEndInstance">("dragEndInstance", () => {
setIsDragging(false);
});
return (
<DndProvider backend={HTML5Backend}>
<ChromeWrapper isPreviewMode={isPreviewMode}>
@@ -170,7 +178,7 @@ export const Designer = ({ config, project }: DesignerProps) => {
isPreviewMode={isPreviewMode}
css={{ overflow: "hidden" }}
>
<SidebarRight publish={publish} />
{isDragging ? <TreePrevew /> : <Inspector publish={publish} />}
</SidePanel>
</ChromeWrapper>
</DndProvider>
@@ -3,6 +3,7 @@ import { Button, Flex, Text } from "~/shared/design-system";
import { useSelectedInstancePath } from "../../shared/instance/use-selected-instance-path";
import { Instance } from "@webstudio-is/sdk";
import { Publish } from "../canvas-iframe";
import { useSelectedInstanceData } from "~/designer/shared/nano-values";
type BreadcrumbProps = {
component: Instance["component"];
@@ -30,7 +31,10 @@ type BreadcrumbsProps = {
publish: Publish;
};
export const Breadcrumbs = ({ publish }: BreadcrumbsProps) => {
const selectedInstancePath = useSelectedInstancePath();
const [selectedInstanceData] = useSelectedInstanceData();
const selectedInstancePath = useSelectedInstancePath(
selectedInstanceData?.id
);
return (
<Flex
as="footer"
+1
View File
@@ -0,0 +1 @@
export * from "./inspector";
@@ -13,7 +13,7 @@ import type { Publish } from "~/designer/features/canvas-iframe";
import { BrushIcon, GearIcon } from "~/shared/icons";
import { useSelectedInstanceData } from "~/designer/shared/nano-values";
type SidebarRightProps = {
type InspectorProps = {
publish: Publish;
};
@@ -22,7 +22,7 @@ const contentStyle = {
overflow: "auto",
};
export const SidebarRight = ({ publish }: SidebarRightProps) => {
export const Inspector = ({ publish }: InspectorProps) => {
const [selectedInstanceData] = useSelectedInstanceData();
if (selectedInstanceData === undefined) {
@@ -1,8 +1,7 @@
import { useEffect } from "react";
import { createPortal } from "react-dom";
import { useDragLayer } from "react-dnd";
import { useDragLayer, type XYCoord } from "react-dnd";
import { type Instance } from "@webstudio-is/sdk";
import type { InitialDragData } from "~/shared/component";
const layerStyles = {
position: "absolute",
@@ -15,7 +14,10 @@ const layerStyles = {
} as const;
export type CustomDragLayerProps = {
onDrag: (dragData: InitialDragData) => void;
onDrag: (dragData: {
component: Instance["component"];
currentOffset: XYCoord;
}) => void;
};
export const CustomDragLayer = ({ onDrag }: CustomDragLayerProps) => {
@@ -9,8 +9,9 @@ import { useDrag } from "react-dnd";
import { type Instance } from "@webstudio-is/sdk";
import { Flex, Text } from "~/shared/design-system";
import { Component1Icon } from "~/shared/icons";
import { primitives, type InitialDragData } from "~/shared/component";
import { primitives, type DragData } from "~/shared/component";
import { type Publish } from "~/designer/features/canvas-iframe";
import { createInstance } from "~/shared/tree-utils";
import { CustomDragLayer } from "../custom-drag-layer";
import type { TabName } from "../types";
@@ -91,8 +92,8 @@ export const TabContent = ({
const handleDragChange = useCallback(
(isDragging: boolean) => {
onDragChange(isDragging);
publish<"dragStartComponent" | "dragEndComponent">({
type: isDragging === true ? "dragStartComponent" : "dragEndComponent",
publish<"dragStartInstance" | "dragEndInstance">({
type: isDragging === true ? "dragStartInstance" : "dragEndInstance",
});
},
[onDragChange, publish]
@@ -108,20 +109,20 @@ export const TabContent = ({
label={primitives[component].label}
onClick={() => {
onSetActiveTab("none");
publish<"insertComponent", { component: Instance["component"] }>({
type: "insertComponent",
payload: { component },
publish<"insertInstance", Instance>({
type: "insertInstance",
payload: createInstance({ component }),
});
}}
onDragChange={handleDragChange}
/>
))}
<CustomDragLayer
onDrag={(dragData: InitialDragData) => {
publish<"dragComponent", InitialDragData>({
type: "dragComponent",
onDrag={(dragData) => {
publish<"dragInstance", DragData>({
type: "dragInstance",
payload: {
...dragData,
instance: createInstance({ component: dragData.component }),
currentOffset: dragData.currentOffset,
},
});
@@ -17,7 +17,9 @@ export const TabContent = ({
selectedInstanceData,
}: TabContentProps) => {
const [rootInstance] = useRootInstance();
const selectedInstancePath = useSelectedInstancePath();
const selectedInstancePath = useSelectedInstancePath(
selectedInstanceData?.id
);
if (rootInstance === undefined) return null;
@@ -1 +0,0 @@
export * from "./sidebar-right";
@@ -0,0 +1 @@
export * from "./tree-preview";
@@ -0,0 +1,78 @@
import { Instance, useSubscribe } from "@webstudio-is/sdk";
import { useState } from "react";
import { useRootInstance } from "~/designer/shared/nano-values";
import { Tree } from "~/designer/shared/tree";
import { type DragData, type DropData } from "~/shared/component";
import { Flex } from "~/shared/design-system";
import {
findInstanceById,
getInstancePath,
insertInstance,
reparentInstance,
} from "~/shared/tree-utils";
export const TreePrevew = () => {
const [rootInstance] = useRootInstance();
const [draftRootInstance, setDraftRootInstance] = useState(rootInstance);
const [instanceId, setInstanceId] = useState<Instance["id"]>();
useSubscribe<"dropPreview", { dragData: DragData; dropData: DropData }>(
"dropPreview",
({ dragData, dropData }) => {
if (rootInstance === undefined) return;
setInstanceId(dragData.instance.id);
const isNew =
findInstanceById(rootInstance, dragData.instance.id) === undefined;
if (isNew) {
const instanceInsertionSpec = {
instance: dragData.instance,
parentId: dropData.instance.id,
position: dropData.position,
};
const { instance } = insertInstance(
instanceInsertionSpec,
rootInstance
);
setDraftRootInstance(instance);
return;
}
// Can't reparent an instance inside itself
if (dropData.instance.id === dragData.instance.id) {
return;
}
const instanceReparentingSpec = {
parentId: dropData.instance.id,
position: dropData.position,
id: dragData.instance.id,
};
const updatedRootInstance = reparentInstance(
rootInstance,
instanceReparentingSpec
);
setDraftRootInstance(updatedRootInstance);
}
);
if (draftRootInstance === undefined || instanceId === undefined) {
return null;
}
const selectedInstancePath = getInstancePath(draftRootInstance, instanceId);
if (selectedInstancePath.length === 0) return null;
return (
<Flex gap="3" direction="column" css={{ padding: "$1" }}>
<Tree
instance={draftRootInstance}
selectedInstancePath={selectedInstancePath}
selectedInstanceId={instanceId}
animate={false}
/>
</Flex>
);
};
@@ -1,16 +1,17 @@
import { Instance } from "@webstudio-is/sdk";
import { useMemo } from "react";
import { getInstancePath } from "~/shared/tree-utils";
import { useRootInstance, useSelectedInstanceData } from "../nano-values";
import { useRootInstance } from "../nano-values";
export const useSelectedInstancePath = (): Array<Instance> => {
export const useSelectedInstancePath = (
selectedInstanceId?: Instance["id"]
): Array<Instance> => {
const [rootInstance] = useRootInstance();
const [selectedInstanceData] = useSelectedInstanceData();
return useMemo(
() =>
selectedInstanceData !== undefined && rootInstance !== undefined
? getInstancePath(rootInstance, selectedInstanceData.id)
selectedInstanceId !== undefined && rootInstance !== undefined
? getInstancePath(rootInstance, selectedInstanceId)
: [],
[selectedInstanceData, rootInstance]
[selectedInstanceId, rootInstance]
);
};
+16 -4
View File
@@ -10,6 +10,7 @@ import {
} from "~/shared/design-system";
import { TriangleRightIcon, TriangleDownIcon } from "~/shared/icons";
import { primitives } from "~/shared/component";
import noop from "lodash.noop";
const openKeyframes = keyframes({
from: { height: 0 },
@@ -21,7 +22,7 @@ const closeKeyframes = keyframes({
to: { height: 0 },
});
const CollapsibleContent = styled(Collapsible.Content, {
const CollapsibleContentAnimated = styled(Collapsible.Content, {
overflow: "hidden",
'&[data-state="open"]': {
animation: `${openKeyframes} 150ms ease-in-out`,
@@ -31,20 +32,26 @@ const CollapsibleContent = styled(Collapsible.Content, {
},
});
const CollapsibleContentUnanimated = styled(Collapsible.Content, {
overflow: "hidden",
});
type TreeProps = {
instance: Instance;
selectedInstanceId?: Instance["id"];
selectedInstancePath: Array<Instance>;
level?: number;
onSelect: (instance: Instance) => void;
onSelect?: (instance: Instance) => void;
animate?: boolean;
};
export const Tree = ({
instance,
selectedInstanceId,
level = 0,
onSelect,
selectedInstancePath,
level = 0,
onSelect = noop,
animate = true,
}: TreeProps) => {
const [isOpen, setIsOpen] = useState(selectedInstancePath.includes(instance));
@@ -67,6 +74,7 @@ export const Tree = ({
level={level + 1}
key={child.id}
onSelect={onSelect}
animate={animate}
/>
);
}
@@ -79,9 +87,13 @@ export const Tree = ({
selectedInstancePath,
onSelect,
showChildren,
animate,
]);
const { Icon, label } = primitives[instance.component];
const CollapsibleContent = animate
? CollapsibleContentAnimated
: CollapsibleContentUnanimated;
return (
<Collapsible.Root open={isOpen} onOpenChange={setIsOpen}>
+4 -4
View File
@@ -1,12 +1,12 @@
import type { XYCoord } from "react-dnd";
import { type Instance } from "@webstudio-is/sdk";
export type InitialDragData = {
export type DragData = {
instance: Instance;
currentOffset: XYCoord;
component: Instance["component"];
};
export type DragData = InitialDragData & {
id: Instance["id"];
export type DropData = {
instance: Instance;
position: number | "end";
};
+16 -18
View File
@@ -1,10 +1,12 @@
import { type Instance } from "@webstudio-is/sdk";
import { type InstanceInsertionSpec } from ".";
import { insertInstance } from "./insert-instance";
import { insertInstance, type InstanceInsertionSpec } from "./insert-instance";
import { createInstance } from "./create-instance";
const instance = createInstance({ id: "3", component: "Box" });
describe("Insert instance", () => {
test(`inside a nested div using "end": div 1 > div 2 > [div 3]`, () => {
const instance: Instance = {
const rootInstance: Instance = {
component: "Box",
id: "1",
style: {},
@@ -18,17 +20,16 @@ describe("Insert instance", () => {
],
};
const instanceInsertionSpec: InstanceInsertionSpec = {
instance,
parentId: "2",
component: "Box",
position: "end",
id: "3",
};
const result = insertInstance(instanceInsertionSpec, instance);
const result = insertInstance(instanceInsertionSpec, rootInstance);
expect(result).toMatchSnapshot();
});
test("after a nested div using index: div 1 > div 2, [div 3]", () => {
const instance: Instance = {
const rootInstance: Instance = {
component: "Box",
id: "1",
style: {},
@@ -42,17 +43,16 @@ describe("Insert instance", () => {
],
};
const instanceInsertionSpec: InstanceInsertionSpec = {
instance,
parentId: "1",
component: "Box",
position: 1,
id: "3",
};
const result = insertInstance(instanceInsertionSpec, instance);
const result = insertInstance(instanceInsertionSpec, rootInstance);
expect(result).toMatchSnapshot();
});
test("before a nested div using index: div 1 > [div 3], div 2", () => {
const instance: Instance = {
const rootInstance: Instance = {
component: "Box",
id: "1",
style: {},
@@ -66,17 +66,16 @@ describe("Insert instance", () => {
],
};
const instanceInsertionSpec: InstanceInsertionSpec = {
instance,
parentId: "1",
component: "Box",
position: 0,
id: "3",
};
const result = insertInstance(instanceInsertionSpec, instance);
const result = insertInstance(instanceInsertionSpec, rootInstance);
expect(result).toMatchSnapshot();
});
test(`after a nested div using "end": div 1 > div 2, [div 3]`, () => {
const instance: Instance = {
const rootInstance: Instance = {
component: "Box",
id: "1",
style: {},
@@ -90,12 +89,11 @@ describe("Insert instance", () => {
],
};
const instanceInsertionSpec: InstanceInsertionSpec = {
instance,
parentId: "1",
component: "Box",
position: "end",
id: "3",
};
const result = insertInstance(instanceInsertionSpec, instance);
const result = insertInstance(instanceInsertionSpec, rootInstance);
expect(result).toMatchSnapshot();
});
});
+3 -9
View File
@@ -1,12 +1,10 @@
import produce from "immer";
import { type Instance } from "@webstudio-is/sdk";
import { populateInstance } from "./populate";
import { createInstance } from "./create-instance";
export type InstanceInsertionSpec = {
component: Instance["component"];
instance: Instance;
parentId: Instance["id"];
id: Instance["id"];
position: number | "end";
};
@@ -44,13 +42,9 @@ export const insertInstance = (
rootInstance: Instance,
options: { populate?: boolean } = { populate: true }
) => {
const newInstance = createInstance({
component: spec.component,
id: spec.id,
});
const populatedInstance = options.populate
? populateInstance(newInstance)
: newInstance;
? populateInstance(spec.instance)
: spec.instance;
const updatedInstance = produce(rootInstance, (draftRootInstance) => {
insertInstanceMutable(draftRootInstance, populatedInstance, spec);
+1 -1
View File
@@ -43,7 +43,7 @@
"@remix-run/vercel": "^1.2.3",
"@stitches/react": "^1.2.6",
"@stitches/stringify": "^1.2.6",
"@webstudio-is/sdk": "^0.1.17",
"@webstudio-is/sdk": "^0.1.18",
"bson-objectid": "^2.0.2",
"camelcase": "^6.3.0",
"hyphenate-style-name": "^1.0.4",
+4 -4
View File
@@ -1820,10 +1820,10 @@
dependencies:
web-streams-polyfill "^3.1.1"
"@webstudio-is/sdk@^0.1.17":
version "0.1.17"
resolved "https://registry.yarnpkg.com/@webstudio-is/sdk/-/sdk-0.1.17.tgz#8cae4ffbb111cf923e28c549771acd240e51a75e"
integrity sha512-kq21HpY2zGADIkVu7Dn6jR08IzR07IE4aSm70roxc/qv+ZEoEmPj3EqJm8KmV+2qQwjUNV9/1ab3mYFi0E1X9Q==
"@webstudio-is/sdk@^0.1.18":
version "0.1.18"
resolved "https://registry.yarnpkg.com/@webstudio-is/sdk/-/sdk-0.1.18.tgz#86fa61b508c0293f0ed9baca6e6b89585f44e320"
integrity sha512-rEgIG4qZMKhyudqVhQGlc7LB1ibMQ6KA9Jr+pjMTAhv3o/Jgqk4YA4eXyui8ZFZvC5ibIakdzqsv+4O6QTXGSQ==
dependencies:
"@prisma/client" "^3.10.0"
"@stitches/react" "^1.2.7"