From a279b2d30236eb3ed54bed80c17c9716616bf9f3 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 24 Mar 2022 16:31:13 +0000 Subject: [PATCH] drag&drop preview on the right sidebar #2 --- .../editable/use-content-editable.tsx | 24 ++- .../features/wrapper-component/use-css.tsx | 19 ++- .../wrapper-component/use-draggable.ts | 26 ++- .../wrapper-component/wrapper-component.tsx | 31 ++-- app/canvas/shared/nano-values.ts | 6 +- app/canvas/shared/use-drag-drop-handlers.ts | 149 +++++++++--------- app/designer/designer.tsx | 14 +- .../features/breadcrumbs/Breadcrumbs.tsx | 6 +- app/designer/features/inspector/index.ts | 1 + .../inspector.tsx} | 4 +- .../sidebar-left/custom-drag-layer.tsx | 8 +- .../sidebar-left/panels/components.tsx | 21 +-- .../features/sidebar-left/panels/tree.tsx | 4 +- app/designer/features/sidebar-right/index.ts | 1 - app/designer/features/tree-preview/index.ts | 1 + .../features/tree-preview/tree-preview.tsx | 78 +++++++++ .../instance/use-selected-instance-path.ts | 13 +- app/designer/shared/tree/Tree.tsx | 20 ++- app/shared/component/draggable.ts | 8 +- app/shared/tree-utils/insert-instance.test.ts | 34 ++-- app/shared/tree-utils/insert-instance.ts | 12 +- package.json | 2 +- yarn.lock | 8 +- 23 files changed, 292 insertions(+), 198 deletions(-) create mode 100644 app/designer/features/inspector/index.ts rename app/designer/features/{sidebar-right/sidebar-right.tsx => inspector/inspector.tsx} (94%) delete mode 100644 app/designer/features/sidebar-right/index.ts create mode 100644 app/designer/features/tree-preview/index.ts create mode 100644 app/designer/features/tree-preview/tree-preview.tsx diff --git a/app/canvas/features/wrapper-component/editable/use-content-editable.tsx b/app/canvas/features/wrapper-component/editable/use-content-editable.tsx index 48bdec6dde..80da5b2e37 100644 --- a/app/canvas/features/wrapper-component/editable/use-content-editable.tsx +++ b/app/canvas/features/wrapper-component/editable/use-content-editable.tsx @@ -113,17 +113,15 @@ const createPlaceholder = ( }; export const useContentEditable = ({ - id, - component, + instance, children, onChangeChildren, }: { - id: Instance["id"]; - component: Instance["component"]; + instance: Instance; children: Array; onChangeChildren?: OnChangeChildren; }) => { - const { isContentEditable } = primitives[component]; + const { isContentEditable } = primitives[instance.component]; const [isDisabled, setIsDisabled] = useState(true); const ref = useRef(); 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 diff --git a/app/canvas/features/wrapper-component/use-css.tsx b/app/canvas/features/wrapper-component/use-css.tsx index d4089f5a5d..4e75ad8f61 100644 --- a/app/canvas/features/wrapper-component/use-css.tsx +++ b/app/canvas/features/wrapper-component/use-css.tsx @@ -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]); }; diff --git a/app/canvas/features/wrapper-component/use-draggable.ts b/app/canvas/features/wrapper-component/use-draggable.ts index 07867f48a9..1e08c44e16 100644 --- a/app/canvas/features/wrapper-component/use-draggable.ts +++ b/app/canvas/features/wrapper-component/use-draggable.ts @@ -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) => { diff --git a/app/canvas/features/wrapper-component/wrapper-component.tsx b/app/canvas/features/wrapper-component/wrapper-component.tsx index abc62feb5c..492d65eddb 100644 --- a/app/canvas/features/wrapper-component/wrapper-component.tsx +++ b/app/canvas/features/wrapper-component/wrapper-component.tsx @@ -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; 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(); } }} diff --git a/app/canvas/shared/nano-values.ts b/app/canvas/shared/nano-values.ts index 22d202e2b6..d23b313d62 100644 --- a/app/canvas/shared/nano-values.ts +++ b/app/canvas/shared/nano-values.ts @@ -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(); -export const useDragData = () => useValue(dragDataContainer); +const dropDataContainer = createValueContainer(); +export const useDropData = () => useValue(dropDataContainer); const selectedInstanceContainer = createValueContainer(); export const useSelectedInstance = () => useValue(selectedInstanceContainer); diff --git a/app/canvas/shared/use-drag-drop-handlers.ts b/app/canvas/shared/use-drag-drop-handlers.ts index eeb92bb38f..f5a86cb03f 100644 --- a/app/canvas/shared/use-drag-drop-handlers.ts +++ b/app/canvas/shared/use-drag-drop-handlers.ts @@ -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(); const [instanceReparentingSpec, setInstanceReparentingSpec] = useState(); - const [dragData, setDragData] = useDragData(); + const [dropData, setDropData] = useDropData(); + const [dragData, setDragData] = useState(); 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 }; }; diff --git a/app/designer/designer.tsx b/app/designer/designer.tsx index 9c367baea4..37c36d24a2 100644 --- a/app/designer/designer.tsx +++ b/app/designer/designer.tsx @@ -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; 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 ( @@ -170,7 +178,7 @@ export const Designer = ({ config, project }: DesignerProps) => { isPreviewMode={isPreviewMode} css={{ overflow: "hidden" }} > - + {isDragging ? : } diff --git a/app/designer/features/breadcrumbs/Breadcrumbs.tsx b/app/designer/features/breadcrumbs/Breadcrumbs.tsx index b9eed76d05..9f2d662d9e 100644 --- a/app/designer/features/breadcrumbs/Breadcrumbs.tsx +++ b/app/designer/features/breadcrumbs/Breadcrumbs.tsx @@ -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 ( { +export const Inspector = ({ publish }: InspectorProps) => { const [selectedInstanceData] = useSelectedInstanceData(); if (selectedInstanceData === undefined) { diff --git a/app/designer/features/sidebar-left/custom-drag-layer.tsx b/app/designer/features/sidebar-left/custom-drag-layer.tsx index 6e99b39194..f2f445ad21 100644 --- a/app/designer/features/sidebar-left/custom-drag-layer.tsx +++ b/app/designer/features/sidebar-left/custom-drag-layer.tsx @@ -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) => { diff --git a/app/designer/features/sidebar-left/panels/components.tsx b/app/designer/features/sidebar-left/panels/components.tsx index ed59c6cea3..93e8efc03b 100644 --- a/app/designer/features/sidebar-left/panels/components.tsx +++ b/app/designer/features/sidebar-left/panels/components.tsx @@ -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} /> ))} { - publish<"dragComponent", InitialDragData>({ - type: "dragComponent", + onDrag={(dragData) => { + publish<"dragInstance", DragData>({ + type: "dragInstance", payload: { - ...dragData, + instance: createInstance({ component: dragData.component }), currentOffset: dragData.currentOffset, }, }); diff --git a/app/designer/features/sidebar-left/panels/tree.tsx b/app/designer/features/sidebar-left/panels/tree.tsx index 81cf0fbf87..a859be58ff 100644 --- a/app/designer/features/sidebar-left/panels/tree.tsx +++ b/app/designer/features/sidebar-left/panels/tree.tsx @@ -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; diff --git a/app/designer/features/sidebar-right/index.ts b/app/designer/features/sidebar-right/index.ts deleted file mode 100644 index 3ce791e7fe..0000000000 --- a/app/designer/features/sidebar-right/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./sidebar-right"; diff --git a/app/designer/features/tree-preview/index.ts b/app/designer/features/tree-preview/index.ts new file mode 100644 index 0000000000..7b0c3f2f2e --- /dev/null +++ b/app/designer/features/tree-preview/index.ts @@ -0,0 +1 @@ +export * from "./tree-preview"; diff --git a/app/designer/features/tree-preview/tree-preview.tsx b/app/designer/features/tree-preview/tree-preview.tsx new file mode 100644 index 0000000000..e367756056 --- /dev/null +++ b/app/designer/features/tree-preview/tree-preview.tsx @@ -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(); + + 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 ( + + + + ); +}; diff --git a/app/designer/shared/instance/use-selected-instance-path.ts b/app/designer/shared/instance/use-selected-instance-path.ts index ff5ef586f1..0e3e7c61aa 100644 --- a/app/designer/shared/instance/use-selected-instance-path.ts +++ b/app/designer/shared/instance/use-selected-instance-path.ts @@ -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 => { +export const useSelectedInstancePath = ( + selectedInstanceId?: Instance["id"] +): Array => { 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] ); }; diff --git a/app/designer/shared/tree/Tree.tsx b/app/designer/shared/tree/Tree.tsx index ea8d83447f..b441e2a312 100644 --- a/app/designer/shared/tree/Tree.tsx +++ b/app/designer/shared/tree/Tree.tsx @@ -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; 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 ( diff --git a/app/shared/component/draggable.ts b/app/shared/component/draggable.ts index a4576c8198..aaa302391d 100644 --- a/app/shared/component/draggable.ts +++ b/app/shared/component/draggable.ts @@ -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"; }; diff --git a/app/shared/tree-utils/insert-instance.test.ts b/app/shared/tree-utils/insert-instance.test.ts index e4c14c5991..b77ebe91a5 100644 --- a/app/shared/tree-utils/insert-instance.test.ts +++ b/app/shared/tree-utils/insert-instance.test.ts @@ -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(); }); }); diff --git a/app/shared/tree-utils/insert-instance.ts b/app/shared/tree-utils/insert-instance.ts index 6e2997cd14..c16bd703ca 100644 --- a/app/shared/tree-utils/insert-instance.ts +++ b/app/shared/tree-utils/insert-instance.ts @@ -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); diff --git a/package.json b/package.json index 07cf4135df..0f8c9e07d2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index e73e13997c..acc00f4606 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"