UNPKG

undox

Version:

Redux implementation of Undo/Redo based on storing actions instead of states.

196 lines (142 loc) 5.71 kB
import { Group, Undo, Redo, Delegate, CalculateState, DoNStatesExist } from './interfaces/internal'; import { Action, Reducer, Comparator, UndoxState, Limit } from './interfaces/public'; import { UndoxTypes, GroupAction, RedoAction, UndoAction, UndoxAction } from './undox.action'; // helper used to flatten the history if grouped actions are in it const flatten = <T> (x: (T | T[])[]) => [].concat(...x as any) as T[] // actions can be an array of arrays because of grouped actions, so we flatten it first const calculateState: CalculateState = (reducer, actions, state) => flatten(actions).reduce(reducer, state) const getFutureActions = <S, A extends Action>(state: UndoxState<S, A>) => state.history.slice(state.index + 1) const getPastActionsWithPresent = <S, A extends Action>(state: UndoxState<S, A>, isPastLimitExceeded: boolean) => { const startIdx = isPastLimitExceeded ? 2 : 1 return [state.history[0],...state.history.slice(startIdx, state.index + 1)] } const getPastActions = <S, A extends Action>(state: UndoxState<S, A>) => state.history.slice(0, state.index) const getPresentState = <S, A extends Action>(state: UndoxState<S, A>) => state.present export const createSelectors = <S, A extends Action>(reducer: Reducer<S, A>) => { return { getPastStates : (state: UndoxState<S, A>): S[] => getPastActions(state) .reduce( (states, a, i) => Array.isArray(a) ? [ ...states, a.reduce(reducer, states[i - 1]) ] : [ ...states, reducer(states[i - 1], a) ] , [ ] as S[] ), getFutureStates : (state: UndoxState<S, A>): S[] => getFutureActions(state) .reduce( (states, a, i) => Array.isArray(a) ? [ ...states, a.reduce(reducer, states[i]) ] : [ ...states, reducer(states[i], a) ] , [ getPresentState(state) ] ).slice(1), getPresentState, getPastActions : (state: UndoxState<S, A>): A[] => flatten(getPastActions(state)), getPresentAction : (state: UndoxState<S, A>): A | A[] => state.history[state.index], getFutureActions : (state: UndoxState<S, A>): A[] => flatten(getFutureActions(state)) } } const doNPastStatesExist : DoNStatesExist = ({ index }, nStates) => index >= nStates const doNFutureStatesExist : DoNStatesExist = ({ history, index }, nStates) => history.length - 1 - index >= nStates const group: Group = (state, action, reducer, comparator, limit) => { const presentState = getPresentState(state) const nextState = action.payload.reduce(reducer, state.present) if (comparator(presentState, nextState)) return state const isPastLimitExceeded = pastLimitExceeded(state.index, limit) return { ...state, history : [ ...getPastActionsWithPresent(state, isPastLimitExceeded), action.payload ], index : state.index + 1, present : nextState } } const undo: Undo = (reducer, state, { payload = 1 }, limit) => { const nPastStatesExist = doNPastStatesExist(state, payload) const index = nPastStatesExist ? state.index - payload : 0 const futureExceeded = (state.history.length - state.index) > limit.future const history = futureExceeded ? state.history.slice(0, -payload) : state.history const isPastLimitExceeded = pastLimitExceeded(state.index, limit) const newState = { ...state, history, index } return { ...newState, present : calculateState(reducer as any, getPastActionsWithPresent(!isPastLimitExceeded ? newState : {...newState, index: state.index }, isPastLimitExceeded), newState.initial) } } const redo: Redo = (reducer, state, { payload = 1 }) => { const latestFuture = state.history.slice(state.index + 1, state.index + 1 + payload) return { ...state, index : doNFutureStatesExist(state, payload) ? state.index + payload : state.history.length - 1, present : calculateState(reducer, latestFuture, getPresentState(state)) } } const pastLimitExceeded = (index: number, limit: Limit) => index >= limit.past const delegate: Delegate = (state, action, reducer, comparator, limit) => { const nextPresent = reducer(state.present, action) const index = state.index const isPastLimitExceeded = pastLimitExceeded(index, limit) if (comparator(state.present, nextPresent)) return state const newHistory = [...getPastActionsWithPresent(state, isPastLimitExceeded), action] const newState = { ...state, history : newHistory, index : isPastLimitExceeded ? state.index : state.index + 1, present : nextPresent, initial : isPastLimitExceeded ? reducer(state.initial, state.history[1] as any) : state.initial } return newState } export const undox = <S, A extends Action>( reducer: Reducer<S, A>, initAction = { type: 'undox/INIT' } as any, comparator: Comparator<S> = (s1, s2) => s1 === s2, limit?: Limit ) => { const noLimit = { past: Infinity, future: Infinity } const initial = reducer(undefined, initAction) const initialState: UndoxState<S, A> = { history : [ initAction ], present : initial, index : 0, initial } return (state = initialState, action: UndoxAction<A>) => { switch (action.type) { case UndoxTypes.UNDO: return undo(reducer, state, action as UndoAction, limit || noLimit) case UndoxTypes.REDO: return redo(reducer, state, action as RedoAction, limit || noLimit) case UndoxTypes.GROUP: return group(state, action as GroupAction<A>, reducer, comparator, limit || noLimit) default: return delegate(state, action as A, reducer, comparator, limit || noLimit) } } }