UNPKG

regular-redux-undo

Version:

the plugin of regular-redux to archieve undo and redo

192 lines (172 loc) 4.78 kB
import { createStore, applyMiddleware } from 'redux'; import { createReducer, combineReducers } from './reducers'; import { typeOf, isEmpty, assert, error, forEachValue } from './util'; import { set } from './util/immuable'; import ModuleCollection from './module/module.collection'; export class Store { constructor(options = {}) { let { name = 'rex', size = 1024, undoable = true, modifiers = [], middlewares = [] } = options; //base config this.name = name; this.size = size; this.modifiers = modifiers; this.middlewares = middlewares; this.undoable = !!undoable; //install modules this._modules = new ModuleCollection(options); //collect reducer this.reducers = Object.create(null); this._modules.root.forEachModule((module) => { forEachValue(module.reducers, (reducer, name) => { assert(!this.reducers[name], `name of reducer must be unique, there is already a reducer named '${name}' exist`) this.reducers[name] = { path: module.path.slice(), handler: reducer } }) }) //add the replace state reducer this.reducers['@replace/state'] = { path: [], handler(state, payload) { return payload.state; } } //deal with the middlewares middlewares = middlewares.map(fn => middlewareWrapper(this, fn)); //add thunk middleware to support the sync action let thunk = store => next => action => { if (typeof action === 'function') { return action(this.dispatch.bind(this)); } return next(action); } middlewares.unshift(thunk); this.store = createStore(createReducer(this), {}, applyMiddleware(...middlewares)); } /** * replace the state * @param {Object} state * @param {Object} payload */ replaceState(state, payload) { payload = Object.assign({}, payload, {state}) this.store.dispatch({ type: '@replace/state', payload }); } /** * undo, go back in timeline */ undo() { if (!this.canUndo()) return false; this.store.dispatch({type: this.name + '_' + 'UNDO'}); } /** * if can undo * @return {Boolean} */ canUndo() { let {timeline, index} = this.store.getState(); return this.undoable && index !== 0 && timeline.length; } /** * redo, go reback in timeline */ redo() { if (!this.canRedo()) return false; this.store.dispatch({type: this.name + '_' + 'REDO'}); } /** * if can redo * @return {Boolean} */ canRedo() { let {timeline, index} = this.store.getState(); return this.undoable && index !== timeline.length - 1 && timeline.length; } /** * subscribe the dispatch of redux action * @param {Function} callback * @return {Function} the function to unsubscribe */ subscribe(callback) { return this.store.subscribe(callback); } /** * to get state, required by rgl-redux */ getState() { return this.undoable ? this.store.getState().current : this.store.getState(); } /** * state getter */ get state() { return this.getState(); } /** * state setter, used to warning people not to change state directly */ set state(v) { if (process.env.NODE_ENV !== 'production') { assert(false, `use store.dispatch to change the state.`); } } /** * make an dispatch */ dispatch(type, payload) { if (type === 'undo') { if (!this.undoable) { return error(true, 'can not undo because of the config.undoable is false'); } return this.undo(); } if (type === 'redo') { if(!this.undoable) { return error(true, 'can not redo because of the config.undoable is false'); } return this.redo(); } if (type === '@init/state') { return this.replaceState(this._modules.state, {clean: true}); } if (typeof type === 'function') { return this.store.dispatch(type); } assert(typeof type === 'string', 'the type of a reducer must be a string.'); let reducer = this.reducers[type]; if (reducer) { let action = {type, payload} this.store.dispatch(action); } else { error(true, `the reducer ${type} is not fount in reducers list.`) } } } /** * wrap the middleware to inject some parameter * @param {*} store * @param {*} fn */ function middlewareWrapper(store, fn) { return $store => next => action => { let nextFn = () => next(action); let context = { undo: store.undo.bind(store), redo: store.redo.bind(store), dispatch: store.dispatch.bind(store), getState: store.getState.bind(store), subscribe: store.subscribe.bind(store) } fn(context, nextFn); } }