|
@@ -1,221 +1,105 @@
|
|
|
-import { get } from "lodash";
|
|
|
+import { set } from "lodash";
|
|
|
import { StateRoot } from "queenjs";
|
|
|
|
|
|
+type RecordOptions = { combine?: boolean };
|
|
|
class State extends StateRoot {
|
|
|
- lenth = 0; //操作栈的长度
|
|
|
- total = 50; //操作栈总长度
|
|
|
- cap = 100; //操作栈的容量
|
|
|
+ currLen = 0; //操作栈的长度
|
|
|
+ maxLen = 100; //操作栈总长度
|
|
|
|
|
|
opIndex = -1; //操作栈的指针
|
|
|
- saveIndex = -1; //保存的指针
|
|
|
- enable = true;
|
|
|
|
|
|
- canSave = this.computed((state) => {
|
|
|
- return state.opIndex != state.saveIndex;
|
|
|
- });
|
|
|
canUndo = this.computed((state) => {
|
|
|
- return state.enable && state.opIndex >= 0;
|
|
|
+ return state.opIndex >= 0;
|
|
|
});
|
|
|
canRedo = this.computed((state) => {
|
|
|
- return state.enable && state.opIndex + 1 < state.lenth;
|
|
|
+ return state.opIndex < state.currLen - 1;
|
|
|
});
|
|
|
}
|
|
|
|
|
|
export class HistoryController {
|
|
|
- combine = false;
|
|
|
- _group = false;
|
|
|
state = new State().reactive();
|
|
|
- queue: Action[] = [];
|
|
|
- groupQueue: Action[] = [];
|
|
|
-
|
|
|
- constructor(cap = 100) {
|
|
|
- this.state.cap = 2 * cap;
|
|
|
-
|
|
|
- //最大缓存行为个数
|
|
|
- this.state.total = cap;
|
|
|
- }
|
|
|
-
|
|
|
- saved() {
|
|
|
- this.state.saveIndex = this.state.opIndex;
|
|
|
- }
|
|
|
+ queue: GroupAction[] = [];
|
|
|
+ cacheGroupAction = new GroupAction();
|
|
|
|
|
|
+ // 添加缓存记录
|
|
|
record(action: Action) {
|
|
|
- if (!action) return;
|
|
|
- if (this.group) {
|
|
|
- this.groupQueue.push(action);
|
|
|
- return;
|
|
|
- }
|
|
|
- const state = this.state;
|
|
|
- if (!state.enable) return;
|
|
|
- const lastAction = this.queue[state.opIndex];
|
|
|
-
|
|
|
- // 同时满足[可合并状态|操作指针指向栈顶|同对象的同种操作]
|
|
|
- if (
|
|
|
- lastAction?.combine &&
|
|
|
- this.queue.length === state.opIndex + 1 &&
|
|
|
- action.name === lastAction.name &&
|
|
|
- action.root === lastAction.root
|
|
|
- ) {
|
|
|
- lastAction.value = action.value;
|
|
|
- lastAction.combine = action.combine;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- let index = state.opIndex + 1;
|
|
|
- if (index >= state.cap) {
|
|
|
- //大于容量了
|
|
|
- this.queue = this.queue.slice(state.total);
|
|
|
- index = this.state.total;
|
|
|
- }
|
|
|
-
|
|
|
- this.state.opIndex = index;
|
|
|
- this.queue[index] = action;
|
|
|
- this.state.lenth = index + 1;
|
|
|
-
|
|
|
- if (this.queue.length > index + 1) {
|
|
|
- //丢掉回退的部分操作
|
|
|
- this.queue.splice(index + 1, this.queue.length - index - 1);
|
|
|
+ this.cacheGroupAction.record(action);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存缓存记录到历史栈中
|
|
|
+ submit(action?: Action) {
|
|
|
+ const { state, queue, cacheGroupAction } = this;
|
|
|
+ if (action) this.record(action);
|
|
|
+ if (!cacheGroupAction.actions.length) return;
|
|
|
+
|
|
|
+ // 将缓存操作记录保存到当前指针的下一栈中
|
|
|
+ queue[++state.opIndex] = cacheGroupAction;
|
|
|
+ // 设置栈的长度为指针的长度,舍弃后面的记录
|
|
|
+ queue.length = state.opIndex + 1;
|
|
|
+ // 若栈长度超过上限, 舍弃之前的记录
|
|
|
+ if (queue.length > state.maxLen) {
|
|
|
+ queue.splice(0, queue.length - state.maxLen);
|
|
|
+ state.opIndex = state.maxLen - 1;
|
|
|
}
|
|
|
+ // 更新当前长度状态
|
|
|
+ state.currLen = queue.length;
|
|
|
+ // 更新当前缓存GroupAction
|
|
|
+ this.cacheGroupAction = new GroupAction();
|
|
|
}
|
|
|
|
|
|
undo() {
|
|
|
- const state = this.state;
|
|
|
- if (!state.enable) return;
|
|
|
-
|
|
|
- if (state.opIndex < 0) return; //已经退到第一步了
|
|
|
-
|
|
|
- const action = this.queue[state.opIndex];
|
|
|
- action.undo();
|
|
|
-
|
|
|
- state.opIndex = state.opIndex - 1;
|
|
|
+ if (!this.state.canUndo) return;
|
|
|
+ this.queue[this.state.opIndex--].undo();
|
|
|
}
|
|
|
|
|
|
redo() {
|
|
|
- const state = this.state;
|
|
|
- if (!state.enable) return;
|
|
|
-
|
|
|
- if (state.opIndex >= this.queue.length - 1) return; //已经是最后一步操作了
|
|
|
-
|
|
|
- const action = this.queue[state.opIndex + 1];
|
|
|
- action.redo();
|
|
|
-
|
|
|
- state.opIndex = state.opIndex + 1;
|
|
|
- }
|
|
|
-
|
|
|
- get group() {
|
|
|
- return this._group;
|
|
|
- }
|
|
|
- set group(state) {
|
|
|
- this._group = state;
|
|
|
- if (!state) {
|
|
|
- if (this.groupQueue.length) {
|
|
|
- this.record(
|
|
|
- this.groupQueue.length == 1
|
|
|
- ? this.groupQueue[0]
|
|
|
- : (new GroupAction(this.groupQueue) as any)
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
- this.groupQueue = [];
|
|
|
- }
|
|
|
-
|
|
|
- removeHead() {
|
|
|
- const state = this.state;
|
|
|
- if (state.opIndex > 0) {
|
|
|
- state.opIndex = state.opIndex - 1;
|
|
|
- this.queue.splice(this.queue.length - 1, 1);
|
|
|
- }
|
|
|
+ if (!this.state.canRedo) return;
|
|
|
+ this.queue[++this.state.opIndex].redo();
|
|
|
}
|
|
|
|
|
|
//清除操作
|
|
|
clear() {
|
|
|
- const len = this.state.opIndex - this.state.saveIndex;
|
|
|
- if (len !== 0) {
|
|
|
- Array.from({ length: Math.abs(len) }).forEach(() => {
|
|
|
- len > 0 ? this.undo() : this.redo();
|
|
|
- });
|
|
|
- }
|
|
|
this.queue = [];
|
|
|
+ this.state.currLen = 0;
|
|
|
this.state.opIndex = -1;
|
|
|
- this.state.lenth = 0;
|
|
|
- this.state.saveIndex = -1;
|
|
|
+ this.cacheGroupAction = new GroupAction();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export class Action {
|
|
|
- combine = false;
|
|
|
- name: string;
|
|
|
- root: any;
|
|
|
- value: any;
|
|
|
- valueOld: any;
|
|
|
-
|
|
|
- constructor(root: any, name: string, value?: any) {
|
|
|
- const [, path] = name.split(":");
|
|
|
- this.name = name;
|
|
|
- this.root = root;
|
|
|
- this.valueOld = get(root, path.split("."));
|
|
|
- this.value = value;
|
|
|
- }
|
|
|
-
|
|
|
- async redo() {
|
|
|
- this._action("redo", this.value);
|
|
|
- }
|
|
|
-
|
|
|
- async undo() {
|
|
|
- this._action("undo", this.valueOld);
|
|
|
+ constructor(
|
|
|
+ public type: "set" | "delete",
|
|
|
+ public root: any,
|
|
|
+ public path: string,
|
|
|
+ public value?: any,
|
|
|
+ public oldValue?: any
|
|
|
+ ) {}
|
|
|
+ undo() {
|
|
|
+ set(this.root, this.path, this.oldValue);
|
|
|
}
|
|
|
-
|
|
|
- _action(actionType: "redo" | "undo", value: any) {
|
|
|
- const [, type, , , parentPath = "", attrName = ""] =
|
|
|
- this.name.match(/^(.+):(((.+)\.)?(.+))?$/) || [];
|
|
|
- let paths: string | string[] = parentPath.split(".");
|
|
|
- if (type == "add" && attrName) {
|
|
|
- if (parentPath) {
|
|
|
- paths.push(attrName);
|
|
|
- } else {
|
|
|
- paths = attrName;
|
|
|
- }
|
|
|
- }
|
|
|
- const parent = get(this.root, paths) || this.root;
|
|
|
- switch (type) {
|
|
|
- case "add":
|
|
|
- if (actionType === "redo") {
|
|
|
- parent.push(value);
|
|
|
- } else {
|
|
|
- parent.pop();
|
|
|
- }
|
|
|
- break;
|
|
|
- case "set":
|
|
|
- parent[attrName] = value;
|
|
|
- break;
|
|
|
- case "remove":
|
|
|
- if (parent instanceof Array) {
|
|
|
- if (actionType === "redo") {
|
|
|
- parent.splice(+attrName, 1);
|
|
|
- } else {
|
|
|
- parent.splice(+attrName, 0, value);
|
|
|
- }
|
|
|
- } else {
|
|
|
- if (actionType === "redo") {
|
|
|
- delete parent[attrName];
|
|
|
- } else {
|
|
|
- parent[attrName] = value;
|
|
|
- }
|
|
|
- }
|
|
|
- break;
|
|
|
- }
|
|
|
+ redo() {
|
|
|
+ set(this.root, this.path, this.value);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export class GroupAction {
|
|
|
- group: Action[];
|
|
|
- constructor(group: Action[]) {
|
|
|
- this.group = group;
|
|
|
+ actions: Action[] = [];
|
|
|
+ record(action: Action, options?: RecordOptions) {
|
|
|
+ const lastAction = this.actions.at(-1);
|
|
|
+ if (
|
|
|
+ options?.combine &&
|
|
|
+ lastAction?.root === action.root &&
|
|
|
+ lastAction?.path === action.path
|
|
|
+ ) {
|
|
|
+ this.actions[this.actions.length - 1] = action;
|
|
|
+ } else {
|
|
|
+ this.actions.push(action);
|
|
|
+ }
|
|
|
}
|
|
|
undo() {
|
|
|
- this.group.reverse().forEach((d) => d.undo());
|
|
|
+ [...this.actions].reverse().forEach((act) => act.undo());
|
|
|
}
|
|
|
redo() {
|
|
|
- this.group.forEach((d) => d.redo());
|
|
|
+ this.actions.forEach((d) => d.redo());
|
|
|
}
|
|
|
}
|