UNPKG

jotai-history-global

Version:

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

131 lines (130 loc) 4.68 kB
/** * History Manager * * Manages a global history stack for tracking state changes across atoms */ import { atom } from 'jotai'; import { createStore } from 'jotai/vanilla'; import { createDiff } from './diffUtils'; // Default history limit per atom const DEFAULT_HISTORY_LIMIT = 50; // Create a global store for history export const historyStore = createStore(); // Global history stack atom export const historyStackAtom = atom({ past: [], future: [] }); // Flag to track if we're currently in the middle of an undo/redo operation // This prevents recording history during undo/redo operations export const isHistoryOperationInProgressAtom = atom(false); // Current group operation tracking export const currentGroupOperationAtom = atom(null); export const isGroupOperationInProgressAtom = atom(false); // Registry to keep track of all atoms with history - using Map for O(1) lookups // eslint-disable-next-line @typescript-eslint/no-explicit-any const atomRegistry = new Map(); /** * Register an atom with the history system * @param atom - The atom to register */ export function registerHistoryAtom(atom) { atomRegistry.set(atom.id, atom); } /** * Unregister an atom from the history system * @param atom - The atom to unregister */ export function unregisterHistoryAtom(atom) { atomRegistry.delete(atom.id); } /** * Find an atom by ID in the registry - O(1) lookup with Map */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function getAtomById(id) { return atomRegistry.get(id); } // Helper to generate unique IDs for atoms if not provided export const generateId = () => { return Math.random().toString(36).substring(2, 9); }; // Push an entry to the history stack export const pushToHistory = (id, prevValue, nextValue, options) => { const historyLimit = options?.historyLimit || DEFAULT_HISTORY_LIMIT; // Compute diff or use full value let entryData; if (options?.useFullValueInstead) { // Store the full previous value instead of a diff entryData = { diff: prevValue, // Use prevValue as the diff for simplicity fullValue: prevValue // Also store the full value }; } else if (options?.customDiff) { // Use custom diff function if provided const customDiff = options.customDiff(prevValue, nextValue); entryData = { diff: customDiff }; } else { // Use built-in diff calculation const diff = createDiff(prevValue, nextValue); if (diff === null) { // No changes detected, don't record history return; } entryData = { diff }; // For complex diffs, store the full value as a fallback const isComplexDiff = (diff.type === 'object' && Object.keys(diff.changed).length > 10) || (diff.type === 'array' && diff.items.length > 20); if (isComplexDiff) { entryData.fullValue = prevValue; } } const entry = { id, diff: entryData.diff, timestamp: Date.now() }; if (entryData.fullValue !== undefined) { entry.fullValue = entryData.fullValue; } // Add to group operation if one is in progress const isGroupOperation = historyStore.get(currentGroupOperationAtom); if (isGroupOperation) { historyStore.set(currentGroupOperationAtom, [ ...(historyStore.get(currentGroupOperationAtom) || []), entry ]); return; } const currentStack = historyStore.get(historyStackAtom); // Add to history, respecting the limit const newPast = [...currentStack.past, entry]; if (newPast.length > historyLimit) { newPast.shift(); // Remove oldest entry if limit reached } // Clear future when a new change occurs historyStore.set(historyStackAtom, { past: newPast, future: [] // Clear redo stack when new changes occur }); }; // Start a group operation export const startGroupOperation = () => { historyStore.set(isGroupOperationInProgressAtom, true); historyStore.set(currentGroupOperationAtom, []); }; // End a group operation and commit it to history export const endGroupOperation = () => { const groupEntries = historyStore.get(currentGroupOperationAtom); if (groupEntries && groupEntries.length > 0) { const currentStack = historyStore.get(historyStackAtom); historyStore.set(historyStackAtom, { past: [...currentStack.past, ...groupEntries], future: [] }); } historyStore.set(isGroupOperationInProgressAtom, false); historyStore.set(currentGroupOperationAtom, null); };