UNPKG

jotai-history-global

Version:

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

208 lines (207 loc) 5.97 kB
/** * Diff utilities for computing and applying differences between values */ /** * Creates a deep diff between two values * @param oldValue - The old value * @param newValue - The new value * @returns The difference or null if no differences */ export function createDiff(oldValue, newValue) { // Handle identity if (oldValue === newValue) return null; // Handle primitive types or when one side is null/undefined if (!oldValue || !newValue || typeof oldValue !== 'object' || typeof newValue !== 'object' || Array.isArray(oldValue) !== Array.isArray(newValue)) { return { type: 'value', before: oldValue, after: newValue }; } // Handle arrays if (Array.isArray(oldValue) && Array.isArray(newValue)) { return createArrayDiff(oldValue, newValue); } // Handle objects return createObjectDiff(oldValue, newValue); } /** * Creates a diff between two objects */ function createObjectDiff(oldObj, newObj) { const changed = {}; const added = {}; const deleted = []; // Check for changed or deleted properties for (const key in oldObj) { if (!(key in newObj)) { deleted.push(key); } else if (newObj[key] !== oldObj[key]) { const childDiff = createDiff(oldObj[key], newObj[key]); if (childDiff) { changed[key] = childDiff; } } } // Check for added properties for (const key in newObj) { if (!(key in oldObj)) { added[key] = newObj[key]; } } // Return null if no changes if (Object.keys(changed).length === 0 && Object.keys(added).length === 0 && deleted.length === 0) { return null; } return { type: 'object', changed, added, deleted }; } /** * Creates a diff between two arrays */ function createArrayDiff(oldArray, newArray) { const items = []; const maxLen = Math.max(oldArray.length, newArray.length); let hasChanges = false; for (let i = 0; i < maxLen; i++) { if (i >= oldArray.length) { // Added item items.push({ index: i, value: newArray[i], added: true }); hasChanges = true; } else if (i >= newArray.length) { // Removed item items.push({ index: i, value: oldArray[i], removed: true }); hasChanges = true; } else if (oldArray[i] !== newArray[i]) { // Changed item items.push({ index: i, value: newArray[i] }); hasChanges = true; } } return hasChanges ? { type: 'array', items } : null; } /** * Applies a diff to a value to get the new value * @param value - The current value * @param diff - The diff to apply * @returns The new value */ export function applyDiff(value, diff) { // Handle primitive values if (diff.type === 'value') { return diff.after; } // Handle arrays if (diff.type === 'array' && Array.isArray(value)) { return applyArrayDiff(value, diff); } // Handle objects if (diff.type === 'object' && value !== null && typeof value === 'object') { return applyObjectDiff(value, diff); } // Fallback if diff can't be applied console.warn('Unable to apply diff, returning original value', { value, diff }); return value; } /** * Applies an object diff */ function applyObjectDiff(obj, diff) { const result = { ...obj }; // Apply changes for (const [key, childDiff] of Object.entries(diff.changed)) { if (key in result) { result[key] = applyDiff(result[key], childDiff); } } // Add new properties for (const [key, value] of Object.entries(diff.added)) { result[key] = value; } // Delete properties for (const key of diff.deleted) { delete result[key]; } return result; } /** * Applies an array diff */ function applyArrayDiff(arr, diff) { // Create a copy of the array const result = [...arr]; // Process removals first (from highest index to lowest) const removals = diff.items .filter(item => item.removed) .sort((a, b) => b.index - a.index); for (const item of removals) { result.splice(item.index, 1); } // Process additions and changes for (const item of diff.items) { if (item.added) { // Insert at specified index result.splice(item.index, 0, item.value); } else if (!item.removed && item.value !== undefined) { // Replace value at index result[item.index] = item.value; } } return result; } /** * Reverses a diff so it can be applied to undo a change */ export function reverseDiff(diff) { if (diff.type === 'value') { return { type: 'value', before: diff.after, after: diff.before }; } if (diff.type === 'array') { return { type: 'array', items: diff.items.map(item => ({ index: item.index, value: item.value, added: item.removed, removed: item.added })) }; } if (diff.type === 'object') { return { type: 'object', added: diff.deleted.reduce((obj, key) => { // We can't know the original values of deleted properties obj[key] = undefined; return obj; }, {}), deleted: Object.keys(diff.added), changed: Object.entries(diff.changed).reduce((obj, [key, childDiff]) => { obj[key] = reverseDiff(childDiff); return obj; }, {}) }; } throw new Error('Unknown diff type'); }