redux-live-undo
Version:
A high-order reducer that enables generic undo/redo functionality without giving up live updates
149 lines (123 loc) • 5.09 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = Undoable;
var _lodash = require('lodash.clone');
var _lodash2 = _interopRequireDefault(_lodash);
var _lodash3 = require('lodash.initial');
var _lodash4 = _interopRequireDefault(_lodash3);
var _lodash5 = require('lodash.last');
var _lodash6 = _interopRequireDefault(_lodash5);
var _lodash7 = require('lodash.mapvalues');
var _lodash8 = _interopRequireDefault(_lodash7);
var _ActionTypes = require('./ActionTypes');
var _NextState2 = require('./NextState');
var _NextState3 = _interopRequireDefault(_NextState2);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/*
* Maintains a state tree for the past, present, and future setting values. This tree allows the `present` state to be
* updated live (ie. for every keystroke) while making checkpoints along the way to allow the user to undo and redo
* checkpointed edits. Below is a table that shows how this state tree changes with each action.
*
* New states are generated by child reducers passed in to the reducers argument.
* Usage: combineReducers(Undoable({ settings: settingsReducer }));
| action | checkpoint | past | present | future |
|----------------------|------------|--------------------------------------|--------------------|----------------------|
| initial | | `[{ title: "a" }]` | `{ title: "a" }` | `[]` |
| UPDATE | false | `[{ title: "a" }]` | `{ title: "ab" }` | `[]` |
| UPDATE | false | `[{ title: "a" }]` | `{ title: "abc" }` | `[]` |
| UPDATE | true | `[{ title: "a" }, { title: "abc" }]` | `{ title: "abc" }` | `[]` |
| UNDO | -- | `[{ title: "a" }]` | `{ title: "a" }` | `[{ title: "abc" }]` |
| REDO | -- | `[{ title: "a" }, { title: "abc" }]` | `{ title: "abc" }` | `[]` |
*/
function Undoable(reducers) {
var initialHistory = (0, _lodash8.default)(reducers, function (r) {
return r();
});
var initialState = {
past: [initialHistory],
present: initialHistory,
future: []
};
return function () {
var state = arguments.length <= 0 || arguments[0] === undefined ? initialState : arguments[0];
var action = arguments[1];
switch (action.type) {
/*
* - Concat the present state to the end of the future array.
* - Copy the last past state to the present state
* - Remove the last past state
*/
case _ActionTypes.UNDO:
{
// Do not allow undos if there is only one object in the past state.
if (state.past.length < 2) {
return state;
}
return {
past: (0, _lodash4.default)((0, _lodash2.default)(state.past)), // all but the last one
present: state.past[state.past.length - 2],
future: state.future.concat([state.present])
};
}
/*
* - Concat the present state to the end of the past array.
* - Copy the last future state to the present state
* - Remove the last future state
*/
case _ActionTypes.REDO:
{
// Do not allow undos if there are no objects in the future state.
if (state.future.length < 1) {
return state;
}
return {
past: state.past.concat([(0, _lodash6.default)(state.future)]),
present: (0, _lodash6.default)(state.future),
future: (0, _lodash4.default)((0, _lodash2.default)(state.future)) // all but last
};
}
/*
* If this is not a history checkpoint:
* - Update the present state
* - Clear the future state
*
* If this is a history checkpoint:
* - Update the present state
* - Push a copy of the present state into the past state
* - Clear the future state
*/
default:
{
var _NextState = (0, _NextState3.default)(reducers, state, action);
var nextState = _NextState.nextState;
var anyChanged = _NextState.anyChanged;
// New states can only be pushed into the history if anything we care about actually changed.
if (anyChanged) {
if (action.undoableIrreversibleCheckpoint) {
return {
past: [nextState], // irreversible checkpoints should clear out history
present: nextState,
future: [] // clear the future state so we don't lose these new edits by a redo
};
} else if (action.undoableHistoryCheckpoint) {
return {
past: state.past.concat([nextState]),
present: nextState,
future: [] // clear the future state so we don't lose these new edits by a redo
};
} else {
return Object.assign({}, state, {
present: nextState,
future: [] // clear the future state so we don't lose these new edits by a redo
});
}
} else {
return Object.assign({}, state, {
present: nextState
});
}
}}
};
}