Skip to content

Commit

Permalink
refactor: element-helper portal
Browse files Browse the repository at this point in the history
  • Loading branch information
bepyan committed Aug 15, 2024
1 parent 562598a commit 3556d41
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 61 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"sonner": "^1.5.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.0",
"zod": "^3.23.8",
"zustand": "^4.5.4"
},
Expand Down
2 changes: 2 additions & 0 deletions src/components/editor/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type EditorActionMap = {
elementDetails: EditorElement;
};
CHANGE_CLICKED_ELEMENT: {
elementRef?: React.RefObject<HTMLDivElement>;
elementDetails?: EditorElement;
};
CHANGE_CURRENT_TAB_VALUE: {
Expand Down Expand Up @@ -178,6 +179,7 @@ const actionHandlers: {
return updateEditorHistory(editor, {
...editor.state,
selectedElement: payload.elementDetails ?? emptyElement,
selectedElementRef: payload.elementRef,
currentTabValue: newTabValue,
});
},
Expand Down
90 changes: 90 additions & 0 deletions src/components/editor/elements/element-helper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use client";

import {
ArrowDownIcon,
ArrowUpIcon,
CopyPlusIcon,
GripVerticalIcon,
Trash2Icon,
} from "lucide-react";
import { createPortal } from "react-dom";
import { useEditor } from "~/components/editor/provider";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils";

import { useRef } from "react";
import { useResizeObserver } from "usehooks-ts";

export default function ElementHelper() {
const { editor, dispatch } = useEditor();
const element = editor.state.selectedElement;
const elementRef = editor.state.selectedElementRef;

const emptyRef = useRef<HTMLDivElement>(null);
const { width, height } = useResizeObserver({
ref: elementRef ?? emptyRef,
box: "border-box",
});

const handleDeleteElement = () => {
dispatch({
type: "DELETE_ELEMENT",
payload: {
elementDetails: element,
},
});
};

return (
document.body &&
createPortal(
!editor.state.isPreviewMode && elementRef?.current && (
<div
style={{
top: elementRef.current.getBoundingClientRect().top,
left: elementRef.current.getBoundingClientRect().left,
width,
height,
}}
className={cn(
"fixed z-50",
!editor.state.isPreviewMode && "ring-1 ring-primary",
)}
>
<Badge className="absolute -left-[1px] -top-[26px] z-10">
{element.name}
</Badge>
<div className="absolute -left-[28px] -top-[1px] z-10">
<div className="flex flex-col gap-0.5">
<IconButton>
<GripVerticalIcon className="h-4 w-4" />
</IconButton>
<IconButton>
<ArrowUpIcon className="h-4 w-4" />
</IconButton>
<IconButton>
<ArrowDownIcon className="h-4 w-4" />
</IconButton>
</div>
</div>
<div className="absolute -right-[28px] -top-[1px] z-10">
<div className="flex flex-col gap-0.5">
<IconButton onClick={handleDeleteElement}>
<Trash2Icon className="h-4 w-4" />
</IconButton>
<IconButton>
<CopyPlusIcon className="h-4 w-4" />
</IconButton>
</div>
</div>
</div>
),
document.body,
)
);
}

function IconButton(props: React.ComponentProps<"button">) {
return <Button {...props} size="icon" className="h-6 w-6 p-0" />;
}
68 changes: 8 additions & 60 deletions src/components/editor/elements/element-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import {
ArrowDownIcon,
ArrowUpIcon,
CopyPlusIcon,
GripVerticalIcon,
Trash2Icon,
} from "lucide-react";
import { useRef } from "react";
import { useEditor } from "~/components/editor/provider";
import type { EditorElement } from "~/components/editor/type";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils";

type Props = React.ComponentProps<"div"> & {
Expand All @@ -23,23 +15,16 @@ export default function ElementWrapper({
}: Props) {
const { editor, dispatch } = useEditor();
const isRoot = element.type === "__body";
const isSelected = editor.state.selectedElement.id === element.id;

const handleClickRoot = (e: React.MouseEvent) => {
const rootRef = useRef<HTMLDivElement>(null);

const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();

dispatch({
type: "CHANGE_CLICKED_ELEMENT",
payload: {
elementDetails: element,
},
});
};

const handleDeleteElement = () => {
dispatch({
type: "DELETE_ELEMENT",
payload: {
elementRef: rootRef,
elementDetails: element,
},
});
Expand All @@ -48,53 +33,16 @@ export default function ElementWrapper({
return (
<div
{...props}
ref={rootRef}
style={element.styles}
className={cn(
"relative w-full p-1 transition-all",
!editor.state.isPreviewMode && [
"border border-dashed border-border",
(isSelected || isRoot) && "border-solid",
isSelected && !isRoot && "border-primary",
],
!editor.state.isPreviewMode && "ring-border hover:ring-1",
className,
)}
onClick={(e) => !isRoot && handleClickRoot(e)}
onClick={(e) => !isRoot && handleClick(e)}
>
{children}
{!isRoot && isSelected && !editor.state.isPreviewMode && (
<>
<Badge className="absolute -left-[1px] -top-[26px] z-10">
{editor.state.selectedElement.name}
</Badge>
<div className="absolute -left-[28px] -top-[1px] z-10">
<div className="flex flex-col gap-0.5">
<IconButton>
<GripVerticalIcon className="h-4 w-4" />
</IconButton>
<IconButton>
<ArrowUpIcon className="h-4 w-4" />
</IconButton>
<IconButton>
<ArrowDownIcon className="h-4 w-4" />
</IconButton>
</div>
</div>
<div className="absolute -right-[28px] -top-[1px] z-10">
<div className="flex flex-col gap-0.5">
<IconButton onClick={handleDeleteElement}>
<Trash2Icon className="h-4 w-4" />
</IconButton>
<IconButton>
<CopyPlusIcon className="h-4 w-4" />
</IconButton>
</div>
</div>
</>
)}
</div>
);
}

function IconButton(props: React.ComponentProps<"button">) {
return <Button {...props} size="icon" className="h-6 w-6 p-0" />;
}
4 changes: 3 additions & 1 deletion src/components/editor/main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { EyeOff } from "lucide-react";
import ElementHelper from "~/components/editor/elements/element-helper";
import Recursive from "~/components/editor/elements/recursive";
import { useEditor } from "~/components/editor/provider";
import { Button } from "~/components/ui/button";
Expand Down Expand Up @@ -28,7 +29,7 @@ export default function EditorMain() {
<div
id="editor-main"
className={cn(
"ml-[10px] mr-[392px] animate-zoom-in rounded-md bg-background transition-all",
"ml-[10px] mr-[392px] animate-zoom-in bg-background transition-all",
editor.state.isPreviewMode && "m-0 overflow-hidden p-0",
editor.state.device === "Desktop" && "h-full w-full",
editor.state.device === "Tablet" && "h-full w-[850px]",
Expand All @@ -50,6 +51,7 @@ export default function EditorMain() {
<Recursive key={childElement.id} element={childElement} />
))}
</div>
<ElementHelper />
</div>
);
}
1 change: 1 addition & 0 deletions src/components/editor/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type InferEditorElement<K extends EditorElementType> = Extract<
export type EditorState = {
elements: EditorElement[];
selectedElement: EditorElement;
selectedElementRef?: React.RefObject<HTMLDivElement>;
currentTabValue: EditorTabTypeValue;
device: DeviceType;
isPreviewMode: boolean;
Expand Down

0 comments on commit 3556d41

Please sign in to comment.