Merge branch 'main' of github.com:webstudio-is/webstudio-designer into auth

This commit is contained in:
Sara Vieira committed 2022-06-16 16:16:43 +02:00
commit d34b2bd0eb
66 files changed
+717 -271

No files matched your search

+16 -4
View File
@@ -22,12 +22,17 @@ import {
useInsertInstance,
useDeleteInstance,
useReparentInstance,
usePublishSelectedInstance,
usePublishSelectedInstanceData,
usePublishRootInstance,
useUpdateSelectedInstance,
usePublishSelectedInstanceDataRect,
usePublishHoveredInstanceRect,
usePublishHoveredInstanceData,
useSetHoveredInstance,
useUnselectInstance,
} from "./shared/instance";
import { useUpdateStyle } from "./shared/style";
import { useActiveElementTracking } from "./shared/active-element";
import { useTrackSelectedElement } from "./shared/use-track-selected-element";
import { WrapperComponentDev } from "./features/wrapper-component";
import { useSync } from "./shared/sync";
import { useManageProps } from "./shared/props";
@@ -42,6 +47,7 @@ import {
useRootInstance,
} from "~/shared/nano-states";
import { registerContainers } from "./shared/immerhin";
import { useTrackHoveredElement } from "./shared/use-track-hovered-element";
registerContainers();
@@ -96,15 +102,21 @@ const DesignMode = ({ treeId, project }: DesignModeProps) => {
useDragDropHandlers();
useUpdateStyle();
useManageProps();
usePublishSelectedInstance({ treeId });
usePublishSelectedInstanceData(treeId);
usePublishHoveredInstanceData();
useHandleBreakpoints();
useInsertInstance();
useReparentInstance();
useDeleteInstance();
usePublishRootInstance();
useActiveElementTracking();
useTrackSelectedElement();
useTrackHoveredElement();
useSetHoveredInstance();
useSync({ project });
useUpdateSelectedInstance();
usePublishSelectedInstanceDataRect();
usePublishHoveredInstanceRect();
useUnselectInstance();
const elements = useElementsTree();
return (
// Using touch backend becuase html5 drag&drop doesn't fire drag events in our case
@@ -4,7 +4,7 @@ import useDebounce from "react-use/lib/useDebounce";
import ObjectId from "bson-objectid";
import { type ChildrenUpdates, type Instance } from "@webstudio-is/sdk";
import { type OnChangeChildren } from "~/shared/tree-utils";
import { primitives } from "~/shared/component";
import { primitives } from "~/shared/canvas-components";
import { useSelectedInstance } from "~/canvas/shared/nano-states";
import { useEditable, type Content } from "./use-editable";
import { useToolbar } from "./use-toolbar";
@@ -6,55 +6,7 @@ import {
useSubscribe,
toValue,
} from "@webstudio-is/sdk";
import { useDropData, useSelectedInstance } from "~/canvas/shared/nano-states";
import { primitives, type StyleUpdates } from "~/shared/component";
// @todo this doesn't work with the node at the top edge of the iframe, tag gets hidden.
const componentTagStyle = {
"&::before": {
display: "flex",
content: "attr(data-label)",
padding: "0 $1",
marginTop: "-$4",
height: "$4",
position: "absolute",
backgroundColor: "$blue10",
color: "$loContrast",
alignItems: "center",
justifyContent: "center",
fontSize: "$1",
lineHeight: 1,
minWidth: "$6",
},
};
const hoverOutlineStyle = {
"&:hover": {
outline: "1px solid $blue10",
outlineOffset: -1,
...componentTagStyle,
},
};
const emptyOutlineStyle = {
"&:empty": {
outline: "1px dashed #555",
outlineOffset: -1,
},
...hoverOutlineStyle,
};
const selectedOutlineStyle = {
outline: "2px solid $blue10",
outlineOffset: -2,
...componentTagStyle,
};
const dragOverOutlineStyle = {
outline: "2px solid $blue10",
outlineOffset: -2,
...componentTagStyle,
};
import { type StyleUpdates } from "~/shared/canvas-components";
type UseCssProps = {
instance: Instance;
@@ -85,45 +37,40 @@ const usePreviewCss = ({ instance, css }: UseCssProps) => {
value: undefined,
}));
setPreviewCss(reset);
// previewCss in deps leads to an infinite loop
// previewCss in deps leads to an infinite loop, css is like a cache key in this case,
// as soon as `css` changes we can reset the preview
// @todo need a more correct approach than this
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [css]);
return previewCss;
};
const voidElements =
"area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr";
const defaultStyle = {
[`&:not(${voidElements}):empty`]: {
outline: "1px dashed #555",
outlineOffset: -1,
paddingTop: "$8",
paddingRight: "$8",
},
"&[contenteditable], &:focus": {
outline: 0,
},
};
export const useCss = ({ instance, css }: UseCssProps): string => {
const [dropData] = useDropData();
const [selectedInstance] = useSelectedInstance();
const previewCss = usePreviewCss({ instance, css });
return useMemo(() => {
const primitive = primitives[instance.component];
let overrides: CSS = hoverOutlineStyle;
if (primitive.canAcceptChild()) {
overrides = emptyOutlineStyle;
}
if (selectedInstance?.id === instance.id) {
overrides = selectedOutlineStyle;
}
if (dropData !== undefined) {
if (dropData.instance.id === instance.id && primitive.canAcceptChild()) {
overrides = dragOverOutlineStyle;
}
}
const overrides: CSS = { ...defaultStyle };
for (const update of previewCss) {
// Delete ephemeral value
if (update.value === undefined) {
delete overrides[update.property];
continue;
}
// @todo workaround for Expression produces a union type that is too complex to represent.
if (update.value === undefined) continue;
overrides[update.property as string] = toValue(update.value);
}
return createCss(css)({ css: overrides });
}, [dropData, selectedInstance, css, previewCss, instance]);
}, [css, previewCss]);
};
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef } from "react";
import { useDrag } from "react-dnd";
import { type Instance, publish } from "@webstudio-is/sdk";
import { type DragData } from "~/shared/component";
import { type DragData } from "~/shared/canvas-components";
//import { usePointerOutline } from "~/canvas/hooks";
type UseDraggable = {
@@ -6,7 +6,7 @@ import {
useUserProps,
renderWrapperComponentChildren,
} from "@webstudio-is/sdk";
import { primitives } from "~/shared/component";
import { primitives } from "~/shared/canvas-components";
import { useContentEditable } from "./editable";
import { useCss } from "./use-css";
import { useDraggable } from "./use-draggable";
@@ -74,7 +74,6 @@ export const WrapperComponentDev = ({
id={instance.id}
tabIndex={0}
data-component={instance.component}
data-label={primitives[instance.component].label}
data-id={instance.id}
ref={refCallback}
onClick={(event: MouseEvent) => {
-66
View File
@@ -1,66 +0,0 @@
import { useCallback, useEffect } from "react";
import { type Instance, publish, useSubscribe } from "@webstudio-is/sdk";
import { findInstanceById } from "~/shared/tree-utils";
import { useSelectedInstance, useSelectedElement } from "./nano-states";
import { useRootInstance } from "~/shared/nano-states";
const eventOptions = {
passive: true,
};
export const useActiveElementTracking = () => {
const [rootInstance] = useRootInstance();
const [selectedInstance, setSelectedInstance] = useSelectedInstance();
const [selectedElement, setSelectedElement] = useSelectedElement();
useSubscribe("focusElement", (id: Instance["id"]) => {
const element = document.getElementById(id);
element?.focus();
select(element);
});
const select = useCallback(
(element: Element | null) => {
if (element === null || rootInstance === undefined) return;
const id = element?.id;
if (!id) return;
if (selectedElement?.id === id && selectedInstance?.id === id) {
return;
}
const instance = findInstanceById(rootInstance, element.id);
if (instance === undefined) return;
setSelectedInstance(instance);
setSelectedElement(element);
},
[
rootInstance,
selectedElement,
setSelectedElement,
selectedInstance,
setSelectedInstance,
]
);
useEffect(() => {
const handleFocus = () => {
select(document.activeElement);
};
window.addEventListener("focus", handleFocus, eventOptions);
const handleClick = (event: MouseEvent) => {
// Notify in general that document was clicked
// e.g. to hide the side panel
publish<"clickCanvas">({ type: "clickCanvas" });
if (event.target instanceof Element) select(event.target);
};
window.addEventListener("click", handleClick, eventOptions);
return () => {
window.removeEventListener("focus", handleFocus);
window.removeEventListener("click", handleClick);
};
}, [select]);
return selectedInstance;
};
+101 -25
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo } from "react";
import ObjectId from "bson-objectid";
import {
type InstanceProps,
@@ -19,9 +19,19 @@ import {
findInstanceById,
} from "~/shared/tree-utils";
import store from "immerhin";
import { DropData, type SelectedInstanceData } from "~/shared/component";
import { useSelectedInstance, useSelectedElement } from "./nano-states";
import {
DropData,
HoveredInstanceData,
type SelectedInstanceData,
} from "~/shared/canvas-components";
import {
useSelectedInstance,
useSelectedElement,
useHoveredElement,
useHoveredInstance,
} from "./nano-states";
import { rootInstanceContainer, useRootInstance } from "~/shared/nano-states";
import { useMeasure } from "~/shared/dom-hooks";
export const usePopulateRootInstance = (tree: Tree) => {
const [, setRootInstance] = useRootInstance();
@@ -112,11 +122,7 @@ export const useDeleteInstance = () => {
);
};
export const usePublishSelectedInstance = ({
treeId,
}: {
treeId: Tree["id"];
}) => {
export const usePublishSelectedInstanceData = (treeId: Tree["id"]) => {
const [instance] = useSelectedInstance();
const [selectedElement] = useSelectedElement();
const [allUserProps] = useAllUserProps();
@@ -146,7 +152,6 @@ export const usePublishSelectedInstance = ({
props,
};
}
publish<"selectInstance", SelectedInstanceData>({
type: "selectInstance",
payload,
@@ -154,24 +159,21 @@ export const usePublishSelectedInstance = ({
}, [instance, allUserProps, treeId, browserStyle]);
};
/**
* We need to set the selected instance after a any root instance update,
* because anything that we change on the selected instance is actually done on the root, so
* when we run "undo", root is going to be undone but not the selected instance, unless we update it here.
*/
export const useUpdateSelectedInstance = () => {
const [rootInstance] = useRootInstance();
const [selectedInstance, setSelectedInstance] = useSelectedInstance();
export const usePublishHoveredInstanceData = () => {
const [instance] = useHoveredInstance();
useEffect(() => {
if (rootInstance === undefined || selectedInstance === undefined) return;
const nextSelectedInstance = findInstanceById(
rootInstance,
selectedInstance.id
);
if (nextSelectedInstance === undefined) return;
setSelectedInstance(nextSelectedInstance);
}, [rootInstance, selectedInstance, setSelectedInstance]);
const payload = instance
? {
id: instance.id,
component: instance.component,
}
: undefined;
publish<"hoverInstance", HoveredInstanceData | undefined>({
type: "hoverInstance",
payload,
});
}, [instance]);
};
export const usePublishRootInstance = () => {
@@ -183,3 +185,77 @@ export const usePublishRootInstance = () => {
});
}, [rootInstance]);
};
const publishRect = (rect: DOMRect) => {
publish<"selectedInstanceRect", DOMRect>({
type: "selectedInstanceRect",
payload: rect,
});
};
export const usePublishSelectedInstanceDataRect = () => {
const [element] = useSelectedElement();
const [refCallback, rect] = useMeasure();
useEffect(() => {
// Disconnect observer when there is no element.
refCallback(element ?? null);
}, [element, refCallback]);
useEffect(() => {
if (rect !== undefined) publishRect(rect);
}, [rect]);
};
export const usePublishHoveredInstanceRect = () => {
const [element] = useHoveredElement();
const publishRect = useCallback(() => {
if (element === undefined) return;
publish<"hoveredInstanceRect", DOMRect>({
type: "hoveredInstanceRect",
payload: element.getBoundingClientRect(),
});
}, [element]);
useEffect(publishRect, [publishRect]);
};
export const useSetHoveredInstance = () => {
const [rootInstance] = useRootInstance();
const [hoveredElement] = useHoveredElement();
const [, setHoveredInstance] = useHoveredInstance();
useEffect(() => {
let instance;
if (rootInstance !== undefined && hoveredElement?.id) {
instance = findInstanceById(rootInstance, hoveredElement.id);
}
setHoveredInstance(instance);
}, [rootInstance, hoveredElement, setHoveredInstance]);
};
/**
* We need to set the selected instance after a any root instance update,
* because anything that we change on the selected instance is actually done on the root, so
* when we run "undo", root is going to be undone but not the selected instance, unless we update it here.
*/
export const useUpdateSelectedInstance = () => {
const [rootInstance] = useRootInstance();
const [selectedInstance, setSelectedInstance] = useSelectedInstance();
// When selected instance or root instance changes - we want to make sure the instance with that id still exists in the root.
useEffect(() => {
let instance;
if (rootInstance !== undefined && selectedInstance?.id) {
instance = findInstanceById(rootInstance, selectedInstance.id);
}
// When it's a new inserted instance, it will be undefined, so we can't set it to undefined and remove it.
if (instance !== undefined) setSelectedInstance(instance);
}, [rootInstance, selectedInstance, setSelectedInstance]);
};
export const useUnselectInstance = () => {
const [, setSelectedInstance] = useSelectedInstance();
useSubscribe("unselectInstance", () => {
setSelectedInstance(undefined);
});
};
+12 -2
View File
@@ -1,6 +1,6 @@
import { createValueContainer, useValue } from "react-nano-state";
import { type Instance } from "@webstudio-is/sdk";
import { type DropData } from "~/shared/component";
import { type DropData } from "~/shared/canvas-components";
const dropDataContainer = createValueContainer<DropData | undefined>();
export const useDropData = () => useValue(dropDataContainer);
@@ -10,5 +10,15 @@ export const selectedInstanceContainer = createValueContainer<
>();
export const useSelectedInstance = () => useValue(selectedInstanceContainer);
const selectedElementContainer = createValueContainer<Element | undefined>();
export const hoveredInstanceContainer = createValueContainer<
Instance | undefined
>();
export const useHoveredInstance = () => useValue(hoveredInstanceContainer);
const selectedElementContainer = createValueContainer<
HTMLElement | undefined
>();
export const useSelectedElement = () => useValue(selectedElementContainer);
const hoveredElementContainer = createValueContainer<HTMLElement | undefined>();
export const useHoveredElement = () => useValue(hoveredElementContainer);
+1 -1
View File
@@ -1,6 +1,6 @@
import store from "immerhin";
import { useSubscribe } from "@webstudio-is/sdk";
import { type StyleUpdates } from "~/shared/component";
import { type StyleUpdates } from "~/shared/canvas-components";
import { setInstanceStyleMutable } from "~/shared/tree-utils";
import { useSelectedInstance } from "./nano-states";
import { rootInstanceContainer } from "~/shared/nano-states";
+19 -9
View File
@@ -1,27 +1,35 @@
import { useState } from "react";
import memoize from "lodash.memoize";
import { publish, useSubscribe } from "@webstudio-is/sdk";
import { type DragData, type DropData } from "~/shared/component";
import { type DragData, type DropData } from "~/shared/canvas-components";
import { findInstanceById } from "~/shared/tree-utils";
import {
findClosestChild,
findInsertionIndex,
getDragOverInfo,
} from "~/shared/dom-utils";
import { useDropData, useSelectedInstance } from "./nano-states";
import {
useDropData,
useHoveredElement,
useHoveredInstance,
useSelectedInstance,
} from "./nano-states";
import { useRootInstance } from "~/shared/nano-states";
import memoize from "lodash.memoize";
//import {usePointerOutline} from './use-pointer-outline'
// Avoid recalculating rects for each node during dragging.
const getBoundingClientRect = memoize((element) =>
const getBoundingClientRect = memoize((element: Element | HTMLElement) =>
element.getBoundingClientRect()
);
const getComputedStyle = memoize((element) => window.getComputedStyle(element));
const getComputedStyle = memoize((element: Element | HTMLElement) =>
window.getComputedStyle(element)
);
export const useDragDropHandlers = () => {
const [rootInstance] = useRootInstance();
const [, setSelectedInstance] = useSelectedInstance();
const [, setHoveredInstance] = useHoveredInstance();
const [, setHoveredElement] = useHoveredElement();
const [dropData, setDropData] = useDropData();
const [dragData, setDragData] = useState<DragData>();
@@ -87,20 +95,22 @@ export const useDragDropHandlers = () => {
getComputedStyle
);
let insertionIndex = 0;
let position = 0;
// When element has children.
if (dragOver.element !== undefined && closestChild !== undefined) {
insertionIndex = findInsertionIndex(dragOver, closestChild);
position = findInsertionIndex(dragOver, closestChild);
}
const dropData = {
instance: dropInstance,
position: insertionIndex,
position,
};
setDragData(dragData);
setDropData(dropData);
setHoveredInstance(dropInstance);
setHoveredElement(dragOver.element);
publish<"dropPreview", { dropData: DropData; dragData: DragData }>({
type: "dropPreview",
payload: { dropData, dragData },
@@ -0,0 +1,36 @@
import { useEffect } from "react";
import { useSelectedElement, useHoveredElement } from "./nano-states";
import { useRootInstance } from "~/shared/nano-states";
const eventOptions = {
passive: true,
};
export const useTrackHoveredElement = () => {
const [rootInstance] = useRootInstance();
const [, setHoveredElement] = useHoveredElement();
const [selectedElement] = useSelectedElement();
useEffect(() => {
const handleMouseOver = (event: MouseEvent) => {
const element = event.target;
if (rootInstance === undefined || !(element instanceof HTMLElement)) {
return;
}
setHoveredElement(element);
};
const handleMouseOut = () => {
if (rootInstance === undefined) return;
setHoveredElement(undefined);
};
window.addEventListener("mouseover", handleMouseOver, eventOptions);
window.addEventListener("mouseout", handleMouseOut, eventOptions);
return () => {
window.removeEventListener("mouseover", handleMouseOver);
window.removeEventListener("mouseout", handleMouseOut);
};
}, [rootInstance, selectedElement, setHoveredElement]);
};
@@ -0,0 +1,63 @@
import { useCallback, useEffect } from "react";
import { type Instance, publish, useSubscribe } from "@webstudio-is/sdk";
import { useSelectedElement, useSelectedInstance } from "./nano-states";
import { useRootInstance } from "~/shared/nano-states";
import { findInstanceById } from "~/shared/tree-utils";
const eventOptions = {
passive: true,
};
export const useTrackSelectedElement = () => {
const [selectedElement, setSelectedElement] = useSelectedElement();
const [selectedInstance, setSelectedInstance] = useSelectedInstance();
const [rootInstance] = useRootInstance();
const selectInstance = useCallback(
(id) => {
if (rootInstance === undefined) return;
const instance = findInstanceById(rootInstance, id);
setSelectedInstance(instance);
},
[setSelectedInstance, rootInstance]
);
const focusAndSelect = useCallback(
(id: Instance["id"]) => {
const element = document.getElementById(id);
if (element === null) return;
element.focus();
setSelectedElement(element);
},
[setSelectedElement]
);
useSubscribe("selectInstance", focusAndSelect);
// Focus and select the element when selected instance changes
useEffect(() => {
if (
selectedInstance !== undefined &&
(selectedElement === undefined ||
selectedInstance?.id !== selectedElement.id)
) {
focusAndSelect(selectedInstance.id);
}
}, [selectedInstance, selectedElement, focusAndSelect]);
useEffect(() => {
const handleClick = (event: MouseEvent) => {
// Notify in general that document was clicked
// e.g. to hide the side panel
publish<"clickCanvas">({ type: "clickCanvas" });
if (event.target instanceof HTMLElement) {
selectInstance(event.target.id);
}
};
window.addEventListener("click", handleClick, eventOptions);
return () => {
window.removeEventListener("click", handleClick);
};
}, [selectInstance]);
};
+18 -7
View File
@@ -3,16 +3,18 @@ import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { type Project, useSubscribe, usePublish } from "@webstudio-is/sdk";
import type { Config } from "~/config";
import type { SelectedInstanceData } from "~/shared/component";
import type {
HoveredInstanceData,
SelectedInstanceData,
} from "~/shared/canvas-components";
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 { Inspector } from "./features/inspector";
import { CanvasIframe } from "./shared/canvas-iframe";
import {
useHoveredInstanceData,
useSelectedInstanceData,
useSyncStatus,
useCanvasWidth,
} from "./shared/nano-states";
import { Topbar } from "./features/topbar";
import designerStyles from "./designer.css";
@@ -22,7 +24,11 @@ import {
useUpdateCanvasWidth,
useSubscribeBreakpoints,
} from "./features/breakpoints";
import { useReadCanvasRect, Workspace } from "./features/workspace";
import {
useReadCanvasRect,
Workspace,
CanvasIframe,
} from "./features/workspace";
import { usePublishShortcuts } from "./shared/shortcuts";
import { type SyncStatus } from "~/shared/sync";
import { useIsPreviewMode, useRootInstance } from "~/shared/nano-states";
@@ -47,6 +53,11 @@ const useSubscribeSelectedInstanceData = () => {
);
};
const useSubscribeHoveredInstanceData = () => {
const [, setValue] = useHoveredInstanceData();
useSubscribe<"hoverInstance", HoveredInstanceData>("hoverInstance", setValue);
};
const useSubscribeSyncStatus = () => {
const [, setValue] = useSyncStatus();
useSubscribe<"syncStatus", SyncStatus>("syncStatus", setValue);
@@ -156,12 +167,12 @@ export const Designer = ({ config, project }: DesignerProps) => {
useSubscribeSyncStatus();
useSubscribeRootInstance();
useSubscribeSelectedInstanceData();
useSubscribeHoveredInstanceData();
useSubscribeBreakpoints();
const [publish, publishRef] = usePublish();
const [isPreviewMode] = useIsPreviewMode();
const [isDragging, setIsDragging] = useIsDragging();
usePublishShortcuts(publish);
const [canvasWidth] = useCanvasWidth();
const onRefReadCanvasWidth = useUpdateCanvasWidth();
const { onRef: onRefReadCanvas, onTransitionEnd } = useReadCanvasRect();
@@ -187,7 +198,7 @@ export const Designer = ({ config, project }: DesignerProps) => {
publish={publish}
/>
<Main>
<Workspace onTransitionEnd={onTransitionEnd}>
<Workspace onTransitionEnd={onTransitionEnd} publish={publish}>
<CanvasIframe
ref={iframeRefCallback}
src={`${config.canvasPath}/${project.id}`}
@@ -195,8 +206,8 @@ export const Designer = ({ config, project }: DesignerProps) => {
title={project.title}
css={{
height: "100%",
width: "100%",
}}
style={{ width: canvasWidth }}
/>
</Workspace>
<Breadcrumbs publish={publish} />
@@ -48,8 +48,8 @@ export const Breadcrumbs = ({ publish }: BreadcrumbsProps) => {
key={instance.id}
component={instance.component}
onClick={() => {
publish<"focusElement", Instance["id"]>({
type: "focusElement",
publish<"selectInstance", Instance["id"]>({
type: "selectInstance",
payload: instance.id,
});
}}
@@ -2,7 +2,7 @@ import { type UserProp, type Publish } from "@webstudio-is/sdk";
import { CollapsibleSection, ComponentInfo } from "~/designer/shared/inspector";
import { TextField, Flex, Button, Box } from "~/shared/design-system";
import { PlusIcon, TrashIcon } from "~/shared/icons";
import type { SelectedInstanceData } from "~/shared/component";
import type { SelectedInstanceData } from "~/shared/canvas-components";
import { usePropsLogic } from "./use-props-logic";
type PropertyProps = {
@@ -8,7 +8,7 @@ import type {
UserPropsUpdates,
Publish,
} from "@webstudio-is/sdk";
import type { SelectedInstanceData } from "~/shared/component";
import type { SelectedInstanceData } from "~/shared/canvas-components";
const getInitialProp = () => ({
id: ObjectId().toString(),
@@ -1,6 +1,6 @@
import { type Instance } from "@webstudio-is/sdk";
import { forwardRef, type ElementRef, type ComponentProps } from "react";
import { primitives } from "~/shared/component";
import { primitives } from "~/shared/canvas-components";
import { Flex, Text, styled } from "~/shared/design-system";
const Thumb = styled(Flex, {
@@ -4,7 +4,7 @@ import { getEmptyImage } from "react-dnd-html5-backend";
import { type Instance, type Publish } from "@webstudio-is/sdk";
import { Flex } from "~/shared/design-system";
import { PlusIcon } from "~/shared/icons";
import { primitives, type DragData } from "~/shared/component";
import { primitives, type DragData } from "~/shared/canvas-components";
import { createInstance } from "~/shared/tree-utils";
import type { TabName } from "../../types";
import { CustomDragLayer } from "./custom-drag-layer";
@@ -1,7 +1,7 @@
import { type Instance, type Publish } from "@webstudio-is/sdk";
import { ListNestedIcon } from "~/shared/icons";
import { Flex } from "~/shared/design-system";
import { type SelectedInstanceData } from "~/shared/component";
import { type SelectedInstanceData } from "~/shared/canvas-components";
import { useSelectedInstancePath } from "~/designer/shared/instance/use-selected-instance-path";
import { Tree } from "~/designer/shared/tree";
import { useRootInstance } from "~/shared/nano-states";
@@ -29,8 +29,8 @@ export const TabContent = ({
selectedInstancePath={selectedInstancePath}
selectedInstanceId={selectedInstanceData?.id}
onSelect={(instance) => {
publish<"focusElement", Instance["id"]>({
type: "focusElement",
publish<"selectInstance", Instance["id"]>({
type: "selectInstance",
payload: instance.id,
});
}}
@@ -14,7 +14,7 @@ import { renderProperty } from "./render-property";
import { dependencies } from "./dependencies";
import { type InheritedStyle } from "./get-inherited-style";
import { type SetProperty } from "./use-style-data";
import { type SelectedInstanceData } from "~/shared/component";
import { type SelectedInstanceData } from "~/shared/canvas-components";
// Finds a property/value by using any available form: property, label, value
const filterProperties = (properties: Array<string>, search: string) => {
@@ -1,7 +1,7 @@
import type { Publish } from "@webstudio-is/sdk";
import { willRender } from "~/designer/shared/breakpoints";
import { Box, Card, Paragraph } from "~/shared/design-system";
import type { SelectedInstanceData } from "~/shared/component";
import type { SelectedInstanceData } from "~/shared/canvas-components";
import { useStyleData } from "./use-style-data";
import { ComponentInfo } from "../../shared/inspector";
import { VisualSettings } from "./settings";
@@ -1,5 +1,5 @@
import { useState, useMemo, useEffect, useCallback } from "react";
import type { SelectedInstanceData, StyleUpdates } from "~/shared/component";
import type { SelectedInstanceData, StyleUpdates } from "~/shared/canvas-components";
import { type StyleProperty, type Publish } from "@webstudio-is/sdk";
import { useSelectedBreakpoint } from "../../shared/nano-states";
import { parseCssValue } from "./parse-css-value";
@@ -2,7 +2,7 @@ import { Instance, useSubscribe } from "@webstudio-is/sdk";
import produce from "immer";
import { useState } from "react";
import { Tree } from "~/designer/shared/tree";
import { type DragData, type DropData } from "~/shared/component";
import { type DragData, type DropData } from "~/shared/canvas-components";
import { Flex } from "~/shared/design-system";
import { useRootInstance } from "~/shared/nano-states";
import {
@@ -0,0 +1,26 @@
import { Box } from "~/shared/design-system";
import { useIsPreviewMode } from "~/shared/nano-states";
import { HoveredInstanceOutline, SelectedInstanceOutline } from "./outline";
import { useSubscribeInstanceRect } from "./use-subscribe-instance-rect";
const toolsStyle = {
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
pointerEvents: "none",
overflow: "hidden",
};
export const CanvasTools = () => {
useSubscribeInstanceRect();
const [isPreviewMode] = useIsPreviewMode();
if (isPreviewMode) return null;
return (
<Box css={toolsStyle}>
<SelectedInstanceOutline />
<HoveredInstanceOutline />
</Box>
);
};
@@ -0,0 +1 @@
export { CanvasTools } from "./canvas-tools";
@@ -0,0 +1,42 @@
import { useMemo } from "react";
import { useHoveredInstanceRect } from "~/shared/nano-states";
import {
useHoveredInstanceData,
useSelectedInstanceData,
} from "~/designer/shared/nano-states";
import { Outline } from "./outline";
import { Label } from "./label";
const useStyle = (rect?: DOMRect) => {
return useMemo(() => {
if (rect === undefined) return;
return {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
};
}, [rect]);
};
export const HoveredInstanceOutline = () => {
const [selectedInstanceData] = useSelectedInstanceData();
const [instanceRect] = useHoveredInstanceRect();
const style = useStyle(instanceRect);
const [instanceData] = useHoveredInstanceData();
if (
style === undefined ||
instanceData === undefined ||
instanceRect === undefined ||
selectedInstanceData?.id === instanceData.id
) {
return null;
}
return (
<Outline style={style}>
<Label component={instanceData.component} instanceRect={instanceRect} />
</Outline>
);
};
@@ -0,0 +1,2 @@
export { SelectedInstanceOutline } from "./selected-instance-outline";
export { HoveredInstanceOutline } from "./hovered-instance-outline";
@@ -0,0 +1,86 @@
import { useCallback, useState } from "react";
import { styled } from "~/shared/design-system";
import { primitives } from "~/shared/canvas-components";
import { type Instance } from "@webstudio-is/sdk";
type LabelPosition = "top" | "inside" | "bottom";
type LabelRefCallback = (element: HTMLElement | null) => void;
/**
* Detects if there is no space on top and for the label and tells to show it inside.
* - if there is enough space for the label on top of the instance - top
* - else if instance height is more than 250px - bottom
* - else inside-top - last resort because it covers a bit of the instance content
*/
const useLabelPosition = (
instanceRect: DOMRect
): [LabelRefCallback, LabelPosition] => {
const [position, setPosition] = useState<LabelPosition>("top");
const ref = useCallback(
(element) => {
if (element === null || instanceRect === undefined) return;
const labelRect = element.getBoundingClientRect();
let nextPosition: LabelPosition = "top";
if (labelRect.height > instanceRect.top) {
nextPosition = instanceRect.height < 250 ? "bottom" : "inside";
}
setPosition(nextPosition);
},
[instanceRect]
);
return [ref, position];
};
const LabelContainer = styled(
"div",
{
position: "absolute",
display: "flex",
padding: "0 $1",
height: "$4",
color: "$hiContrast",
alignItems: "center",
justifyContent: "center",
gap: "$1",
fontSize: "$2",
fontFamily: "$sans",
lineHeight: 1,
minWidth: "$6",
whiteSpace: "nowrap",
backgroundColor: "$blue9",
},
{
variants: {
position: {
top: {
top: "-$4",
},
inside: {
top: 0,
},
bottom: {
bottom: "-$4",
},
},
},
}
);
type LabelProps = {
component: Instance["component"];
instanceRect: DOMRect;
};
export const Label = ({ component, instanceRect }: LabelProps) => {
const [labelRef, position] = useLabelPosition(instanceRect);
const primitive = primitives[component];
const { Icon } = primitive;
return (
<LabelContainer position={position} ref={labelRef}>
<Icon width="1em" height="1em" />
{primitive.label}
</LabelContainer>
);
};
@@ -0,0 +1,10 @@
import { styled } from "~/shared/design-system";
export const Outline = styled("div", {
position: "absolute",
pointerEvents: "none",
outline: "2px solid $blue9",
outlineOffset: -2,
top: 0,
left: 0,
});
@@ -0,0 +1,37 @@
import { useMemo } from "react";
import { useSelectedInstanceRect } from "~/shared/nano-states";
import { useSelectedInstanceData } from "~/designer/shared/nano-states";
import { Outline } from "./outline";
import { Label } from "./label";
const useStyle = (rect?: DOMRect) => {
return useMemo(() => {
if (rect === undefined) return;
return {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
};
}, [rect]);
};
export const SelectedInstanceOutline = () => {
const [instanceRect] = useSelectedInstanceRect();
const style = useStyle(instanceRect);
const [instanceData] = useSelectedInstanceData();
if (
style === undefined ||
instanceData === undefined ||
instanceRect === undefined
) {
return null;
}
return (
<Outline style={style}>
<Label component={instanceData.component} instanceRect={instanceRect} />
</Outline>
);
};
@@ -0,0 +1,18 @@
import { useSubscribe } from "@webstudio-is/sdk";
import {
useSelectedInstanceRect,
useHoveredInstanceRect,
} from "~/shared/nano-states";
export const useSubscribeInstanceRect = () => {
const [, setSelectedRect] = useSelectedInstanceRect();
useSubscribe<"selectedInstanceRect", DOMRect>(
"selectedInstanceRect",
setSelectedRect
);
const [, setHoveredRect] = useHoveredInstanceRect();
useSubscribe<"hoveredInstanceRect", DOMRect>(
"hoveredInstanceRect",
setHoveredRect
);
};
+1
View File
@@ -1,2 +1,3 @@
export * from "./workspace";
export * from "./use-read-canvas-rect";
export * from "./canvas-iframe";
@@ -1,29 +1,41 @@
import { useCallback, useEffect, useRef } from "react";
import { useCanvasRect } from "~/designer/shared/nano-states";
import { useCallback, useEffect, useState } from "react";
import { useZoom } from "~/designer/shared/nano-states";
import { useCanvasRect, useCanvasWidth } from "~/designer/shared/nano-states";
import { useWindowResize } from "~/shared/dom-hooks";
/**
* Reads the canvas iframe dom rect and puts it into nano state
* so that this is the only place we do it.
*/
export const useReadCanvasRect = () => {
const iframeRef = useRef<HTMLIFrameElement>();
const [iframeElement, setIframeElement] = useState<HTMLIFrameElement | null>(
null
);
const [, setCanvasRect] = useCanvasRect();
const [canvasWidth] = useCanvasWidth();
const [zoom] = useZoom();
const [recalcFlag, forceRecalc] = useState(false);
useWindowResize(() => {
forceRecalc(!recalcFlag);
});
const handleRef = useCallback((iframe: HTMLIFrameElement | null) => {
if (iframe === null || iframeRef.current === iframe) return;
iframeRef.current = iframe;
}, []);
const readRect = useCallback(() => {
if (iframeRef.current === undefined) return;
const rect = iframeRef.current.getBoundingClientRect();
setCanvasRect(rect);
}, [setCanvasRect]);
const readRect = useCallback(
() => {
if (iframeElement === null) return;
requestAnimationFrame(() => {
const rect = iframeElement.getBoundingClientRect();
setCanvasRect(rect);
});
},
// canvasWidth will change the canvas width, so we need to React it to it and update the rect, even though we don't necessary usenthe value
// eslint-disable-next-line react-hooks/exhaustive-deps
[iframeElement, setCanvasRect, canvasWidth, zoom, recalcFlag]
);
useEffect(readRect, [readRect]);
return {
onRef: handleRef,
onRef: setIframeElement,
onTransitionEnd: readRect,
};
};
+40 -20
View File
@@ -1,38 +1,58 @@
import { Box, Flex } from "~/shared/design-system";
import { useZoom } from "~/designer/shared/nano-states";
import { useCanvasWidth, useZoom } from "~/designer/shared/nano-states";
import { CanvasTools } from "./canvas-tools";
import { type Publish } from "@webstudio-is/sdk";
const workspaceStyle = {
flexGrow: 1,
background: "$gray8",
overflow: "auto",
scrollbarGutter: "stable",
};
const zoomStyle = {
transformStyle: "preserve-3d",
transition: "transform 200ms ease-out",
height: "100%",
width: "100%",
};
const canvasContainerStyle = {
position: "relative",
height: "100%",
};
type WorkspaceProps = {
children: JSX.Element;
onTransitionEnd: () => void;
publish: Publish;
};
export const Workspace = ({ children, onTransitionEnd }: WorkspaceProps) => {
export const Workspace = ({
children,
onTransitionEnd,
publish,
}: WorkspaceProps) => {
const [zoom] = useZoom();
const [canvasWidth] = useCanvasWidth();
const handleWorkspaceClick = () => {
publish<"unselectInstance">({ type: "unselectInstance" });
};
return (
<Box
css={{
flexGrow: 1,
background: "$gray8",
overflow: "auto",
scrollbarGutter: "stable",
}}
>
<Box css={workspaceStyle} onClick={handleWorkspaceClick}>
<Flex
direction="column"
align="center"
css={{
transformStyle: "preserve-3d",
transition: "transform 200ms ease-out",
height: "100%",
width: "100%",
}}
style={{
transform: `scale(${zoom / 100})`,
}}
css={zoomStyle}
style={{ transform: `scale(${zoom / 100})` }}
onTransitionEnd={onTransitionEnd}
>
{children}
<Box css={canvasContainerStyle} style={{ width: canvasWidth }}>
{children}
<CanvasTools />
</Box>
</Flex>
</Box>
);
@@ -1 +0,0 @@
export { CanvasIframe } from "./canvas-iframe";
@@ -1,6 +1,6 @@
import { Flex, Text } from "~/shared/design-system";
import type { SelectedInstanceData } from "~/shared/component";
import { primitives } from "~/shared/component";
import type { SelectedInstanceData } from "~/shared/canvas-components";
import { primitives } from "~/shared/canvas-components";
export const ComponentInfo = ({
selectedInstanceData,
+10 -1
View File
@@ -1,6 +1,9 @@
import { createValueContainer, useValue } from "react-nano-state";
import { type Breakpoint } from "@webstudio-is/sdk";
import { SelectedInstanceData } from "~/shared/component";
import {
type SelectedInstanceData,
type HoveredInstanceData,
} from "~/shared/canvas-components";
import { type SyncStatus } from "~/shared/sync";
const selectedInstanceDataContainer = createValueContainer<
@@ -9,6 +12,12 @@ const selectedInstanceDataContainer = createValueContainer<
export const useSelectedInstanceData = () =>
useValue(selectedInstanceDataContainer);
const hoveredInstanceDataContainer = createValueContainer<
HoveredInstanceData | undefined
>();
export const useHoveredInstanceData = () =>
useValue(hoveredInstanceDataContainer);
const isShareDialogOpenContainer = createValueContainer<boolean>(false);
export const useIsShareDialogOpen = () => useValue(isShareDialogOpenContainer);
+1 -1
View File
@@ -9,7 +9,7 @@ import {
styled,
} from "~/shared/design-system";
import { TriangleRightIcon, TriangleDownIcon } from "~/shared/icons";
import { primitives } from "~/shared/component";
import { primitives } from "~/shared/canvas-components";
import noop from "lodash.noop";
const openKeyframes = keyframes({
File renamed without changes.
File renamed without changes.
@@ -16,6 +16,11 @@ export type SelectedInstanceData = {
props: InstanceProps;
};
export type HoveredInstanceData = {
id: Instance["id"];
component: Instance["component"];
}
type StyleUpdate = {
property: StyleProperty;
value: StyleValue;
@@ -3,11 +3,6 @@ export { SquareIcon as Icon } from "~/shared/icons";
// Webstudio specific default styles for the node type.
export const defaultStyle: Style = {
minHeight: {
type: "unit",
unit: "px",
value: 100,
},
boxSizing: {
type: "keyword",
value: "border-box",
@@ -2,13 +2,7 @@ import { type Style, components } from "@webstudio-is/sdk";
export { HeadingIcon as Icon } from "~/shared/icons";
// Webstudio specific default styles for the node type.
export const defaultStyle: Style = {
minHeight: {
type: "unit",
unit: "em",
value: 1,
},
};
export const defaultStyle: Style = {};
export const children = ["Heading you can edit"];
@@ -2,13 +2,7 @@ import { type Style, components } from "@webstudio-is/sdk";
export { TextAlignLeftIcon as Icon } from "~/shared/icons";
// Webstudio specific default styles for the node type.
export const defaultStyle: Style = {
minHeight: {
type: "unit",
unit: "em",
value: 1,
},
};
export const defaultStyle: Style = {};
export const children = ["Pragraph you can edit"];
+2 -2
View File
@@ -34,8 +34,8 @@ export const createRootInstance = (breakpoints: Array<Breakpoint>) => {
},
lineHeight: {
type: "unit",
unit: "px",
value: 20,
unit: "number",
value: 1.5,
},
color: {
type: "keyword",
+2
View File
@@ -0,0 +1,2 @@
export * from "./use-window-resize";
export * from "./use-measure";
+42
View File
@@ -0,0 +1,42 @@
// This hook is based on https://github.com/streamich/react-use/blob/master/src/useMeasure.ts
// The problem is that contentRect has wrong x/y values for absolutely positioned element.
// We have to use getBoundingClientRect instead.
// @todo optimize for the case when many consumers need to measure the same element
import { useEffect, useMemo, useState } from "react";
export type UseMeasureRef<MeasuredElement extends HTMLElement = HTMLElement> = (
element: MeasuredElement | null
) => void;
export type UseMeasureResult<
MeasuredElement extends HTMLElement = HTMLElement
> = [UseMeasureRef<MeasuredElement>, DOMRect | undefined];
export const useMeasure = <
MeasuredElement extends HTMLElement = HTMLElement
>(): UseMeasureResult<MeasuredElement> => {
const [element, ref] = useState<MeasuredElement | null>(null);
const [rect, setRect] = useState<DOMRect>();
const observer = useMemo(() => {
if (element === null || typeof window === "undefined") return;
return new window.ResizeObserver(() => {
if (element === null) return;
requestAnimationFrame(() => {
setRect(element.getBoundingClientRect());
});
});
}, [element]);
useEffect(() => {
if (observer) {
if (element === null) observer.disconnect();
else observer.observe(element);
}
return () => {
observer?.disconnect();
};
}, [element, observer]);
return [ref, rect];
};
+28
View File
@@ -0,0 +1,28 @@
import { useEffect } from "react";
import mitt from "mitt";
const emitter = mitt();
if (typeof window === "object") {
window.addEventListener(
"resize",
() => {
emitter.emit("resize");
},
false
);
}
/**
* Subscribe to DOM resize event only once and then notify all listeners over js only.
* This allows us to subscribe to resize by many listeners without perf issues, since its going to be the same resize event.
* TODO: We can add throttling and RAF if needed.
*/
export const useWindowResize = (onResize: () => void) => {
useEffect(() => {
emitter.on("resize", onResize);
return () => {
emitter.off("resize", onResize);
};
}, [onResize]);
};
+6 -1
View File
@@ -1,5 +1,10 @@
import type { Coordinate, DragOverInfo } from "./types";
const elementFromPoint = (coordinate: Coordinate): HTMLElement | undefined => {
const element = document.elementFromPoint(coordinate.x, coordinate.y);
if (element instanceof HTMLElement) return element;
};
/**
* Get element we are hovering over from coordinate.
* When nearing to an edge of an element - return the parent.
@@ -8,7 +13,7 @@ export const getDragOverInfo = (
offset: Coordinate,
getBoundingClientRect: (element: Element) => DOMRect
): DragOverInfo => {
let element = document.elementFromPoint(offset.x, offset.y) || undefined;
let element = elementFromPoint(offset);
let edge: DragOverInfo["edge"] = "none";
if (element === undefined) return { element, edge };
+1 -1
View File
@@ -1,6 +1,6 @@
export type Coordinate = { x: number; y: number };
export type DragOverInfo = {
element: Element | undefined;
element: HTMLElement | undefined;
edge: "top" | "bottom" | "none";
};
export type ClosestChildInfo = {
+12
View File
@@ -11,3 +11,15 @@ export const useBreakpoints = () => useValue(breakpointsContainer);
const isPreviewModeContainer = createValueContainer<boolean>(false);
export const useIsPreviewMode = () => useValue(isPreviewModeContainer);
const selectedInstanceRectContainer = createValueContainer<
DOMRect | undefined
>();
export const useSelectedInstanceRect = () =>
useValue(selectedInstanceRectContainer);
const hoveredInstanceRectContainer = createValueContainer<
DOMRect | undefined
>();
export const useHoveredInstanceRect = () =>
useValue(hoveredInstanceRectContainer);
+1 -1
View File
@@ -1,6 +1,6 @@
import ObjectId from "bson-objectid";
import { type CssRule, type Instance } from "@webstudio-is/sdk";
import { primitives } from "~/shared/component";
import { primitives } from "~/shared/canvas-components";
export const createInstance = ({
component,
+1
View File
@@ -1,5 +1,6 @@
import { type Instance } from "@webstudio-is/sdk";
// @todo should be optimized because it will be a frequent operation on a potentially large tree.
export const findInstanceById = (
instance: Instance,
id: Instance["id"]
+1 -1
View File
@@ -1,5 +1,5 @@
import { type Instance } from "@webstudio-is/sdk";
import { primitives } from "~/shared/component";
import { primitives } from "~/shared/canvas-components";
/**
* Populate instance with:
+1 -1
View File
@@ -1,5 +1,5 @@
import { Breakpoint, type Instance } from "@webstudio-is/sdk";
import { type StyleUpdates } from "~/shared/component";
import { type StyleUpdates } from "~/shared/canvas-components";
import { findInstanceById } from "./find-instance";
export const setInstanceStyleMutable = (
+2
View File
@@ -59,6 +59,7 @@
"lodash.merge": "^4.6.2",
"lodash.noop": "^3.0.1",
"lodash.snakecase": "^4.1.1",
"mitt": "^3.0.0",
"nanoid": "^3.2.0",
"react": "^17.0.2",
"react-color": "^2.19.3",
@@ -105,6 +106,7 @@
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.3.0",
"jest": "^27.5.1",
"prettier": "^2.6.2",
"prisma": "^3.14.0",
"relative-deps": "^1.0.7",
"typescript": "^4.1.2"
+5
View File
@@ -11347,6 +11347,11 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==
prettier@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032"
integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==
pretty-error@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6"