mirror of
https://github.com/r2r90/canvas-label.git
synced 2025-12-16 12:35:43 +00:00
wip: editable text working
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -40,3 +40,4 @@ yarn-error.log*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
.idea
|
||||
@@ -145,6 +145,8 @@ const Canvas = () => {
|
||||
if (item.type === StageItemType.Text) {
|
||||
return (
|
||||
<TransformableText
|
||||
isEditing={isEditing}
|
||||
onEditChange={toggleEdit}
|
||||
onSelect={() => selectItem(item.id)}
|
||||
isSelected={item.id === selectedItemId}
|
||||
onChange={(newAttrs) => {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { EditableTextInput } from "./EditableText";
|
||||
import TransformableText from "@/components/transformable-text";
|
||||
import { ResizableText } from "@/components/text";
|
||||
|
||||
const RETURN_KEY = 13;
|
||||
const ESCAPE_KEY = 27;
|
||||
|
||||
export function EditableText({
|
||||
x,
|
||||
y,
|
||||
isEditing,
|
||||
isTransforming,
|
||||
onToggleEdit,
|
||||
onToggleTransform,
|
||||
onChange,
|
||||
onResize,
|
||||
text,
|
||||
width,
|
||||
height,
|
||||
}: any) {
|
||||
function handleEscapeKeys(e: any) {
|
||||
if ((e.keyCode === RETURN_KEY && !e.shiftKey) || e.keyCode === ESCAPE_KEY) {
|
||||
onToggleEdit(e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleTextChange(e: any) {
|
||||
onChange(e.currentTarget.value);
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<EditableTextInput
|
||||
x={x}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
value={text}
|
||||
onChange={handleTextChange}
|
||||
onKeyDown={handleEscapeKeys}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ResizableText
|
||||
x={x}
|
||||
y={y}
|
||||
isSelected={isTransforming}
|
||||
onClick={onToggleTransform}
|
||||
onDoubleClick={onToggleEdit}
|
||||
onResize={onResize}
|
||||
text={text}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
72
src/components/editable-text-input.tsx
Normal file
72
src/components/editable-text-input.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { type CSSProperties } from "react";
|
||||
import { Html } from "react-konva-utils";
|
||||
|
||||
type Props = {
|
||||
x: number;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
align: CSSProperties["textAlign"];
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
value?: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
};
|
||||
|
||||
export function EditableTextInput({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
value = "",
|
||||
onChange,
|
||||
onKeyDown,
|
||||
fontSize,
|
||||
fontFamily,
|
||||
align,
|
||||
}: Props) {
|
||||
const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (textAreaRef.current) {
|
||||
textAreaRef.current.style.height = `${height + 5}px`;
|
||||
textAreaRef.current.style.height = "auto";
|
||||
// after browsers resized it we can set actual value
|
||||
textAreaRef.current.style.height =
|
||||
textAreaRef.current.scrollHeight + 3 + "px";
|
||||
textAreaRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
const style: CSSProperties = {
|
||||
textAlign: align,
|
||||
width: `${width}px`,
|
||||
height: `${height + 5}px`,
|
||||
border: "none",
|
||||
padding: 0,
|
||||
margin: "-2px 0 0",
|
||||
background: "none",
|
||||
outline: "none",
|
||||
resize: "none",
|
||||
color: "black",
|
||||
lineHeight: 1,
|
||||
overflow: "hidden",
|
||||
fontSize,
|
||||
fontFamily,
|
||||
};
|
||||
|
||||
return (
|
||||
<Html
|
||||
groupProps={{ x, y }}
|
||||
divProps={{ style: { opacity: 1, display: "flex" } }}
|
||||
>
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
style={style}
|
||||
ref={textAreaRef}
|
||||
/>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
@@ -2,10 +2,9 @@ import React from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { useAppDispatch } from "@/hooks";
|
||||
import { updateImage, updateText } from "@/store/app.slice";
|
||||
import { updateImage } from "@/store/app.slice";
|
||||
import { HoverCard, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||
import { type ImageConfig } from "konva/lib/shapes/Image";
|
||||
import { TransformableImageProps } from "@/components/transformable-image";
|
||||
|
||||
type Props = {
|
||||
currentImage: ImageConfig;
|
||||
@@ -20,12 +19,12 @@ export default function ImageOpacityTool({
|
||||
|
||||
const value = currentImage.opacity ?? 1;
|
||||
|
||||
const handleOpacityChange = (e) => {
|
||||
const handleOpacityChange = (values: number[]) => {
|
||||
if (!selectedItemId) return;
|
||||
dispatch(
|
||||
updateImage({
|
||||
id: selectedItemId,
|
||||
opacity: e,
|
||||
opacity: values[0],
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -35,20 +34,18 @@ export default function ImageOpacityTool({
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="fontSize">Opacity</Label>
|
||||
<Label htmlFor="opacity">Opacity</Label>
|
||||
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="fontSize"
|
||||
id="opacity"
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={[currentImage.opacity ?? 16]}
|
||||
step={0.1}
|
||||
onValueChange={(e) => {
|
||||
handleOpacityChange(e[0]);
|
||||
}}
|
||||
onValueChange={handleOpacityChange}
|
||||
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4"
|
||||
aria-label="opacity"
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Transformer, Image } from "react-konva";
|
||||
import useImage from "use-image";
|
||||
|
||||
type TransformableImageConfig = Omit<ImageConfig, "image"> & {
|
||||
image?: HTMLImageElement;
|
||||
imageUrl: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import type { TextConfig } from "konva/lib/shapes/Text";
|
||||
import { type ElementRef, useEffect, useRef } from "react";
|
||||
import {
|
||||
type CSSProperties,
|
||||
type ElementRef,
|
||||
type KeyboardEvent,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Text, Transformer } from "react-konva";
|
||||
import { EditableTextInput } from "@/components/editable-text-input";
|
||||
|
||||
type TransformableTextConfig = Omit<TextConfig, "text"> & {
|
||||
text?: string;
|
||||
@@ -11,13 +19,15 @@ type TransformableTextConfig = Omit<TextConfig, "text"> & {
|
||||
fontStyle?: string;
|
||||
fontVariant?: string;
|
||||
textDecoration?: string;
|
||||
align?: string;
|
||||
align?: CSSProperties["textAlign"];
|
||||
verticalAlign?: string;
|
||||
padding?: number;
|
||||
lineHeight?: number;
|
||||
letterSpacing?: number;
|
||||
wrap?: string;
|
||||
ellipsis?: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type TransformableTextProps = {
|
||||
@@ -26,6 +36,8 @@ export type TransformableTextProps = {
|
||||
onSelect: () => void;
|
||||
onChange: (newAttrs: TransformableTextConfig) => void;
|
||||
id: string;
|
||||
isEditing?: boolean;
|
||||
onEditChange?: (isEditing: boolean) => void;
|
||||
};
|
||||
|
||||
const TransformableText = ({
|
||||
@@ -34,10 +46,33 @@ const TransformableText = ({
|
||||
onSelect,
|
||||
onChange,
|
||||
id,
|
||||
isEditing,
|
||||
onEditChange,
|
||||
}: TransformableTextProps) => {
|
||||
console.log(textProps);
|
||||
const [value, setText] = useState<string | undefined>("");
|
||||
const textRef = useRef<ElementRef<typeof Text>>(null);
|
||||
const trRef = useRef<ElementRef<typeof Transformer>>(null);
|
||||
const text = textProps.text;
|
||||
console.log(textRef.current);
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
onChange({
|
||||
...textProps,
|
||||
text: value,
|
||||
});
|
||||
onEditChange?.(false);
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
setText("");
|
||||
onEditChange?.(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDblClick = () => {
|
||||
setText(text);
|
||||
onEditChange?.(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!textRef.current) return;
|
||||
@@ -48,7 +83,24 @@ const TransformableText = ({
|
||||
trRef.current?.getLayer()?.batchDraw();
|
||||
}
|
||||
}, [isSelected]);
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<EditableTextInput
|
||||
fontSize={textProps.fontSize ?? 16}
|
||||
fontFamily={textProps.fontFamily}
|
||||
align={textProps.align}
|
||||
x={textProps.x}
|
||||
y={textProps.y}
|
||||
width={textProps.width}
|
||||
height={textProps.height}
|
||||
onChange={(e) => {
|
||||
setText(e.target.value);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Text
|
||||
@@ -78,7 +130,8 @@ const TransformableText = ({
|
||||
y: e.target.y(),
|
||||
});
|
||||
}}
|
||||
onTransformEnd={(e) => {
|
||||
onDblClick={handleDblClick}
|
||||
onTransformEnd={() => {
|
||||
const node = textRef.current;
|
||||
if (!node) return;
|
||||
|
||||
@@ -111,4 +164,5 @@ const TransformableText = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransformableText;
|
||||
|
||||
@@ -39,8 +39,12 @@ const initialState = {
|
||||
|
||||
const defaultTextConfig = {
|
||||
fontSize: 16,
|
||||
align: "center",
|
||||
align: "center" as const,
|
||||
fontFamily: "Roboto",
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 100,
|
||||
height: 16,
|
||||
};
|
||||
|
||||
const defaultImageConfig = {
|
||||
@@ -58,8 +62,6 @@ export enum OrderDirection {
|
||||
Down = "down",
|
||||
}
|
||||
|
||||
type AddStageItemAction = PayloadAction<StageItem>;
|
||||
|
||||
export const appSlice = createSlice({
|
||||
name: "app",
|
||||
initialState,
|
||||
|
||||
Reference in New Issue
Block a user