aboutsummaryrefslogtreecommitdiff
path: root/editor/editor.ts
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2021-07-29 02:53:30 +0700
committerShulhan <ms@kilabit.info>2021-07-29 02:58:22 +0700
commit4dd3fdf6de4fd4f57a7465ed247007c64f830bd4 (patch)
treea557a500306e8e36a26ca95adf1cde85cc66bc38 /editor/editor.ts
parent52cd98e02cdc998064cc263b008748e4fa94d269 (diff)
downloadpakakeh.ts-4dd3fdf6de4fd4f57a7465ed247007c64f830bd4.tar.xz
editor: handle undo with CTRL+Z
Diffstat (limited to 'editor/editor.ts')
-rw-r--r--editor/editor.ts208
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
}