ngrx-undoable
Version:
[Redux](https://github.com/reactjs/redux)/[Ngrx](https://github.com/ngrx) implementation of [Undo/Redo](http://redux.js.org/docs/recipes/ImplementingUndoHistory.html) based on Actions instead of States
140 lines (139 loc) • 6.53 kB
JavaScript
;
exports.__esModule = true;
var undoable_action_1 = require("./undoable.action");
// when grouping actions we will get multidimensional arrays
// so this helper is used to flatten the history
var flatten = function (x) { return [].concat.apply([], x); };
// since the oldest past is the init action we never want to remove it from the past
var doNPastStatesExist = function (past, nStates) { return past.length > nStates; };
var doNFutureStatesExist = function (future, nStates) { return future.length >= nStates; };
// actions can be an array of arrays because of grouped actions, so we flatten it first
var calculateState = function (reducer, actions, state) {
return flatten(actions).reduce(reducer, state);
};
var travelNStates = function (state, nStates, travelOne) {
if (nStates === void 0) { nStates = 1; }
if (nStates === 0)
return state;
return travelNStates(travelOne(state, nStates), --nStates, travelOne);
};
var createUndo = function (reducer) { return function (state, nStates) {
if (nStates === void 0) { nStates = 1; }
if (!doNPastStatesExist(state.past, nStates))
return state;
var latestPast = state.past[state.past.length - 1];
var futureWithLatestPast = [latestPast].concat(state.future);
var pastWithoutLatest = state.past.slice(0, -1);
return {
past: pastWithoutLatest,
present: calculateState(reducer, pastWithoutLatest),
future: futureWithLatestPast
};
}; };
var createRedo = function (reducer) { return function (state, nStates) {
if (nStates === void 0) { nStates = 1; }
if (!doNFutureStatesExist(state.future, nStates))
return state;
var _a = state.future, latestFuture = _a[0], futureWithoutLatest = _a.slice(1);
var pastWithLatestFuture = state.past.concat([latestFuture]);
return {
past: pastWithLatestFuture,
present: calculateState(reducer, [latestFuture], state.present),
future: futureWithoutLatest
};
}; };
var updateHistory = function (state, newPresent, action, comparator) {
if (comparator(state.present, newPresent))
return state;
var newPast = state.past.concat([action]);
return {
past: newPast,
present: newPresent,
future: []
};
};
var getPastActions = function (state) { return state.past; };
var getPresentAction = function (state) { return state.past[state.past.length - 1]; };
var getFutureActions = function (state) { return state.future; };
var getPresentState = function (state) { return state.present; };
var getPastActionsFlattened = function (state) { return flatten(state.past); };
var getPresentActionFlattened = function (state) { return getPastActionsFlattened(state).slice(-1)[0]; };
var getFutureActionsFlattened = function (state) { return flatten(state.future); };
var getLatestFutureAction = function (state) { return getFutureActions(state)[0]; };
var getLatestFutureActionFlattened = function (state) { return getFutureActionsFlattened(state)[0]; };
/**
* Creates the getFutureStates selector.
*
* The selector is mapping the future actions to future states.
* It uses `reduce` instead of `map`, because this way we can reuse the
* previous future state to calculate the next future state.
*
* @param reducer The Reducer that is used to replay actions from the future
*/
var createGetFutureStates = function (reducer) { return function (state) {
return getFutureActions(state)
.reduce(function (states, a, i) {
return Array.isArray(a) // check if action is grouped
? states.concat([a.reduce(reducer, states[i])]) : states.concat([reducer(states[i], a)]);
}, [getPresentState(state)]).slice(1);
}; }; // We start with present to calculate future states, but present state should not part be of future so we slice it off
/**
* Creates the getPastStates selector.
*
* The selector is mapping the past actions to past states.
* It uses `reduce` instead of `map`, because this way we can reuse the
* previous past state to calculate the next past state.
*
* @param reducer The Reducer that is used to replay actions from the past
*/
var createGetPastStates = function (reducer) { return function (state) {
return getPastActions(state).reduce(function (states, a, i) {
return Array.isArray(a) // check if action is grouped
? states.concat([a.reduce(reducer, states[i - 1])]) : states.concat([reducer(states[i - 1], a)]);
}, []).slice(0, -1);
}; }; // Slice the last state since its the present
exports.createSelectors = function (reducer) {
return {
getPresentState: getPresentState,
getPastStates: createGetPastStates(reducer),
getFutureStates: createGetFutureStates(reducer),
getPastActions: getPastActions,
getPastActionsFlattened: getPastActionsFlattened,
getPresentAction: getPresentAction,
getPresentActionFlattened: getPresentActionFlattened,
getFutureActions: getFutureActions,
getFutureActionsFlattened: getFutureActionsFlattened,
getLatestFutureAction: getLatestFutureAction,
getLatestFutureActionFlattened: getLatestFutureActionFlattened
};
};
var createUndoableReducer = function (reducer, initAction, comparator) {
var initialState = {
past: [initAction],
present: reducer(undefined, initAction),
future: []
};
var undo = createUndo(reducer);
var redo = createRedo(reducer);
return function (state, action) {
if (state === void 0) { state = initialState; }
switch (action.type) {
case undoable_action_1.UndoableTypes.UNDO:
return travelNStates(state, action.payload, undo);
case undoable_action_1.UndoableTypes.REDO:
return travelNStates(state, action.payload, redo);
case undoable_action_1.UndoableTypes.GROUP:
return updateHistory(state, action.payload.reduce(reducer, state.present), action.payload, comparator);
default:
return updateHistory(state, reducer(state.present, action), action, comparator);
}
};
};
exports.undoable = function (reducer, initAction, comparator) {
if (initAction === void 0) { initAction = { type: 'ngrx-undoable/INIT' }; }
if (comparator === void 0) { comparator = function (s1, s2) { return s1 === s2; }; }
return {
reducer: createUndoableReducer(reducer, initAction, comparator),
selectors: exports.createSelectors(reducer)
};
};