mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 04:55:06 +09:00
drag&drop preview on the right sidebar #2
This commit is contained in:
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();
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./inspector";
|
||||
+2
-2
@@ -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]
|
||||
);
|
||||
};
|
||||
@@ -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}>
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user