handle hover outline

This commit is contained in:
Oleg Isonen committed 2022-06-13 15:01:39 +02:00
1 parent 2cdf649cc9
commit 673c94e3ce
17 files changed
+231 -112

No files matched your search

+8 -4
View File
@@ -22,10 +22,12 @@ import {
useInsertInstance,
useDeleteInstance,
useReparentInstance,
usePublishSelectedInstance,
usePublishSelectedInstanceData,
usePublishRootInstance,
useUpdateSelectedInstance,
usePublishSlectedInstanceRect,
usePublishSelectedInstanceDataRect,
usePublishHoveredInstanceRect,
usePublishHoveredInstanceData,
} from "./shared/instance";
import { useUpdateStyle } from "./shared/style";
import { useActiveElementTracking } from "./shared/active-element";
@@ -97,7 +99,8 @@ const DesignMode = ({ treeId, project }: DesignModeProps) => {
useDragDropHandlers();
useUpdateStyle();
useManageProps();
usePublishSelectedInstance({ treeId });
usePublishSelectedInstanceData(treeId);
usePublishHoveredInstanceData()
useHandleBreakpoints();
useInsertInstance();
useReparentInstance();
@@ -106,7 +109,8 @@ const DesignMode = ({ treeId, project }: DesignModeProps) => {
useActiveElementTracking();
useSync({ project });
useUpdateSelectedInstance();
usePublishSlectedInstanceRect();
usePublishSelectedInstanceDataRect();
usePublishHoveredInstanceRect()
const elements = useElementsTree();
return (
// Using touch backend becuase html5 drag&drop doesn't fire drag events in our case
@@ -1,20 +0,0 @@
import { useMemo, type MouseEvent } from "react";
import { useHoveredElement } from "~/canvas/shared/nano-states";
export const useSetHoveredInstance = () => {
const [, setHoveredElement] = useHoveredElement();
return useMemo(
() => ({
onMouseOver(event: MouseEvent<HTMLElement>) {
if (event.target instanceof HTMLElement) {
setHoveredElement(event.target);
}
},
onMouseOut() {
setHoveredElement(undefined);
},
}),
[setHoveredElement]
);
};
@@ -11,7 +11,6 @@ import { useContentEditable } from "./editable";
import { useCss } from "./use-css";
import { useDraggable } from "./use-draggable";
import { useEnsureFocus } from "./use-ensure-focus";
import { useSetHoveredInstance } from "./use-set-hovered-instance";
import { useOutline } from "~/canvas/shared/use-outline";
type WrapperComponentDevProps = {
@@ -29,7 +28,6 @@ export const WrapperComponentDev = ({
...rest
}: WrapperComponentDevProps) => {
const className = useCss({ instance, css });
const mouseEnterExitHandlers = useSetHoveredInstance();
const outline = useOutline(instance);
const {
@@ -75,7 +73,6 @@ export const WrapperComponentDev = ({
{...contentEditableProps}
{...draggableProps}
{...readonly}
{...mouseEnterExitHandlers}
className={className}
id={instance.id}
tabIndex={0}
+29 -1
View File
@@ -1,7 +1,7 @@
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 { useSelectedInstance, useSelectedElement, useHoveredElement, useHoveredInstance } from "./nano-states";
import { useRootInstance } from "~/shared/nano-states";
const eventOptions = {
@@ -12,6 +12,8 @@ export const useActiveElementTracking = () => {
const [rootInstance] = useRootInstance();
const [selectedInstance, setSelectedInstance] = useSelectedInstance();
const [selectedElement, setSelectedElement] = useSelectedElement();
const [, setHoveredElement] = useHoveredElement();
const [, setHoveredInstance] = useHoveredInstance();
useSubscribe("focusElement", (id: Instance["id"]) => {
const element = document.getElementById(id);
@@ -68,5 +70,31 @@ export const useActiveElementTracking = () => {
};
}, [select]);
useEffect(() => {
const handleMouseOver = (event: MouseEvent) => {
if (rootInstance === undefined) return
const element = event.target
if (element instanceof HTMLElement) {
setHoveredElement(element);
const instance = findInstanceById(rootInstance, element.id);
setHoveredInstance(instance)
}
}
const handleMouseOut = () => {
if (rootInstance === undefined) return
setHoveredElement(undefined);
setHoveredInstance(undefined)
}
window.addEventListener('mouseover', handleMouseOver, eventOptions)
window.addEventListener('mouseout', handleMouseOut, eventOptions)
return () => {
window.removeEventListener("mouseover", handleMouseOver);
window.removeEventListener("mouseout", handleMouseOut);
}
}, [rootInstance, setHoveredElement, setHoveredInstance])
return selectedInstance;
};
+35 -12
View File
@@ -19,8 +19,8 @@ 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/component";
import { useSelectedInstance, useSelectedElement, useHoveredElement, useHoveredInstance } from "./nano-states";
import { rootInstanceContainer, useRootInstance } from "~/shared/nano-states";
import { useAfterRender } from "./use-after-render";
import { useWindowResize } from "~/shared/dom-hooks";
@@ -114,11 +114,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();
@@ -156,6 +152,21 @@ export const usePublishSelectedInstance = ({
}, [instance, allUserProps, treeId, browserStyle]);
};
export const usePublishHoveredInstanceData = () => {
const [instance] = useHoveredInstance()
useEffect(() => {
if (instance === undefined) return
publish<"hoverInstance", HoveredInstanceData>({
type: "hoverInstance",
payload: {
id: instance.id,
component: instance.component
},
});
}, [instance])
}
/**
* 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
@@ -188,18 +199,30 @@ export const usePublishRootInstance = () => {
}, [rootInstance]);
};
export const usePublishSlectedInstanceRect = () => {
const [selectedElement] = useSelectedElement();
export const usePublishSelectedInstanceDataRect = () => {
const [element] = useSelectedElement();
const publishRect = useCallback(() => {
if (selectedElement === undefined) return;
if (element === undefined) return;
publish<"selectedInstanceRect", DOMRect>({
type: "selectedInstanceRect",
payload: selectedElement.getBoundingClientRect(),
payload: element.getBoundingClientRect(),
});
}, [selectedElement]);
}, [element]);
useAfterRender(publishRect);
useWindowResize(publishRect);
useEffect(publishRect, [publishRect]);
};
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]);
};
+6
View File
@@ -10,6 +10,12 @@ export const selectedInstanceContainer = createValueContainer<
>();
export const useSelectedInstance = () => useValue(selectedInstanceContainer);
export const hoveredInstanceContainer = createValueContainer<
Instance | undefined
>();
export const useHoveredInstance = () => useValue(hoveredInstanceContainer);
const selectedElementContainer = createValueContainer<
HTMLElement | undefined
>();
+11 -2
View File
@@ -3,12 +3,12 @@ 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/component";
import { Box, Flex, Grid, type CSS } from "~/shared/design-system";
import interStyles from "~/shared/font-faces/inter.css";
import { SidebarLeft } from "./features/sidebar-left";
import { Inspector } from "./features/inspector";
import { useSelectedInstanceData, useSyncStatus } from "./shared/nano-states";
import { useHoveredInstanceData, useSelectedInstanceData, useSyncStatus } from "./shared/nano-states";
import { Topbar } from "./features/topbar";
import designerStyles from "./designer.css";
import { Breadcrumbs } from "./features/breadcrumbs";
@@ -46,6 +46,14 @@ const useSubscribeSelectedInstanceData = () => {
);
};
const useSubscribeHoveredInstanceData = () => {
const [, setValue] = useHoveredInstanceData();
useSubscribe<"hoverInstance", HoveredInstanceData>(
"hoverInstance",
setValue
);
};
const useSubscribeSyncStatus = () => {
const [, setValue] = useSyncStatus();
useSubscribe<"syncStatus", SyncStatus>("syncStatus", setValue);
@@ -155,6 +163,7 @@ export const Designer = ({ config, project }: DesignerProps) => {
useSubscribeSyncStatus();
useSubscribeRootInstance();
useSubscribeSelectedInstanceData();
useSubscribeHoveredInstanceData()
useSubscribeBreakpoints();
const [publish, publishRef] = usePublish();
const [isPreviewMode] = useIsPreviewMode();
@@ -1,15 +1,6 @@
import { useSubscribe } from "@webstudio-is/sdk";
import { Box } from "~/shared/design-system";
import { useSelectedInstanceRect } from "~/shared/nano-states";
import { SelectedInstanceOutline } from "./outlines";
const useLink = () => {
const [, setRect] = useSelectedInstanceRect();
useSubscribe<"selectedInstanceRect", DOMRect>(
"selectedInstanceRect",
setRect
);
};
import { HoveredInstanceOutline, SelectedInstanceOutline } from "./outline";
import { useSubscribeInstanceRect } from "./use-subscribe-instance-rect";
const toolsStyle = {
position: "absolute",
@@ -22,10 +13,11 @@ const toolsStyle = {
};
export const CanvasTools = () => {
useLink();
useSubscribeInstanceRect();
return (
<Box css={toolsStyle}>
<SelectedInstanceOutline />
<HoveredInstanceOutline />
</Box>
);
};
@@ -0,0 +1,37 @@
import { useMemo } from "react";
import { useHoveredInstanceRect } from "~/shared/nano-states";
import { useHoveredInstanceData } 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 [instanceRect] = useHoveredInstanceRect();
const style = useStyle(instanceRect);
const [instanceData] = useHoveredInstanceData();
if (
style === undefined ||
instanceData === undefined ||
instanceRect === undefined
) {
return null;
}
return (
<Outline state="selected" 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";
@@ -1,23 +1,8 @@
import { useCallback, useMemo, useState } from "react";
import { useCallback, useState } from "react";
import { styled } from "~/shared/design-system";
import { useSelectedInstanceRect } from "~/shared/nano-states";
import { useSelectedInstanceData } from "~/designer/shared/nano-states";
import { primitives } from "~/shared/component";
import { type Instance } from "@webstudio-is/sdk";
const useSelectedInstanceOutlineStyle = () => {
const [rect] = useSelectedInstanceRect();
return useMemo(() => {
if (rect === undefined) return;
return {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
};
}, [rect]);
};
type LabelPosition = "top" | "inside" | "bottom";
type LabelRefCallback = (element: HTMLElement | null) => void;
@@ -46,30 +31,6 @@ const useLabelPosition = (instanceRect: DOMRect): [LabelRefCallback, LabelPositi
return [ref, position];
};
const Outline = styled(
"div",
{
position: "absolute",
pointerEvents: "none",
outline: "2px solid $blue9",
outlineOffset: -2,
top: 0,
left: 0,
},
{
variants: {
state: {
selected: {
zIndex: "$4",
},
hovered: {
zIndex: "$3",
},
},
},
}
);
const LabelContainer = styled(
"div",
{
@@ -111,7 +72,11 @@ const LabelContainer = styled(
}
);
const Label = ({component, instanceRect}: {component: Instance['component'], instanceRect: DOMRect}) => {
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;
@@ -122,19 +87,3 @@ const Label = ({component, instanceRect}: {component: Instance['component'], ins
</LabelContainer>
);
};
export const SelectedInstanceOutline = () => {
const style = useSelectedInstanceOutlineStyle();
const [selectedInstanceData] = useSelectedInstanceData();
const [instanceRect] = useSelectedInstanceRect();
if (style === undefined || selectedInstanceData === undefined || instanceRect === undefined) {
return null;
}
return (
<Outline state="selected" style={style}>
<Label component={selectedInstanceData.component} instanceRect={instanceRect}/>
</Outline>
);
};
@@ -0,0 +1,25 @@
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,
},
{
variants: {
state: {
selected: {
zIndex: "$4",
},
hovered: {
zIndex: "$3",
},
},
},
}
);
@@ -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 state="selected" 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
);
};
+7 -1
View File
@@ -1,6 +1,6 @@
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/component";
import { type SyncStatus } from "~/shared/sync";
const selectedInstanceDataContainer = createValueContainer<
@@ -9,6 +9,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);
+5
View File
@@ -16,6 +16,11 @@ export type SelectedInstanceData = {
props: InstanceProps;
};
export type HoveredInstanceData = {
id: Instance["id"];
component: Instance["component"];
}
type StyleUpdate = {
property: StyleProperty;
value: StyleValue;
+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"]