/* eslint-disable @typescript-eslint/no-explicit-any */ import { getTextLabelSize, TextUtils, TLBounds, TLResizeStartInfo, TLTextShape, TLTextShapeProps, } from '@tldraw/core' import { HTMLContainer, TLComponentProps } from '@tldraw/react' import { action, computed } from 'mobx' import { observer } from 'mobx-react-lite' import * as React from 'react' import type { SizeLevel } from '.' import { CustomStyleProps, withClampedStyles } from './style-props' import { TextAreaUtils } from './text/TextAreaUtils' export interface TextShapeProps extends TLTextShapeProps, CustomStyleProps { borderRadius: number fontFamily: string fontSize: number fontWeight: number italic: boolean lineHeight: number padding: number type: 'text' scaleLevel?: SizeLevel } const levelToScale = { xs: 10, sm: 16, md: 20, lg: 32, xl: 48, xxl: 60, } export class TextShape extends TLTextShape { static id = 'text' static defaultProps: TextShapeProps = { id: 'box', parentId: 'page', type: 'text', point: [0, 0], size: [100, 100], isSizeLocked: true, text: '', lineHeight: 1.2, fontSize: 20, fontWeight: 400, italic: false, padding: 4, fontFamily: "var(--ls-font-family), 'Helvetica Neue', Helvetica, Arial, sans-serif", borderRadius: 0, stroke: 'var(--tl-foreground, #000)', fill: '#ffffff', noFill: true, strokeType: 'line', strokeWidth: 2, opacity: 1, } ReactComponent = observer(({ events, isErasing, isEditing, onEditingEnd }: TLComponentProps) => { const { props: { opacity, fontFamily, fontSize, fontWeight, italic, lineHeight, text, stroke, padding, }, } = this const rInput = React.useRef(null) const rIsMounted = React.useRef(false) const rInnerWrapper = React.useRef(null) // When the text changes, update the text—and, const handleChange = React.useCallback((e: React.ChangeEvent) => { const { isSizeLocked } = this.props const text = TextUtils.normalizeText(e.currentTarget.value) if (isSizeLocked) { this.update({ text, size: this.getAutoSizedBoundingBox({ text }) }) return } // If not autosizing, update just the text this.update({ text }) }, []) const handleKeyDown = React.useCallback((e: React.KeyboardEvent) => { if (e.key === 'Escape') return if (e.key === 'Tab' && text.length === 0) { e.preventDefault() return } if (!(e.key === 'Meta' || e.metaKey)) { e.stopPropagation() } else if (e.key === 'z' && e.metaKey) { if (e.shiftKey) { document.execCommand('redo', false) } else { document.execCommand('undo', false) } e.stopPropagation() e.preventDefault() return } if (e.key === 'Tab') { e.preventDefault() if (e.shiftKey) { TextAreaUtils.unindent(e.currentTarget) } else { TextAreaUtils.indent(e.currentTarget) } this.update({ text: TextUtils.normalizeText(e.currentTarget.value) }) } }, []) const handleBlur = React.useCallback( (e: React.FocusEvent) => { e.currentTarget.setSelectionRange(0, 0) onEditingEnd?.() }, [onEditingEnd] ) const handleFocus = React.useCallback( (e: React.FocusEvent) => { if (!isEditing) return if (!rIsMounted.current) return if (document.activeElement === e.currentTarget) { e.currentTarget.select() } }, [isEditing] ) const handlePointerDown = React.useCallback( e => { if (isEditing) e.stopPropagation() }, [isEditing] ) React.useEffect(() => { if (isEditing) { requestAnimationFrame(() => { rIsMounted.current = true const elm = rInput.current if (elm) { elm.focus() elm.select() } }) } else { onEditingEnd?.() } }, [isEditing, onEditingEnd]) React.useLayoutEffect(() => { const { fontFamily, fontSize, fontWeight, lineHeight, padding } = this.props const [width, height] = getTextLabelSize( text, { fontFamily, fontSize, fontWeight, lineHeight }, padding ) this.update({ size: [width, height] }) }, []) return (
{isEditing ? (