regular-redux-undo
Version:
the plugin of regular-redux to archieve undo and redo
192 lines (172 loc) • 4.78 kB
JavaScript
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);
}
}