can edit text + fix font weight - remove character space
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,102 @@
|
|||||||
|
import { type FC, type InputHTMLAttributes, useState, useRef, useEffect } from 'react'
|
||||||
|
import { clx } from '../helpers/utils'
|
||||||
|
import Error from './Error'
|
||||||
|
import ColorfilterIcon from '@/assets/icons/color.png'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label?: string
|
||||||
|
className?: string
|
||||||
|
error_text?: string
|
||||||
|
isNotRequired?: boolean
|
||||||
|
value?: string
|
||||||
|
onChange?: (value: string) => void
|
||||||
|
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'>
|
||||||
|
|
||||||
|
const ColorPicker: FC<Props> = (props: Props) => {
|
||||||
|
const [color, setColor] = useState<string>(props.value || '#000000')
|
||||||
|
const colorInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const inputClass = clx(
|
||||||
|
'w-full bg-white h-10 text-black block pl-10 pr-10 text-xs rounded-xl border border-border',
|
||||||
|
props.readOnly && 'bg-gray-100 border-0 text-description',
|
||||||
|
props.className
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newColor = event.target.value
|
||||||
|
setColor(newColor)
|
||||||
|
props.onChange?.(newColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value
|
||||||
|
if (/^#[0-9A-Fa-f]{0,6}$/.test(value) || value === '') {
|
||||||
|
const finalColor = value || '#000000'
|
||||||
|
setColor(finalColor)
|
||||||
|
props.onChange?.(finalColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleColorIconClick = () => {
|
||||||
|
if (!props.readOnly) {
|
||||||
|
colorInputRef.current?.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.value !== undefined) {
|
||||||
|
setColor(props.value)
|
||||||
|
}
|
||||||
|
}, [props.value])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
|
<label className='text-sm'>
|
||||||
|
{props.label}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className='w-full relative mt-1'>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={color}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
placeholder='#000000'
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={handleColorIconClick}
|
||||||
|
disabled={props.readOnly}
|
||||||
|
className={clx(
|
||||||
|
'absolute top-0 bottom-0 my-auto left-3 cursor-pointer',
|
||||||
|
props.readOnly && 'cursor-not-allowed opacity-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img src={ColorfilterIcon} alt='color-filter' className='size-7' />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className='absolute top-0 bottom-0 my-auto right-3 w-5 h-5 rounded-full'
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={colorInputRef}
|
||||||
|
type='color'
|
||||||
|
value={color}
|
||||||
|
onChange={handleColorChange}
|
||||||
|
className='absolute opacity-0 pointer-events-none w-0 h-0'
|
||||||
|
/>
|
||||||
|
|
||||||
|
{props.error_text && (
|
||||||
|
<Error errorText={props.error_text} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColorPicker
|
||||||
|
|
||||||
@@ -78,6 +78,13 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
}, [search, props])
|
}, [search, props])
|
||||||
|
|
||||||
|
|
||||||
|
const resolvedType =
|
||||||
|
props.type === 'password'
|
||||||
|
? showPassword
|
||||||
|
? 'text'
|
||||||
|
: 'password'
|
||||||
|
: props.type;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<label className='text-sm'>
|
<label className='text-sm'>
|
||||||
@@ -88,7 +95,7 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
<input {...props} onChange={(e) => {
|
<input {...props} onChange={(e) => {
|
||||||
setSearch(e.target.value)
|
setSearch(e.target.value)
|
||||||
handleInputChange(e)
|
handleInputChange(e)
|
||||||
}} value={props.seprator ? formattedValue : props.value} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
|
}} value={props.seprator ? formattedValue : props.value} type={resolvedType} className={inputClass} />
|
||||||
|
|
||||||
{
|
{
|
||||||
props.type === 'password' &&
|
props.type === 'password' &&
|
||||||
|
|||||||
@@ -65,33 +65,40 @@ const EditorCanvas = () => {
|
|||||||
draggable={tool === "select"}
|
draggable={tool === "select"}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{selectedObjectId && (
|
{selectedObjectId && (() => {
|
||||||
<Transformer
|
const selectedObject = objects.find(obj => obj.id === selectedObjectId);
|
||||||
ref={transformerRef}
|
const isText = selectedObject?.type === "text";
|
||||||
boundBoxFunc={(oldBox, newBox) => {
|
|
||||||
if (Math.abs(newBox.width) < 5 || Math.abs(newBox.height) < 5) {
|
return (
|
||||||
return oldBox;
|
<Transformer
|
||||||
}
|
ref={transformerRef}
|
||||||
return newBox;
|
boundBoxFunc={(oldBox, newBox) => {
|
||||||
}}
|
if (Math.abs(newBox.width) < 5 || Math.abs(newBox.height) < 5) {
|
||||||
anchorFill="#3b82f6"
|
return oldBox;
|
||||||
anchorStroke="#ffffff"
|
}
|
||||||
borderStroke="#3b82f6"
|
return newBox;
|
||||||
borderStrokeWidth={2}
|
}}
|
||||||
anchorSize={8}
|
ignoreStroke={isText}
|
||||||
rotateEnabled={true}
|
perfectDrawEnabled={false}
|
||||||
enabledAnchors={[
|
anchorFill="#3b82f6"
|
||||||
"top-left",
|
anchorStroke="#ffffff"
|
||||||
"top-right",
|
borderStroke="#3b82f6"
|
||||||
"bottom-left",
|
borderStrokeWidth={2}
|
||||||
"bottom-right",
|
anchorSize={8}
|
||||||
"top-center",
|
rotateEnabled={true}
|
||||||
"bottom-center",
|
enabledAnchors={[
|
||||||
"middle-left",
|
"top-left",
|
||||||
"middle-right",
|
"top-right",
|
||||||
]}
|
"bottom-left",
|
||||||
/>
|
"bottom-right",
|
||||||
)}
|
"top-center",
|
||||||
|
"bottom-center",
|
||||||
|
"middle-left",
|
||||||
|
"middle-right",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</Layer>
|
</Layer>
|
||||||
</Stage>
|
</Stage>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const createDrawingObject = ({
|
|||||||
height: Math.abs(height),
|
height: Math.abs(height),
|
||||||
fill: "#3b82f6",
|
fill: "#3b82f6",
|
||||||
stroke: "#1e40af",
|
stroke: "#1e40af",
|
||||||
strokeWidth: 2,
|
strokeWidth: 0,
|
||||||
shapeType: activeShape,
|
shapeType: activeShape,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ export const createDrawingObject = ({
|
|||||||
height: radius * 2,
|
height: radius * 2,
|
||||||
fill: "#10b981",
|
fill: "#10b981",
|
||||||
stroke: "#059669",
|
stroke: "#059669",
|
||||||
strokeWidth: 2,
|
strokeWidth: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { type FC, useRef, useState, useEffect } from "react";
|
import { type FC, useState, useEffect, useMemo } from "react";
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
import Select from "@/components/Select";
|
import Select from "@/components/Select";
|
||||||
|
import ColorPicker from "@/components/ColorPicker";
|
||||||
|
import Button from "@/components/Button";
|
||||||
import { useEditorStore } from "@/pages/editor/store/editorStore";
|
import { useEditorStore } from "@/pages/editor/store/editorStore";
|
||||||
import { useTextDefaultsStore } from "@/pages/editor/store/textDefaultsStore";
|
import { useTextDefaultsStore } from "@/pages/editor/store/textDefaultsStore";
|
||||||
|
|
||||||
@@ -15,6 +17,17 @@ const fontWeightOptions = [
|
|||||||
{ label: "Light", value: "300" },
|
{ label: "Light", value: "300" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type TextFormState = {
|
||||||
|
text: string;
|
||||||
|
fontFamily: string;
|
||||||
|
fontWeight: string;
|
||||||
|
fontSize: number;
|
||||||
|
fill: string;
|
||||||
|
opacity: number;
|
||||||
|
letterSpacing: number;
|
||||||
|
wordSpacing: number;
|
||||||
|
};
|
||||||
|
|
||||||
const TextInstruction: FC = () => {
|
const TextInstruction: FC = () => {
|
||||||
const { selectedObjectId, objects, updateObject } = useEditorStore();
|
const { selectedObjectId, objects, updateObject } = useEditorStore();
|
||||||
const { defaults, updateDefaults } = useTextDefaultsStore();
|
const { defaults, updateDefaults } = useTextDefaultsStore();
|
||||||
@@ -24,39 +37,63 @@ const TextInstruction: FC = () => {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
const isEditing = !!selectedObject;
|
const isEditing = !!selectedObject;
|
||||||
const currentValues = isEditing
|
const baseValues = useMemo(() => {
|
||||||
? {
|
|
||||||
fontFamily: selectedObject.fontFamily || defaults.fontFamily,
|
|
||||||
fontWeight: selectedObject.fontWeight || defaults.fontWeight,
|
|
||||||
fontSize: selectedObject.fontSize || defaults.fontSize,
|
|
||||||
fill: selectedObject.fill || defaults.fill,
|
|
||||||
opacity: selectedObject.opacity ?? defaults.opacity,
|
|
||||||
letterSpacing: selectedObject.letterSpacing ?? defaults.letterSpacing,
|
|
||||||
wordSpacing: selectedObject.wordSpacing ?? defaults.wordSpacing,
|
|
||||||
}
|
|
||||||
: defaults;
|
|
||||||
|
|
||||||
const handleChange = (field: string, value: string | number) => {
|
|
||||||
if (isEditing && selectedObject) {
|
if (isEditing && selectedObject) {
|
||||||
updateObject(selectedObject.id, { [field]: value });
|
return {
|
||||||
|
text: selectedObject.text || "",
|
||||||
|
fontFamily: selectedObject.fontFamily || defaults.fontFamily,
|
||||||
|
fontWeight: selectedObject.fontWeight || defaults.fontWeight,
|
||||||
|
fontSize: selectedObject.fontSize || defaults.fontSize,
|
||||||
|
fill: selectedObject.fill || defaults.fill,
|
||||||
|
opacity: selectedObject.opacity ?? defaults.opacity,
|
||||||
|
letterSpacing: selectedObject.letterSpacing ?? defaults.letterSpacing,
|
||||||
|
wordSpacing: selectedObject.wordSpacing ?? defaults.wordSpacing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { text: "", ...defaults };
|
||||||
|
}, [isEditing, selectedObject, defaults]);
|
||||||
|
|
||||||
|
const [formState, setFormState] = useState<TextFormState>(baseValues);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFormState(baseValues);
|
||||||
|
}, [baseValues]);
|
||||||
|
|
||||||
|
const handleChange = (field: keyof TextFormState, value: string | number) => {
|
||||||
|
if (isEditing && selectedObject) {
|
||||||
|
setFormState((prev) => ({ ...prev, [field]: value }));
|
||||||
} else {
|
} else {
|
||||||
updateDefaults({ [field]: value });
|
// text فقط برای حالت ویرایش است و نباید در defaults ذخیره شود
|
||||||
|
if (field !== "text") {
|
||||||
|
updateDefaults({ [field]: value });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const hexColor = (currentValues.fill || "#000000").replace("#", "").toUpperCase();
|
const handleSave = () => {
|
||||||
const opacityValue = currentValues.opacity;
|
if (isEditing && selectedObject) {
|
||||||
const colorInputRef = useRef<HTMLInputElement>(null);
|
updateObject(selectedObject.id, formState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDirty = isEditing && selectedObject
|
||||||
|
? formState.text !== baseValues.text ||
|
||||||
|
formState.fontFamily !== baseValues.fontFamily ||
|
||||||
|
formState.fontWeight !== baseValues.fontWeight ||
|
||||||
|
formState.fontSize !== baseValues.fontSize ||
|
||||||
|
formState.fill !== baseValues.fill ||
|
||||||
|
formState.opacity !== baseValues.opacity ||
|
||||||
|
formState.letterSpacing !== baseValues.letterSpacing ||
|
||||||
|
formState.wordSpacing !== baseValues.wordSpacing
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const opacityValue = formState.opacity;
|
||||||
const [opacityInput, setOpacityInput] = useState<string>(`${opacityValue}`);
|
const [opacityInput, setOpacityInput] = useState<string>(`${opacityValue}`);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpacityInput(`${opacityValue}`);
|
setOpacityInput(`${opacityValue}`);
|
||||||
}, [opacityValue]);
|
}, [opacityValue]);
|
||||||
|
|
||||||
const handleColorClick = () => {
|
|
||||||
colorInputRef.current?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpacityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleOpacityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value.replace("%", "").trim();
|
const value = e.target.value.replace("%", "").trim();
|
||||||
setOpacityInput(value);
|
setOpacityInput(value);
|
||||||
@@ -66,20 +103,31 @@ const TextInstruction: FC = () => {
|
|||||||
const numValue = parseInt(opacityInput) || 0;
|
const numValue = parseInt(opacityInput) || 0;
|
||||||
const clampedValue = Math.min(100, Math.max(0, numValue));
|
const clampedValue = Math.min(100, Math.max(0, numValue));
|
||||||
setOpacityInput(`${clampedValue}`);
|
setOpacityInput(`${clampedValue}`);
|
||||||
if (clampedValue !== opacityValue) {
|
handleChange("opacity", clampedValue);
|
||||||
handleChange("opacity", clampedValue);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-bold">متن</h2>
|
<h2 className="font-bold">متن</h2>
|
||||||
<div className="mt-8">
|
|
||||||
|
{isEditing && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<label className="text-sm mb-1 block">متن</label>
|
||||||
|
<textarea
|
||||||
|
className="w-full min-h-[80px] rounded-xl border border-border px-3 py-2 text-sm outline-none focus:border-primary resize-y"
|
||||||
|
value={formState.text}
|
||||||
|
onChange={(e) => handleChange("text", e.target.value)}
|
||||||
|
placeholder="متن خود را وارد کنید..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
<Select
|
<Select
|
||||||
items={fontOptions}
|
items={fontOptions}
|
||||||
placeholder="انتخاب فونت"
|
placeholder="انتخاب فونت"
|
||||||
label="فونت"
|
label="فونت"
|
||||||
value={currentValues.fontFamily}
|
value={formState.fontFamily}
|
||||||
onChange={(e) => handleChange("fontFamily", e.target.value)}
|
onChange={(e) => handleChange("fontFamily", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,12 +136,12 @@ const TextInstruction: FC = () => {
|
|||||||
<Select
|
<Select
|
||||||
label="وزن"
|
label="وزن"
|
||||||
items={fontWeightOptions}
|
items={fontWeightOptions}
|
||||||
value={currentValues.fontWeight}
|
value={formState.fontWeight}
|
||||||
onChange={(e) => handleChange("fontWeight", e.target.value)}
|
onChange={(e) => handleChange("fontWeight", e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
value={`${currentValues.fontSize}px`}
|
value={`${formState.fontSize}px`}
|
||||||
label="سایز"
|
label="سایز"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value.replace("px", "").trim();
|
const value = e.target.value.replace("px", "").trim();
|
||||||
@@ -103,48 +151,38 @@ const TextInstruction: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
<div className="text-sm">رنگ</div>
|
<div className="flex gap-3">
|
||||||
<div className="mt-2 border border-border rounded-xl p-2 flex items-center">
|
<ColorPicker
|
||||||
<button
|
label="رنگ"
|
||||||
type="button"
|
className="flex-1"
|
||||||
onClick={handleColorClick}
|
value={formState.fill}
|
||||||
className="flex-1 flex items-center gap-1.5 cursor-pointer hover:opacity-80 transition-opacity"
|
onChange={(value) => handleChange("fill", value)}
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="size-6 rounded"
|
|
||||||
style={{ backgroundColor: currentValues.fill }}
|
|
||||||
/>
|
|
||||||
<div className="text-sm">{hexColor}</div>
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
ref={colorInputRef}
|
|
||||||
type="color"
|
|
||||||
value={currentValues.fill}
|
|
||||||
onChange={(e) => handleChange("fill", e.target.value)}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
/>
|
||||||
<div className="text-sm flex px-2 mr-3 border-r border-border">
|
<div className="w-28">
|
||||||
<input
|
<label className="text-sm">شفافیت</label>
|
||||||
type="text"
|
<div className="mt-1 flex h-10 items-center rounded-xl border border-border px-2 text-sm">
|
||||||
className="text-center w-14 border-none outline-none bg-transparent text-sm"
|
<input
|
||||||
value={`${opacityInput}%`}
|
type="text"
|
||||||
onChange={handleOpacityChange}
|
className="w-full border-none bg-transparent text-center text-sm outline-none"
|
||||||
onBlur={handleOpacityBlur}
|
value={`${opacityInput}%`}
|
||||||
onKeyDown={(e) => {
|
onChange={handleOpacityChange}
|
||||||
if (e.key === "Enter") {
|
onBlur={handleOpacityBlur}
|
||||||
e.currentTarget.blur();
|
onKeyDown={(e) => {
|
||||||
}
|
if (e.key === "Enter") {
|
||||||
}}
|
e.currentTarget.blur();
|
||||||
/>
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex gap-2">
|
{/* <div className="mt-4 flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
label="فاصله کلمات"
|
label="فاصله کلمات"
|
||||||
value={`${currentValues.wordSpacing}px`}
|
value={`${formState.wordSpacing}px`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value.replace("px", "").trim();
|
const value = e.target.value.replace("px", "").trim();
|
||||||
const numValue = parseInt(value) || 0;
|
const numValue = parseInt(value) || 0;
|
||||||
@@ -154,14 +192,22 @@ const TextInstruction: FC = () => {
|
|||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="فاصله حروف"
|
label="فاصله حروف"
|
||||||
value={`${currentValues.letterSpacing}px`}
|
value={`${formState.letterSpacing}px`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value.replace("px", "").trim();
|
const value = e.target.value.replace("px", "").trim();
|
||||||
const numValue = parseInt(value) || 0;
|
const numValue = parseInt(value) || 0;
|
||||||
handleChange("letterSpacing", numValue);
|
handleChange("letterSpacing", numValue);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button type="button" onClick={handleSave} disabled={!isDirty} className="w-full">
|
||||||
|
ذخیره تغییرات
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { EditorObject } from "../../../store/editorStore";
|
import type { EditorObject } from "../../../store/editorStore";
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
|
import ColorPicker from "@/components/ColorPicker";
|
||||||
|
|
||||||
type LineSettingsProps = {
|
type LineSettingsProps = {
|
||||||
selectedObject: EditorObject;
|
selectedObject: EditorObject;
|
||||||
@@ -9,19 +10,19 @@ type LineSettingsProps = {
|
|||||||
const LineSettings = ({ selectedObject, onUpdate }: LineSettingsProps) => {
|
const LineSettings = ({ selectedObject, onUpdate }: LineSettingsProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<ColorPicker
|
||||||
label="رنگ خط"
|
label="رنگ خط"
|
||||||
type="color"
|
|
||||||
value={selectedObject.stroke || "#000000"}
|
value={selectedObject.stroke || "#000000"}
|
||||||
onChange={(e) => onUpdate(selectedObject.id, { stroke: e.target.value })}
|
onChange={(value) => onUpdate(selectedObject.id, { stroke: value })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="ضخامت خط"
|
label="ضخامت خط"
|
||||||
type="number"
|
type="number"
|
||||||
value={selectedObject.strokeWidth || 2}
|
min={0}
|
||||||
|
value={selectedObject.strokeWidth ?? 0}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onUpdate(selectedObject.id, {
|
onUpdate(selectedObject.id, {
|
||||||
strokeWidth: parseInt(e.target.value) || 2,
|
strokeWidth: parseInt(e.target.value) || 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { EditorObject } from "../../../store/editorStore";
|
import type { EditorObject } from "../../../store/editorStore";
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
|
import ColorPicker from "@/components/ColorPicker";
|
||||||
|
|
||||||
type LinkSettingsProps = {
|
type LinkSettingsProps = {
|
||||||
selectedObject: EditorObject;
|
selectedObject: EditorObject;
|
||||||
@@ -30,11 +31,10 @@ const LinkSettings = ({ selectedObject, onUpdate }: LinkSettingsProps) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<ColorPicker
|
||||||
label="رنگ متن"
|
label="رنگ متن"
|
||||||
type="color"
|
|
||||||
value={selectedObject.fill || "#0000ff"}
|
value={selectedObject.fill || "#0000ff"}
|
||||||
onChange={(e) => onUpdate(selectedObject.id, { fill: e.target.value })}
|
onChange={(value) => onUpdate(selectedObject.id, { fill: value })}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Button from "@/components/Button";
|
import Button from "@/components/Button";
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
|
import ColorPicker from "@/components/ColorPicker";
|
||||||
import type { EditorObject } from "@/pages/editor/store/editorStore";
|
import type { EditorObject } from "@/pages/editor/store/editorStore";
|
||||||
|
|
||||||
type ShapeSettingsProps = {
|
type ShapeSettingsProps = {
|
||||||
@@ -21,7 +22,7 @@ const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
|
|||||||
const baseHeight = selectedObject.height ?? 100;
|
const baseHeight = selectedObject.height ?? 100;
|
||||||
const baseFill = selectedObject.fill ?? "#3b82f6";
|
const baseFill = selectedObject.fill ?? "#3b82f6";
|
||||||
const baseStroke = selectedObject.stroke ?? "#1e40af";
|
const baseStroke = selectedObject.stroke ?? "#1e40af";
|
||||||
const baseStrokeWidth = selectedObject.strokeWidth ?? 2;
|
const baseStrokeWidth = selectedObject.strokeWidth ?? 0;
|
||||||
|
|
||||||
const [formState, setFormState] = useState<ShapeFormState>({
|
const [formState, setFormState] = useState<ShapeFormState>({
|
||||||
width: String(baseWidth),
|
width: String(baseWidth),
|
||||||
@@ -87,25 +88,23 @@ const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<ColorPicker
|
||||||
label="رنگ پسزمینه"
|
label="رنگ پسزمینه"
|
||||||
type="color"
|
|
||||||
value={formState.fill}
|
value={formState.fill}
|
||||||
onChange={(e) =>
|
onChange={(value) =>
|
||||||
setFormState((prev) => ({
|
setFormState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
fill: e.target.value,
|
fill: value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<ColorPicker
|
||||||
label="رنگ خط"
|
label="رنگ خط"
|
||||||
type="color"
|
|
||||||
value={formState.stroke}
|
value={formState.stroke}
|
||||||
onChange={(e) =>
|
onChange={(value) =>
|
||||||
setFormState((prev) => ({
|
setFormState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
stroke: e.target.value,
|
stroke: value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -119,7 +118,7 @@ const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
|
|||||||
strokeWidth: e.target.value,
|
strokeWidth: e.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
min={1}
|
min={0}
|
||||||
/>
|
/>
|
||||||
<Button type="button" onClick={handleSave} disabled={!isDirty} className="w-full">
|
<Button type="button" onClick={handleSave} disabled={!isDirty} className="w-full">
|
||||||
ذخیره تغییرات
|
ذخیره تغییرات
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { EditorObject } from "../../../store/editorStore";
|
import type { EditorObject } from "../../../store/editorStore";
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
|
import ColorPicker from "@/components/ColorPicker";
|
||||||
|
|
||||||
type TextSettingsProps = {
|
type TextSettingsProps = {
|
||||||
selectedObject: EditorObject;
|
selectedObject: EditorObject;
|
||||||
@@ -24,11 +25,10 @@ const TextSettings = ({ selectedObject, onUpdate }: TextSettingsProps) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<ColorPicker
|
||||||
label="رنگ متن"
|
label="رنگ متن"
|
||||||
type="color"
|
|
||||||
value={selectedObject.fill || "#000000"}
|
value={selectedObject.fill || "#000000"}
|
||||||
onChange={(e) => onUpdate(selectedObject.id, { fill: e.target.value })}
|
onChange={(value) => onUpdate(selectedObject.id, { fill: value })}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const AbstractShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, dr
|
|||||||
outerRadius={radius}
|
outerRadius={radius}
|
||||||
fill={obj.fill}
|
fill={obj.fill}
|
||||||
stroke={isSelected ? "#3b82f6" : obj.stroke}
|
stroke={isSelected ? "#3b82f6" : obj.stroke}
|
||||||
strokeWidth={isSelected ? 3 : obj.strokeWidth || 2}
|
strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const ArrowShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, dragg
|
|||||||
y={obj.y}
|
y={obj.y}
|
||||||
points={[0, 0, (obj.width || 0) - obj.x, (obj.height || 0) - obj.y]}
|
points={[0, 0, (obj.width || 0) - obj.x, (obj.height || 0) - obj.y]}
|
||||||
stroke={isSelected ? "#3b82f6" : obj.stroke || "#000000"}
|
stroke={isSelected ? "#3b82f6" : obj.stroke || "#000000"}
|
||||||
strokeWidth={isSelected ? 3 : obj.strokeWidth || 2}
|
strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)}
|
||||||
fill={isSelected ? "#3b82f6" : obj.stroke || "#000000"}
|
fill={isSelected ? "#3b82f6" : obj.stroke || "#000000"}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const CircleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, drag
|
|||||||
radius={(obj.width || 50) / 2}
|
radius={(obj.width || 50) / 2}
|
||||||
fill={obj.fill}
|
fill={obj.fill}
|
||||||
stroke={isSelected ? "#3b82f6" : obj.stroke}
|
stroke={isSelected ? "#3b82f6" : obj.stroke}
|
||||||
strokeWidth={isSelected ? 3 : obj.strokeWidth || 2}
|
strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const LineShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, dragga
|
|||||||
y={obj.y}
|
y={obj.y}
|
||||||
points={[0, 0, (obj.width || 0) - obj.x, (obj.height || 0) - obj.y]}
|
points={[0, 0, (obj.width || 0) - obj.x, (obj.height || 0) - obj.y]}
|
||||||
stroke={isSelected ? "#3b82f6" : obj.stroke || "#000000"}
|
stroke={isSelected ? "#3b82f6" : obj.stroke || "#000000"}
|
||||||
strokeWidth={isSelected ? 3 : obj.strokeWidth || 2}
|
strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const RectangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, d
|
|||||||
height={obj.height || 100}
|
height={obj.height || 100}
|
||||||
fill={obj.fill}
|
fill={obj.fill}
|
||||||
stroke={isSelected ? "#3b82f6" : obj.stroke}
|
stroke={isSelected ? "#3b82f6" : obj.stroke}
|
||||||
strokeWidth={isSelected ? 3 : obj.strokeWidth || 2}
|
strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -4,33 +4,33 @@ import Konva from "konva";
|
|||||||
import type { TextShapeProps } from "./types";
|
import type { TextShapeProps } from "./types";
|
||||||
|
|
||||||
const getFontFamily = (fontFamily?: string): string => {
|
const getFontFamily = (fontFamily?: string): string => {
|
||||||
const fontMap: Record<string, string> = {
|
const fontMap: Record<string, string> = {
|
||||||
"1": "irancell",
|
"1": "irancell",
|
||||||
"2": "irancell",
|
"2": "irancell",
|
||||||
};
|
};
|
||||||
return fontMap[fontFamily || "1"] || "irancell";
|
return fontMap[fontFamily || "1"] || "irancell";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFontWeight = (fontWeight?: string): string | number => {
|
const getFontWeight = (fontWeight?: string): string => {
|
||||||
const weightMap: Record<string, string | number> = {
|
const weightMap: Record<string, string> = {
|
||||||
bold: "600",
|
bold: "bold",
|
||||||
normal: "300",
|
normal: "normal",
|
||||||
"300": "300",
|
"300": "300",
|
||||||
};
|
};
|
||||||
return weightMap[fontWeight || "normal"] || "300";
|
return weightMap[fontWeight || "normal"] || "normal";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getColorWithOpacity = (fill?: string, opacity?: number): string => {
|
const getColorWithOpacity = (fill?: string, opacity?: number): string => {
|
||||||
if (!fill) return "#000000";
|
if (!fill) return "#000000";
|
||||||
if (opacity === undefined || opacity === 100) return fill;
|
if (opacity === undefined || opacity === 100) return fill;
|
||||||
|
|
||||||
const hex = fill.replace("#", "");
|
const hex = fill.replace("#", "");
|
||||||
const r = parseInt(hex.substring(0, 2), 16);
|
const r = parseInt(hex.substring(0, 2), 16);
|
||||||
const g = parseInt(hex.substring(2, 4), 16);
|
const g = parseInt(hex.substring(2, 4), 16);
|
||||||
const b = parseInt(hex.substring(4, 6), 16);
|
const b = parseInt(hex.substring(4, 6), 16);
|
||||||
const alpha = opacity / 100;
|
const alpha = opacity / 100;
|
||||||
|
|
||||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TextShape = ({
|
const TextShape = ({
|
||||||
@@ -46,10 +46,23 @@ const TextShape = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSelected && shapeRef.current && transformerRef.current) {
|
if (isSelected && shapeRef.current && transformerRef.current) {
|
||||||
transformerRef.current.nodes([shapeRef.current]);
|
const textNode = shapeRef.current;
|
||||||
transformerRef.current.getLayer()?.batchDraw();
|
|
||||||
|
// تنظیم offset برای حذف فضای خالی
|
||||||
|
textNode.offsetX(0);
|
||||||
|
textNode.offsetY(0);
|
||||||
|
|
||||||
|
transformerRef.current.nodes([textNode]);
|
||||||
|
|
||||||
|
// بهروزرسانی transformer بعد از یک تیک برای محاسبه دقیق bounds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (transformerRef.current && shapeRef.current) {
|
||||||
|
transformerRef.current.forceUpdate();
|
||||||
|
transformerRef.current.getLayer()?.batchDraw();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
}, [isSelected, transformerRef]);
|
}, [isSelected, transformerRef, obj.text, obj.width, obj.height, obj.fontSize, obj.fontFamily, obj.fontWeight]);
|
||||||
|
|
||||||
const fontFamily = useMemo(() => getFontFamily(obj.fontFamily), [obj.fontFamily]);
|
const fontFamily = useMemo(() => getFontFamily(obj.fontFamily), [obj.fontFamily]);
|
||||||
const fontWeight = useMemo(() => getFontWeight(obj.fontWeight), [obj.fontWeight]);
|
const fontWeight = useMemo(() => getFontWeight(obj.fontWeight), [obj.fontWeight]);
|
||||||
@@ -64,10 +77,11 @@ const TextShape = ({
|
|||||||
fontSize={obj.fontSize || 24}
|
fontSize={obj.fontSize || 24}
|
||||||
fill={fillColor}
|
fill={fillColor}
|
||||||
fontFamily={fontFamily}
|
fontFamily={fontFamily}
|
||||||
fontWeight={fontWeight}
|
fontStyle={fontWeight}
|
||||||
letterSpacing={obj.letterSpacing}
|
letterSpacing={obj.letterSpacing ?? 0}
|
||||||
width={obj.width}
|
width={obj.width}
|
||||||
height={obj.height}
|
height={obj.height}
|
||||||
|
align="right"
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -105,35 +119,42 @@ const TextShape = ({
|
|||||||
const stage = textNode.getStage();
|
const stage = textNode.getStage();
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
|
|
||||||
|
const stageBox = stage.container().getBoundingClientRect();
|
||||||
const textPosition = textNode.absolutePosition();
|
const textPosition = textNode.absolutePosition();
|
||||||
|
const scaleX = stage.scaleX() || 1;
|
||||||
|
const scaleY = stage.scaleY() || 1;
|
||||||
const areaPosition = {
|
const areaPosition = {
|
||||||
x: stage.container().offsetLeft + textPosition.x,
|
x: stageBox.left + textPosition.x * scaleX,
|
||||||
y: stage.container().offsetTop + textPosition.y,
|
y: stageBox.top + textPosition.y * scaleY,
|
||||||
};
|
};
|
||||||
|
|
||||||
const textarea = document.createElement("textarea");
|
const textarea = document.createElement("textarea");
|
||||||
document.body.appendChild(textarea);
|
document.body.appendChild(textarea);
|
||||||
|
textNode.hide();
|
||||||
|
layerRef.current?.draw();
|
||||||
textarea.value = textNode.text();
|
textarea.value = textNode.text();
|
||||||
textarea.style.position = "absolute";
|
textarea.style.position = "absolute";
|
||||||
textarea.style.top = `${areaPosition.y}px`;
|
textarea.style.top = `${areaPosition.y}px`;
|
||||||
textarea.style.left = `${areaPosition.x}px`;
|
textarea.style.left = `${areaPosition.x}px`;
|
||||||
textarea.style.width = `${textNode.width()}px`;
|
textarea.style.width = `${textNode.width() * scaleX}px`;
|
||||||
textarea.style.fontSize = `${textNode.fontSize()}px`;
|
textarea.style.fontSize = `${textNode.fontSize() * scaleY}px`;
|
||||||
textarea.style.border = "none";
|
textarea.style.border = "none";
|
||||||
textarea.style.padding = "0px";
|
textarea.style.padding = "50px";
|
||||||
textarea.style.margin = "0px";
|
textarea.style.margin = "0px";
|
||||||
textarea.style.overflow = "hidden";
|
textarea.style.overflow = "hidden";
|
||||||
textarea.style.background = "transparent";
|
textarea.style.background = "transparent";
|
||||||
textarea.style.outline = "none";
|
textarea.style.outline = "none";
|
||||||
textarea.style.resize = "none";
|
textarea.style.resize = "none";
|
||||||
textarea.style.fontFamily = fontFamily;
|
textarea.style.fontFamily = fontFamily;
|
||||||
textarea.style.fontWeight = String(fontWeight);
|
textarea.style.fontWeight = fontWeight === "bold" ? "bold" : "normal";
|
||||||
textarea.style.letterSpacing = `${obj.letterSpacing || 0}px`;
|
textarea.style.letterSpacing = `${obj.letterSpacing ?? 0}px`;
|
||||||
|
textarea.style.textAlign = textNode.align() || "right";
|
||||||
textarea.style.direction = "rtl";
|
textarea.style.direction = "rtl";
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
|
|
||||||
const removeTextarea = () => {
|
const removeTextarea = () => {
|
||||||
document.body.removeChild(textarea);
|
document.body.removeChild(textarea);
|
||||||
|
textNode.show();
|
||||||
textNode.text(textarea.value);
|
textNode.text(textarea.value);
|
||||||
onUpdate(obj.id, { text: textarea.value });
|
onUpdate(obj.id, { text: textarea.value });
|
||||||
layerRef.current?.draw();
|
layerRef.current?.draw();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const TriangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, dr
|
|||||||
radius={radius}
|
radius={radius}
|
||||||
fill={obj.fill}
|
fill={obj.fill}
|
||||||
stroke={isSelected ? "#3b82f6" : obj.stroke}
|
stroke={isSelected ? "#3b82f6" : obj.stroke}
|
||||||
strokeWidth={isSelected ? 3 : obj.strokeWidth || 2}
|
strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const VideoShape = ({
|
|||||||
height={obj.height || 300}
|
height={obj.height || 300}
|
||||||
fill="#000000"
|
fill="#000000"
|
||||||
stroke={isSelected ? "#3b82f6" : "#666666"}
|
stroke={isSelected ? "#3b82f6" : "#666666"}
|
||||||
strokeWidth={isSelected ? 3 : 2}
|
strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ export const useTextDefaultsStore = create<TextDefaultsStoreType>((set) => ({
|
|||||||
fontFamily: "1",
|
fontFamily: "1",
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fill: "#A27BC1",
|
fill: "#000000",
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
letterSpacing: 10,
|
letterSpacing: 0,
|
||||||
wordSpacing: 10,
|
wordSpacing: 10,
|
||||||
},
|
},
|
||||||
updateDefaults: (updates) =>
|
updateDefaults: (updates) =>
|
||||||
@@ -30,4 +30,3 @@ export const useTextDefaultsStore = create<TextDefaultsStoreType>((set) => ({
|
|||||||
defaults: { ...state.defaults, ...updates },
|
defaults: { ...state.defaults, ...updates },
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user