From 6898cde048609062294e3b9c5ac87c44edd1e466 Mon Sep 17 00:00:00 2001 From: Daniel Bugl Date: Mon, 21 Sep 2015 16:36:55 +0200 Subject: [PATCH] 0.3: refactor history to be an UndoList, fixes #6 --- README.md | 28 ++++++----- package.json | 2 +- src/index.js | 136 +++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 116 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 0eb217b..72f3839 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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`: @@ -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 }) ``` @@ -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 diff --git a/package.json b/package.json index 61e862e..35f7f1c 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/index.js b/src/index.js index 824e859..9052495 100644 --- a/src/index.js +++ b/src/index.js @@ -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); @@ -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, }; } };