123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545 |
- '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
|