textfields [1 of 3]: add text into speech bubble; also add rich text example (#3050)

This is the first of three textfield changes. This starts with making
the speech bubble actually have text. Also, it creates a TipTap example
and how that would be wired up.

🎵 this is dangerous, I walk through textfields so watch your head rock 🎵

### Change Type

- [x] `minor` — New feature

### Release Notes

- Refactor textfields be composable/swappable.
This commit is contained in:
Mime Čuvalo 2024-03-27 09:33:48 +00:00 committed by GitHub
parent 3593799d9e
commit d76d53db95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 616 additions and 346 deletions

View file

@ -2,7 +2,7 @@
title: Speech bubble title: Speech bubble
component: ./CustomShapeWithHandles.tsx component: ./CustomShapeWithHandles.tsx
category: shapes/tools category: shapes/tools
priority: 2 priority: 1
--- ---
A custom shape with handles A custom shape with handles

View file

@ -1,19 +1,24 @@
import { ShapePropsType } from '@tldraw/tlschema/src/shapes/TLBaseShape'
import { import {
DefaultColorStyle, DefaultColorStyle,
DefaultFontStyle,
DefaultHorizontalAlignStyle,
DefaultSizeStyle, DefaultSizeStyle,
DefaultVerticalAlignStyle,
FONT_FAMILIES,
Geometry2d, Geometry2d,
LABEL_FONT_SIZES,
Polygon2d, Polygon2d,
ShapeUtil, ShapeUtil,
T, T,
TEXT_PROPS,
TLBaseShape, TLBaseShape,
TLDefaultColorStyle,
TLDefaultSizeStyle,
TLHandle, TLHandle,
TLOnBeforeUpdateHandler, TLOnBeforeUpdateHandler,
TLOnHandleDragHandler, TLOnHandleDragHandler,
TLOnResizeHandler, TLOnResizeHandler,
TextLabel,
Vec, Vec,
VecModel,
ZERO_INDEX_KEY, ZERO_INDEX_KEY,
getDefaultColorTheme, getDefaultColorTheme,
resizeBox, resizeBox,
@ -33,28 +38,28 @@ export const STROKE_SIZES = {
// There's a guide at the bottom of this file! // There's a guide at the bottom of this file!
// [1] // [1]
export type SpeechBubbleShape = TLBaseShape<
'speech-bubble', export const speechBubbleShapeProps = {
{ w: T.number,
w: number h: T.number,
h: number size: DefaultSizeStyle,
size: TLDefaultSizeStyle color: DefaultColorStyle,
color: TLDefaultColorStyle font: DefaultFontStyle,
tail: VecModel align: DefaultHorizontalAlignStyle,
verticalAlign: DefaultVerticalAlignStyle,
growY: T.positiveNumber,
text: T.string,
tail: vecModelValidator,
} }
>
export type SpeechBubbleShapeProps = ShapePropsType<typeof speechBubbleShapeProps>
export type SpeechBubbleShape = TLBaseShape<'speech-bubble', SpeechBubbleShapeProps>
export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> { export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
static override type = 'speech-bubble' as const static override type = 'speech-bubble' as const
// [2] // [2]
static override props = { static override props = speechBubbleShapeProps
w: T.number,
h: T.number,
size: DefaultSizeStyle,
color: DefaultColorStyle,
tail: vecModelValidator,
}
override isAspectRatioLocked = (_shape: SpeechBubbleShape) => false override isAspectRatioLocked = (_shape: SpeechBubbleShape) => false
@ -62,17 +67,28 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
override canBind = (_shape: SpeechBubbleShape) => true override canBind = (_shape: SpeechBubbleShape) => true
override canEdit = () => true
// [3] // [3]
getDefaultProps(): SpeechBubbleShape['props'] { getDefaultProps(): SpeechBubbleShapeProps {
return { return {
w: 200, w: 200,
h: 130, h: 130,
color: 'black', color: 'black',
size: 'm', size: 'm',
font: 'draw',
align: 'middle',
verticalAlign: 'start',
growY: 0,
text: '',
tail: { x: 0.5, y: 1.5 }, tail: { x: 0.5, y: 1.5 },
} }
} }
getHeight(shape: SpeechBubbleShape) {
return shape.props.h + shape.props.growY
}
getGeometry(shape: SpeechBubbleShape): Geometry2d { getGeometry(shape: SpeechBubbleShape): Geometry2d {
const speechBubbleGeometry = getSpeechBubbleVertices(shape) const speechBubbleGeometry = getSpeechBubbleVertices(shape)
const body = new Polygon2d({ const body = new Polygon2d({
@ -84,7 +100,7 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
// [4] // [4]
override getHandles(shape: SpeechBubbleShape): TLHandle[] { override getHandles(shape: SpeechBubbleShape): TLHandle[] {
const { tail, w, h } = shape.props const { tail, w } = shape.props
return [ return [
{ {
@ -94,7 +110,7 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
// props.tail coordinates are normalized // props.tail coordinates are normalized
// but here we need them in shape space // but here we need them in shape space
x: tail.x * w, x: tail.x * w,
y: tail.y * h, y: tail.y * this.getHeight(shape),
}, },
] ]
} }
@ -105,29 +121,34 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
props: { props: {
tail: { tail: {
x: handle.x / shape.props.w, x: handle.x / shape.props.w,
y: handle.y / shape.props.h, y: handle.y / this.getHeight(shape),
}, },
}, },
} }
} }
override onBeforeCreate = (next: SpeechBubbleShape) => {
return this.getGrowY(next, next.props.growY)
}
// [5] // [5]
override onBeforeUpdate: TLOnBeforeUpdateHandler<SpeechBubbleShape> | undefined = ( override onBeforeUpdate: TLOnBeforeUpdateHandler<SpeechBubbleShape> | undefined = (
_: SpeechBubbleShape, prev: SpeechBubbleShape,
shape: SpeechBubbleShape shape: SpeechBubbleShape
) => { ) => {
const { w, h, tail } = shape.props const { w, tail } = shape.props
const fullHeight = this.getHeight(shape)
const { segmentsIntersection, insideShape } = getTailIntersectionPoint(shape) const { segmentsIntersection, insideShape } = getTailIntersectionPoint(shape)
const slantedLength = Math.hypot(w, h) const slantedLength = Math.hypot(w, fullHeight)
const MIN_DISTANCE = slantedLength / 5 const MIN_DISTANCE = slantedLength / 5
const MAX_DISTANCE = slantedLength / 1.5 const MAX_DISTANCE = slantedLength / 1.5
const tailInShapeSpace = new Vec(tail.x * w, tail.y * h) const tailInShapeSpace = new Vec(tail.x * w, tail.y * fullHeight)
const distanceToIntersection = tailInShapeSpace.dist(segmentsIntersection) const distanceToIntersection = tailInShapeSpace.dist(segmentsIntersection)
const center = new Vec(w / 2, h / 2) const center = new Vec(w / 2, fullHeight / 2)
const tailDirection = Vec.Sub(tailInShapeSpace, center).uni() const tailDirection = Vec.Sub(tailInShapeSpace, center).uni()
let newPoint = tailInShapeSpace let newPoint = tailInShapeSpace
@ -144,12 +165,17 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
const next = structuredClone(shape) const next = structuredClone(shape)
next.props.tail.x = newPoint.x / w next.props.tail.x = newPoint.x / w
next.props.tail.y = newPoint.y / h next.props.tail.y = newPoint.y / fullHeight
return next return this.getGrowY(next, prev.props.growY)
} }
component(shape: SpeechBubbleShape) { component(shape: SpeechBubbleShape) {
const {
id,
type,
props: { color, font, size, align, text },
} = shape
const theme = getDefaultColorTheme({ const theme = getDefaultColorTheme({
isDarkMode: this.editor.user.getIsDarkMode(), isDarkMode: this.editor.user.getIsDarkMode(),
}) })
@ -161,11 +187,24 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
<svg className="tl-svg-container"> <svg className="tl-svg-container">
<path <path
d={pathData} d={pathData}
strokeWidth={STROKE_SIZES[shape.props.size]} strokeWidth={STROKE_SIZES[size]}
stroke={theme[shape.props.color].solid} stroke={theme[color].solid}
fill={'none'} fill={'none'}
/> />
</svg> </svg>
<TextLabel
id={id}
type={type}
font={font}
fontSize={LABEL_FONT_SIZES[size]}
lineHeight={TEXT_PROPS.lineHeight}
align={align}
verticalAlign="start"
text={text}
labelColor={color}
wrap
/>
</> </>
) )
} }
@ -185,6 +224,37 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
next.props.h = resized.props.h next.props.h = resized.props.h
return next return next
} }
getGrowY(shape: SpeechBubbleShape, prevGrowY = 0) {
const PADDING = 17
const nextTextSize = this.editor.textMeasure.measureText(shape.props.text, {
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: LABEL_FONT_SIZES[shape.props.size],
maxWidth: shape.props.w - PADDING * 2,
})
const nextHeight = nextTextSize.h + PADDING * 2
let growY = 0
if (nextHeight > shape.props.h) {
growY = nextHeight - shape.props.h
} else {
if (prevGrowY) {
growY = 0
}
}
return {
...shape,
props: {
...shape.props,
growY,
},
}
}
} }
/* /*

View file

@ -2,14 +2,20 @@ import { Vec, VecLike, lerp, pointInPolygon } from 'tldraw'
import { SpeechBubbleShape } from './SpeechBubbleUtil' import { SpeechBubbleShape } from './SpeechBubbleUtil'
export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec[] => { export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec[] => {
const { w, h, tail } = shape.props const { w, tail } = shape.props
const tailInShapeSpace = new Vec(tail.x * w, tail.y * h) const fullHeight = shape.props.h + shape.props.growY
const tailInShapeSpace = new Vec(tail.x * w, tail.y * fullHeight)
const [tl, tr, br, bl] = [new Vec(0, 0), new Vec(w, 0), new Vec(w, h), new Vec(0, h)] const [tl, tr, br, bl] = [
new Vec(0, 0),
new Vec(w, 0),
new Vec(w, fullHeight),
new Vec(0, fullHeight),
]
const offsetH = w / 10 const offsetH = w / 10
const offsetV = h / 10 const offsetV = fullHeight / 10
const { adjustedIntersection, intersectionSegmentIndex } = getTailIntersectionPoint(shape) const { adjustedIntersection, intersectionSegmentIndex } = getTailIntersectionPoint(shape)
@ -73,11 +79,12 @@ export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec[] => {
} }
export function getTailIntersectionPoint(shape: SpeechBubbleShape) { export function getTailIntersectionPoint(shape: SpeechBubbleShape) {
const { w, h, tail } = shape.props const { w, tail } = shape.props
const tailInShapeSpace = new Vec(tail.x * w, tail.y * h) const fullHeight = shape.props.h + shape.props.growY
const tailInShapeSpace = new Vec(tail.x * w, tail.y * fullHeight)
const center = new Vec(w / 2, h / 2) const center = new Vec(w / 2, fullHeight / 2)
const corners = [new Vec(0, 0), new Vec(w, 0), new Vec(w, h), new Vec(0, h)] const corners = [new Vec(0, 0), new Vec(w, 0), new Vec(w, fullHeight), new Vec(0, fullHeight)]
const segments = [ const segments = [
[corners[0], corners[1]], [corners[0], corners[1]],
[corners[1], corners[2]], [corners[1], corners[2]],
@ -132,7 +139,7 @@ export function getTailIntersectionPoint(shape: SpeechBubbleShape) {
const squared = mapRange(-1, 1, 0, totalDistance, squaredRelative) // -1 to 1 -> absolute const squared = mapRange(-1, 1, 0, totalDistance, squaredRelative) // -1 to 1 -> absolute
//keep it away from the edges //keep it away from the edges
const offset = (segments.indexOf(intersectionSegment) % 2 === 0 ? w / 10 : h / 10) * 3 const offset = (segments.indexOf(intersectionSegment) % 2 === 0 ? w / 10 : fullHeight / 10) * 3
const constrained = mapRange(0, totalDistance, offset, totalDistance - offset, distance) const constrained = mapRange(0, totalDistance, offset, totalDistance - offset, distance)
// combine the two // combine the two

View file

@ -2,3 +2,9 @@
.tl-user-handles { .tl-user-handles {
z-index: 101; z-index: 101;
} }
/* The text label doesn't normally deal with text that goes sideways,
* so this accounts for that */
.tl-shape[data-shape-type='speech-bubble'] .tl-text-label {
justify-content: flex-start !important;
}

View file

@ -682,7 +682,7 @@ input,
/* ------------------- Text Shape ------------------- */ /* ------------------- Text Shape ------------------- */
.tl-text-shape__wrapper { .tl-text-shape-label {
position: relative; position: relative;
font-weight: normal; font-weight: normal;
min-width: 1px; min-width: 1px;
@ -698,35 +698,38 @@ input,
text-shadow: var(--tl-text-outline); text-shadow: var(--tl-text-outline);
} }
.tl-text-shape__wrapper[data-align='start'] { .tl-text-wrapper[data-font='draw'] {
text-align: left;
}
.tl-text-shape__wrapper[data-align='middle'] {
text-align: center;
}
.tl-text-shape__wrapper[data-align='end'] {
text-align: right;
}
.tl-text-shape__wrapper[data-font='draw'] {
font-family: var(--tl-font-draw); font-family: var(--tl-font-draw);
} }
.tl-text-shape__wrapper[data-font='sans'] { .tl-text-wrapper[data-font='sans'] {
font-family: var(--tl-font-sans); font-family: var(--tl-font-sans);
} }
.tl-text-shape__wrapper[data-font='serif'] { .tl-text-wrapper[data-font='serif'] {
font-family: var(--tl-font-serif); font-family: var(--tl-font-serif);
} }
.tl-text-shape__wrapper[data-font='mono'] { .tl-text-wrapper[data-font='mono'] {
font-family: var(--tl-font-mono); font-family: var(--tl-font-mono);
} }
.tl-text-shape__wrapper[data-isediting='true'] .tl-text-content { .tl-text-wrapper[data-align='start'],
.tl-text-wrapper[data-align='start-legacy'] {
text-align: left;
}
.tl-text-wrapper[data-align='middle'],
.tl-text-wrapper[data-align='middle-legacy'] {
text-align: center;
}
.tl-text-wrapper[data-align='end'],
.tl-text-wrapper[data-align='end-legacy'] {
text-align: right;
}
.tl-text-wrapper[data-isediting='true'] .tl-text-content {
opacity: 0; opacity: 0;
} }
@ -995,10 +998,6 @@ input,
z-index: 10; z-index: 10;
} }
.tl-text-label[data-isediting='true'] .tl-text-content {
opacity: 0;
}
.tl-text-label[data-hastext='false'][data-isediting='false'] > .tl-text-label__inner { .tl-text-label[data-hastext='false'][data-isediting='false'] > .tl-text-label__inner {
width: 40px; width: 40px;
height: 40px; height: 40px;
@ -1053,21 +1052,6 @@ input,
opacity: 0; opacity: 0;
} }
.tl-text-label[data-align='start'],
.tl-text-label[data-align='start-legacy'] {
text-align: left;
}
.tl-text-label[data-align='middle'],
.tl-text-label[data-align='middle-legacy'] {
text-align: center;
}
.tl-text-label[data-align='end'],
.tl-text-label[data-align='end-legacy'] {
text-align: right;
}
.tl-arrow-hint { .tl-arrow-hint {
stroke: var(--color-text-1); stroke: var(--color-text-1);
fill: none; fill: none;
@ -1075,26 +1059,6 @@ input,
overflow: visible; overflow: visible;
} }
.tl-arrow-label[data-font='draw'],
.tl-text-label[data-font='draw'] {
font-family: var(--tl-font-draw);
}
.tl-arrow-label[data-font='sans'],
.tl-text-label[data-font='sans'] {
font-family: var(--tl-font-sans);
}
.tl-arrow-label[data-font='serif'],
.tl-text-label[data-font='serif'] {
font-family: var(--tl-font-serif);
}
.tl-arrow-label[data-font='mono'],
.tl-text-label[data-font='mono'] {
font-family: var(--tl-font-mono);
}
/* ------------------- Arrow Shape ------------------ */ /* ------------------- Arrow Shape ------------------ */
.tl-arrow-label { .tl-arrow-label {
@ -1107,6 +1071,7 @@ input,
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
text-align: center;
color: var(--color-text); color: var(--color-text);
text-shadow: var(--tl-text-outline); text-shadow: var(--tl-text-outline);
} }
@ -1131,40 +1096,7 @@ input,
align-items: center; align-items: center;
} }
.tl-arrow-label p, .tl-arrow-label .tl-arrow {
.tl-arrow-label textarea {
margin: 0px;
padding: 0px;
border: 0px;
color: inherit;
caret-color: var(--color-text);
background: none;
border-image: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
font-variant: inherit;
font-style: inherit;
text-align: inherit;
letter-spacing: inherit;
text-shadow: inherit;
outline: none;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
pointer-events: all;
text-rendering: auto;
text-transform: none;
text-indent: 0px;
display: inline-block;
appearance: auto;
column-count: initial !important;
writing-mode: horizontal-tb !important;
word-spacing: 0px;
}
.tl-arrow-label p {
position: relative; position: relative;
height: max-content; height: max-content;
z-index: 2; z-index: 2;

View file

@ -62,8 +62,13 @@ import { TLBookmarkShape } from '@tldraw/editor';
import { TLCancelEvent } from '@tldraw/editor'; import { TLCancelEvent } from '@tldraw/editor';
import { TLClickEvent } from '@tldraw/editor'; import { TLClickEvent } from '@tldraw/editor';
import { TLClickEventInfo } from '@tldraw/editor'; import { TLClickEventInfo } from '@tldraw/editor';
import { TLDefaultColorStyle } from '@tldraw/editor';
import { TLDefaultColorTheme } from '@tldraw/editor'; import { TLDefaultColorTheme } from '@tldraw/editor';
import { TLDefaultFillStyle } from '@tldraw/editor';
import { TLDefaultFontStyle } from '@tldraw/editor';
import { TLDefaultHorizontalAlignStyle } from '@tldraw/editor';
import { TLDefaultSizeStyle } from '@tldraw/editor'; import { TLDefaultSizeStyle } from '@tldraw/editor';
import { TLDefaultVerticalAlignStyle } from '@tldraw/editor';
import { TldrawEditorBaseProps } from '@tldraw/editor'; import { TldrawEditorBaseProps } from '@tldraw/editor';
import { TLDrawShape } from '@tldraw/editor'; import { TLDrawShape } from '@tldraw/editor';
import { TLDrawShapeSegment } from '@tldraw/editor'; import { TLDrawShapeSegment } from '@tldraw/editor';
@ -613,6 +618,9 @@ export function fitFrameToContent(editor: Editor, id: TLShapeId, opts?: {
// @public (undocumented) // @public (undocumented)
export function FitFrameToContentMenuItem(): JSX_2.Element | null; export function FitFrameToContentMenuItem(): JSX_2.Element | null;
// @public (undocumented)
export const FONT_FAMILIES: Record<TLDefaultFontStyle, string>;
// @public (undocumented) // @public (undocumented)
export class FrameShapeTool extends BaseBoxShapeTool { export class FrameShapeTool extends BaseBoxShapeTool {
// (undocumented) // (undocumented)
@ -966,6 +974,9 @@ export function isGifAnimated(file: Blob): Promise<boolean>;
// @public (undocumented) // @public (undocumented)
export function KeyboardShortcutsMenuItem(): JSX_2.Element | null; export function KeyboardShortcutsMenuItem(): JSX_2.Element | null;
// @public (undocumented)
export const LABEL_FONT_SIZES: Record<TLDefaultSizeStyle, number>;
// @public (undocumented) // @public (undocumented)
export function LanguageMenu(): JSX_2.Element; export function LanguageMenu(): JSX_2.Element;
@ -1279,6 +1290,18 @@ export function StackMenuItems(): JSX_2.Element;
// @public (undocumented) // @public (undocumented)
export function StarToolbarItem(): JSX_2.Element; export function StarToolbarItem(): JSX_2.Element;
// @public (undocumented)
export const TEXT_PROPS: {
lineHeight: number;
fontWeight: string;
fontVariant: string;
fontStyle: string;
padding: string;
};
// @public (undocumented)
export const TextLabel: React_2.NamedExoticComponent<TextLabelProps>;
// @public (undocumented) // @public (undocumented)
export class TextShapeTool extends StateNode { export class TextShapeTool extends StateNode {
// (undocumented) // (undocumented)
@ -2475,6 +2498,19 @@ export function useDefaultHelpers(): {
// @public (undocumented) // @public (undocumented)
export function useDialogs(): TLUiDialogsContextType; export function useDialogs(): TLUiDialogsContextType;
// @public (undocumented)
export function useEditableText(id: TLShapeId, type: string, text: string): {
rInput: React_2.RefObject<HTMLTextAreaElement>;
isEditing: boolean;
handleFocus: () => void;
handleBlur: () => void;
handleKeyDown: (e: React_2.KeyboardEvent<HTMLTextAreaElement>) => void;
handleChange: (e: React_2.ChangeEvent<HTMLTextAreaElement>) => void;
handleInputPointerDown: (e: React_2.PointerEvent) => void;
handleDoubleClick: (e: any) => any;
isEmpty: boolean;
};
// @public (undocumented) // @public (undocumented)
export function useExportAs(): (ids: TLShapeId[], format: TLExportType | undefined, name: string | undefined) => void; export function useExportAs(): (ids: TLShapeId[], format: TLExportType | undefined, name: string | undefined) => void;

View file

@ -6887,6 +6887,43 @@
"parameters": [], "parameters": [],
"name": "FitFrameToContentMenuItem" "name": "FitFrameToContentMenuItem"
}, },
{
"kind": "Variable",
"canonicalReference": "tldraw!FONT_FAMILIES:var",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "FONT_FAMILIES: "
},
{
"kind": "Reference",
"text": "Record",
"canonicalReference": "!Record:type"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "TLDefaultFontStyle",
"canonicalReference": "@tldraw/tlschema!TLDefaultFontStyle:type"
},
{
"kind": "Content",
"text": ", string>"
}
],
"fileUrlPath": "packages/tldraw/src/lib/shapes/shared/default-shape-constants.ts",
"isReadonly": true,
"releaseTag": "Public",
"name": "FONT_FAMILIES",
"variableTypeTokenRange": {
"startIndex": 1,
"endIndex": 5
}
},
{ {
"kind": "Class", "kind": "Class",
"canonicalReference": "tldraw!FrameShapeTool:class", "canonicalReference": "tldraw!FrameShapeTool:class",
@ -11244,6 +11281,43 @@
"parameters": [], "parameters": [],
"name": "KeyboardShortcutsMenuItem" "name": "KeyboardShortcutsMenuItem"
}, },
{
"kind": "Variable",
"canonicalReference": "tldraw!LABEL_FONT_SIZES:var",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "LABEL_FONT_SIZES: "
},
{
"kind": "Reference",
"text": "Record",
"canonicalReference": "!Record:type"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "TLDefaultSizeStyle",
"canonicalReference": "@tldraw/tlschema!TLDefaultSizeStyle:type"
},
{
"kind": "Content",
"text": ", number>"
}
],
"fileUrlPath": "packages/tldraw/src/lib/shapes/shared/default-shape-constants.ts",
"isReadonly": true,
"releaseTag": "Public",
"name": "LABEL_FONT_SIZES",
"variableTypeTokenRange": {
"startIndex": 1,
"endIndex": 5
}
},
{ {
"kind": "Function", "kind": "Function",
"canonicalReference": "tldraw!LanguageMenu:function(1)", "canonicalReference": "tldraw!LanguageMenu:function(1)",
@ -14997,6 +15071,66 @@
"parameters": [], "parameters": [],
"name": "StarToolbarItem" "name": "StarToolbarItem"
}, },
{
"kind": "Variable",
"canonicalReference": "tldraw!TEXT_PROPS:var",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "TEXT_PROPS: "
},
{
"kind": "Content",
"text": "{\n lineHeight: number;\n fontWeight: string;\n fontVariant: string;\n fontStyle: string;\n padding: string;\n}"
}
],
"fileUrlPath": "packages/tldraw/src/lib/shapes/shared/default-shape-constants.ts",
"isReadonly": true,
"releaseTag": "Public",
"name": "TEXT_PROPS",
"variableTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
}
},
{
"kind": "Variable",
"canonicalReference": "tldraw!TextLabel:var",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "TextLabel: "
},
{
"kind": "Reference",
"text": "React.NamedExoticComponent",
"canonicalReference": "@types/react!React.NamedExoticComponent:interface"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "TextLabelProps",
"canonicalReference": "tldraw!~TextLabelProps:type"
},
{
"kind": "Content",
"text": ">"
}
],
"fileUrlPath": "packages/tldraw/src/lib/shapes/shared/TextLabel.tsx",
"isReadonly": true,
"releaseTag": "Public",
"name": "TextLabel",
"variableTypeTokenRange": {
"startIndex": 1,
"endIndex": 5
}
},
{ {
"kind": "Class", "kind": "Class",
"canonicalReference": "tldraw!TextShapeTool:class", "canonicalReference": "tldraw!TextShapeTool:class",
@ -27289,6 +27423,147 @@
"parameters": [], "parameters": [],
"name": "useDialogs" "name": "useDialogs"
}, },
{
"kind": "Function",
"canonicalReference": "tldraw!useEditableText:function(1)",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function useEditableText(id: "
},
{
"kind": "Reference",
"text": "TLShapeId",
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
},
{
"kind": "Content",
"text": ", type: "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": ", text: "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "{\n rInput: "
},
{
"kind": "Reference",
"text": "React.RefObject",
"canonicalReference": "@types/react!React.RefObject:interface"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "HTMLTextAreaElement",
"canonicalReference": "!HTMLTextAreaElement:interface"
},
{
"kind": "Content",
"text": ">;\n isEditing: boolean;\n handleFocus: () => void;\n handleBlur: () => void;\n handleKeyDown: (e: "
},
{
"kind": "Reference",
"text": "React.KeyboardEvent",
"canonicalReference": "@types/react!React.KeyboardEvent:interface"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "HTMLTextAreaElement",
"canonicalReference": "!HTMLTextAreaElement:interface"
},
{
"kind": "Content",
"text": ">) => void;\n handleChange: (e: "
},
{
"kind": "Reference",
"text": "React.ChangeEvent",
"canonicalReference": "@types/react!React.ChangeEvent:interface"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "HTMLTextAreaElement",
"canonicalReference": "!HTMLTextAreaElement:interface"
},
{
"kind": "Content",
"text": ">) => void;\n handleInputPointerDown: (e: "
},
{
"kind": "Reference",
"text": "React.PointerEvent",
"canonicalReference": "@types/react!React.PointerEvent:interface"
},
{
"kind": "Content",
"text": ") => void;\n handleDoubleClick: (e: any) => any;\n isEmpty: boolean;\n}"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/tldraw/src/lib/shapes/shared/useEditableText.ts",
"returnTypeTokenRange": {
"startIndex": 7,
"endIndex": 22
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "id",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "type",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
},
{
"parameterName": "text",
"parameterTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"isOptional": false
}
],
"name": "useEditableText"
},
{ {
"kind": "Function", "kind": "Function",
"canonicalReference": "tldraw!useExportAs:function(1)", "canonicalReference": "tldraw!useExportAs:function(1)",

View file

@ -33,6 +33,7 @@ export { LineShapeTool } from './lib/shapes/line/LineShapeTool'
export { LineShapeUtil } from './lib/shapes/line/LineShapeUtil' export { LineShapeUtil } from './lib/shapes/line/LineShapeUtil'
export { NoteShapeTool } from './lib/shapes/note/NoteShapeTool' export { NoteShapeTool } from './lib/shapes/note/NoteShapeTool'
export { NoteShapeUtil } from './lib/shapes/note/NoteShapeUtil' export { NoteShapeUtil } from './lib/shapes/note/NoteShapeUtil'
export { TextLabel } from './lib/shapes/shared/TextLabel'
export { TextShapeTool } from './lib/shapes/text/TextShapeTool' export { TextShapeTool } from './lib/shapes/text/TextShapeTool'
export { TextShapeUtil } from './lib/shapes/text/TextShapeUtil' export { TextShapeUtil } from './lib/shapes/text/TextShapeUtil'
export { VideoShapeUtil } from './lib/shapes/video/VideoShapeUtil' export { VideoShapeUtil } from './lib/shapes/video/VideoShapeUtil'
@ -42,6 +43,7 @@ export { LaserTool } from './lib/tools/LaserTool/LaserTool'
export { SelectTool } from './lib/tools/SelectTool/SelectTool' export { SelectTool } from './lib/tools/SelectTool/SelectTool'
export { ZoomTool } from './lib/tools/ZoomTool/ZoomTool' export { ZoomTool } from './lib/tools/ZoomTool/ZoomTool'
// UI // UI
export { useEditableText } from './lib/shapes/shared/useEditableText'
export { TldrawUi, type TldrawUiBaseProps, type TldrawUiProps } from './lib/ui/TldrawUi' export { TldrawUi, type TldrawUiBaseProps, type TldrawUiProps } from './lib/ui/TldrawUi'
export { setDefaultUiAssetUrls, type TLUiAssetUrlOverrides } from './lib/ui/assetUrls' export { setDefaultUiAssetUrls, type TLUiAssetUrlOverrides } from './lib/ui/assetUrls'
export { OfflineIndicator } from './lib/ui/components/OfflineIndicator/OfflineIndicator' export { OfflineIndicator } from './lib/ui/components/OfflineIndicator/OfflineIndicator'
@ -422,3 +424,11 @@ export {
TldrawUiMenuSubmenu, TldrawUiMenuSubmenu,
type TLUiMenuSubmenuProps, type TLUiMenuSubmenuProps,
} from './lib/ui/components/primitives/menus/TldrawUiMenuSubmenu' } from './lib/ui/components/primitives/menus/TldrawUiMenuSubmenu'
/* ----------------- Constants ---------------- */
export {
FONT_FAMILIES,
LABEL_FONT_SIZES,
TEXT_PROPS,
} from './lib/shapes/shared/default-shape-constants'

View file

@ -516,9 +516,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
} }
component(shape: TLArrowShape) { component(shape: TLArrowShape) {
// Not a class component, but eslint can't tell that :(
// eslint-disable-next-line react-hooks/rules-of-hooks
const theme = useDefaultColorTheme()
const onlySelectedShape = this.editor.getOnlySelectedShape() const onlySelectedShape = this.editor.getOnlySelectedShape()
const shouldDisplayHandles = const shouldDisplayHandles =
this.editor.isInAny( this.editor.isInAny(
@ -549,7 +546,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
size={shape.props.size} size={shape.props.size}
position={labelPosition.box.center} position={labelPosition.box.center}
width={labelPosition.box.w} width={labelPosition.box.w}
labelColor={theme[shape.props.labelColor].solid} labelColor={shape.props.labelColor}
/> />
</> </>
) )

View file

@ -1,8 +1,7 @@
import { TLArrowShape, TLShapeId, VecLike, stopEventPropagation } from '@tldraw/editor' import { TLArrowShape, TLDefaultColorStyle, TLShapeId, VecLike } from '@tldraw/editor'
import * as React from 'react' import * as React from 'react'
import { TextHelpers } from '../../shared/TextHelpers' import { TextLabel } from '../../shared/TextLabel'
import { ARROW_LABEL_FONT_SIZES, TEXT_PROPS } from '../../shared/default-shape-constants' import { ARROW_LABEL_FONT_SIZES, TEXT_PROPS } from '../../shared/default-shape-constants'
import { useEditableText } from '../../shared/useEditableText'
export const ArrowTextLabel = React.memo(function ArrowTextLabel({ export const ArrowTextLabel = React.memo(function ArrowTextLabel({
id, id,
@ -12,77 +11,26 @@ export const ArrowTextLabel = React.memo(function ArrowTextLabel({
position, position,
width, width,
labelColor, labelColor,
}: { id: TLShapeId; position: VecLike; width?: number; labelColor: string } & Pick< }: { id: TLShapeId; position: VecLike; width?: number; labelColor: TLDefaultColorStyle } & Pick<
TLArrowShape['props'], TLArrowShape['props'],
'text' | 'size' | 'font' 'text' | 'size' | 'font'
>) { >) {
const {
rInput,
isEditing,
handleFocus,
handleBlur,
handleKeyDown,
handleChange,
isEmpty,
handleInputPointerDown,
handleDoubleClick,
} = useEditableText(id, 'arrow', text)
const finalText = TextHelpers.normalizeTextForDom(text)
const hasText = finalText.trim().length > 0
if (!isEditing && !hasText) {
return null
}
return ( return (
<div <TextLabel
className="tl-arrow-label" id={id}
data-font={font} classNamePrefix="tl-arrow"
data-align={'center'} type="arrow"
data-hastext={!isEmpty} font={font}
data-isediting={isEditing} fontSize={ARROW_LABEL_FONT_SIZES[size]}
lineHeight={TEXT_PROPS.lineHeight}
align="middle"
verticalAlign="middle"
text={text}
labelColor={labelColor}
textWidth={width}
style={{ style={{
textAlign: 'center',
fontSize: ARROW_LABEL_FONT_SIZES[size],
lineHeight: ARROW_LABEL_FONT_SIZES[size] * TEXT_PROPS.lineHeight + 'px',
transform: `translate(${position.x}px, ${position.y}px)`, transform: `translate(${position.x}px, ${position.y}px)`,
color: labelColor,
}} }}
>
<div className="tl-arrow-label__inner">
<p style={{ width: width ? width : '9px' }}>
{text ? TextHelpers.normalizeTextForDom(text) : ' '}
</p>
{isEditing && (
// Consider replacing with content-editable
<textarea
ref={rInput}
className="tl-text tl-text-input"
name="text"
tabIndex={-1}
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
autoSave="off"
autoFocus
placeholder=""
spellCheck="true"
wrap="off"
dir="auto"
datatype="wysiwyg"
defaultValue={text}
onFocus={handleFocus}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
onTouchEnd={stopEventPropagation}
onContextMenu={stopEventPropagation}
onPointerDown={handleInputPointerDown}
onDoubleClick={handleDoubleClick}
/> />
)}
</div>
</div>
) )
}) })

View file

@ -31,6 +31,7 @@ import { TextLabel } from '../shared/TextLabel'
import { import {
FONT_FAMILIES, FONT_FAMILIES,
LABEL_FONT_SIZES, LABEL_FONT_SIZES,
LABEL_PADDING,
STROKE_SIZES, STROKE_SIZES,
TEXT_PROPS, TEXT_PROPS,
} from '../shared/default-shape-constants' } from '../shared/default-shape-constants'
@ -46,7 +47,6 @@ import { GeoShapeBody } from './components/GeoShapeBody'
import { getOvalIndicatorPath } from './components/SolidStyleOval' import { getOvalIndicatorPath } from './components/SolidStyleOval'
import { getLines } from './getLines' import { getLines } from './getLines'
const LABEL_PADDING = 16
const MIN_SIZE_WITH_LABEL = 17 * 3 const MIN_SIZE_WITH_LABEL = 17 * 3
/** @public */ /** @public */
@ -396,8 +396,9 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
id={id} id={id}
type={type} type={type}
font={font} font={font}
fontSize={LABEL_FONT_SIZES[size]}
lineHeight={TEXT_PROPS.lineHeight}
fill={fill} fill={fill}
size={size}
align={align} align={align}
verticalAlign={verticalAlign} verticalAlign={verticalAlign}
text={text} text={text}

View file

@ -83,7 +83,8 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
id={id} id={id}
type={type} type={type}
font={font} font={font}
size={size} fontSize={LABEL_FONT_SIZES[size]}
lineHeight={TEXT_PROPS.lineHeight}
align={align} align={align}
verticalAlign={verticalAlign} verticalAlign={verticalAlign}
text={text} text={text}

View file

@ -4,36 +4,23 @@ import {
TLDefaultFillStyle, TLDefaultFillStyle,
TLDefaultFontStyle, TLDefaultFontStyle,
TLDefaultHorizontalAlignStyle, TLDefaultHorizontalAlignStyle,
TLDefaultSizeStyle,
TLDefaultVerticalAlignStyle, TLDefaultVerticalAlignStyle,
TLShape, TLShapeId,
stopEventPropagation, getDefaultColorTheme,
useIsDarkMode,
} from '@tldraw/editor' } from '@tldraw/editor'
import React from 'react' import React from 'react'
import { useDefaultColorTheme } from './ShapeFill' import { TextArea } from '../text/TextArea'
import { TextHelpers } from './TextHelpers' import { TextHelpers } from './TextHelpers'
import { LABEL_FONT_SIZES, TEXT_PROPS } from './default-shape-constants'
import { isLegacyAlign } from './legacyProps' import { isLegacyAlign } from './legacyProps'
import { useEditableText } from './useEditableText' import { useEditableText } from './useEditableText'
export const TextLabel = React.memo(function TextLabel< type TextLabelProps = {
T extends Extract<TLShape, { props: { text: string } }>, id: TLShapeId
>({ type: string
id,
type,
text,
size,
labelColor,
font,
align,
verticalAlign,
wrap,
bounds,
}: {
id: T['id']
type: T['type']
size: TLDefaultSizeStyle
font: TLDefaultFontStyle font: TLDefaultFontStyle
fontSize: number
lineHeight: number
fill?: TLDefaultFillStyle fill?: TLDefaultFillStyle
align: TLDefaultHorizontalAlignStyle align: TLDefaultHorizontalAlignStyle
verticalAlign: TLDefaultVerticalAlignStyle verticalAlign: TLDefaultVerticalAlignStyle
@ -41,32 +28,47 @@ export const TextLabel = React.memo(function TextLabel<
text: string text: string
labelColor: TLDefaultColorStyle labelColor: TLDefaultColorStyle
bounds?: Box bounds?: Box
}) { classNamePrefix?: string
const { style?: React.CSSProperties
rInput, textWidth?: number
isEmpty, textHeight?: number
isEditing, }
handleFocus,
handleChange, /** @public */
handleKeyDown, export const TextLabel = React.memo(function TextLabel({
handleBlur, id,
handleInputPointerDown, type,
handleDoubleClick, text,
} = useEditableText(id, type, text) labelColor,
font,
fontSize,
lineHeight,
align,
verticalAlign,
wrap,
bounds,
classNamePrefix,
style,
textWidth,
textHeight,
}: TextLabelProps) {
const { rInput, isEmpty, isEditing, ...editableTextRest } = useEditableText(id, type, text)
const finalText = TextHelpers.normalizeTextForDom(text) const finalText = TextHelpers.normalizeTextForDom(text)
const hasText = finalText.length > 0 const hasText = finalText.length > 0
const legacyAlign = isLegacyAlign(align) const legacyAlign = isLegacyAlign(align)
const theme = useDefaultColorTheme() const theme = getDefaultColorTheme({ isDarkMode: useIsDarkMode() })
if (!isEditing && !hasText) { if (!isEditing && !hasText) {
return null return null
} }
// TODO: probably combine tl-text and tl-arrow eventually
const cssPrefix = classNamePrefix || 'tl-text'
return ( return (
<div <div
className="tl-text-label" className={`${cssPrefix}-label tl-text-wrapper`}
data-font={font} data-font={font}
data-align={align} data-align={align}
data-hastext={!isEmpty} data-hastext={!isEmpty}
@ -84,48 +86,25 @@ export const TextLabel = React.memo(function TextLabel<
position: 'absolute', position: 'absolute',
} }
: {}), : {}),
...style,
}} }}
> >
<div <div
className="tl-text-label__inner" className={`${cssPrefix}-label__inner`}
style={{ style={{
fontSize: LABEL_FONT_SIZES[size], fontSize,
lineHeight: LABEL_FONT_SIZES[size] * TEXT_PROPS.lineHeight + 'px', lineHeight: fontSize * lineHeight + 'px',
minHeight: TEXT_PROPS.lineHeight + 32, minHeight: lineHeight + 32,
minWidth: 0, minWidth: textWidth || 0,
color: theme[labelColor].solid, color: theme[labelColor].solid,
width: textWidth,
height: textHeight,
}} }}
> >
<div className="tl-text tl-text-content" dir="ltr"> <div className={`${cssPrefix} tl-text tl-text-content`} dir="ltr">
{finalText} {finalText}
</div> </div>
{isEditing && ( {isEditing && <TextArea ref={rInput} text={text} {...editableTextRest} />}
<textarea
ref={rInput}
className="tl-text tl-text-input"
name="text"
tabIndex={-1}
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
autoSave="off"
autoFocus
placeholder=""
spellCheck="true"
wrap="off"
dir="auto"
datatype="wysiwyg"
defaultValue={text}
onFocus={handleFocus}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
onTouchEnd={stopEventPropagation}
onContextMenu={stopEventPropagation}
onPointerDown={handleInputPointerDown}
onDoubleClick={handleDoubleClick}
/>
)}
</div> </div>
</div> </div>
) )

View file

@ -53,3 +53,5 @@ export const FONT_FAMILIES: Record<TLDefaultFontStyle, string> = {
export const LABEL_TO_ARROW_PADDING = 20 export const LABEL_TO_ARROW_PADDING = 20
/** @internal */ /** @internal */
export const ARROW_LABEL_PADDING = 4.25 export const ARROW_LABEL_PADDING = 4.25
/** @internal */
export const LABEL_PADDING = 16

View file

@ -2,6 +2,7 @@
import { import {
TLShape, TLShape,
TLShapeId,
TLUnknownShape, TLUnknownShape,
getPointerInfo, getPointerInfo,
preventDefault, preventDefault,
@ -12,11 +13,8 @@ import {
import React, { useCallback, useEffect, useRef } from 'react' import React, { useCallback, useEffect, useRef } from 'react'
import { INDENT, TextHelpers } from './TextHelpers' import { INDENT, TextHelpers } from './TextHelpers'
export function useEditableText<T extends Extract<TLShape, { props: { text: string } }>>( /** @public */
id: T['id'], export function useEditableText(id: TLShapeId, type: string, text: string) {
type: T['type'],
text: string
) {
const editor = useEditor() const editor = useEditor()
const rInput = useRef<HTMLTextAreaElement>(null) const rInput = useRef<HTMLTextAreaElement>(null)

View file

@ -0,0 +1,53 @@
import { stopEventPropagation } from '@tldraw/editor'
import { forwardRef } from 'react'
type TextAreaProps = {
text: string
handleFocus: () => void
handleBlur: () => void
handleKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
handleChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
handleInputPointerDown: (e: React.PointerEvent<HTMLTextAreaElement>) => void
handleDoubleClick: (e: any) => any
}
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function TextArea(
{
text,
handleFocus,
handleChange,
handleKeyDown,
handleBlur,
handleInputPointerDown,
handleDoubleClick,
},
ref
) {
return (
<textarea
ref={ref}
className="tl-text tl-text-input"
name="text"
tabIndex={-1}
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
autoSave="off"
autoFocus
placeholder=""
spellCheck="true"
wrap="off"
dir="auto"
datatype="wysiwyg"
defaultValue={text}
onFocus={handleFocus}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
onTouchEnd={stopEventPropagation}
onContextMenu={stopEventPropagation}
onPointerDown={handleInputPointerDown}
onDoubleClick={handleDoubleClick}
/>
)
})

View file

@ -12,18 +12,16 @@ import {
TLTextShape, TLTextShape,
Vec, Vec,
WeakMapCache, WeakMapCache,
getDefaultColorTheme,
stopEventPropagation,
textShapeMigrations, textShapeMigrations,
textShapeProps, textShapeProps,
toDomPrecision, toDomPrecision,
useEditor, useEditor,
} from '@tldraw/editor' } from '@tldraw/editor'
import { SvgTextLabel } from '../shared/SvgTextLabel' import { SvgTextLabel } from '../shared/SvgTextLabel'
import { TextLabel } from '../shared/TextLabel'
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants' import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
import { getFontDefForExport } from '../shared/defaultStyleDefs' import { getFontDefForExport } from '../shared/defaultStyleDefs'
import { resizeScaled } from '../shared/resizeScaled' import { resizeScaled } from '../shared/resizeScaled'
import { useEditableText } from '../shared/useEditableText'
const sizeCache = new WeakMapCache<TLTextShape['props'], { height: number; width: number }>() const sizeCache = new WeakMapCache<TLTextShape['props'], { height: number; width: number }>()
@ -67,75 +65,32 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
component(shape: TLTextShape) { component(shape: TLTextShape) {
const { const {
id, id,
type, props: { font, size, text, color, scale, align },
props: { text, color },
} = shape } = shape
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode() })
const { width, height } = this.getMinDimensions(shape) const { width, height } = this.getMinDimensions(shape)
const {
rInput,
isEmpty,
isEditing,
handleFocus,
handleChange,
handleKeyDown,
handleBlur,
handleInputPointerDown,
handleDoubleClick,
} = useEditableText(id, type, text)
return ( return (
<HTMLContainer id={shape.id}> <HTMLContainer id={shape.id}>
<div <TextLabel
className="tl-text-shape__wrapper tl-text-shadow" id={id}
data-font={shape.props.font} classNamePrefix="tl-text-shape"
data-align={shape.props.align} type="text"
data-hastext={!isEmpty} font={font}
data-isediting={isEditing} fontSize={FONT_SIZES[size]}
data-textwrap={true} lineHeight={TEXT_PROPS.lineHeight}
align={align}
verticalAlign="middle"
text={text}
labelColor={color}
textWidth={width}
textHeight={height}
style={{ style={{
fontSize: FONT_SIZES[shape.props.size], transform: `scale(${scale})`,
lineHeight: FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight + 'px',
transform: `scale(${shape.props.scale})`,
transformOrigin: 'top left', transformOrigin: 'top left',
width: Math.max(1, width),
height: Math.max(FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight, height),
color: theme[color].solid,
}} }}
> wrap
<div className="tl-text tl-text-content" dir="ltr">
{text}
</div>
{isEditing ? (
<textarea
ref={rInput}
className="tl-text tl-text-input"
name="text"
tabIndex={-1}
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
autoSave="off"
autoFocus
placeholder=""
spellCheck="true"
wrap="off"
dir="auto"
datatype="wysiwyg"
defaultValue={text}
onFocus={handleFocus}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
onTouchEnd={stopEventPropagation}
onContextMenu={stopEventPropagation}
onPointerDown={handleInputPointerDown}
onDoubleClick={handleDoubleClick}
/> />
) : null}
</div>
</HTMLContainer> </HTMLContainer>
) )
} }