Skip to content

Commit

Permalink
0.3: refactor history to be an UndoList, fixes #6
Browse files Browse the repository at this point in the history
  • Loading branch information
omnidan committed Sep 21, 2015
1 parent 9acbc39 commit 6898cde
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 50 deletions.
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
# redux undo/redo

[![NPM version (>=0.2)](https://img.shields.io/npm/v/redux-undo.svg?style=flat-square)](https://www.npmjs.com/package/redux-undo) [![Dependencies](https://img.shields.io/david/omnidan/redux-undo.svg?style=flat-square)](https://david-dm.org/omnidan/redux-undo)
[![NPM version (>=0.3)](https://img.shields.io/npm/v/redux-undo.svg?style=flat-square)](https://www.npmjs.com/package/redux-undo) [![Dependencies](https://img.shields.io/david/omnidan/redux-undo.svg?style=flat-square)](https://david-dm.org/omnidan/redux-undo)

_simple undo/redo functionality for [redux](https://github.com/rackt/redux) state containers_
_simple undo/redo functionality for redux state containers_

**Protip:** You can use the [redux-undo-boilerplate](https://github.com/omnidan/redux-undo-boilerplate) to quickly get started with `redux-undo`.

[![https://i.imgur.com/M2KR4uo.gif](https://i.imgur.com/M2KR4uo.gif)](https://github.com/omnidan/redux-undo-boilerplate)


## Installation

Expand All @@ -22,8 +20,7 @@ takes an existing reducer and a configuration object and enhances your existing
reducer with undo functionality.

**Note:** If you were accessing `state.counter` before, you have to access
`state.counter.currentState` after wrapping your reducer with `undoable`.
To access the history, simply use `state.counter.history`.
`state.counter.currentState` after wrapping your reducer with `undoable`.

To install, firstly import `redux-undo`:

Expand Down Expand Up @@ -84,13 +81,21 @@ are default values):

```js
undoable({
initialHistory: [], // initial history (e.g. for loading history)
initialIndex: [], // initial index (e.g. for loading history)
limit: false, // set to a number to turn on a limit for the history
debug: false, // set to `true` to turn on debugging

filter: () => true, // see `Filtering Actions` section

undoType: ActionTypes.UNDO, // define a custom action type for this undo action
redoType: ActionTypes.REDO, // define a custom action type for this redo action

initialState: undefined, // initial state (e.g. for loading)
initialHistory: { // initial history (e.g. for loading)
past: [],
present: config.initialState,
future: [],
},

debug: false, // set to `true` to turn on debugging
})
```

Expand Down Expand Up @@ -126,9 +131,8 @@ undoable({ filter: ifAction([SOME_ACTION, SOME_OTHER_ACTION]) })
undoable({ filter: excludeAction([SOME_ACTION, SOME_OTHER_ACTION]) })
```

Note that the helpers always accept `@@INIT` and `@@redux/INIT` too in
order to store your initial state. If you don't want this, define your
own filter function.
Note that the helpers always accept `@@redux/INIT` too in order to store your
initial state. If you don't want this, define your own filter function.


## License
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux-undo",
"version": "0.2.5",
"version": "0.3.0",
"description": "simple undo/redo functionality for redux state containers",
"main": "lib/index.js",
"scripts": {
Expand Down
136 changes: 99 additions & 37 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,67 +23,134 @@ export const ActionCreators = {
};
// /action creators

// seek: jump to a certain point in history (restores state)
function seek(state, rawIndex) {
debug('seek', state, rawIndex);
const history = state.history;
if (history.length < 2) return false;

let index = rawIndex;
if (index < 0) {
index = 0;
}

const maxIndex = history.length - 1;
if (index > maxIndex) {
index = maxIndex;
// length: get length of history
function length(history) {
const { past, future } = history;
return past.length + 1 + future.length;
}
// /length

// insert: insert `state` into history, which means adding the current state
// into `past`, setting the new `state` as `present` and erasing
// the `future`.
function insert(history, state, limit) {
debug('insert(', history, state, limit, ')');

const { past, present } = history;
const historyOverflow = limit && length(history) >= limit;

if (present === undefined) {
// init history
return {
past: [],
present: state,
future: [],
};
}

return {
...state,
currentState: history[index],
index,
history,
past: [
...past.slice(historyOverflow ? 1 : 0),
present,
],
present: state,
future: [],
};
}
// /seek
// /insert

// undo: go back to the previous point in history
function undo(state, steps) {
return seek(state, state.index - (steps || 1));
function undo(history) {
debug('undo(', history, ')');

const { past, present, future } = history;

if (past.length <= 0) return history;

return {
past: past.slice(0, past.length - 1), // remove last element from past
present: past[past.length - 1], // set element as new present
future: [
present, // old present state is in the future now
...future,
],
};
}
// /undo

// redo: go to the next point in history
function redo(state, steps) {
return seek(state, state.index + (steps || 1));
function redo(history) {
debug('redo(', history, ')');

const { past, present, future } = history;

if (future.length <= 0) return history;

return {
future: future.slice(1, future.length), // remove element from future
present: future[0], // set element as new present
past: [
...past,
present, // old present state is in the past now
],
};
}
// /redo

// updateState
function updateState(state, history) {
return {
...state,
history,
currentState: history.present,
};
}
// /updateState

// arrayToString
function arrayToString(array) {
if (!array || array.length <= 0) return '_';
return array.join(',');
}

// historyToString
function historyToString(history) {
return arrayToString(history.past) +
' | ' + history.present +
' | ' + arrayToString(history.future);
}
// /historyToString

// redux-undo higher order reducer
export default function undoable(reducer, rawConfig = {}) {
__DEBUG__ = rawConfig.debug;

const config = {
history: rawConfig.initialHistory || [],
index: rawConfig.initialIndex || -1,
initialState: rawConfig.initialState,
limit: rawConfig.limit,
filter: rawConfig.filter || () => true,
undoType: rawConfig.undoType || ActionTypes.UNDO,
redoType: rawConfig.redoType || ActionTypes.REDO,
};
config.history = rawConfig.initialHistory || {
past: [],
present: config.initialState,
future: [],
};

return (state, action) => {
debug('enhanced reducer called:', state, action);
let res;
switch (action.type) {
case config.undoType:
res = undo(state, action.steps);
return res ? res : state;
res = undo(state.history, action.steps);
debug('history (undo):', historyToString(state.history), '->', historyToString(res));
return res ? updateState(state, res) : state;

case config.redoType:
res = redo(state, action.steps);
return res ? res : state;
res = redo(state.history, action.steps);
debug('history (redo):', historyToString(state.history), '->', historyToString(res));
return res ? updateState(state, res) : state;

default:
res = reducer(state && state.currentState, action);
Expand All @@ -98,19 +165,14 @@ export default function undoable(reducer, rawConfig = {}) {
}
}

const currentIndex = (state && state.index !== undefined) ? state.index : config.index;
const history = (state && state.history !== undefined) ? state.history : config.history;
const historyOverflow = config.limit && history.length >= config.limit;
const updatedHistory = insert(history, res, config.limit);
debug('history (insert):', historyToString(history), '->', historyToString(updatedHistory));

return {
...state,
currentState: res,
index: currentIndex + 1, // update index
history: [
...history.slice(historyOverflow ? 1 : 0, currentIndex + 1),
res, // insert after current index
...history.slice(currentIndex + 1),
],
history: updatedHistory,
};
}
};
Expand Down

0 comments on commit 6898cde

Please sign in to comment.