diff --git a/web/main.js b/web/main.js index 83464e9..899687f 100644 --- a/web/main.js +++ b/web/main.js @@ -158,6 +158,15 @@ function makeEmoteImg(name) { // Plain-text source; images contribute their literal names. function editorSource(root = editor) { + if (root === editor && root.childNodes.length === 1) { + const only = root.firstChild; + if (only.nodeName === "BR" || + (only.nodeName === "DIV" && only.childNodes.length === 1 && + only.firstChild.nodeName === "BR")) { + return ""; + } + } + let src = ""; for (const node of root.childNodes) { if (node.nodeType === Node.TEXT_NODE) { @@ -212,6 +221,14 @@ function getCaretOffset() { return count; } +function sourceOffsetAt(node, offset) { + if (node !== editor && !editor.contains(node)) return null; + const range = document.createRange(); + range.setStart(editor, 0); + range.setEnd(node, offset); + return editorSource(range.cloneContents()).length; +} + function setCaretOffset(offset) { const boundaries = []; // {at, make} const walk = (node, base) => { @@ -619,6 +636,24 @@ editor.addEventListener("keydown", (e) => { return; } + if (e.key === "Enter" && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + const sel = getSelection(); + if (!sel.rangeCount) return; + const range = sel.getRangeAt(0); + const start = sourceOffsetAt(range.startContainer, range.startOffset); + const end = sourceOffsetAt(range.endContainer, range.endOffset); + if (start === null || end === null) return; + const src = editorSource(editor); + const next = src.slice(0, start) + "\n" + src.slice(end); + editor.replaceChildren(buildEditorFragment(next)); + setCaretOffset(start + 1); + renderGutter(next); + scheduleUndoCommit(); + updateAutocomplete(); + return; + } + if (e.key === "Tab") { // No popup yet: complete directly when there is exactly one // candidate, otherwise open the popup for the word at the caret.