463 lines
14 KiB
JavaScript
463 lines
14 KiB
JavaScript
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectGroup,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
import {
|
|
AlignLeft,
|
|
AlignCenter,
|
|
AlignRight,
|
|
Bold,
|
|
Italic,
|
|
Underline,
|
|
Strikethrough,
|
|
Minus,
|
|
Plus,
|
|
} from "lucide-react";
|
|
import { RiLineHeight } from "react-icons/ri";
|
|
import { Slider } from "@/components/ui/slider";
|
|
import { Tooltip } from "react-tooltip";
|
|
|
|
const fonts = [
|
|
"Roboto",
|
|
"Open Sans",
|
|
"Lato",
|
|
"Montserrat",
|
|
"Raleway",
|
|
"Poppins",
|
|
"Merriweather",
|
|
"Playfair Display",
|
|
"Nunito",
|
|
"Oswald",
|
|
"Source Sans Pro",
|
|
"Ubuntu",
|
|
"Noto Sans",
|
|
"Work Sans",
|
|
"Bebas Neue",
|
|
"Arimo",
|
|
"PT Sans",
|
|
"PT Serif",
|
|
"Titillium Web",
|
|
"Fira Sans",
|
|
"Karla",
|
|
"Josefin Sans",
|
|
"Cairo",
|
|
"Rubik",
|
|
"Mulish",
|
|
"IBM Plex Sans",
|
|
"Quicksand",
|
|
"Cabin",
|
|
"Heebo",
|
|
"Exo 2",
|
|
"Manrope",
|
|
"Jost",
|
|
"Anton",
|
|
"Asap",
|
|
"Baloo 2",
|
|
"Barlow",
|
|
"Cantarell",
|
|
"Chivo",
|
|
"Inter",
|
|
"Dosis",
|
|
"Crimson Text",
|
|
"Amatic SC",
|
|
"ABeeZee",
|
|
"Raleway Dots",
|
|
"Pacifico",
|
|
"Orbitron",
|
|
"Varela Round",
|
|
"Acme",
|
|
"Teko",
|
|
];
|
|
|
|
const TextCustomization = () => {
|
|
const { activeObject } = useContext(ActiveObjectContext);
|
|
const { canvas, setSelectedPanel, textColor } = useContext(CanvasContext);
|
|
|
|
const activeObjectType = activeObject?.type;
|
|
const hasClipPath = !!activeObject?.clipPath;
|
|
const customClipPath = activeObject?.isClipPath;
|
|
|
|
const prevTextRef = useRef("");
|
|
const [text, setText] = useState("");
|
|
const [fontFamily, setFontFamily] = useState("Arial");
|
|
const [fontSize, setFontSize] = useState(20);
|
|
const [fontStyle, setFontStyle] = useState("normal");
|
|
const [fontWeight, setFontWeight] = useState("normal");
|
|
const [lineHeight, setLineHeight] = useState(1.16);
|
|
const [charSpacing, setCharSpacing] = useState(0);
|
|
const [underline, setUnderline] = useState(false);
|
|
const [linethrough, setLinethrough] = useState(false);
|
|
const [textAlign, setTextAlign] = useState("left");
|
|
const [fillColor, setFillColor] = useState("black");
|
|
|
|
useEffect(() => {
|
|
if (activeObject?.type === "i-text") {
|
|
if (
|
|
activeObject?.text !== undefined &&
|
|
activeObject.text !== prevTextRef.current
|
|
) {
|
|
setText(activeObject.text);
|
|
prevTextRef.current = activeObject.text;
|
|
}
|
|
setText(activeObject?.text || "");
|
|
setFontFamily(activeObject?.fontFamily || "Arial");
|
|
setFontSize(activeObject?.fontSize || 20);
|
|
setFontStyle(activeObject?.fontStyle || "normal");
|
|
setFontWeight(activeObject?.fontWeight || "normal");
|
|
setLineHeight(activeObject?.lineHeight || 1.16);
|
|
setCharSpacing(activeObject?.charSpacing || 0);
|
|
setUnderline(activeObject?.underline || false);
|
|
setLinethrough(activeObject?.linethrough || false);
|
|
setTextAlign(activeObject?.textAlign || "left");
|
|
setFillColor(textColor?.fill || "black");
|
|
}
|
|
}, [activeObject, textColor]);
|
|
|
|
const updateActiveObject = useCallback(
|
|
(properties) => {
|
|
if (activeObject?.type === "i-text") {
|
|
// Preserve the text value when updating other properties
|
|
const updatedProperties = {
|
|
...properties,
|
|
text: text || prevTextRef.current || properties.text,
|
|
};
|
|
activeObject.set(updatedProperties);
|
|
canvas?.renderAll();
|
|
}
|
|
},
|
|
[activeObject, canvas, text]
|
|
); // Add dependencies
|
|
|
|
const applyChanges = useCallback(() => {
|
|
updateActiveObject({
|
|
text,
|
|
fontFamily,
|
|
fontSize,
|
|
fontStyle,
|
|
fontWeight,
|
|
lineHeight,
|
|
charSpacing,
|
|
underline,
|
|
linethrough,
|
|
textAlign,
|
|
});
|
|
}, [
|
|
text,
|
|
fontFamily,
|
|
fontSize,
|
|
fontStyle,
|
|
fontWeight,
|
|
lineHeight,
|
|
charSpacing,
|
|
underline,
|
|
linethrough,
|
|
textAlign,
|
|
updateActiveObject, // Add this dependency
|
|
]);
|
|
|
|
const handleColorPanelClick = () => {
|
|
// Store current text value before switching panels
|
|
prevTextRef.current = text;
|
|
setSelectedPanel("color");
|
|
};
|
|
|
|
// Automatically apply changes when state updates
|
|
useEffect(() => {
|
|
if (activeObject?.type === "i-text") {
|
|
applyChanges();
|
|
}
|
|
}, [
|
|
applyChanges, // Now included in dependencies
|
|
activeObject?.type, // Track active object type
|
|
]);
|
|
|
|
const handleFontFamilyChange = (newFontFamily) => {
|
|
setFontFamily(newFontFamily);
|
|
};
|
|
|
|
const handleFontSizeChange = (newFontSize) => {
|
|
setFontSize(newFontSize);
|
|
};
|
|
|
|
const handleTextAlignChange = (newTextAlign) => {
|
|
setTextAlign(newTextAlign);
|
|
};
|
|
|
|
const handleFontStyleChange = () => {
|
|
setFontStyle(fontStyle === "normal" ? "italic" : "normal");
|
|
};
|
|
|
|
const handleFontWeightChange = () => {
|
|
setFontWeight(fontWeight === "normal" ? "bold" : "normal");
|
|
};
|
|
|
|
const handleLineHeightChange = (newLineHeight) => {
|
|
setLineHeight(parseFloat(newLineHeight));
|
|
};
|
|
|
|
const handleCharSpacingChange = (newCharSpacing) => {
|
|
setCharSpacing(parseInt(newCharSpacing));
|
|
};
|
|
|
|
const handleUnderlineChange = () => {
|
|
setUnderline(!underline);
|
|
};
|
|
|
|
const handleLinethroughChange = () => {
|
|
setLinethrough(!linethrough);
|
|
};
|
|
|
|
const content = () => {
|
|
if (!(activeObject?.type === "i-text")) {
|
|
return <div></div>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* New Toolbar Design */}
|
|
<div className="flex w-full items-center space-x-2 rounded-lg p-1 bg-white">
|
|
{/* Font Family Select */}
|
|
<a data-tooltip-id="fonts">
|
|
<Select
|
|
value={fontFamily}
|
|
onValueChange={handleFontFamilyChange}
|
|
title="Font Family"
|
|
>
|
|
<SelectTrigger className="min-w-[140px] h-8">
|
|
<SelectValue placeholder="Select a font" />
|
|
</SelectTrigger>
|
|
<SelectContent className="h-[250px] z-[9999]">
|
|
<SelectGroup>
|
|
{fonts.map((font) => (
|
|
<SelectItem
|
|
key={font}
|
|
value={font}
|
|
style={{ fontFamily: font }}
|
|
>
|
|
{font}
|
|
</SelectItem>
|
|
))}
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</Select>
|
|
</a>
|
|
<Tooltip id="fonts" content="Font Family" place="bottom" />
|
|
|
|
{/* Font Size Controls */}
|
|
<div className="flex items-center border rounded-md">
|
|
<a data-tooltip-id="font-dec">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 rounded-none"
|
|
onClick={() => handleFontSizeChange(Math.max(8, fontSize - 1))}
|
|
>
|
|
<Minus className="h-4 w-4" />
|
|
</Button>
|
|
</a>
|
|
<Tooltip
|
|
id="font-dec"
|
|
content="Decrease font size"
|
|
place="bottom"
|
|
/>
|
|
<a data-tooltip-id="font-size">
|
|
<Input
|
|
type="text"
|
|
value={fontSize}
|
|
onChange={(e) => {
|
|
const numericValue = e.target.value.replace(/\D/g, "");
|
|
handleFontSizeChange(parseInt(numericValue) || 12);
|
|
}}
|
|
className="w-12 h-8 border-0 text-center"
|
|
/>
|
|
</a>
|
|
<Tooltip id="font-size" content="Font size" place="bottom" />
|
|
<a data-tooltip-id="font-inc">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 rounded-none"
|
|
onClick={() => handleFontSizeChange(Math.min(72, fontSize + 1))}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</Button>
|
|
</a>
|
|
<Tooltip
|
|
id="font-inc"
|
|
content="Increase font size"
|
|
place="bottom"
|
|
/>
|
|
</div>
|
|
|
|
{/* Vertical Separator */}
|
|
<div className="h-6 w-px bg-gray-200" />
|
|
|
|
{/* Text Formatting Controls */}
|
|
<div className="flex items-center space-x-1">
|
|
{activeObjectType !== "image" &&
|
|
!hasClipPath &&
|
|
!customClipPath && (
|
|
<a data-tooltip-id="text-color">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="relative"
|
|
onClick={handleColorPanelClick} // Updated onClick handler
|
|
>
|
|
<div className="relative">
|
|
<span className="text-lg font-semibold">A</span>
|
|
<div
|
|
className="absolute -bottom-0.5 -left-1 right-0 h-1 w-5 transition-all duration-200"
|
|
style={{ backgroundColor: fillColor }}
|
|
/>
|
|
</div>
|
|
</Button>
|
|
</a>
|
|
)}
|
|
|
|
<Tooltip id="text-color" content="Text color" place="bottom" />
|
|
<a data-tooltip-id="text-left">
|
|
<Button
|
|
variant={textAlign === "left" ? "secondary" : "ghost"}
|
|
size="icon"
|
|
onClick={() => handleTextAlignChange("left")}
|
|
>
|
|
<AlignLeft className="h-4 w-4" />
|
|
</Button>
|
|
</a>
|
|
<a data-tooltip-id="text-center">
|
|
<Button
|
|
variant={textAlign === "center" ? "secondary" : "ghost"}
|
|
size="icon"
|
|
onClick={() => handleTextAlignChange("center")}
|
|
>
|
|
<AlignCenter className="h-4 w-4" />
|
|
</Button>
|
|
</a>
|
|
<a data-tooltip-id="text-right">
|
|
<Button
|
|
variant={textAlign === "right" ? "secondary" : "ghost"}
|
|
size="icon"
|
|
onClick={() => handleTextAlignChange("right")}
|
|
>
|
|
<AlignRight className="h-4 w-4" />
|
|
</Button>
|
|
</a>
|
|
<a data-tooltip-id="text-bold">
|
|
<Button
|
|
variant={fontWeight === "bold" ? "secondary" : "ghost"}
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={handleFontWeightChange}
|
|
>
|
|
<Bold className="h-4 w-4" />
|
|
</Button>
|
|
</a>
|
|
<a data-tooltip-id="text-underline">
|
|
<Button
|
|
variant={underline ? "secondary" : "ghost"}
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={handleUnderlineChange}
|
|
>
|
|
<Underline className="h-4 w-4" />
|
|
</Button>
|
|
</a>
|
|
<a data-tooltip-id="text-italic">
|
|
<Button
|
|
variant={fontStyle === "italic" ? "secondary" : "ghost"}
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={handleFontStyleChange}
|
|
>
|
|
<Italic className="h-4 w-4" />
|
|
</Button>
|
|
</a>
|
|
<a data-tooltip-id="line-through">
|
|
<Button
|
|
variant={linethrough ? "secondary" : "ghost"}
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={handleLinethroughChange}
|
|
>
|
|
<Strikethrough className="h-4 w-4" />
|
|
</Button>
|
|
</a>
|
|
<Tooltip id="text-left" content="Left align" place="bottom" />
|
|
<Tooltip id="text-center" content="Center align" place="bottom" />
|
|
<Tooltip id="text-right" content="Right align" place="bottom" />
|
|
<Tooltip id="text-bold" content="Bold" place="bottom" />
|
|
<Tooltip id="text-underline" content="Underline" place="bottom" />
|
|
<Tooltip id="text-italic" content="Italics" place="bottom" />
|
|
<Tooltip id="line-through" content="StrikeThrough" place="bottom" />
|
|
</div>
|
|
|
|
{/* Vertical Separator */}
|
|
<div className="h-6 w-px bg-gray-200" />
|
|
|
|
{/* Spacing Controls */}
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<a data-tooltip-id="spacing">
|
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
<RiLineHeight className="h-4 w-4" />
|
|
</Button>
|
|
</a>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-44 mt-3">
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>Line Spacing</Label>
|
|
<div className="flex items-center space-x-2">
|
|
<Slider
|
|
value={[lineHeight]}
|
|
onValueChange={([value]) => handleLineHeightChange(value)}
|
|
min={0.5}
|
|
max={3}
|
|
step={0.1}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Letter Spacing</Label>
|
|
<div className="flex items-center space-x-2">
|
|
<Slider
|
|
value={[charSpacing]}
|
|
onValueChange={([value]) =>
|
|
handleCharSpacingChange(value)
|
|
}
|
|
min={-20}
|
|
max={100}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<Tooltip id="spacing" content="Spacing" place="bottom" />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return <div className="">{content()}</div>;
|
|
};
|
|
|
|
export default TextCustomization;
|