mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-10 06:24:35 +09:00
handle hover outline
This commit is contained in:
1 parent
2cdf649cc9
commit
673c94e3ce
17 files changed
+231
-112
No files matched your search
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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
|
||||
>();
|
||||
|
||||
@@ -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";
|
||||
+6
-57
@@ -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
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,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"]
|
||||
|
||||
Reference in new issue
Block a user