jotai-history
Version:
107 lines (104 loc) • 3.19 kB
text/typescript
import type { WritableAtom } from 'jotai/vanilla'
import { atom } from 'jotai/vanilla'
import { REDO, RESET, UNDO } from './actions'
import type { withHistory } from './withHistory'
export type Indicators = {
canUndo: boolean
canRedo: boolean
}
/**
* @param targetAtom a primitive atom or equivalent
* @param limit the maximum number of history states to keep
* @returns an atom with undo/redo capabilities
*/
export function withUndo<Value, Args extends unknown[], Result>(
historyAtom: ReturnType<typeof withHistory>,
targetAtom: WritableAtom<Value, Args, Result>,
limit: number,
getArgs?: (value: Value) => Args
): WritableAtom<Indicators, [UNDO | REDO | RESET], void> {
const createRef = () => ({
index: 0,
stack: [] as Value[],
action: null as UNDO | REDO | RESET | null,
})
const refreshAtom = atom(0)
const refAtom = atom(createRef, (get, set, action: RESET) => {
if (action === RESET) {
void Object.assign(get(refAtom), createRef())
set(refreshAtom, (v) => v + 1)
}
})
refAtom.onMount = (setAtom) => () => setAtom(RESET)
refAtom.debugPrivate = true
const updateRefAtom = atom(
(get) => {
const history = get(historyAtom)
const ref = get(refAtom)
get(refreshAtom)
if (ref.action) {
// recalculation caused by undo/redo/reset
ref.action = null
} else {
// Remove future states if any
ref.stack = ref.stack.slice(0, ref.index + 1)
// Push the current state to the stack
ref.stack.push(history[0] as Value)
// Limit the stack
ref.stack = ref.stack.slice(-limit)
// Move the current index to the end
ref.index = ref.stack.length - 1
}
return { ...ref }
},
(get, set) => {
const ref = get(refAtom)
ref.stack = [get(targetAtom)]
return () => set(refAtom, RESET)
}
)
updateRefAtom.onMount = (mount) => mount()
updateRefAtom.debugPrivate = true
const canUndoAtom = atom((get) => {
return get(updateRefAtom).index > 0
})
const canRedoAtom = atom((get) => {
const ref = get(updateRefAtom)
return ref.index < ref.stack.length - 1
})
return atom<Indicators, [UNDO | REDO | RESET], void>(
(get) => ({
canUndo: get(canUndoAtom),
canRedo: get(canRedoAtom),
}),
(get, set, action) => {
const ref = get(refAtom)
const setCurrentState = (index: number) => {
if (index in ref.stack) {
const value = ref.stack[index]!
const args = typeof getArgs === 'function' ? getArgs(value) : [value]
set(targetAtom, ...(args as Args))
}
}
if (action === UNDO) {
if (get(canUndoAtom)) {
ref.action = UNDO
get(historyAtom).shift()
setCurrentState(--ref.index)
get(historyAtom).shift()
}
} else if (action === REDO) {
if (get(canRedoAtom)) {
ref.action = REDO
setCurrentState(++ref.index)
}
} else if (action === RESET) {
ref.action = RESET
set(refAtom, action)
} else {
return
}
set(refreshAtom, (v) => v + 1)
}
)
}