import { fabric } from 'fabric'; fabric.Canvas.prototype.initialize = (function (originalFn) { return function (...args) { originalFn.call(this, ...args); this._historyInit(); return this; }; })(fabric.Canvas.prototype.initialize); /** * Override the dispose function for the _historyDispose(); */ fabric.Canvas.prototype.dispose = (function (originalFn) { return function (...args) { originalFn.call(this, ...args); this._historyDispose(); return this; }; })(fabric.Canvas.prototype.dispose); /** * Returns current state of the string of the canvas */ fabric.Canvas.prototype._historyNext = function () { return JSON.stringify(this.toDatalessJSON(this.extraProps)); }; /** * Returns an object with fabricjs event mappings */ fabric.Canvas.prototype._historyEvents = function () { return { 'object:added': (e) => this._historySaveAction(e), 'object:removed': (e) => this._historySaveAction(e), 'object:modified': (e) => this._historySaveAction(e), 'object:skewing': (e) => this._historySaveAction(e), }; }; /** * Initialization of the plugin */ fabric.Canvas.prototype._historyInit = function () { this.historyStack = []; this.historyIndex = 0; this.historyMaxLength = 100; this.extraProps = [ 'id', 'gradientAngle', 'selectable', 'hasControls', 'linkData', 'editable', 'extensionType', 'extension', ]; this.historyNextState = this._historyNext(); // 需要两次操作的标记,为true时表示当前操作记录为最新记录,需要撤销两步,因为最顶层的是当前的最新记录,undo一次后后置为false this.isLatestHistoryState = true; // 正在读取历史记录的标记,为 true 时不允许 undo/redo this.isLoadingHistory = false; this.on(this._historyEvents()); }; /** * Remove the custom event listeners */ fabric.Canvas.prototype._historyDispose = function () { this.off(this._historyEvents()); }; /** * It pushes the state of the canvas into history stack */ fabric.Canvas.prototype._historySaveAction = function (e) { if (this.historyProcessing) return; if (!e || (e.target && !e.target.excludeFromExport)) { const json = this._historyNext(); // 当前操作记录非最新记录,更新记录前需要校正历史索引,不然会丢失一个记录(undo时撤销了两次记录)。理论上不会超出历史记录上限,不过还是加了限制 !this.isLatestHistoryState && (this.isLatestHistoryState = true) && this.historyIndex < this.historyMaxLength && this.historyIndex++; // 每次的最新操作都要清空历史索引之后的记录,防止redo旧记录,不然可能会redo之前某个阶段的操作记录 this.historyStack.length > this.historyIndex && this.historyStack.splice(this.historyIndex); // 最多保存 historyMaxLength 条记录 if (this.historyIndex >= this.historyMaxLength) this.historyStack.shift(); this.historyIndex < this.historyMaxLength && this.historyIndex++; this.historyStack.push(json); this.historyNextState = this._historyNext(); this.fire('history:append', { json }); } }; /** * Undo to latest history. * Pop the latest state of the history. Re-render. * Also, pushes into redo history. */ fabric.Canvas.prototype.undo = function (callback) { if (this.isLoadingHistory) return; if (this.historyIndex <= 0) return; // The undo process will render the new states of the objects // Therefore, object:added and object:modified events will triggered again // To ignore those events, we are setting a flag. this.historyProcessing = true; // 当前操作记录为最新记录,需要撤销两步,因为最顶层的是当前的最新记录 this.isLatestHistoryState && this.historyIndex-- && (this.isLatestHistoryState = false); const history = this.historyStack[--this.historyIndex]; if (history) { // Push the current state to the redo history this.historyNextState = history; this._loadHistory(history, 'history:undo', callback); } else { console.log(1111); this.historyIndex < 0 && (this.historyIndex = 0); this.historyProcessing = false; } }; /** * Redo to latest undo history. */ fabric.Canvas.prototype.redo = function (callback) { if (this.isLoadingHistory) return; if (this.historyIndex >= this.historyStack.length) return; // The undo process will render the new states of the objects // Therefore, object:added and object:modified events will triggered again // To ignore those events, we are setting a flag. this.historyProcessing = true; // 当前操作记录不是最新记录(被撤销过),需要恢复两步,抵消最初撤销时撤销两步的操作 !this.isLatestHistoryState && ++this.historyIndex && (this.isLatestHistoryState = true); const history = this.historyStack[this.historyIndex]; if (history) { // Every redo action is actually a new action to the undo history this.historyNextState = history; this._loadHistory(history, 'history:redo', callback); this.historyIndex++; } else { this.historyProcessing = false; } }; // loadFromJSON 是异步操作,所以通过 isLoadingHistory = true 表示历史读取中,不可 undo/redo, // 不然当页面复杂且快速 undo/redo 多次后,可能会在之前的历史上 redo/undo fabric.Canvas.prototype._loadHistory = function (history, event, callback) { this.isLoadingHistory = true; const that = this; // 需要把历史记录中的 workspace 的 evented 属性设置为 false,否则会导致历史记录恢复后,鼠标悬浮 workspace 出现可操作的样式 const workspaceHistory = history.objects?.find((item) => item.id === 'workspace'); workspaceHistory && (workspaceHistory.evented = false); this.loadFromJSON(history, function () { that.renderAll(); that.fire(event); that.historyProcessing = false; that.isLoadingHistory = false; if (callback && typeof callback === 'function') callback(); }); }; /** * Clear undo and redo history stacks */ fabric.Canvas.prototype.clearHistory = function (type) { const one = this.historyStack.pop(); if (!type || !one) { this.historyStack = []; this.historyIndex = 0; this.fire('history:clear'); } else { this.historyStack = [one]; this.historyIndex = 1; this.fire('history:clear'); } this.isLatestHistoryState = true; }; fabric.Canvas.prototype.clearUndo = function () { this.historyStack.splice(this.historyIndex); }; // 如果在做一些操作之后,需要撤销上一步的操作并刷新历史记录(想在监听modified事件后做些额外的操作并记录操作后的历史),可以调用这个方法 fabric.Canvas.prototype.refreshHistory = function () { this.historyProcessing = false; this.historyStack.splice(--this.historyIndex); this._historySaveAction(); }; /** * On the history */ fabric.Canvas.prototype.onHistory = function () { this.historyProcessing = false; this._historySaveAction(); }; /** * Check if there are actions that can be undone */ fabric.Canvas.prototype.canUndo = function () { return this.historyIndex > 0; }; /** * Check if there are actions that can be redone */ fabric.Canvas.prototype.canRedo = function () { return this.historyStack.length > this.historyIndex; }; /** * Off the history */ fabric.Canvas.prototype.offHistory = function () { this.historyProcessing = true; };