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:
parent
3593799d9e
commit
d76d53db95
17 changed files with 616 additions and 346 deletions
|
@ -2,7 +2,7 @@
|
|||
title: Speech bubble
|
||||
component: ./CustomShapeWithHandles.tsx
|
||||
category: shapes/tools
|
||||
priority: 2
|
||||
priority: 1
|
||||
---
|
||||
|
||||
A custom shape with handles
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
import { ShapePropsType } from '@tldraw/tlschema/src/shapes/TLBaseShape'
|
||||
import {
|
||||
DefaultColorStyle,
|
||||
DefaultFontStyle,
|
||||
DefaultHorizontalAlignStyle,
|
||||
DefaultSizeStyle,
|
||||
DefaultVerticalAlignStyle,
|
||||
FONT_FAMILIES,
|
||||
Geometry2d,
|
||||
LABEL_FONT_SIZES,
|
||||
Polygon2d,
|
||||
ShapeUtil,
|
||||
T,
|
||||
TEXT_PROPS,
|
||||
TLBaseShape,
|
||||
TLDefaultColorStyle,
|
||||
TLDefaultSizeStyle,
|
||||
TLHandle,
|
||||
TLOnBeforeUpdateHandler,
|
||||
TLOnHandleDragHandler,
|
||||
TLOnResizeHandler,
|
||||
TextLabel,
|
||||
Vec,
|
||||
VecModel,
|
||||
ZERO_INDEX_KEY,
|
||||
getDefaultColorTheme,
|
||||
resizeBox,
|
||||
|
@ -33,28 +38,28 @@ export const STROKE_SIZES = {
|
|||
// There's a guide at the bottom of this file!
|
||||
|
||||
// [1]
|
||||
export type SpeechBubbleShape = TLBaseShape<
|
||||
'speech-bubble',
|
||||
{
|
||||
w: number
|
||||
h: number
|
||||
size: TLDefaultSizeStyle
|
||||
color: TLDefaultColorStyle
|
||||
tail: VecModel
|
||||
}
|
||||
>
|
||||
|
||||
export const speechBubbleShapeProps = {
|
||||
w: T.number,
|
||||
h: T.number,
|
||||
size: DefaultSizeStyle,
|
||||
color: DefaultColorStyle,
|
||||
font: DefaultFontStyle,
|
||||
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> {
|
||||
static override type = 'speech-bubble' as const
|
||||
|
||||
// [2]
|
||||
static override props = {
|
||||
w: T.number,
|
||||
h: T.number,
|
||||
size: DefaultSizeStyle,
|
||||
color: DefaultColorStyle,
|
||||
tail: vecModelValidator,
|
||||
}
|
||||
static override props = speechBubbleShapeProps
|
||||
|
||||
override isAspectRatioLocked = (_shape: SpeechBubbleShape) => false
|
||||
|
||||
|
@ -62,17 +67,28 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
|||
|
||||
override canBind = (_shape: SpeechBubbleShape) => true
|
||||
|
||||
override canEdit = () => true
|
||||
|
||||
// [3]
|
||||
getDefaultProps(): SpeechBubbleShape['props'] {
|
||||
getDefaultProps(): SpeechBubbleShapeProps {
|
||||
return {
|
||||
w: 200,
|
||||
h: 130,
|
||||
color: 'black',
|
||||
size: 'm',
|
||||
font: 'draw',
|
||||
align: 'middle',
|
||||
verticalAlign: 'start',
|
||||
growY: 0,
|
||||
text: '',
|
||||
tail: { x: 0.5, y: 1.5 },
|
||||
}
|
||||
}
|
||||
|
||||
getHeight(shape: SpeechBubbleShape) {
|
||||
return shape.props.h + shape.props.growY
|
||||
}
|
||||
|
||||
getGeometry(shape: SpeechBubbleShape): Geometry2d {
|
||||
const speechBubbleGeometry = getSpeechBubbleVertices(shape)
|
||||
const body = new Polygon2d({
|
||||
|
@ -84,7 +100,7 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
|||
|
||||
// [4]
|
||||
override getHandles(shape: SpeechBubbleShape): TLHandle[] {
|
||||
const { tail, w, h } = shape.props
|
||||
const { tail, w } = shape.props
|
||||
|
||||
return [
|
||||
{
|
||||
|
@ -94,7 +110,7 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
|||
// props.tail coordinates are normalized
|
||||
// but here we need them in shape space
|
||||
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: {
|
||||
tail: {
|
||||
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]
|
||||
override onBeforeUpdate: TLOnBeforeUpdateHandler<SpeechBubbleShape> | undefined = (
|
||||
_: SpeechBubbleShape,
|
||||
prev: 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 slantedLength = Math.hypot(w, h)
|
||||
const slantedLength = Math.hypot(w, fullHeight)
|
||||
const MIN_DISTANCE = slantedLength / 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 center = new Vec(w / 2, h / 2)
|
||||
const center = new Vec(w / 2, fullHeight / 2)
|
||||
const tailDirection = Vec.Sub(tailInShapeSpace, center).uni()
|
||||
|
||||
let newPoint = tailInShapeSpace
|
||||
|
@ -144,12 +165,17 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
|||
|
||||
const next = structuredClone(shape)
|
||||
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) {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
props: { color, font, size, align, text },
|
||||
} = shape
|
||||
const theme = getDefaultColorTheme({
|
||||
isDarkMode: this.editor.user.getIsDarkMode(),
|
||||
})
|
||||
|
@ -161,11 +187,24 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
|||
<svg className="tl-svg-container">
|
||||
<path
|
||||
d={pathData}
|
||||
strokeWidth={STROKE_SIZES[shape.props.size]}
|
||||
stroke={theme[shape.props.color].solid}
|
||||
strokeWidth={STROKE_SIZES[size]}
|
||||
stroke={theme[color].solid}
|
||||
fill={'none'}
|
||||
/>
|
||||
</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
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -2,14 +2,20 @@ import { Vec, VecLike, lerp, pointInPolygon } from 'tldraw'
|
|||
import { SpeechBubbleShape } from './SpeechBubbleUtil'
|
||||
|
||||
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 offsetV = h / 10
|
||||
const offsetV = fullHeight / 10
|
||||
|
||||
const { adjustedIntersection, intersectionSegmentIndex } = getTailIntersectionPoint(shape)
|
||||
|
||||
|
@ -73,11 +79,12 @@ export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec[] => {
|
|||
}
|
||||
|
||||
export function getTailIntersectionPoint(shape: SpeechBubbleShape) {
|
||||
const { w, h, tail } = shape.props
|
||||
const tailInShapeSpace = new Vec(tail.x * w, tail.y * h)
|
||||
const { w, tail } = shape.props
|
||||
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 corners = [new Vec(0, 0), new Vec(w, 0), new Vec(w, h), new Vec(0, h)]
|
||||
const center = new Vec(w / 2, fullHeight / 2)
|
||||
const corners = [new Vec(0, 0), new Vec(w, 0), new Vec(w, fullHeight), new Vec(0, fullHeight)]
|
||||
const segments = [
|
||||
[corners[0], corners[1]],
|
||||
[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
|
||||
|
||||
//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)
|
||||
|
||||
// combine the two
|
||||
|
|
|
@ -2,3 +2,9 @@
|
|||
.tl-user-handles {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -682,7 +682,7 @@ input,
|
|||
|
||||
/* ------------------- Text Shape ------------------- */
|
||||
|
||||
.tl-text-shape__wrapper {
|
||||
.tl-text-shape-label {
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
min-width: 1px;
|
||||
|
@ -698,35 +698,38 @@ input,
|
|||
text-shadow: var(--tl-text-outline);
|
||||
}
|
||||
|
||||
.tl-text-shape__wrapper[data-align='start'] {
|
||||
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'] {
|
||||
.tl-text-wrapper[data-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);
|
||||
}
|
||||
|
||||
.tl-text-shape__wrapper[data-font='serif'] {
|
||||
.tl-text-wrapper[data-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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
@ -995,10 +998,6 @@ input,
|
|||
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 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
@ -1053,21 +1052,6 @@ input,
|
|||
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 {
|
||||
stroke: var(--color-text-1);
|
||||
fill: none;
|
||||
|
@ -1075,26 +1059,6 @@ input,
|
|||
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 ------------------ */
|
||||
|
||||
.tl-arrow-label {
|
||||
|
@ -1107,6 +1071,7 @@ input,
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: var(--color-text);
|
||||
text-shadow: var(--tl-text-outline);
|
||||
}
|
||||
|
@ -1131,40 +1096,7 @@ input,
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.tl-arrow-label p,
|
||||
.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 {
|
||||
.tl-arrow-label .tl-arrow {
|
||||
position: relative;
|
||||
height: max-content;
|
||||
z-index: 2;
|
||||
|
|
|
@ -62,8 +62,13 @@ import { TLBookmarkShape } from '@tldraw/editor';
|
|||
import { TLCancelEvent } from '@tldraw/editor';
|
||||
import { TLClickEvent } from '@tldraw/editor';
|
||||
import { TLClickEventInfo } from '@tldraw/editor';
|
||||
import { TLDefaultColorStyle } 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 { TLDefaultVerticalAlignStyle } from '@tldraw/editor';
|
||||
import { TldrawEditorBaseProps } from '@tldraw/editor';
|
||||
import { TLDrawShape } from '@tldraw/editor';
|
||||
import { TLDrawShapeSegment } from '@tldraw/editor';
|
||||
|
@ -613,6 +618,9 @@ export function fitFrameToContent(editor: Editor, id: TLShapeId, opts?: {
|
|||
// @public (undocumented)
|
||||
export function FitFrameToContentMenuItem(): JSX_2.Element | null;
|
||||
|
||||
// @public (undocumented)
|
||||
export const FONT_FAMILIES: Record<TLDefaultFontStyle, string>;
|
||||
|
||||
// @public (undocumented)
|
||||
export class FrameShapeTool extends BaseBoxShapeTool {
|
||||
// (undocumented)
|
||||
|
@ -966,6 +974,9 @@ export function isGifAnimated(file: Blob): Promise<boolean>;
|
|||
// @public (undocumented)
|
||||
export function KeyboardShortcutsMenuItem(): JSX_2.Element | null;
|
||||
|
||||
// @public (undocumented)
|
||||
export const LABEL_FONT_SIZES: Record<TLDefaultSizeStyle, number>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function LanguageMenu(): JSX_2.Element;
|
||||
|
||||
|
@ -1279,6 +1290,18 @@ export function StackMenuItems(): JSX_2.Element;
|
|||
// @public (undocumented)
|
||||
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)
|
||||
export class TextShapeTool extends StateNode {
|
||||
// (undocumented)
|
||||
|
@ -2475,6 +2498,19 @@ export function useDefaultHelpers(): {
|
|||
// @public (undocumented)
|
||||
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)
|
||||
export function useExportAs(): (ids: TLShapeId[], format: TLExportType | undefined, name: string | undefined) => void;
|
||||
|
||||
|
|
|
@ -6887,6 +6887,43 @@
|
|||
"parameters": [],
|
||||
"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",
|
||||
"canonicalReference": "tldraw!FrameShapeTool:class",
|
||||
|
@ -11244,6 +11281,43 @@
|
|||
"parameters": [],
|
||||
"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",
|
||||
"canonicalReference": "tldraw!LanguageMenu:function(1)",
|
||||
|
@ -14997,6 +15071,66 @@
|
|||
"parameters": [],
|
||||
"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",
|
||||
"canonicalReference": "tldraw!TextShapeTool:class",
|
||||
|
@ -27289,6 +27423,147 @@
|
|||
"parameters": [],
|
||||
"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",
|
||||
"canonicalReference": "tldraw!useExportAs:function(1)",
|
||||
|
|
|
@ -33,6 +33,7 @@ export { LineShapeTool } from './lib/shapes/line/LineShapeTool'
|
|||
export { LineShapeUtil } from './lib/shapes/line/LineShapeUtil'
|
||||
export { NoteShapeTool } from './lib/shapes/note/NoteShapeTool'
|
||||
export { NoteShapeUtil } from './lib/shapes/note/NoteShapeUtil'
|
||||
export { TextLabel } from './lib/shapes/shared/TextLabel'
|
||||
export { TextShapeTool } from './lib/shapes/text/TextShapeTool'
|
||||
export { TextShapeUtil } from './lib/shapes/text/TextShapeUtil'
|
||||
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 { ZoomTool } from './lib/tools/ZoomTool/ZoomTool'
|
||||
// UI
|
||||
export { useEditableText } from './lib/shapes/shared/useEditableText'
|
||||
export { TldrawUi, type TldrawUiBaseProps, type TldrawUiProps } from './lib/ui/TldrawUi'
|
||||
export { setDefaultUiAssetUrls, type TLUiAssetUrlOverrides } from './lib/ui/assetUrls'
|
||||
export { OfflineIndicator } from './lib/ui/components/OfflineIndicator/OfflineIndicator'
|
||||
|
@ -422,3 +424,11 @@ export {
|
|||
TldrawUiMenuSubmenu,
|
||||
type TLUiMenuSubmenuProps,
|
||||
} from './lib/ui/components/primitives/menus/TldrawUiMenuSubmenu'
|
||||
|
||||
/* ----------------- Constants ---------------- */
|
||||
|
||||
export {
|
||||
FONT_FAMILIES,
|
||||
LABEL_FONT_SIZES,
|
||||
TEXT_PROPS,
|
||||
} from './lib/shapes/shared/default-shape-constants'
|
||||
|
|
|
@ -516,9 +516,6 @@ export class ArrowShapeUtil extends ShapeUtil<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 shouldDisplayHandles =
|
||||
this.editor.isInAny(
|
||||
|
@ -549,7 +546,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
size={shape.props.size}
|
||||
position={labelPosition.box.center}
|
||||
width={labelPosition.box.w}
|
||||
labelColor={theme[shape.props.labelColor].solid}
|
||||
labelColor={shape.props.labelColor}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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 { TextHelpers } from '../../shared/TextHelpers'
|
||||
import { TextLabel } from '../../shared/TextLabel'
|
||||
import { ARROW_LABEL_FONT_SIZES, TEXT_PROPS } from '../../shared/default-shape-constants'
|
||||
import { useEditableText } from '../../shared/useEditableText'
|
||||
|
||||
export const ArrowTextLabel = React.memo(function ArrowTextLabel({
|
||||
id,
|
||||
|
@ -12,77 +11,26 @@ export const ArrowTextLabel = React.memo(function ArrowTextLabel({
|
|||
position,
|
||||
width,
|
||||
labelColor,
|
||||
}: { id: TLShapeId; position: VecLike; width?: number; labelColor: string } & Pick<
|
||||
}: { id: TLShapeId; position: VecLike; width?: number; labelColor: TLDefaultColorStyle } & Pick<
|
||||
TLArrowShape['props'],
|
||||
'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 (
|
||||
<div
|
||||
className="tl-arrow-label"
|
||||
data-font={font}
|
||||
data-align={'center'}
|
||||
data-hastext={!isEmpty}
|
||||
data-isediting={isEditing}
|
||||
<TextLabel
|
||||
id={id}
|
||||
classNamePrefix="tl-arrow"
|
||||
type="arrow"
|
||||
font={font}
|
||||
fontSize={ARROW_LABEL_FONT_SIZES[size]}
|
||||
lineHeight={TEXT_PROPS.lineHeight}
|
||||
align="middle"
|
||||
verticalAlign="middle"
|
||||
text={text}
|
||||
labelColor={labelColor}
|
||||
textWidth={width}
|
||||
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)`,
|
||||
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>
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -31,6 +31,7 @@ import { TextLabel } from '../shared/TextLabel'
|
|||
import {
|
||||
FONT_FAMILIES,
|
||||
LABEL_FONT_SIZES,
|
||||
LABEL_PADDING,
|
||||
STROKE_SIZES,
|
||||
TEXT_PROPS,
|
||||
} from '../shared/default-shape-constants'
|
||||
|
@ -46,7 +47,6 @@ import { GeoShapeBody } from './components/GeoShapeBody'
|
|||
import { getOvalIndicatorPath } from './components/SolidStyleOval'
|
||||
import { getLines } from './getLines'
|
||||
|
||||
const LABEL_PADDING = 16
|
||||
const MIN_SIZE_WITH_LABEL = 17 * 3
|
||||
|
||||
/** @public */
|
||||
|
@ -396,8 +396,9 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
id={id}
|
||||
type={type}
|
||||
font={font}
|
||||
fontSize={LABEL_FONT_SIZES[size]}
|
||||
lineHeight={TEXT_PROPS.lineHeight}
|
||||
fill={fill}
|
||||
size={size}
|
||||
align={align}
|
||||
verticalAlign={verticalAlign}
|
||||
text={text}
|
||||
|
|
|
@ -83,7 +83,8 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
id={id}
|
||||
type={type}
|
||||
font={font}
|
||||
size={size}
|
||||
fontSize={LABEL_FONT_SIZES[size]}
|
||||
lineHeight={TEXT_PROPS.lineHeight}
|
||||
align={align}
|
||||
verticalAlign={verticalAlign}
|
||||
text={text}
|
||||
|
|
|
@ -4,36 +4,23 @@ import {
|
|||
TLDefaultFillStyle,
|
||||
TLDefaultFontStyle,
|
||||
TLDefaultHorizontalAlignStyle,
|
||||
TLDefaultSizeStyle,
|
||||
TLDefaultVerticalAlignStyle,
|
||||
TLShape,
|
||||
stopEventPropagation,
|
||||
TLShapeId,
|
||||
getDefaultColorTheme,
|
||||
useIsDarkMode,
|
||||
} from '@tldraw/editor'
|
||||
import React from 'react'
|
||||
import { useDefaultColorTheme } from './ShapeFill'
|
||||
import { TextArea } from '../text/TextArea'
|
||||
import { TextHelpers } from './TextHelpers'
|
||||
import { LABEL_FONT_SIZES, TEXT_PROPS } from './default-shape-constants'
|
||||
import { isLegacyAlign } from './legacyProps'
|
||||
import { useEditableText } from './useEditableText'
|
||||
|
||||
export const TextLabel = React.memo(function TextLabel<
|
||||
T extends Extract<TLShape, { props: { text: string } }>,
|
||||
>({
|
||||
id,
|
||||
type,
|
||||
text,
|
||||
size,
|
||||
labelColor,
|
||||
font,
|
||||
align,
|
||||
verticalAlign,
|
||||
wrap,
|
||||
bounds,
|
||||
}: {
|
||||
id: T['id']
|
||||
type: T['type']
|
||||
size: TLDefaultSizeStyle
|
||||
type TextLabelProps = {
|
||||
id: TLShapeId
|
||||
type: string
|
||||
font: TLDefaultFontStyle
|
||||
fontSize: number
|
||||
lineHeight: number
|
||||
fill?: TLDefaultFillStyle
|
||||
align: TLDefaultHorizontalAlignStyle
|
||||
verticalAlign: TLDefaultVerticalAlignStyle
|
||||
|
@ -41,32 +28,47 @@ export const TextLabel = React.memo(function TextLabel<
|
|||
text: string
|
||||
labelColor: TLDefaultColorStyle
|
||||
bounds?: Box
|
||||
}) {
|
||||
const {
|
||||
rInput,
|
||||
isEmpty,
|
||||
isEditing,
|
||||
handleFocus,
|
||||
handleChange,
|
||||
handleKeyDown,
|
||||
handleBlur,
|
||||
handleInputPointerDown,
|
||||
handleDoubleClick,
|
||||
} = useEditableText(id, type, text)
|
||||
classNamePrefix?: string
|
||||
style?: React.CSSProperties
|
||||
textWidth?: number
|
||||
textHeight?: number
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const TextLabel = React.memo(function TextLabel({
|
||||
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 hasText = finalText.length > 0
|
||||
|
||||
const legacyAlign = isLegacyAlign(align)
|
||||
const theme = useDefaultColorTheme()
|
||||
const theme = getDefaultColorTheme({ isDarkMode: useIsDarkMode() })
|
||||
|
||||
if (!isEditing && !hasText) {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: probably combine tl-text and tl-arrow eventually
|
||||
const cssPrefix = classNamePrefix || 'tl-text'
|
||||
return (
|
||||
<div
|
||||
className="tl-text-label"
|
||||
className={`${cssPrefix}-label tl-text-wrapper`}
|
||||
data-font={font}
|
||||
data-align={align}
|
||||
data-hastext={!isEmpty}
|
||||
|
@ -84,48 +86,25 @@ export const TextLabel = React.memo(function TextLabel<
|
|||
position: 'absolute',
|
||||
}
|
||||
: {}),
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="tl-text-label__inner"
|
||||
className={`${cssPrefix}-label__inner`}
|
||||
style={{
|
||||
fontSize: LABEL_FONT_SIZES[size],
|
||||
lineHeight: LABEL_FONT_SIZES[size] * TEXT_PROPS.lineHeight + 'px',
|
||||
minHeight: TEXT_PROPS.lineHeight + 32,
|
||||
minWidth: 0,
|
||||
fontSize,
|
||||
lineHeight: fontSize * lineHeight + 'px',
|
||||
minHeight: lineHeight + 32,
|
||||
minWidth: textWidth || 0,
|
||||
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}
|
||||
</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}
|
||||
/>
|
||||
)}
|
||||
{isEditing && <TextArea ref={rInput} text={text} {...editableTextRest} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -53,3 +53,5 @@ export const FONT_FAMILIES: Record<TLDefaultFontStyle, string> = {
|
|||
export const LABEL_TO_ARROW_PADDING = 20
|
||||
/** @internal */
|
||||
export const ARROW_LABEL_PADDING = 4.25
|
||||
/** @internal */
|
||||
export const LABEL_PADDING = 16
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import {
|
||||
TLShape,
|
||||
TLShapeId,
|
||||
TLUnknownShape,
|
||||
getPointerInfo,
|
||||
preventDefault,
|
||||
|
@ -12,11 +13,8 @@ import {
|
|||
import React, { useCallback, useEffect, useRef } from 'react'
|
||||
import { INDENT, TextHelpers } from './TextHelpers'
|
||||
|
||||
export function useEditableText<T extends Extract<TLShape, { props: { text: string } }>>(
|
||||
id: T['id'],
|
||||
type: T['type'],
|
||||
text: string
|
||||
) {
|
||||
/** @public */
|
||||
export function useEditableText(id: TLShapeId, type: string, text: string) {
|
||||
const editor = useEditor()
|
||||
|
||||
const rInput = useRef<HTMLTextAreaElement>(null)
|
||||
|
|
53
packages/tldraw/src/lib/shapes/text/TextArea.tsx
Normal file
53
packages/tldraw/src/lib/shapes/text/TextArea.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
})
|
|
@ -12,18 +12,16 @@ import {
|
|||
TLTextShape,
|
||||
Vec,
|
||||
WeakMapCache,
|
||||
getDefaultColorTheme,
|
||||
stopEventPropagation,
|
||||
textShapeMigrations,
|
||||
textShapeProps,
|
||||
toDomPrecision,
|
||||
useEditor,
|
||||
} from '@tldraw/editor'
|
||||
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
||||
import { TextLabel } from '../shared/TextLabel'
|
||||
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
||||
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
||||
import { resizeScaled } from '../shared/resizeScaled'
|
||||
import { useEditableText } from '../shared/useEditableText'
|
||||
|
||||
const sizeCache = new WeakMapCache<TLTextShape['props'], { height: number; width: number }>()
|
||||
|
||||
|
@ -67,75 +65,32 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
component(shape: TLTextShape) {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
props: { text, color },
|
||||
props: { font, size, text, color, scale, align },
|
||||
} = shape
|
||||
|
||||
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode() })
|
||||
const { width, height } = this.getMinDimensions(shape)
|
||||
|
||||
const {
|
||||
rInput,
|
||||
isEmpty,
|
||||
isEditing,
|
||||
handleFocus,
|
||||
handleChange,
|
||||
handleKeyDown,
|
||||
handleBlur,
|
||||
handleInputPointerDown,
|
||||
handleDoubleClick,
|
||||
} = useEditableText(id, type, text)
|
||||
|
||||
return (
|
||||
<HTMLContainer id={shape.id}>
|
||||
<div
|
||||
className="tl-text-shape__wrapper tl-text-shadow"
|
||||
data-font={shape.props.font}
|
||||
data-align={shape.props.align}
|
||||
data-hastext={!isEmpty}
|
||||
data-isediting={isEditing}
|
||||
data-textwrap={true}
|
||||
<TextLabel
|
||||
id={id}
|
||||
classNamePrefix="tl-text-shape"
|
||||
type="text"
|
||||
font={font}
|
||||
fontSize={FONT_SIZES[size]}
|
||||
lineHeight={TEXT_PROPS.lineHeight}
|
||||
align={align}
|
||||
verticalAlign="middle"
|
||||
text={text}
|
||||
labelColor={color}
|
||||
textWidth={width}
|
||||
textHeight={height}
|
||||
style={{
|
||||
fontSize: FONT_SIZES[shape.props.size],
|
||||
lineHeight: FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight + 'px',
|
||||
transform: `scale(${shape.props.scale})`,
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'top left',
|
||||
width: Math.max(1, width),
|
||||
height: Math.max(FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight, height),
|
||||
color: theme[color].solid,
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
wrap
|
||||
/>
|
||||
</HTMLContainer>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue