wip: editable text working

This commit is contained in:
2023-11-29 14:35:27 +01:00
parent 6e91fb6447
commit 3c94fa37be
8 changed files with 144 additions and 75 deletions

1
.gitignore vendored
View File

@@ -40,3 +40,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
.idea

View File

@@ -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) => {

View File

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

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

View File

@@ -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"
/>

View File

@@ -4,7 +4,6 @@ import { Transformer, Image } from "react-konva";
import useImage from "use-image";
type TransformableImageConfig = Omit<ImageConfig, "image"> & {
image?: HTMLImageElement;
imageUrl: string;
};

View File

@@ -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;

View File

@@ -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,