vditor
Version:
♏ 易于使用的 Markdown 编辑器,为适配不同的应用场景而生
271 lines (247 loc) • 10.8 kB
text/typescript
import * as DiffMatchPatch from "diff-match-patch";
import {disableToolbar, enableToolbar, hidePanel} from "../toolbar/setToolbar";
import {isFirefox, isSafari} from "../util/compatibility";
import {scrollCenter} from "../util/editorCommonEvent";
import {execAfterRender} from "../util/fixBrowserBehavior";
import {highlightToolbar} from "../util/highlightToolbar";
import {processCodeRender} from "../util/processCode";
import {setRangeByWbr, setSelectionFocus} from "../util/selection";
import {renderToc} from "../util/toc";
interface IUndo {
hasUndo: boolean;
lastText: string;
redoStack: DiffMatchPatch.patch_obj[][];
undoStack: DiffMatchPatch.patch_obj[][];
}
class Undo {
private stackSize = 50;
private dmp: DiffMatchPatch.diff_match_patch;
private wysiwyg: IUndo;
private ir: IUndo;
private sv: IUndo;
constructor() {
this.resetStack();
// @ts-ignore
this.dmp = new DiffMatchPatch();
}
public clearStack(vditor: IVditor) {
this.resetStack();
this.resetIcon(vditor);
}
public resetIcon(vditor: IVditor) {
if (!vditor.toolbar) {
return;
}
if (this[vditor.currentMode].undoStack.length > 1) {
enableToolbar(vditor.toolbar.elements, ["undo"]);
} else {
disableToolbar(vditor.toolbar.elements, ["undo"]);
}
if (this[vditor.currentMode].redoStack.length !== 0) {
enableToolbar(vditor.toolbar.elements, ["redo"]);
} else {
disableToolbar(vditor.toolbar.elements, ["redo"]);
}
}
public undo(vditor: IVditor) {
if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") {
return;
}
if (this[vditor.currentMode].undoStack.length < 2) {
return;
}
const state = this[vditor.currentMode].undoStack.pop();
if (!state) {
return;
}
this[vditor.currentMode].redoStack.push(state);
this.renderDiff(state, vditor);
this[vditor.currentMode].hasUndo = true;
// undo 操作后,需要关闭 hint
hidePanel(vditor, ["hint"]);
}
public redo(vditor: IVditor) {
if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") {
return;
}
const state = this[vditor.currentMode].redoStack.pop();
if (!state) {
return;
}
this[vditor.currentMode].undoStack.push(state);
this.renderDiff(state, vditor, true);
}
public recordFirstPosition(vditor: IVditor, event: KeyboardEvent) {
if (getSelection().rangeCount === 0) {
return;
}
if (this[vditor.currentMode].undoStack.length !== 1 || this[vditor.currentMode].undoStack[0].length === 0 ||
this[vditor.currentMode].redoStack.length > 0) {
return;
}
if (isFirefox() && event.key === "Backspace") {
// Firefox 第一次删除无效
return;
}
if (isSafari()) {
// Safari keydown 在 input 之后,不需要重复记录历史
return;
}
const text = this.addCaret(vditor);
if (text.replace("<wbr>", "").replace(" vditor-ir__node--expand", "")
!== this[vditor.currentMode].undoStack[0][0].diffs[0][1].replace("<wbr>", "")) {
// 当还不没有存入 undo 栈时,按下 ctrl 后会覆盖 lastText
return;
}
this[vditor.currentMode].undoStack[0][0].diffs[0][1] = text;
this[vditor.currentMode].lastText = text;
// 不能添加 setSelectionFocus(cloneRange); 否则 windows chrome 首次输入会烂
}
public addToUndoStack(vditor: IVditor) {
// afterRenderEvent.ts 已经 debounce
const text = this.addCaret(vditor, true);
const diff = this.dmp.diff_main(text, this[vditor.currentMode].lastText, true);
const patchList = this.dmp.patch_make(text, this[vditor.currentMode].lastText, diff);
if (patchList.length === 0 && this[vditor.currentMode].undoStack.length > 0) {
return;
}
this[vditor.currentMode].lastText = text;
this[vditor.currentMode].undoStack.push(patchList);
if (this[vditor.currentMode].undoStack.length > this.stackSize) {
this[vditor.currentMode].undoStack.shift();
}
if (this[vditor.currentMode].hasUndo) {
this[vditor.currentMode].redoStack = [];
this[vditor.currentMode].hasUndo = false;
disableToolbar(vditor.toolbar.elements, ["redo"]);
}
if (this[vditor.currentMode].undoStack.length > 1) {
enableToolbar(vditor.toolbar.elements, ["undo"]);
}
}
private renderDiff(state: DiffMatchPatch.patch_obj[], vditor: IVditor, isRedo: boolean = false) {
let text;
if (isRedo) {
const redoPatchList = this.dmp.patch_deepCopy(state).reverse();
redoPatchList.forEach((patch) => {
patch.diffs.forEach((diff) => {
diff[0] = -diff[0];
});
});
text = this.dmp.patch_apply(redoPatchList, this[vditor.currentMode].lastText)[0];
} else {
text = this.dmp.patch_apply(state, this[vditor.currentMode].lastText)[0];
}
this[vditor.currentMode].lastText = text;
vditor[vditor.currentMode].element.innerHTML = text;
if (vditor.currentMode !== "sv") {
vditor[vditor.currentMode].element.querySelectorAll(`.vditor-${vditor.currentMode}__preview`)
.forEach((blockElement: HTMLElement) => {
if (blockElement.parentElement.querySelector(".language-echarts")) {
if (vditor.currentMode === "ir") {
blockElement.parentElement.outerHTML = vditor.lute.SpinVditorIRDOM(blockElement.parentElement.outerHTML);
} else {
blockElement.parentElement.outerHTML = vditor.lute.SpinVditorDOM(blockElement.parentElement.outerHTML);
}
}
});
vditor[vditor.currentMode].element.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`)
.forEach((blockElement: HTMLElement) => {
processCodeRender(blockElement, vditor);
});
}
if (!vditor[vditor.currentMode].element.querySelector("wbr")) {
// Safari 第一次输入没有光标,需手动定位到结尾
const range = getSelection().getRangeAt(0);
range.setEndBefore(vditor[vditor.currentMode].element);
range.collapse(false);
} else {
setRangeByWbr(
vditor[vditor.currentMode].element, vditor[vditor.currentMode].element.ownerDocument.createRange());
scrollCenter(vditor);
}
renderToc(vditor);
execAfterRender(vditor, {
enableAddUndoStack: false,
enableHint: false,
enableInput: true,
});
highlightToolbar(vditor);
vditor[vditor.currentMode].element.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`)
.forEach((item: HTMLElement) => {
processCodeRender(item, vditor);
});
if (this[vditor.currentMode].undoStack.length > 1) {
enableToolbar(vditor.toolbar.elements, ["undo"]);
} else {
disableToolbar(vditor.toolbar.elements, ["undo"]);
}
if (this[vditor.currentMode].redoStack.length !== 0) {
enableToolbar(vditor.toolbar.elements, ["redo"]);
} else {
disableToolbar(vditor.toolbar.elements, ["redo"]);
}
}
private resetStack() {
this.ir = {
hasUndo: false,
lastText: "",
redoStack: [],
undoStack: [],
};
this.sv = {
hasUndo: false,
lastText: "",
redoStack: [],
undoStack: [],
};
this.wysiwyg = {
hasUndo: false,
lastText: "",
redoStack: [],
undoStack: [],
};
}
private addCaret(vditor: IVditor, setFocus = false) {
let cloneRange: Range;
if (getSelection().rangeCount !== 0 && !vditor[vditor.currentMode].element.querySelector("wbr")) {
const range = getSelection().getRangeAt(0);
if (vditor[vditor.currentMode].element.contains(range.startContainer)) {
cloneRange = range.cloneRange();
const wbrElement = document.createElement("span");
wbrElement.className = "vditor-wbr";
range.insertNode(wbrElement);
}
}
// 移除数学公式、echart 渲染 https://github.com/Vanessa219/vditor/issues/1738
const cloneElement = vditor[vditor.currentMode].element.cloneNode(true) as HTMLElement;
cloneElement.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='1']`)
.forEach((item: HTMLElement) => {
if (!item.firstElementChild) {
return;
}
if (item.firstElementChild.classList.contains("language-echarts") ||
item.firstElementChild.classList.contains("language-plantuml") ||
item.firstElementChild.classList.contains("language-mindmap")) {
item.firstElementChild.removeAttribute("_echarts_instance_");
item.firstElementChild.removeAttribute("data-processed");
item.firstElementChild.innerHTML = item.previousElementSibling.firstElementChild.innerHTML;
item.setAttribute("data-render", "2");
} else if (item.firstElementChild.classList.contains("language-math")) {
item.setAttribute("data-render", "2");
item.firstElementChild.textContent = item.firstElementChild.getAttribute("data-math");
item.firstElementChild.removeAttribute("data-math");
}
});
const text = cloneElement.innerHTML;
vditor[vditor.currentMode].element.querySelectorAll(".vditor-wbr").forEach((item) => {
item.remove();
// 使用 item.outerHTML = "" 会产生 https://github.com/Vanessa219/vditor/pull/686;
});
if (setFocus && cloneRange) {
setSelectionFocus(cloneRange);
}
return text.replace('<span class="vditor-wbr"></span>', "<wbr>");
}
}
export {Undo};