mirror of
https://github.com/r2r90/canvas-label.git
synced 2025-12-17 05:29:27 +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
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
.idea
|
||||||
@@ -145,6 +145,8 @@ const Canvas = () => {
|
|||||||
if (item.type === StageItemType.Text) {
|
if (item.type === StageItemType.Text) {
|
||||||
return (
|
return (
|
||||||
<TransformableText
|
<TransformableText
|
||||||
|
isEditing={isEditing}
|
||||||
|
onEditChange={toggleEdit}
|
||||||
onSelect={() => selectItem(item.id)}
|
onSelect={() => selectItem(item.id)}
|
||||||
isSelected={item.id === selectedItemId}
|
isSelected={item.id === selectedItemId}
|
||||||
onChange={(newAttrs) => {
|
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 { Label } from "@/components/ui/label";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { useAppDispatch } from "@/hooks";
|
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 { HoverCard, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||||
import { type ImageConfig } from "konva/lib/shapes/Image";
|
import { type ImageConfig } from "konva/lib/shapes/Image";
|
||||||
import { TransformableImageProps } from "@/components/transformable-image";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
currentImage: ImageConfig;
|
currentImage: ImageConfig;
|
||||||
@@ -20,12 +19,12 @@ export default function ImageOpacityTool({
|
|||||||
|
|
||||||
const value = currentImage.opacity ?? 1;
|
const value = currentImage.opacity ?? 1;
|
||||||
|
|
||||||
const handleOpacityChange = (e) => {
|
const handleOpacityChange = (values: number[]) => {
|
||||||
if (!selectedItemId) return;
|
if (!selectedItemId) return;
|
||||||
dispatch(
|
dispatch(
|
||||||
updateImage({
|
updateImage({
|
||||||
id: selectedItemId,
|
id: selectedItemId,
|
||||||
opacity: e,
|
opacity: values[0],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -35,20 +34,18 @@ export default function ImageOpacityTool({
|
|||||||
<HoverCardTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="flex items-center justify-between">
|
<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">
|
<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}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
id="fontSize"
|
id="opacity"
|
||||||
min={0}
|
min={0}
|
||||||
max={1}
|
max={1}
|
||||||
defaultValue={[currentImage.opacity ?? 16]}
|
defaultValue={[currentImage.opacity ?? 16]}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
onValueChange={(e) => {
|
onValueChange={handleOpacityChange}
|
||||||
handleOpacityChange(e[0]);
|
|
||||||
}}
|
|
||||||
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4"
|
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4"
|
||||||
aria-label="opacity"
|
aria-label="opacity"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Transformer, Image } from "react-konva";
|
|||||||
import useImage from "use-image";
|
import useImage from "use-image";
|
||||||
|
|
||||||
type TransformableImageConfig = Omit<ImageConfig, "image"> & {
|
type TransformableImageConfig = Omit<ImageConfig, "image"> & {
|
||||||
image?: HTMLImageElement;
|
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import type { TextConfig } from "konva/lib/shapes/Text";
|
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 { Text, Transformer } from "react-konva";
|
||||||
|
import { EditableTextInput } from "@/components/editable-text-input";
|
||||||
|
|
||||||
type TransformableTextConfig = Omit<TextConfig, "text"> & {
|
type TransformableTextConfig = Omit<TextConfig, "text"> & {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -11,13 +19,15 @@ type TransformableTextConfig = Omit<TextConfig, "text"> & {
|
|||||||
fontStyle?: string;
|
fontStyle?: string;
|
||||||
fontVariant?: string;
|
fontVariant?: string;
|
||||||
textDecoration?: string;
|
textDecoration?: string;
|
||||||
align?: string;
|
align?: CSSProperties["textAlign"];
|
||||||
verticalAlign?: string;
|
verticalAlign?: string;
|
||||||
padding?: number;
|
padding?: number;
|
||||||
lineHeight?: number;
|
lineHeight?: number;
|
||||||
letterSpacing?: number;
|
letterSpacing?: number;
|
||||||
wrap?: string;
|
wrap?: string;
|
||||||
ellipsis?: boolean;
|
ellipsis?: boolean;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TransformableTextProps = {
|
export type TransformableTextProps = {
|
||||||
@@ -26,6 +36,8 @@ export type TransformableTextProps = {
|
|||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onChange: (newAttrs: TransformableTextConfig) => void;
|
onChange: (newAttrs: TransformableTextConfig) => void;
|
||||||
id: string;
|
id: string;
|
||||||
|
isEditing?: boolean;
|
||||||
|
onEditChange?: (isEditing: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TransformableText = ({
|
const TransformableText = ({
|
||||||
@@ -34,10 +46,33 @@ const TransformableText = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onChange,
|
onChange,
|
||||||
id,
|
id,
|
||||||
|
isEditing,
|
||||||
|
onEditChange,
|
||||||
}: TransformableTextProps) => {
|
}: TransformableTextProps) => {
|
||||||
|
console.log(textProps);
|
||||||
|
const [value, setText] = useState<string | undefined>("");
|
||||||
const textRef = useRef<ElementRef<typeof Text>>(null);
|
const textRef = useRef<ElementRef<typeof Text>>(null);
|
||||||
const trRef = useRef<ElementRef<typeof Transformer>>(null);
|
const trRef = useRef<ElementRef<typeof Transformer>>(null);
|
||||||
const text = textProps.text;
|
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(() => {
|
useEffect(() => {
|
||||||
if (!textRef.current) return;
|
if (!textRef.current) return;
|
||||||
@@ -48,7 +83,24 @@ const TransformableText = ({
|
|||||||
trRef.current?.getLayer()?.batchDraw();
|
trRef.current?.getLayer()?.batchDraw();
|
||||||
}
|
}
|
||||||
}, [isSelected]);
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
@@ -78,7 +130,8 @@ const TransformableText = ({
|
|||||||
y: e.target.y(),
|
y: e.target.y(),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onTransformEnd={(e) => {
|
onDblClick={handleDblClick}
|
||||||
|
onTransformEnd={() => {
|
||||||
const node = textRef.current;
|
const node = textRef.current;
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
@@ -111,4 +164,5 @@ const TransformableText = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TransformableText;
|
export default TransformableText;
|
||||||
|
|||||||
@@ -39,8 +39,12 @@ const initialState = {
|
|||||||
|
|
||||||
const defaultTextConfig = {
|
const defaultTextConfig = {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
align: "center",
|
align: "center" as const,
|
||||||
fontFamily: "Roboto",
|
fontFamily: "Roboto",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 100,
|
||||||
|
height: 16,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultImageConfig = {
|
const defaultImageConfig = {
|
||||||
@@ -58,8 +62,6 @@ export enum OrderDirection {
|
|||||||
Down = "down",
|
Down = "down",
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddStageItemAction = PayloadAction<StageItem>;
|
|
||||||
|
|
||||||
export const appSlice = createSlice({
|
export const appSlice = createSlice({
|
||||||
name: "app",
|
name: "app",
|
||||||
initialState,
|
initialState,
|
||||||
|
|||||||
Reference in New Issue
Block a user