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;
|
};
|