mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 13:05:08 +09:00
Merge branch 'main' of github.com:webstudio-is/webstudio-designer into auth
This commit is contained in:
commit
d34b2bd0eb
66 files changed
+717
-271
No files matched your search
+16
-4
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
};
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
File renamed without changes.
@@ -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,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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
File renamed without changes.
-5
@@ -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",
|
||||
File renamed without changes.
File renamed without changes.
+1
-7
@@ -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"];
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-7
@@ -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"];
|
||||
|
||||
File renamed without changes.
@@ -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",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./use-window-resize";
|
||||
export * from "./use-measure";
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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,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 = {
|
||||
|
||||
@@ -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,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,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,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,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 = (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user