/* eslint-disable no-inner-declarations */ import { TLShapeId, TLUnknownShape, getPointerInfo, setPointerCapture, stopEventPropagation, useEditor, useValue, } from '@tldraw/editor' import React, { useCallback, useEffect, useRef } from 'react' import { INDENT, TextHelpers } from './TextHelpers' /** @public */ export function useEditableText(id: TLShapeId, type: string, text: string) { const editor = useEditor() const rInput = useRef(null) const rSelectionRanges = useRef() const isEditingAnything = useValue( 'isEditingAnything', () => editor.getEditingShapeId() !== null, [editor] ) const isEditing = useValue('isEditing', () => editor.getEditingShapeId() === id, [editor, id]) // If the shape is editing but the input element not focused, focus the element useEffect(() => { const elm = rInput.current if (elm && isEditing && document.activeElement !== elm) { elm.focus() } }, [isEditing]) // When the label blurs, deselect all of the text and complete. // This makes it so that the canvas does not have to be focused // in order to exit the editing state and complete the editing state const handleBlur = useCallback(() => { const ranges = rSelectionRanges.current requestAnimationFrame(() => { const elm = rInput.current const editingShapeId = editor.getEditingShapeId() // Did we move to a different shape? if (editingShapeId) { // important! these ^v are two different things // is that shape OUR shape? if (elm && editingShapeId === id) { if (ranges) { if (!ranges.length) { // If we don't have any ranges, restore selection // and select all of the text elm.focus() } else { // Restore the selection elm.focus() const selection = window.getSelection() if (selection) { ranges.forEach((range) => selection.addRange(range)) } } } else { elm.focus() } } } else { window.getSelection()?.removeAllRanges() } }) }, [editor, id]) // When the user presses ctrl / meta enter, complete the editing state. // When the user presses tab, indent or unindent the text. const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (!isEditing) return switch (e.key) { case 'Enter': { if (e.ctrlKey || e.metaKey) { editor.complete() } break } } }, [editor, isEditing] ) // When the text changes, update the text value. const handleChange = useCallback( (e: React.ChangeEvent) => { if (!isEditing) return let text = TextHelpers.normalizeText(e.currentTarget.value) // ------- Bug fix ------------ // Replace tabs with spaces when pasting const untabbedText = text.replace(/\t/g, INDENT) if (untabbedText !== text) { const selectionStart = e.currentTarget.selectionStart e.currentTarget.value = untabbedText e.currentTarget.selectionStart = selectionStart + (untabbedText.length - text.length) e.currentTarget.selectionEnd = selectionStart + (untabbedText.length - text.length) text = untabbedText } // ---------------------------- editor.updateShapes([ { id, type, props: { text } }, ]) }, [editor, id, type, isEditing] ) const isEmpty = text.trim().length === 0 useEffect(() => { if (!isEditing) return const elm = rInput.current if (elm) { function updateSelection() { const selection = window.getSelection?.() if (selection && selection.type !== 'None') { const ranges: Range[] = [] if (selection) { for (let i = 0; i < selection.rangeCount; i++) { ranges.push(selection.getRangeAt?.(i)) } } rSelectionRanges.current = ranges } } document.addEventListener('selectionchange', updateSelection) return () => { document.removeEventListener('selectionchange', updateSelection) } } }, [isEditing]) const handleInputPointerDown = useCallback( (e: React.PointerEvent) => { editor.dispatch({ ...getPointerInfo(e), type: 'pointer', name: 'pointer_down', target: 'shape', shape: editor.getShape(id)!, }) stopEventPropagation(e) // we need to prevent blurring the input // This is important so that when dragging a shape using the text label, // the shape continues to be dragged, even if the cursor is over the UI. setPointerCapture(e.currentTarget, e) }, [editor, id] ) const handleDoubleClick = stopEventPropagation return { rInput, isEditing, handleFocus: noop, handleBlur, handleKeyDown, handleChange, handleInputPointerDown, handleDoubleClick, isEmpty, isEditingAnything, } } function noop() { return }