diff --git a/src/snek/static/editor.js b/src/snek/static/editor.js new file mode 100644 index 0000000..87f2da8 --- /dev/null +++ b/src/snek/static/editor.js @@ -0,0 +1,226 @@ +import { NjetComponent} from "/njext.ks" + + class NjetEditor extends NjetComponent { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + + const style = document.createElement('style'); + style.textContent = ` + #editor { + padding: 1rem; + outline: none; + white-space: pre-wrap; + line-height: 1.5; + height: 100%; + overflow-y: auto; + background: #1e1e1e; + color: #d4d4d4; + } + #command-line { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + padding: 0.2rem 1rem; + background: #333; + color: #0f0; + display: none; + font-family: monospace; + } + `; + + this.editor = document.createElement('div'); + this.editor.id = 'editor'; + this.editor.contentEditable = true; + this.editor.innerText = `Welcome to VimEditor Component +Line 2 here +Another line +Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`; + + this.cmdLine = document.createElement('div'); + this.cmdLine.id = 'command-line'; + this.shadowRoot.append(style, this.editor, this.cmdLine); + + this.mode = 'normal'; // normal | insert | visual | command + this.keyBuffer = ''; + this.lastDeletedLine = ''; + this.yankedLine = ''; + + this.editor.addEventListener('keydown', this.handleKeydown.bind(this)); + } + + connectedCallback() { + this.editor.focus(); + } + + getCaretOffset() { + let caretOffset = 0; + const sel = this.shadowRoot.getSelection(); + if (!sel || sel.rangeCount === 0) return 0; + + const range = sel.getRangeAt(0); + const preCaretRange = range.cloneRange(); + preCaretRange.selectNodeContents(this.editor); + preCaretRange.setEnd(range.endContainer, range.endOffset); + caretOffset = preCaretRange.toString().length; + return caretOffset; + } + + setCaretOffset(offset) { + const range = document.createRange(); + const sel = this.shadowRoot.getSelection(); + const walker = document.createTreeWalker(this.editor, NodeFilter.SHOW_TEXT, null, false); + + let currentOffset = 0; + let node; + while ((node = walker.nextNode())) { + if (currentOffset + node.length >= offset) { + range.setStart(node, offset - currentOffset); + range.collapse(true); + sel.removeAllRanges(); + sel.addRange(range); + return; + } + currentOffset += node.length; + } + } + + handleKeydown(e) { + const key = e.key; + + if (this.mode === 'insert') { + if (key === 'Escape') { + e.preventDefault(); + this.mode = 'normal'; + this.editor.blur(); + this.editor.focus(); + } + return; + } + + if (this.mode === 'command') { + if (key === 'Enter' || key === 'Escape') { + e.preventDefault(); + this.cmdLine.style.display = 'none'; + this.mode = 'normal'; + this.keyBuffer = ''; + } + return; + } + + if (this.mode === 'visual') { + if (key === 'Escape') { + e.preventDefault(); + this.mode = 'normal'; + } + return; + } + + // Handle normal mode + this.keyBuffer += key; + + const text = this.editor.innerText; + const caretPos = this.getCaretOffset(); + const lines = text.split('\n'); + + let charCount = 0, lineIdx = 0; + for (let i = 0; i < lines.length; i++) { + if (caretPos <= charCount + lines[i].length) { + lineIdx = i; + break; + } + charCount += lines[i].length + 1; + } + + const offsetToLine = idx => + text.split('\n').slice(0, idx).reduce((acc, l) => acc + l.length + 1, 0); + + switch (this.keyBuffer) { + case 'i': + e.preventDefault(); + this.mode = 'insert'; + this.keyBuffer = ''; + break; + + case 'v': + e.preventDefault(); + this.mode = 'visual'; + this.keyBuffer = ''; + break; + + case ':': + e.preventDefault(); + this.mode = 'command'; + this.cmdLine.style.display = 'block'; + this.cmdLine.textContent = ':'; + this.keyBuffer = ''; + break; + + case 'yy': + e.preventDefault(); + this.yankedLine = lines[lineIdx]; + this.keyBuffer = ''; + break; + + case 'dd': + e.preventDefault(); + this.lastDeletedLine = lines[lineIdx]; + lines.splice(lineIdx, 1); + this.editor.innerText = lines.join('\n'); + this.setCaretOffset(offsetToLine(lineIdx)); + this.keyBuffer = ''; + break; + + case 'p': + e.preventDefault(); + const lineToPaste = this.yankedLine || this.lastDeletedLine; + if (lineToPaste) { + lines.splice(lineIdx + 1, 0, lineToPaste); + this.editor.innerText = lines.join('\n'); + this.setCaretOffset(offsetToLine(lineIdx + 1)); + } + this.keyBuffer = ''; + break; + + case '0': + e.preventDefault(); + this.setCaretOffset(offsetToLine(lineIdx)); + this.keyBuffer = ''; + break; + + case '$': + e.preventDefault(); + this.setCaretOffset(offsetToLine(lineIdx) + lines[lineIdx].length); + this.keyBuffer = ''; + break; + + case 'gg': + e.preventDefault(); + this.setCaretOffset(0); + this.keyBuffer = ''; + break; + + case 'G': + e.preventDefault(); + this.setCaretOffset(text.length); + this.keyBuffer = ''; + break; + + case 'Escape': + e.preventDefault(); + this.mode = 'normal'; + this.keyBuffer = ''; + this.cmdLine.style.display = 'none'; + break; + + default: + // allow up to 2 chars for combos + if (this.keyBuffer.length > 2) this.keyBuffer = ''; + break; + } + } + } + + customElements.define('njet-editor', NjetEditor); +export {NjetEditor}