canvas-backend/src/components/ObjectShortcut.jsx
2025-02-11 17:55:34 +06:00

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>
);
}