diff --git a/src/common/components/ContentEditable.tsx b/src/common/components/ContentEditable.tsx index 3151a3c..1f61fde 100644 --- a/src/common/components/ContentEditable.tsx +++ b/src/common/components/ContentEditable.tsx @@ -59,10 +59,47 @@ export const ContentEditable = ({ value, onInput, ...props }: Props) => { if (offset !== null) setCaretOffset(el, offset); }, [value]); + const handleKeyDown: JSX.KeyboardEventHandler = (e) => { + if (e.key !== 'Enter') return; + const prevTextContent = (e.target as HTMLDivElement).textContent; + e.preventDefault(); + + const sel = window.getSelection(); + if (!sel || sel.rangeCount === 0) return; + + const range = sel.getRangeAt(0); + range.deleteContents(); + + const newline = document.createTextNode('\n'); + range.insertNode(newline); + range.setStartAfter(newline); + range.collapse(true); + + const nextTextContent = (e.target as HTMLDivElement).textContent; + + // A trailing \n needs a following character to render in pre-line. + // If nothing follows the inserted newline, add a sentinel \n so the + // new line is visible, then place the caret before it. + const atEnd = nextTextContent.startsWith(prevTextContent) && nextTextContent !== prevTextContent && nextTextContent.length === prevTextContent.length + 1 && nextTextContent.at(-1) === '\n'; + if (atEnd) { + const sentinel = document.createTextNode('\n'); + range.insertNode(sentinel); + range.setStartBefore(sentinel); + range.collapse(true); + } + + sel.removeAllRanges(); + sel.addRange(range); + + ref.current?.dispatchEvent(new InputEvent('input', { bubbles: true })); + }; + return (