UNPKG

jotai-history-global

Version:

Lightweight global undo/redo extension for Jotai. / 为 Jotai 提供全局撤销、重做功能的轻量扩展。

198 lines (197 loc) 8.19 kB
/** * useHistory Hook * * React hook for managing the history system (undo/redo) */ import { useAtom } from 'jotai'; import { useCallback } from 'react'; import { historyStackAtom, historyStore, isHistoryOperationInProgressAtom, startGroupOperation, endGroupOperation, getAtomById } from './historyManager'; import { applyDiff, reverseDiff } from './diffUtils'; /** * Hook to access and control the global history system * * @returns Actions and state for history management */ export function useHistory() { const [historyStack, setHistoryStack] = useAtom(historyStackAtom); const [isHistoryOperation, setIsHistoryOperation] = useAtom(isHistoryOperationInProgressAtom); /** * Performs an undo operation */ const undo = useCallback(() => { setHistoryStack((currentStack) => { if (currentStack.past.length === 0) return currentStack; // Get the most recent history entry const lastEntry = currentStack.past[currentStack.past.length - 1]; const newPast = currentStack.past.slice(0, -1); // Find the atom by ID - O(1) lookup with Map const atomWithId = getAtomById(lastEntry.id); if (!atomWithId) return currentStack; // Mark that we're in an undo operation to prevent recording this change in history setIsHistoryOperation(true); try { // Get the current value const currentValue = historyStore.get(atomWithId); // Apply the reverse of the diff or use stored full value let previousValue; if (lastEntry.fullValue !== undefined) { // Use the stored full value directly if available previousValue = lastEntry.fullValue; } else { // Apply reversed diff to restore previous state try { // Type guard to ensure the diff is of type Diff if (typeof lastEntry.diff === 'object' && lastEntry.diff !== null && 'type' in lastEntry.diff) { const reversedDiff = reverseDiff(lastEntry.diff); previousValue = applyDiff(currentValue, reversedDiff); } else { throw new Error('Invalid diff format'); } } catch (error) { console.error('Error applying diff during undo:', error); // Fallback to using diff directly if it's a value diff if (typeof lastEntry.diff === 'object' && lastEntry.diff !== null && 'type' in lastEntry.diff && lastEntry.diff.type === 'value' && 'before' in lastEntry.diff) { previousValue = lastEntry.diff.before; } else { console.error('Cannot undo - unable to apply diff and no fullValue available'); return currentStack; } } } // Restore the previous value historyStore.set(atomWithId, previousValue); // Update the history stacks return { past: newPast, future: [ ...currentStack.future, { id: lastEntry.id, diff: lastEntry.diff, fullValue: lastEntry.fullValue, // Keep the fullValue if it exists timestamp: Date.now() } ] }; } finally { // Reset the flag setIsHistoryOperation(false); } }); }, [setHistoryStack, setIsHistoryOperation]); /** * Performs a redo operation */ const redo = useCallback(() => { setHistoryStack((currentStack) => { if (currentStack.future.length === 0) return currentStack; // Get the next future entry const nextEntry = currentStack.future[currentStack.future.length - 1]; const newFuture = currentStack.future.slice(0, -1); // Find the atom by ID - O(1) lookup const atomWithId = getAtomById(nextEntry.id); if (!atomWithId) return currentStack; // Mark that we're in a redo operation setIsHistoryOperation(true); try { // Get the current value const currentValue = historyStore.get(atomWithId); // Apply the diff or use stored full value let nextValue; if (nextEntry.fullValue !== undefined) { // Use the stored full value directly if available nextValue = nextEntry.fullValue; } else { // Apply diff to restore next state try { // Type guard to ensure the diff is of type Diff if (typeof nextEntry.diff === 'object' && nextEntry.diff !== null && 'type' in nextEntry.diff) { nextValue = applyDiff(currentValue, nextEntry.diff); } else { throw new Error('Invalid diff format'); } } catch (error) { console.error('Error applying diff during redo:', error); // Fallback to using diff directly if it's a value diff if (typeof nextEntry.diff === 'object' && nextEntry.diff !== null && 'type' in nextEntry.diff && nextEntry.diff.type === 'value' && 'after' in nextEntry.diff) { nextValue = nextEntry.diff.after; } else { console.error('Cannot redo - unable to apply diff and no fullValue available'); return currentStack; } } } // Apply the future value historyStore.set(atomWithId, nextValue); // Update the history stacks return { past: [ ...currentStack.past, { id: nextEntry.id, diff: nextEntry.diff, fullValue: nextEntry.fullValue, // Keep the fullValue if it exists timestamp: Date.now() } ], future: newFuture }; } finally { // Reset the flag setIsHistoryOperation(false); } }); }, [setHistoryStack, setIsHistoryOperation]); /** * Clears the history stacks */ const clear = useCallback(() => { setHistoryStack({ past: [], future: [] }); }, [setHistoryStack]); /** * Groups multiple operations into a single history entry */ const groupOperations = useCallback((callback) => { startGroupOperation(); try { callback(); } finally { endGroupOperation(); } }, []); return { undo, redo, canUndo: historyStack.past.length > 0, canRedo: historyStack.future.length > 0, clear, groupOperations, }; }