394 lines
13 KiB
JavaScript
394 lines
13 KiB
JavaScript
import { useCallback, useContext, useMemo, useState } from "react";
|
|
import CanvasContext from "./Context/canvasContext/CanvasContext";
|
|
import ActiveObjectContext from "./Context/activeObject/ObjectContext";
|
|
import { fabric } from "fabric";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "./ui/tooltip";
|
|
import { Button } from "./ui/button";
|
|
import {
|
|
BringToFront,
|
|
SendToBack,
|
|
CopyPlus,
|
|
GroupIcon,
|
|
SquareX,
|
|
Trash2,
|
|
UngroupIcon,
|
|
Layers,
|
|
} from "lucide-react";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import { deleteImage } from "../api/uploadApi";
|
|
import { toast } from "../hooks/use-toast";
|
|
import useProject from "@/hooks/useProject";
|
|
|
|
export const ObjectShortcut = ({ value }) => {
|
|
const { canvas } = useContext(CanvasContext);
|
|
const { setActiveObject, activeObject } = useContext(ActiveObjectContext);
|
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
|
|
const activeObjects = canvas.getActiveObjects();
|
|
const multipleObjects = activeObjects.length > 1;
|
|
const objectActive = canvas.getActiveObject();
|
|
const isGroupObject = objectActive && objectActive.type === "group";
|
|
|
|
const { projectData, projectUpdate, id } = useProject();
|
|
|
|
const groupSelectedObjects = () => {
|
|
const activeObjects = canvas.getActiveObjects(); // Get selected objects
|
|
if (activeObjects.length > 1) {
|
|
canvas.discardActiveObject();
|
|
|
|
const group = new fabric.Group(activeObjects, {
|
|
left: canvas?.width / 2,
|
|
top: canvas?.height / 2,
|
|
originX: "center",
|
|
originY: "center",
|
|
selectable: true, // Allow group selection
|
|
subTargetCheck: true, // Allow individual object selection
|
|
hasControls: true, // Enable resizing/movement of the group
|
|
});
|
|
canvas.remove(...activeObjects);
|
|
canvas.add(group);
|
|
canvas.setActiveObject(group);
|
|
setActiveObject(group);
|
|
canvas.renderAll();
|
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
|
const updateData = { ...projectData?.data, object };
|
|
// Wait for the project update before continuing
|
|
projectUpdate({ id, updateData });
|
|
} else {
|
|
toast({
|
|
title: "Select at least two objects",
|
|
description: "Please select at least two objects to group.",
|
|
variant: "destructive",
|
|
})
|
|
}
|
|
};
|
|
|
|
const ungroupSelectedObjects = () => {
|
|
const activeObject = canvas.getActiveObject();
|
|
if (activeObject && activeObject.type === "group") {
|
|
const groupObjects = activeObject._objects;
|
|
|
|
canvas.discardActiveObject();
|
|
canvas.remove(activeObject);
|
|
setActiveObject(null);
|
|
|
|
const ungroupedObjects = [];
|
|
|
|
groupObjects.forEach((object) => {
|
|
// Calculate absolute position
|
|
const objLeft = object.left * activeObject.scaleX + activeObject.left;
|
|
const objTop = object.top * activeObject.scaleY + activeObject.top;
|
|
|
|
object.set({
|
|
left: objLeft,
|
|
top: objTop,
|
|
scaleX: object.scaleX * activeObject.scaleX,
|
|
scaleY: object.scaleY * activeObject.scaleY,
|
|
angle: object.angle + activeObject.angle,
|
|
hasControls: true,
|
|
selectable: true,
|
|
group: null,
|
|
originX: "center",
|
|
originY: "center",
|
|
});
|
|
|
|
object.setCoords();
|
|
|
|
canvas.add(object);
|
|
|
|
ungroupedObjects.push(object);
|
|
});
|
|
|
|
const selection = new fabric.ActiveSelection(ungroupedObjects, {
|
|
canvas: canvas,
|
|
originX: "center",
|
|
originY: "center",
|
|
});
|
|
|
|
canvas.setActiveObject(selection);
|
|
setActiveObject(selection);
|
|
|
|
canvas.renderAll();
|
|
|
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
|
const updateData = { ...projectData?.data, object };
|
|
// Wait for the project update before continuing
|
|
projectUpdate({ id, updateData });
|
|
}
|
|
};
|
|
|
|
// Check if object is at front or back
|
|
const objectPosition = useMemo(() => {
|
|
if (!activeObject || !canvas) {
|
|
return { isAtFront: false, isAtBack: false };
|
|
}
|
|
|
|
const allObjects = canvas.getObjects();
|
|
const index = allObjects.indexOf(activeObject);
|
|
|
|
return {
|
|
isAtFront: index === allObjects.length - 1,
|
|
isAtBack: index === 0,
|
|
};
|
|
}, [activeObject, canvas]);
|
|
|
|
// Layer ordering functions with checks
|
|
const bringToFront = () => {
|
|
if (activeObject && !objectPosition.isAtFront) {
|
|
activeObject.bringToFront();
|
|
canvas.renderAll();
|
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
|
const updateData = { ...projectData?.data, object };
|
|
// Wait for the project update before continuing
|
|
projectUpdate({ id, updateData });
|
|
setIsPopoverOpen(false);
|
|
}
|
|
};
|
|
|
|
const sendToBack = () => {
|
|
if (activeObject && !objectPosition.isAtBack) {
|
|
activeObject.sendToBack();
|
|
canvas.renderAll();
|
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
|
const updateData = { ...projectData?.data, object };
|
|
// Wait for the project update before continuing
|
|
projectUpdate({ id, updateData });
|
|
setIsPopoverOpen(false);
|
|
}
|
|
};
|
|
|
|
const { mutate: deleteMutate } = useMutation({
|
|
mutationFn: async (url) => {
|
|
return await deleteImage(url);
|
|
},
|
|
onSuccess: (data) => {
|
|
if (data?.status === 200) {
|
|
toast({
|
|
title: data?.status,
|
|
description: data?.message
|
|
})
|
|
if (canvas) {
|
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
|
const updateData = { ...projectData?.data, object };
|
|
// Wait for the project update before continuing
|
|
projectUpdate({ id, updateData });
|
|
}
|
|
}
|
|
else {
|
|
toast({
|
|
variant: "destructive",
|
|
title: data?.status,
|
|
description: data?.message
|
|
})
|
|
}
|
|
}
|
|
});
|
|
|
|
// Remove Selected Element
|
|
const removeSelected = useCallback(() => {
|
|
const activeObject = canvas?.getActiveObject();
|
|
|
|
const allObjects = canvas?.getObjects();
|
|
|
|
const textObjects = allObjects.filter(
|
|
(obj) =>
|
|
obj.type === "textbox" || obj.type === "text" || obj.type === "i-text"
|
|
);
|
|
|
|
if (activeObject) {
|
|
canvas.remove(activeObject);
|
|
setActiveObject(null);
|
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
|
const updateData = { ...projectData?.data, object };
|
|
// Wait for the project update before continuing
|
|
projectUpdate({ id, updateData });
|
|
}
|
|
if (activeObject && textObjects?.length === 1) {
|
|
canvas.remove(activeObject);
|
|
setActiveObject(null);
|
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
|
const updateData = { ...projectData?.data, object };
|
|
// Wait for the project update before continuing
|
|
projectUpdate({ id, updateData });
|
|
}
|
|
if (activeObject.length > 1) {
|
|
canvas.remove(...activeObject);
|
|
setActiveObject(null);
|
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
|
const updateData = { ...projectData?.data, object };
|
|
// Wait for the project update before continuing
|
|
projectUpdate({ id, updateData });
|
|
}
|
|
|
|
if (activeObject?.type === "image") {
|
|
const imgUrl = activeObject?._originalElement?.currentSrc;
|
|
canvas.remove(activeObject);
|
|
setActiveObject(null);
|
|
canvas.renderAll();
|
|
deleteMutate(imgUrl);
|
|
}
|
|
}, [canvas, setActiveObject, deleteMutate, id, projectData, projectUpdate]);
|
|
|
|
// duplicating current objects
|
|
const duplicating = () => {
|
|
// Clone the active object to create a true deep copy
|
|
activeObject.clone((clonedObject) => {
|
|
// Add the cloned object to the canvas
|
|
clonedObject.set("left", clonedObject?.left + 30);
|
|
canvas.add(clonedObject);
|
|
canvas.renderAll();
|
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
|
const updateData = { ...projectData?.data, object };
|
|
// Wait for the project update before continuing
|
|
projectUpdate({ id, updateData });
|
|
});
|
|
};
|
|
|
|
// for clear canvas
|
|
const clearCanvas = () => {
|
|
canvas.clear();
|
|
canvas.renderAll();
|
|
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
|
|
setActiveObject(null);
|
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
|
const updateData = { ...projectData?.data, object };
|
|
// Wait for the project update before continuing
|
|
projectUpdate({ id, updateData });
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<TooltipProvider>
|
|
<div
|
|
className={`flex items-center gap-2 ${value === "default"
|
|
? "space-x-4"
|
|
: "xl:grid-cols-3 lg:grid-cols-3 md:grid-cols-3"
|
|
}`}
|
|
>
|
|
{multipleObjects && (
|
|
<ActionButton
|
|
icon={<GroupIcon className="h-4 w-4" />}
|
|
label="Group"
|
|
onClick={groupSelectedObjects}
|
|
tooltipContent={
|
|
<div className="text-sm">
|
|
<p className="font-semibold mb-1">Group selected objects</p>
|
|
<p>To select multiple objects:</p>
|
|
<ol className="list-decimal list-inside mt-1">
|
|
<li>Hold down the Shift key</li>
|
|
<li>
|
|
Click and drag with the left mouse button to select
|
|
objects
|
|
</li>
|
|
<li>Release the Shift key and mouse button</li>
|
|
</ol>
|
|
<p className="mt-1">
|
|
Then click this button to group the selected objects.
|
|
</p>
|
|
</div>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{isGroupObject && (
|
|
<ActionButton
|
|
icon={<UngroupIcon className="h-4 w-4" />}
|
|
label="Ungroup"
|
|
onClick={ungroupSelectedObjects}
|
|
tooltipContent="Ungroup selected objects"
|
|
/>
|
|
)}
|
|
|
|
<ActionButton
|
|
icon={<CopyPlus className="h-4 w-4" />}
|
|
label="Duplicate"
|
|
onClick={duplicating}
|
|
tooltipContent="Duplicate selected objects"
|
|
/>
|
|
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" size="md" className="w-full">
|
|
<div className="flex items-center gap-1 p-1">
|
|
<Layers className="h-4 w-4" />
|
|
<span className="text-[10px] font-bold">Position</span>
|
|
</div>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" align="center">
|
|
<p>Change object position</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<PopoverContent className="w-40 p-2 z-[9999]">
|
|
<div className="flex flex-col gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={bringToFront}
|
|
disabled={objectPosition.isAtFront}
|
|
>
|
|
<BringToFront className="h-4 w-4 mr-2" />
|
|
<span className="text-sm">Bring to Front</span>
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={sendToBack}
|
|
disabled={objectPosition.isAtBack}
|
|
>
|
|
<SendToBack className="h-4 w-4 mr-2" />
|
|
<span className="text-sm">Send to Back</span>
|
|
</Button>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
|
|
<ActionButton
|
|
icon={<Trash2 className="h-4 w-4" />}
|
|
label="Remove"
|
|
onClick={removeSelected}
|
|
tooltipContent="Remove selected objects"
|
|
/>
|
|
|
|
<ActionButton
|
|
icon={<SquareX className="h-4 w-4" />}
|
|
label="Clear"
|
|
onClick={clearCanvas}
|
|
tooltipContent="Clear entire canvas"
|
|
/>
|
|
</div>
|
|
</TooltipProvider>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
function ActionButton({ icon, label, onClick, tooltipContent }) {
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="md"
|
|
className="w-full"
|
|
onClick={onClick}
|
|
>
|
|
<div className="flex items-center gap-1 p-1">
|
|
{icon}
|
|
<span className="text-[10px] font-bold">{label}</span>
|
|
</div>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" align="center" className="max-w-xs z-[9999]">
|
|
<p>{tooltipContent}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|