160 lines
3.6 KiB
TypeScript
160 lines
3.6 KiB
TypeScript
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<T> {
|
|
set: (
|
|
newPresent: T | ((current: T) => T),
|
|
options?: { updateDate: boolean }
|
|
) => void
|
|
undo: () => void
|
|
redo: () => void
|
|
flush: () => void
|
|
canUndo: boolean
|
|
canRedo: boolean
|
|
presentRef: React.MutableRefObject<T>
|
|
}
|
|
|
|
interface Action<T> {
|
|
type: ActionType
|
|
newPresent?: T
|
|
updateDate?: boolean
|
|
}
|
|
|
|
export interface State<T> {
|
|
past: T[]
|
|
present: T
|
|
future: T[]
|
|
}
|
|
|
|
const initialState = {
|
|
past: [],
|
|
present: null,
|
|
future: [],
|
|
}
|
|
|
|
const reducer = <T>(state: State<T>, action: Action<T>) => {
|
|
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, updateDate } = 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,
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
//@ts-ignore
|
|
updatedAt: updateDate ? new Date() : newPresent.updatedAt,
|
|
},
|
|
future: [],
|
|
}
|
|
}
|
|
|
|
case ActionType.Flush:
|
|
return { ...initialState, present }
|
|
}
|
|
}
|
|
|
|
const useUndo = <T>(initialPresent: T): [State<T>, Actions<T>] => {
|
|
const [state, dispatch] = useReducer(reducer, {
|
|
...initialState,
|
|
present: initialPresent,
|
|
}) as [State<T>, React.Dispatch<Action<T>>]
|
|
const presentRef = useRef<T>(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), options = { updateDate: true }) => {
|
|
const updatedTypebot =
|
|
'id' in newPresent
|
|
? newPresent
|
|
: (newPresent as (current: T) => T)(presentRef.current)
|
|
presentRef.current = updatedTypebot
|
|
dispatch({
|
|
type: ActionType.Set,
|
|
newPresent: updatedTypebot,
|
|
updateDate: options.updateDate,
|
|
})
|
|
},
|
|
[]
|
|
)
|
|
|
|
const flush = useCallback(() => {
|
|
dispatch({ type: ActionType.Flush })
|
|
}, [])
|
|
|
|
return [state, { set, undo, redo, flush, canUndo, canRedo, presentRef }]
|
|
}
|
|
|
|
export default useUndo
|