/* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' import { Utils, HTMLContainer, TLBounds } from '@tldraw/core' import { TextShape, TDMeta, TDShapeType, TransformInfo, AlignStyle } from '~types' import { BINDING_DISTANCE, GHOSTED_OPACITY, LETTER_SPACING } from '~constants' import { TDShapeUtil } from '../TDShapeUtil' import { styled } from '~styles' import { Vec } from '@tldraw/vec' import { TLDR } from '~state/TLDR' import { stopPropagation } from '~components/stopPropagation' import { getTextSvgElement, TextAreaUtils, defaultTextStyle, getShapeStyle, getFontStyle, getTextAlign, } from '../shared' type T = TextShape type E = HTMLDivElement export class TextUtil extends TDShapeUtil { type = TDShapeType.Text as const isAspectRatioLocked = true canEdit = true canBind = true canClone = true bindingDistance = BINDING_DISTANCE / 2 getShape = (props: Partial): T => { return Utils.deepMerge( { id: 'id', type: TDShapeType.Text, name: 'Text', parentId: 'page', childIndex: 1, point: [0, 0], rotation: 0, text: ' ', style: defaultTextStyle, }, props ) } texts = new Map() Component = TDShapeUtil.Component( ({ shape, isBinding, isGhost, isEditing, onShapeBlur, onShapeChange, meta, events }, ref) => { const { text, style } = shape const styles = getShapeStyle(style, meta.isDarkMode) const font = getFontStyle(shape.style) const rInput = React.useRef(null) const rIsMounted = React.useRef(false) const handleChange = React.useCallback( (e: React.ChangeEvent) => { let delta = [0, 0] const newText = TLDR.normalizeText(e.currentTarget.value) const currentBounds = this.getBounds(shape) this.texts.set(shape.id, newText) const nextBounds = this.getBounds({ ...shape, text: newText, }) switch (shape.style.textAlign) { case AlignStyle.Start: { break } case AlignStyle.Middle: { delta = Vec.div([nextBounds.width - currentBounds.width, 0], 2) break } case AlignStyle.End: { delta = [nextBounds.width - currentBounds.width, 0] break } } onShapeChange?.({ ...shape, id: shape.id, point: Vec.sub(shape.point, delta), text: newText, }) }, [shape.id, shape.point] ) const handleKeyDown = React.useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Escape') return if (e.key === 'Tab' && shape.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) } onShapeChange?.({ ...shape, text: TLDR.normalizeText(e.currentTarget.value) }) } }, [shape, onShapeChange] ) const handleBlur = React.useCallback((e: React.FocusEvent) => { e.currentTarget.setSelectionRange(0, 0) onShapeBlur?.() }, []) 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: React.PointerEvent) => { if (isEditing) { e.stopPropagation() } }, [isEditing] ) React.useEffect(() => { if (isEditing) { this.texts.set(shape.id, text) requestAnimationFrame(() => { rIsMounted.current = true const elm = rInput.current if (elm) { elm.focus() elm.select() } }) } else { onShapeBlur?.() } }, [isEditing]) return ( {isBinding && (
)} {isEditing ? (