{ "version": 3, "sources": ["../../../../src/lib/shapes/shared/useEditableText.ts"], "sourcesContent": ["/* eslint-disable no-inner-declarations */\n\nimport {\n\tTLShape,\n\tTLUnknownShape,\n\tgetPointerInfo,\n\tpreventDefault,\n\tstopEventPropagation,\n\tuseEditor,\n\tuseValue,\n} from '@tldraw/editor'\nimport React, { useCallback, useEffect, useRef } from 'react'\nimport { INDENT, TextHelpers } from './TextHelpers'\n\nexport function useEditableText>(\n\tid: T['id'],\n\ttype: T['type'],\n\ttext: string\n) {\n\tconst editor = useEditor()\n\n\tconst rInput = useRef(null)\n\tconst rSkipSelectOnFocus = useRef(false)\n\tconst rSelectionRanges = useRef()\n\n\tconst isEditing = useValue('isEditing', () => editor.editingShapeId === id, [editor, id])\n\n\t// If the shape is editing but the input element not focused, focus the element\n\tuseEffect(() => {\n\t\tconst elm = rInput.current\n\t\tif (elm && isEditing && document.activeElement !== elm) {\n\t\t\telm.focus()\n\t\t}\n\t}, [isEditing])\n\n\t// When the label receives focus, set the value to the most recent text value and select all of the text\n\tconst handleFocus = useCallback(() => {\n\t\t// Store and turn off the skipSelectOnFocus flag\n\t\tconst skipSelect = rSkipSelectOnFocus.current\n\t\trSkipSelectOnFocus.current = false\n\n\t\t// On the next frame, if we're not skipping select AND we have text in the element, then focus the text\n\t\trequestAnimationFrame(() => {\n\t\t\tconst elm = rInput.current\n\t\t\tif (!elm) return\n\n\t\t\tconst shape = editor.getShape(id)\n\n\t\t\tif (shape) {\n\t\t\t\telm.value = shape.props.text\n\t\t\t\tif (elm.value.length && !skipSelect) {\n\t\t\t\t\telm.select()\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}, [editor, id])\n\n\t// When the label blurs, deselect all of the text and complete.\n\t// This makes it so that the canvas does not have to be focused\n\t// in order to exit the editing state and complete the editing state\n\tconst handleBlur = useCallback(() => {\n\t\tconst ranges = rSelectionRanges.current\n\n\t\trequestAnimationFrame(() => {\n\t\t\tconst elm = rInput.current\n\t\t\tconst { editingShapeId } = editor\n\t\t\t// Did we move to a different shape?\n\t\t\tif (elm && editingShapeId) {\n\t\t\t\t// important! these ^v are two different things\n\t\t\t\t// is that shape OUR shape?\n\t\t\t\tif (editingShapeId === id) {\n\t\t\t\t\tif (ranges) {\n\t\t\t\t\t\tif (!ranges.length) {\n\t\t\t\t\t\t\t// If we don't have any ranges, restore selection\n\t\t\t\t\t\t\t// and select all of the text\n\t\t\t\t\t\t\telm.focus()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Otherwise, skip the select-all-on-focus behavior\n\t\t\t\t\t\t\t// and restore the selection\n\t\t\t\t\t\t\trSkipSelectOnFocus.current = true\n\t\t\t\t\t\t\telm.focus()\n\t\t\t\t\t\t\tconst selection = window.getSelection()\n\t\t\t\t\t\t\tif (selection) {\n\t\t\t\t\t\t\t\tranges.forEach((range) => selection.addRange(range))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\telm.focus()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twindow.getSelection()?.removeAllRanges()\n\t\t\t\teditor.complete()\n\t\t\t}\n\t\t})\n\t}, [editor, id])\n\n\t// When the user presses ctrl / meta enter, complete the editing state.\n\t// When the user presses tab, indent or unindent the text.\n\tconst handleKeyDown = useCallback(\n\t\t(e: React.KeyboardEvent) => {\n\t\t\tif (!isEditing) return\n\n\t\t\tif (e.ctrlKey || e.metaKey) stopEventPropagation(e)\n\n\t\t\tswitch (e.key) {\n\t\t\t\tcase 'Enter': {\n\t\t\t\t\tif (e.ctrlKey || e.metaKey) {\n\t\t\t\t\t\teditor.complete()\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'Tab': {\n\t\t\t\t\tpreventDefault(e)\n\t\t\t\t\tif (e.shiftKey) {\n\t\t\t\t\t\tTextHelpers.unindent(e.currentTarget)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tTextHelpers.indent(e.currentTarget)\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t[editor, isEditing]\n\t)\n\n\t// When the text changes, update the text value.\n\tconst handleChange = useCallback(\n\t\t(e: React.ChangeEvent) => {\n\t\t\tif (!isEditing) return\n\n\t\t\tlet text = TextHelpers.normalizeText(e.currentTarget.value)\n\n\t\t\t// ------- Bug fix ------------\n\t\t\t// Replace tabs with spaces when pasting\n\t\t\tconst untabbedText = text.replace(/\\t/g, INDENT)\n\t\t\tif (untabbedText !== text) {\n\t\t\t\tconst selectionStart = e.currentTarget.selectionStart\n\t\t\t\te.currentTarget.value = untabbedText\n\t\t\t\te.currentTarget.selectionStart = selectionStart + (untabbedText.length - text.length)\n\t\t\t\te.currentTarget.selectionEnd = selectionStart + (untabbedText.length - text.length)\n\t\t\t\ttext = untabbedText\n\t\t\t}\n\t\t\t// ----------------------------\n\n\t\t\teditor.updateShapes([\n\t\t\t\t{ id, type, props: { text } },\n\t\t\t])\n\t\t},\n\t\t[editor, id, type, isEditing]\n\t)\n\n\tconst isEmpty = text.trim().length === 0\n\n\tuseEffect(() => {\n\t\tif (!isEditing) return\n\n\t\tconst elm = rInput.current\n\t\tif (elm) {\n\t\t\tfunction updateSelection() {\n\t\t\t\tconst selection = window.getSelection?.()\n\t\t\t\tif (selection && selection.type !== 'None') {\n\t\t\t\t\tconst ranges: Range[] = []\n\n\t\t\t\t\tif (selection) {\n\t\t\t\t\t\tfor (let i = 0; i < selection.rangeCount; i++) {\n\t\t\t\t\t\t\tranges.push(selection.getRangeAt?.(i))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\trSelectionRanges.current = ranges\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdocument.addEventListener('selectionchange', updateSelection)\n\n\t\t\treturn () => {\n\t\t\t\tdocument.removeEventListener('selectionchange', updateSelection)\n\t\t\t}\n\t\t}\n\t}, [isEditing])\n\n\tconst handleInputPointerDown = useCallback(\n\t\t(e: React.PointerEvent) => {\n\t\t\teditor.dispatch({\n\t\t\t\t...getPointerInfo(e),\n\t\t\t\ttype: 'pointer',\n\t\t\t\tname: 'pointer_down',\n\t\t\t\ttarget: 'shape',\n\t\t\t\tshape: editor.getShape(id)!,\n\t\t\t})\n\n\t\t\tstopEventPropagation(e) // we need to prevent blurring the input\n\t\t},\n\t\t[editor, id]\n\t)\n\n\tconst handleDoubleClick = stopEventPropagation\n\n\treturn {\n\t\trInput,\n\t\tisEditing,\n\t\thandleFocus,\n\t\thandleBlur,\n\t\thandleKeyDown,\n\t\thandleChange,\n\t\thandleInputPointerDown,\n\t\thandleDoubleClick,\n\t\tisEmpty,\n\t}\n}\n"], "mappings": "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", "names": ["text", "updateSelection"] }