给编辑器实现基本的 撤销/重做 功能有多难?

陪她去流浪 桃子 阅读次数:234

选择 Tiny-Markdown-Editor1 作为文章编辑器已经一年了。虽然前一周提了近🔟个Bugs,好在作者修复积极,一个不剩。

但是这款编辑器有一个比较致命的缺陷:不支持 撤销(Undo)重做(Redo) 功能。 鉴于我现在编辑器日益完善、日常牢骚多了起来,打开 VSCode 写文章有点麻烦。所以,对编辑器的要求就更多了;进而,无法再忍受一个编辑器不支持撤销与重做。

鉴于作者希望实现一个高效的撤销/重做,而且比较复杂2,暂无计划增加此功能,所以我需要自己增加此功能。 实现一个高效的基于 diff 的变更记录确实很麻烦(需要维护文档结构、仅处理脏掉的部分),而开发富文本编辑器素来又有前端地狱之称,所以我觉得我也应该放弃这种想法💭。

可是 Markdown 就一个纯文本编辑器啊,搞不定复杂的“脏矩形”,还不能一把梭全文替换掉吗? 谁说不行呢?!几十行代码就搞定了,真的。

需要的步骤大概如下:

数据结构:

  • 一个数组:用来模拟 Undo StackRedo Stack。左边是 Undo,右边是 Redo。
  • 一个指针:用于记录当前的 Undo Stack 位置,最开始在最左边。

编辑与栈操作:

  1. ✍️ 编辑时(save)

    • 把最新的编辑数据入 Undo 栈,指针右移;
    • 清空 Redo 栈。
  2. ↩️ 撤销时(undo)

    • 指针左移,使用前一个 Undo 数据;
    • 左移之前的数据因为指针移动变成 Redo 数据。
  3. ↪️ 重做时(redo)

    • 指针右移,使用后一个 Redo 数据;
    • 左移之前的 Redo 数据变成 Undo 数据。

是的,就是6️⃣个操作。写作 JavaScript,也只有6️⃣行:34

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
save(content, start, end) {
	this._stack[++this._index] = { content, start, end };
	this._stack.length = this._index+1;
}
undo() {
	if (this._index <= 0) { return; }
	this._use(this._stack[--this._index]);
}
redo() {
	if(this._index+1 >= this._stack.length) { return; }
	this._use(this._stack[++this._index]);
}

上面的 startend 还提供了光标位置、选区记录恢复功能,你就说这糊得能不能用吧?!

现代的浏览器真高级,完全替换 DOM 元素的 innerHTML 居然还能精准保持滚动条位置!