import { isDefined } from '@udecode/plate-core' import { dequal } from 'dequal' // import { diff } from 'deep-object-diff' import { useReducer, useCallback, useRef } from 'react' import { isNotDefined } from 'utils' enum ActionType { Undo = 'UNDO', Redo = 'REDO', Set = 'SET', Flush = 'FLUSH', } export interface Actions { set: (newPresent: T | ((current: T) => T)) => void undo: () => void redo: () => void flush: () => void canUndo: boolean canRedo: boolean presentRef: React.MutableRefObject } interface Action { type: ActionType newPresent?: T } export interface State { past: T[] present: T future: T[] } const initialState = { past: [], present: null, future: [], } const reducer = (state: State, action: Action) => { const { past, present, future } = state switch (action.type) { case ActionType.Undo: { if (past.length === 0) { return state } const previous = past[past.length - 1] const newPast = past.slice(0, past.length - 1) return { past: newPast, present: previous, future: [present, ...future], } } case ActionType.Redo: { if (future.length === 0) { return state } const next = future[0] const newFuture = future.slice(1) return { past: [...past, present], present: next, future: newFuture, } } case ActionType.Set: { const { newPresent } = action if ( isNotDefined(newPresent) || (present && dequal( JSON.parse(JSON.stringify(newPresent)), JSON.parse(JSON.stringify(present)) )) ) { return state } // Uncomment to debug history ⬇️ // console.log( // diff( // JSON.parse(JSON.stringify(newPresent)), // present ? JSON.parse(JSON.stringify(present)) : {} // ) // ) return { past: [...past, present].filter(isDefined), present: newPresent, future: [], } } case ActionType.Flush: return { ...initialState, present } } } const useUndo = (initialPresent: T): [State, Actions] => { const [state, dispatch] = useReducer(reducer, { ...initialState, present: initialPresent, }) as [State, React.Dispatch>] const presentRef = useRef(initialPresent) const canUndo = state.past.length !== 0 const canRedo = state.future.length !== 0 const undo = useCallback(() => { if (canUndo) { dispatch({ type: ActionType.Undo }) } }, [canUndo]) const redo = useCallback(() => { if (canRedo) { dispatch({ type: ActionType.Redo }) } }, [canRedo]) const set = useCallback((newPresent: T | ((current: T) => T)) => { const updatedTypebot = 'id' in newPresent ? newPresent : (newPresent as (current: T) => T)(presentRef.current) presentRef.current = updatedTypebot dispatch({ type: ActionType.Set, newPresent: updatedTypebot }) }, []) const flush = useCallback(() => { dispatch({ type: ActionType.Flush }) }, []) return [state, { set, undo, redo, flush, canUndo, canRedo, presentRef }] } export default useUndo