diff --git a/src/js/editor.ts b/src/js/editor.ts index 46d2328..4ec7ede 100644 --- a/src/js/editor.ts +++ b/src/js/editor.ts @@ -5,16 +5,19 @@ import { focusInput } from "./focus.js" const INDENT_SIZE = 2 export function initEditor() { - document.addEventListener("input", adjustHeight) + document.addEventListener("input", handleAdjustHeight) focusTextareaOnCreation() } -function adjustHeight(e: Event) { +function handleAdjustHeight(e: Event) { const target = e.target as HTMLElement - if (target?.matches(".editor")) { - target.style.height = "auto" - target.style.height = target.scrollHeight + "px" - } + if (target?.matches(".editor")) + adjustHeight(target as HTMLTextAreaElement) +} + +function adjustHeight(editor: HTMLTextAreaElement) { + editor.style.height = "auto" + editor.style.height = editor.scrollHeight + "px" } function focusTextareaOnCreation() { @@ -33,25 +36,75 @@ function focusTextareaOnCreation() { } function keydownHandler(e: KeyboardEvent) { - const target = e.target as HTMLTextAreaElement + const editor = e.target as HTMLTextAreaElement + if (e.key === "Tab") { e.preventDefault() if (e.shiftKey) - removeTab(target) + removeTab(editor) else - insertTab(target) + insertTab(editor) } else if (e.ctrlKey && e.key === "c") { focusInput() } else if ((e.ctrlKey && e.key === "s") || (e.ctrlKey && e.key === "Enter")) { e.preventDefault() send({ - id: target.dataset.path, + id: editor.dataset.path, type: "save-file", - data: target.value + data: editor.value }) + } else if (e.key === "{") { + if (editor.selectionStart !== editor.selectionEnd) { + insertAroundSelection(editor, '{', '}') + e.preventDefault() + } else { + setTimeout(() => insertAfterCaret(editor, "}"), 0) + } + } else if (e.key === "}" && isNextChar(editor, '}')) { + moveOneRight(editor) + e.preventDefault() + + } else if (e.key === "[") { + if (editor.selectionStart !== editor.selectionEnd) { + insertAroundSelection(editor, '[', ']') + e.preventDefault() + } else { + setTimeout(() => insertAfterCaret(editor, "]"), 0) + } + } else if (e.key === "]" && isNextChar(editor, ']')) { + moveOneRight(editor) + e.preventDefault() + + } else if (e.key === "(") { + if (editor.selectionStart !== editor.selectionEnd) { + insertAroundSelection(editor, '(', ')') + e.preventDefault() + } else { + setTimeout(() => insertAfterCaret(editor, ")"), 0) + } + } else if (e.key === ")" && isNextChar(editor, ')')) { + moveOneRight(editor) + e.preventDefault() + } else if (e.key === '"') { + if (isNextChar(editor, '"')) { + moveOneRight(editor) + e.preventDefault() + } else if (editor.selectionStart !== editor.selectionEnd) { + insertAroundSelection(editor, '"', '"') + e.preventDefault() + } else { + setTimeout(() => insertAfterCaret(editor, '"'), 0) + } + } else if (e.key === "Enter") { + indentNewlineForBraces(e, editor) } } +function moveOneRight(editor: HTMLTextAreaElement) { + const pos = editor.selectionStart + editor.selectionStart = editor.selectionEnd = pos + 1 +} + function insertTab(editor: HTMLTextAreaElement) { const start = editor.selectionStart const end = editor.selectionEnd @@ -72,4 +125,84 @@ function removeTab(editor: HTMLTextAreaElement) { editor.selectionStart = start - INDENT_SIZE editor.selectionEnd = end - INDENT_SIZE } -} \ No newline at end of file +} + +function insertAfterCaret(editor: HTMLTextAreaElement, char: string) { + const pos = editor.selectionStart + editor.value = editor.value.slice(0, pos) + char + editor.value.slice(pos) + editor.selectionStart = editor.selectionEnd = pos +} + +function isNextChar(editor: HTMLTextAreaElement, char: string): boolean { + return editor.value[editor.selectionStart] === char +} + +function indentNewlineForBraces(e: KeyboardEvent, editor: HTMLTextAreaElement) { + e.preventDefault() + + if (isBetween(editor, "{", "}") || isBetween(editor, "[", "]")) { + setTimeout(() => insertMoreIndentedNewline(editor), 0) + } else { + setTimeout(() => insertIndentedNewline(editor), 0) + } +} + +function isBetween(editor: HTMLTextAreaElement, start: string, end: string): boolean { + const pos = editor.selectionStart + const val = editor.value + + if (pos <= 0 || pos >= val.length) return false + return val[pos - 1] === start && val[pos] === end +} + +function insertIndentedNewline(editor: HTMLTextAreaElement) { + const pos = editor.selectionStart + + const before = editor.value.slice(0, pos) + + const prevLineStart = before.lastIndexOf("\n", pos - 1) + 1 + const prevLine = before.slice(prevLineStart) + + let leading = 0 + while (prevLine[leading] === " ") leading++ + + const indent = " ".repeat(leading) + + const insert = "\n" + indent + editor.value = editor.value.slice(0, pos) + insert + editor.value.slice(pos) + + const newPos = pos + insert.length + editor.selectionStart = editor.selectionEnd = newPos + + adjustHeight(editor) +} + +function insertAroundSelection(editor: HTMLTextAreaElement, before: string, after: string) { + const start = editor.selectionStart + const end = editor.selectionEnd + + editor.value = editor.value.slice(0, start) + before + editor.value.slice(start, end) + after + editor.value.slice(end) +} + +function insertMoreIndentedNewline(editor: HTMLTextAreaElement) { + const pos = editor.selectionStart + + const before = editor.value.slice(0, pos) + + const prevLineStart = before.lastIndexOf("\n", pos - 1) + 1 + const prevLine = before.slice(prevLineStart) + + let leading = 0 + while (prevLine[leading] === " ") leading++ + + const oldIndent = " ".repeat(leading) + const newIndent = " ".repeat(leading + INDENT_SIZE) + + const insert = "\n" + newIndent + "\n" + oldIndent + editor.value = editor.value.slice(0, pos) + insert + editor.value.slice(pos) + + const newPos = pos + insert.length + editor.selectionStart = editor.selectionEnd = newPos - 1 - oldIndent.length + + adjustHeight(editor) +}