diff options
| author | Shulhan <ms@kilabit.info> | 2021-07-29 02:53:30 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2021-07-29 02:58:22 +0700 |
| commit | 4dd3fdf6de4fd4f57a7465ed247007c64f830bd4 (patch) | |
| tree | a557a500306e8e36a26ca95adf1cde85cc66bc38 /editor/editor.ts | |
| parent | 52cd98e02cdc998064cc263b008748e4fa94d269 (diff) | |
| download | pakakeh.ts-4dd3fdf6de4fd4f57a7465ed247007c64f830bd4.tar.xz | |
editor: handle undo with CTRL+Z
Diffstat (limited to 'editor/editor.ts')
| -rw-r--r-- | editor/editor.ts | 208 |
1 files changed, 189 insertions, 19 deletions
diff --git a/editor/editor.ts b/editor/editor.ts index 880ef93..48e0e50 100644 --- a/editor/editor.ts +++ b/editor/editor.ts @@ -19,6 +19,8 @@ export class Editor implements IEditor { private lines: EditorLine[] = [] private sel: Selection | null = null private range!: Range + private isKeyControl: boolean = false + private unre: UndoRedo = new UndoRedo() constructor(public opts: IEditor) { this.id = opts.id @@ -117,10 +119,44 @@ export class Editor implements IEditor { document.head.appendChild(style) } + doUndo() { + const act = this.unre.Undo() + if (!act) { + return + } + switch (act.kind) { + case "join": + this.lines[act.currLine].elText.innerText = act.currText + this.insertNewline(act.nextLine, act.nextText) + break + + case "split": + this.lines[act.currLine].elText.innerText = act.currText + this.deleteLine(act.nextLine) + this.setCaret(this.lines[act.currLine].elText, 0) + break + + case "update": + this.lines[act.currLine].elText.innerText = act.currText + this.setCaret(this.lines[act.currLine].elText, 0) + break + } + } + + deleteLine(x: number) { + this.lines.splice(x, 1) + + // Reset the line numbers. + for (; x < this.lines.length; x++) { + this.lines[x].setNumber(x) + } + this.render() + } + insertNewline(x: number, text: string) { let newline = new EditorLine(x, text, this) for (let y = x; y < this.lines.length; y++) { - this.lines[y].setNumber(y + 2) + this.lines[y].setNumber(y + 1) } this.lines.splice(x, 0, newline) this.render() @@ -133,6 +169,25 @@ export class Editor implements IEditor { onKeydownText(x: number, text: HTMLElement, ev: KeyboardEvent) { switch (ev.key) { + case "Alt": + case "ArrowLeft": + case "ArrowRight": + case "CapsLock": + case "ContextMenu": + case "Delete": + case "End": + case "Escape": + case "Home": + case "Insert": + case "OS": + case "PageUp": + case "PageDown": + case "Pause": + case "PrintScreen": + case "ScrollLock": + case "Shift": + break + case "ArrowUp": if (x == 0) { return false @@ -166,32 +221,33 @@ export class Editor implements IEditor { break case "Backspace": - if (x == 0) { - return - } if (!this.sel) { return } + ev.preventDefault() + + let currLine = this.lines[x].elText + let currText = currLine.innerText + off = this.sel.focusOffset if (off > 0) { + this.unre.DoUpdate(x, currText) + currLine.innerText = + currText.slice(0, off - 1) + currText.slice(off + 1, currText.length) + this.setCaret(currLine, off - 1) return } - ev.preventDefault() // Join current line with previous. - let lineCurr = this.lines[x].elText let linePrev = this.lines[x - 1].elText + + this.unre.DoJoin(x - 1, linePrev.innerText, currLine.innerText) + off = linePrev.innerText.length - linePrev.innerText = linePrev.innerText + lineCurr.innerText + linePrev.innerText = linePrev.innerText + currLine.innerText // Remove the current line - this.lines.splice(x, 1) - - // Reset the line numbers. - for (; x < this.lines.length; x++) { - this.lines[x].setNumber(x + 1) - } - this.render() + this.deleteLine(x) this.setCaret(linePrev, off) break @@ -204,6 +260,9 @@ export class Editor implements IEditor { off = this.sel.focusOffset let text = this.lines[x].elText.innerText let newText = text.slice(off, text.length) + + this.unre.DoSplit(x, text, newText) + this.lines[x].elText.innerText = text.slice(0, off) this.insertNewline(x + 1, newText) break @@ -216,15 +275,41 @@ export class Editor implements IEditor { elText = this.lines[x].elText off = this.sel.focusOffset text = elText.innerText + + this.unre.DoUpdate(x, text) + elText.innerText = text.slice(0, off) + "\t" + text.slice(off, text.length) this.setCaret(elText, off + 1) ev.preventDefault() break + + case "Control": + this.isKeyControl = true + break + + case "z": + if (this.isKeyControl) { + ev.preventDefault() + this.doUndo() + return + } + break + + default: + this.unre.DoUpdate(x, this.lines[x].elText.innerText) } return true } + onKeyupText(x: number, elText: HTMLElement, ev: KeyboardEvent) { + switch (ev.key) { + case "Control": + this.isKeyControl = false + break + } + } + onMouseDownAtLine(x: number) { this.rangeBegin = x } @@ -275,23 +360,25 @@ export class Editor implements IEditor { } class EditorLine { + private lineNum: number = 0 el!: HTMLElement elNumber!: HTMLElement elText!: HTMLElement constructor(public x: number, public text: string, ed: Editor) { + this.lineNum = x this.el = document.createElement("div") this.el.classList.add("wui-editor-line") this.elNumber = document.createElement("span") this.elNumber.classList.add("wui-line-number") - this.elNumber.innerText = x + 1 + "" + this.elNumber.innerText = this.lineNum + 1 + "" this.elNumber.onmousedown = (ev: MouseEvent) => { - ed.onMouseDownAtLine(x) + ed.onMouseDownAtLine(this.lineNum) } this.elNumber.onmouseup = (ev: MouseEvent) => { - ed.onMouseUpAtLine(x) + ed.onMouseUpAtLine(this.lineNum) } this.elText = document.createElement("span") @@ -304,7 +391,10 @@ class EditorLine { } this.elText.onkeydown = (ev: KeyboardEvent) => { - return ed.onKeydownText(x, this.elText, ev) + return ed.onKeydownText(this.lineNum, this.elText, ev) + } + this.elText.onkeyup = (ev: KeyboardEvent) => { + return ed.onKeyupText(this.lineNum, this.elText, ev) } this.elText.addEventListener("paste", (ev: ClipboardEvent) => { @@ -321,6 +411,86 @@ class EditorLine { } setNumber(x: number) { - this.elNumber.innerText = x + "" + this.lineNum = x + this.elNumber.innerText = x + 1 + "" + } +} + +// +// UndoRedo store the state of actions. +// +class UndoRedo { + idx: number = 0 + actions: Action[] = [] + + constructor() {} + + DoJoin(prevLine: number, prevText: string, currText: string) { + let currLine = prevLine + 1 + let action: Action = { + kind: "join", + currLine: prevLine, + currText: prevText, + nextLine: prevLine + 1, + nextText: currText, + } + if (this.actions.length > 0) { + this.actions = this.actions.slice(0, this.idx) + } + this.actions.push(action) + this.idx++ } + + DoSplit(currLine: number, currText: string, nextText: string) { + let action = { + kind: "split", + currLine: currLine, + currText: currText, + nextLine: currLine + 1, + nextText: nextText, + } + if (this.actions.length > 0) { + this.actions = this.actions.slice(0, this.idx) + } + this.actions.push(action) + this.idx++ + } + + DoUpdate(lineNum: number, text: string) { + const action: Action = { + kind: "update", + currLine: lineNum, + currText: text, + nextLine: 0, + nextText: "", + } + + if (this.actions.length > 0) { + this.actions = this.actions.slice(0, this.idx) + } + this.actions.push(action) + this.idx++ + } + + Undo(): Action | null { + if (this.idx == 0) { + return null + } + this.idx-- + return this.actions[this.idx] + } +} + +// There are three kind of action +// +// * update: change a single line +// * enter: split line using enter +// * backspace: join line using backspace. +// +interface Action { + kind: string + currLine: number + currText: string + nextLine: number + nextText: string } |
