Update.
This commit is contained in:
parent
75f12c1971
commit
52538d0181
226
src/snek/static/editor.js
Normal file
226
src/snek/static/editor.js
Normal file
@ -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}
|
Loading…
Reference in New Issue
Block a user