'use strict'; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * Licensed Materials - Property of IBM * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2014, 2021 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. */ define(['underscore', 'chronology', './util/EventConstants', '../lib/@waca/dashboard-common/dist/core/APIContext'], function (_, Chronology, EventConstants, APIContext) { var STACK_SIZE_LIMIT = 150; var UndoRedoController = function () { function UndoRedoController(canvasModel) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, UndoRedoController); this.canvas = options.canvas; this._registerCanvasEvents(); //Events to exclude from the undo/redo stack this.undoRedoExclusionList = { 'change:name': 1, 'change:id': 1, 'change:_meta': 1 }; this.canvasModel = canvasModel; this.transactionApi = options.transaction; this.listeners = {}; this.canvasModel.on('all', this.addToUndoRedoStack.bind(this), this.canvasModel); this.logger = options.logger; this.currentMode = options.mode || 'canvasMode'; this.isStateDirty = options.isStateDirty || false; this.undoRedoStacks = {}; this.changeModeName = EventConstants.changeModeAction; //Ensure the stack is created first this._getUndoRedoStack(); return this.getPublicProperties(); } UndoRedoController.prototype._registerCanvasEvents = function _registerCanvasEvents() { var _this = this; if (this.canvas) { this.canvas.on('all', function (event) { // For now, we only support the operations that explicitly indicate the undo/redo support if (event.info && event.info.supportsUndoRedo) { _this._addCanvasActionToUndoRedo(event); } }); } }; UndoRedoController.prototype._addCanvasActionToUndoRedo = function _addCanvasActionToUndoRedo(event) { // the event must have a tracking and a reversAction to support undo/redo if (event.tracking && event.tracking.undoActions) { this.addToUndoRedoStack(this._convertCanvasEventToUndoAction(event)); } }; // Create an action object that is compatible with the existing undo/redo // This object will have a "applyFn" that will do the actual undo/redo based on the info in the event UndoRedoController.prototype._convertCanvasEventToUndoAction = function _convertCanvasEventToUndoAction(event) { var undoRedoTransactionId = event && event.transactionToken && event.transactionToken.transactionId; return { value: 'redo', // dummy value to identify that we doing a redo prevValue: 'undo', // dummy value to identify that we doing an undo data: { undoRedoTransactionId: undoRedoTransactionId, transactionToken: event.transactionToken, transactionParamIndex: event.info.transactionParamIndex }, sender: event.context && event.context.undoRedo ? 'UndoRedoController' : 'canvas', senderContext: { applyFn: this._canvasEventApplyFn.bind(this, event) } }; }; UndoRedoController.prototype._canvasEventApplyFn = function _canvasEventApplyFn(event, value, sender, name, data) { var _this2 = this; var response = void 0; // if the operartion is part of a transaction, // the undoRedoController will create a transaction and pass it here var transactionToken = data && data.transactionToken; // Create an api context that is used to identify that the context of the operation // is undo/redo so that we don't add it again var apiContext = new APIContext({ undoRedo: true, transactionToken: transactionToken }); // Start with the canvas as a root API since we are listening on the canvas object var apiStack = this._executeTrace(event.tracking.callStack, [this.canvas]); if (value === 'undo') { //undo var undoActions = event.tracking.undoActions; var actionsToExecute = []; undoActions.forEach(function (undoAction) { actionsToExecute.push({ fn: _this2._executeAction.bind(_this2), args: [undoAction, apiStack, apiContext] }); }); response = this._executeActions(actionsToExecute); } else { // redo var action = event.tracking.action; response = this._executeAction(action, apiStack, apiContext); } if (response instanceof Promise) { response = response.catch(function (err) { _this2.logger.error('An error occured while executing an API undo trace', err); throw err; }); } return response; }; UndoRedoController.prototype._executeTrace = function _executeTrace(callStack) { var apiStack = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; var localCopyApiStack = [].concat(apiStack); if (callStack) { callStack.forEach(function (step) { var currentAPI = localCopyApiStack[localCopyApiStack.length - 1]; if (step && step.name === '..') { localCopyApiStack.pop(); } else if (step && currentAPI && typeof currentAPI[step.name] === 'function') { // execute the step in the current context localCopyApiStack.push(currentAPI[step.name].apply(currentAPI, step.params)); } }); } return localCopyApiStack; }; UndoRedoController.prototype._executeAction = function _executeAction(action, apiStack, apiContext) { var _this3 = this; var response = void 0; // If the action has a stack callStack, then execute to get the api context var newApiStack = this._executeTrace(action.callStack, apiStack); var currentAPI = newApiStack[newApiStack.length - 1]; if (action && currentAPI && typeof currentAPI[action.name] === 'function') { // event objects are frozen.. so take a copy of the parameters bebore passing to the API. var params = JSON.parse(JSON.stringify(action.params || [])); params.unshift(apiContext); response = currentAPI[action.name].apply(currentAPI, params); // if we have result actions, go over the response and execute the actions if (action.resultActions) { var responseList = Array.isArray(response) ? response : [response]; responseList.forEach(function (resultObject, index) { var actions = action.resultActions[index]; if (actions) { actions.forEach(function (resultAction) { _this3._executeAction(resultAction, resultObject, apiContext); }); } }); } } else { this.logger.warn('undo/redo: missing action in event or action does not exist in API ', action, currentAPI); } return response; }; UndoRedoController.prototype.getPublicProperties = function getPublicProperties() { return { addToUndoRedoStack: this.addToUndoRedoStack.bind(this), clearStack: this.clearStack.bind(this), addListener: this.addListener.bind(this), removeListener: this.removeListener.bind(this), canUndo: this.canUndo.bind(this), canRedo: this.canRedo.bind(this), undo: this.undo.bind(this), redo: this.redo.bind(this), setMode: this.setMode.bind(this), setDirty: this.setDirty.bind(this), registerSaveAction: this.registerSaveAction.bind(this), isDirty: this.isDirty.bind(this) }; }; UndoRedoController.prototype._validateUndoRedoAction = function _validateUndoRedoAction(action) { if (!action || !action.senderContext || !action.senderContext.applyFn) { return false; } if (action.sender === 'UndoRedoController') { return false; } if (action.data && action.data.undoRedoTransactionId && action.data.undoRedoTransactionId.indexOf('undoRedoTransaction') === 0) { return false; } if (!action.data || !action.data.transactionToken) { // TODO: no data means it's a valid transaction? confusing!!! re-evaluate this condition return true; } var _ref = action.data.transactionToken || {}, eventGenerated = _ref.eventGenerated; if (!eventGenerated) { return true; } return false; }; /** * Add the action to the undo/redo stack. * * If the action has a transaction id 'action.data.undoRedoTransactionId', then it will be combined with other actions that have the same id. * Transactions retriggered by a secondary event will not be kept since the initial transaction is already part of the undo stack. * * Note that the undo/redo assumes that all actions within a transaction happen in the same thread (no setTimeout). * If one action is using setTimeout and takes longer to execute and the user selects undo before it finishes, * then the undo for the transaction will not include this action. The action will be added separately when it is completed. * * @param action */ UndoRedoController.prototype.addToUndoRedoStack = function addToUndoRedoStack(action) { if (this._validateUndoRedoAction(action)) { var upDownActions = this._checkTransaction(action); if (upDownActions) { var undoRedoObj = { up: function up() { if (!this.call) { upDownActions.up(action.upValue ? action.upValue : action.value, 'UndoRedoController', action.name); } }, down: function down() { upDownActions.down(action.prevValue, 'UndoRedoController', action.name); }, call: false, name: action.name, prevDirtyState: this.isStateDirty }; this.logger.debug('UndoRedoController:addToUndoRedoStack', action.data ? action.data.undoRedoTransactionId : action.data, action.name, action.value, action.prevValue); var stack = this._getUndoRedoStack(); stack.add(undoRedoObj); } } }; UndoRedoController.prototype.setDirty = function setDirty(bool) { this.isStateDirty = bool; }; UndoRedoController.prototype.clearStack = function clearStack() { var _this4 = this; var stack = this._getUndoRedoStack(); if (stack) { stack.clear(); } _.each(this.listeners, function (listener) { listener.onStateChange(_this4); }); }; UndoRedoController.prototype._checkTransaction = function _checkTransaction(action) { if (action.data && (action.data.saveOnly || action.data.skipUndoRedo || action.data.runtimeOnly)) { return null; } var upDownAction = { up: action.senderContext.applyFn, down: action.senderContext.applyFn }; var transactionId = action.data ? action.data.undoRedoTransactionId : null; if (transactionId) { var transactionsMap = this._getTransactionsMap(); var actions = transactionsMap[transactionId]; if (actions) { actions.push(action); return null; } actions = [action]; transactionsMap[transactionId] = actions; upDownAction.up = function () { this._redoTransaction(actions); }.bind(this); upDownAction.down = function () { // cleanup the transactions list delete transactionsMap[transactionId]; this._undoTransaction(actions); }.bind(this); } return upDownAction; }; UndoRedoController.prototype._createTransation = function _createTransation() { var id = _.uniqueId('undoRedoTransaction'); var transactionToken = this.transactionApi.startTransactionById(id); return transactionToken; }; UndoRedoController.prototype._redoTransaction = function _redoTransaction(actions) { var _this5 = this; var transactionToken = this._createTransation(); var action = void 0, applyAction = void 0; var actionsToExecute = []; for (var i = 0; i < actions.length; i++) { action = actions[i]; applyAction = action.senderContext.applyFn; if (applyAction) { action.data.transactionToken = transactionToken; action.data.undoRedoTransactionId = transactionToken.transactionId; action.data.undoRedoTransactionType = 'redo'; actionsToExecute.push({ fn: applyAction, args: [action.upValue ? action.upValue : action.value, 'UndoRedoController', action.name, action.data] }); } } var response = this._executeActions(actionsToExecute); if (response instanceof Promise) { this.lastUndoRedoOpPromise = response.catch(function (err) { _this5.logger.error('An error occured while executing the redo stack', err); throw err; }).finally(function () { _this5.transactionApi.endTransaction(transactionToken); }); } else { this.transactionApi.endTransaction(transactionToken); } }; UndoRedoController.prototype._undoTransaction = function _undoTransaction(actions) { var _this6 = this; var transactionToken = this._createTransation(); var action = void 0, applyAction = void 0; var actionsToExecute = []; for (var i = actions.length - 1; i >= 0; i--) { action = actions[i]; applyAction = action.senderContext.applyFn; if (applyAction) { action.data.transactionToken = transactionToken; action.data.undoRedoTransactionId = transactionToken.transactionId; action.data.undoRedoTransactionType = 'undo'; actionsToExecute.push({ fn: applyAction, args: [action.prevValue, 'UndoRedoController', action.name, action.data] }); } } var response = this._executeActions(actionsToExecute); if (response instanceof Promise) { this.lastUndoRedoOpPromise = response.catch(function (err) { _this6.logger.error('An error occured while executing the undo stack', err); throw err; }).finally(function () { _this6.transactionApi.endTransaction(transactionToken); }); } else { this.transactionApi.endTransaction(transactionToken); } }; // Execute a series of async actions sequentially .. UndoRedoController.prototype._executeActions = function _executeActions(actions) { var _this7 = this; var startIndex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; var response = void 0; while (startIndex < actions.length) { var _actions$startIndex; response = (_actions$startIndex = actions[startIndex]).fn.apply(_actions$startIndex, actions[startIndex].args); startIndex++; if (response instanceof Promise) { // one of the action returned a promise. // break the loop and continue it after the promise is resolved response = response.then(function () { return _this7._executeActions(actions, startIndex); }); break; } } return response; }; UndoRedoController.prototype.addListener = function addListener(id, listener) { if (typeof listener.onStateChange === 'function') { this.listeners[id] = listener; } }; UndoRedoController.prototype.removeListener = function removeListener(id) { delete this.listeners[id]; }; UndoRedoController.prototype.canUndo = function canUndo() { var stack = this._getUndoRedoStack(); return stack.undos.length > 0; }; UndoRedoController.prototype.canRedo = function canRedo() { var stack = this._getUndoRedoStack(); return stack.redos.length > 0; }; UndoRedoController.prototype._trackDirtyState = function _trackDirtyState(stack) { if (!stack || !stack[0]) { return; } var peek = stack[0].prevDirtyState; // We flip the state for moving undo items to the redo stack and vice versa stack[0].prevDirtyState = this.isStateDirty; if (stack[0].name !== this.changeModeName) { this.setDirty(peek); } }; UndoRedoController.prototype.undo = function undo() { var stack = this._getUndoRedoStack(); this._trackDirtyState(stack.undos); stack.undo(); var promise = this.lastUndoRedoOpPromise || Promise.resolve(); if (this.lastUndoRedoOpPromise) { this.lastUndoRedoOpPromise = null; } return promise; }; UndoRedoController.prototype.redo = function redo() { var stack = this._getUndoRedoStack(); this._trackDirtyState(stack.redos); stack.redo(); var promise = this.lastUndoRedoOpPromise || Promise.resolve(); if (this.lastUndoRedoOpPromise) { this.lastUndoRedoOpPromise = null; } return promise; }; UndoRedoController.prototype.onAdd = function onAdd() { this.enableState('undo', true); this.enableState('redo', false); }; UndoRedoController.prototype.enableState = function enableState(state, enabled) { this.canvasModel.trigger('undo:state', { senderContext: this, 'stack': state, 'enabled': enabled }); _.each(this.listeners, function (listener) { listener.onStateChange(this); }.bind(this)); }; UndoRedoController.prototype.setMode = function setMode(mode) { var transactionsToMergeFrom = void 0; if (this.currentMode !== 'canvasMode') { transactionsToMergeFrom = this.undoRedoStacks[this.currentMode].transactions; delete this.undoRedoStacks[this.currentMode]; } this.currentMode = mode || 'canvasMode'; this._mergeUndoStacks(transactionsToMergeFrom); var stack = this._getUndoRedoStack(); this.enableState('undo', stack.undos.length > 0); this.enableState('redo', stack.redos.length > 0); }; UndoRedoController.prototype.registerSaveAction = function registerSaveAction() { // When we save we need to cleanup the items in the undo stack and mark them all as dirty var stack = this._getUndoRedoStack(); _.forEach(stack.undos, function (ele) { ele.prevDirtyState = true; }); }; UndoRedoController.prototype.isDirty = function isDirty() { /** * Board Logic * - We want to track the dirty/clean state of the DB from here * - changeMode actions are added to the stack but dont count as 'dirty' * all other actions count as dirty * - If we save, the save action will clear the dirty flag and we need to track that boundary * - We can have an initially dirty db if created from reloadFromJSONSpec */ return this.isStateDirty; }; UndoRedoController.prototype._getUndoRedoStack = function _getUndoRedoStack() { if (!this.undoRedoStacks[this.currentMode]) { this.undoRedoStacks[this.currentMode] = new Chronology({ limit: STACK_SIZE_LIMIT, onAdd: this.onAdd.bind(this), onRedo: this.enableState.bind(this, 'undo', true), onUndo: this.enableState.bind(this, 'redo', true), onBegin: this.enableState.bind(this, 'undo', false), onEnd: this.enableState.bind(this, 'redo', false) }); this.undoRedoStacks[this.currentMode].transactions = {}; } return this.undoRedoStacks[this.currentMode]; }; UndoRedoController.prototype._getTransactionsMap = function _getTransactionsMap() { return this.undoRedoStacks[this.currentMode].transactions; }; UndoRedoController.prototype._mergeUndoStacks = function _mergeUndoStacks() { var _this8 = this; var transactionsToMergeFrom = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; _.each(transactionsToMergeFrom, function (actions) { actions.forEach(function (action) { _this8.addToUndoRedoStack(action); }); }); }; return UndoRedoController; }(); return UndoRedoController; }); //# sourceMappingURL=UndoRedoController.js.map