kopia lustrzana https://github.com/Tldraw/Tldraw
187 wiersze
4.7 KiB
TypeScript
187 wiersze
4.7 KiB
TypeScript
/* 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<HTMLTextAreaElement>(null)
|
|
const rSelectionRanges = useRef<Range[] | null>()
|
|
|
|
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<HTMLTextAreaElement>) => {
|
|
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<HTMLTextAreaElement>) => {
|
|
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<TLUnknownShape & { props: { text: string } }>([
|
|
{ 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
|
|
}
|