UNPKG

simple-undo-redo

Version:

Simple undo-redo functionality with branching support for JavaScript applications

263 lines (212 loc) 6.75 kB
const { deepCopy, generateId, isEqual, saveToStorage, loadFromStorage } = require('./utils.js'); // Main UndoRedoJS class - simple undo/redo with branching class UndoRedoJS { constructor(initialData = {}, options = {}) { // Simple options setup this.maxSize = options.maxSize || 50; this.enableStorage = options.enableStorage || false; this.storageKey = options.storageKey || 'undo-redo-js'; // Create first state const firstState = this.createState(initialData, 'Initial state'); // Simple branch structure this.branches = { main: { states: [firstState], currentIndex: 0 } }; this.currentBranch = 'main'; // Load from storage if needed if (this.enableStorage) { this.loadFromStorage(); } } // Create a new state createState = (data, message) => { return { data: deepCopy(data), message: message, timestamp: Date.now(), id: generateId() }; }; // Get current branch getCurrentBranch = () => { return this.branches[this.currentBranch]; }; // Get current state getCurrentState = () => { const branch = this.getCurrentBranch(); return branch.states[branch.currentIndex]; }; // Get current data getCurrentData = () => { const currentState = this.getCurrentState(); return deepCopy(currentState.data); }; // Update data update = (newData, message = '') => { const branch = this.getCurrentBranch(); const currentState = this.getCurrentState(); // Don't add if same data if (isEqual(currentState.data, newData)) { return this.getCurrentData(); } // Remove future states if not at end const isAtEnd = branch.currentIndex === branch.states.length - 1; if (!isAtEnd) { branch.states = branch.states.slice(0, branch.currentIndex + 1); } // Add new state const newState = this.createState(newData, message); branch.states.push(newState); branch.currentIndex = branch.currentIndex + 1; // Keep size under control if (branch.states.length > this.maxSize) { branch.states.shift(); // Remove first item branch.currentIndex = branch.currentIndex - 1; } // Save if needed if (this.enableStorage) { this.saveToStorage(); } return this.getCurrentData(); }; // Go back one step undo = () => { const branch = this.getCurrentBranch(); if (branch.currentIndex > 0) { branch.currentIndex = branch.currentIndex - 1; if (this.enableStorage) { this.saveToStorage(); } } return this.getCurrentData(); }; // Go forward one step redo = () => { const branch = this.getCurrentBranch(); const maxIndex = branch.states.length - 1; if (branch.currentIndex < maxIndex) { branch.currentIndex = branch.currentIndex + 1; if (this.enableStorage) { this.saveToStorage(); } } return this.getCurrentData(); }; // Check if can undo canUndo = () => { const branch = this.getCurrentBranch(); return branch.currentIndex > 0; }; // Check if can redo canRedo = () => { const branch = this.getCurrentBranch(); const maxIndex = branch.states.length - 1; return branch.currentIndex < maxIndex; }; // Create new branch createBranch = (branchName) => { if (this.branches[branchName]) { throw new Error('Branch ' + branchName + ' already exists'); } const currentState = this.getCurrentState(); const newState = this.createState(currentState.data, 'Created branch ' + branchName); this.branches[branchName] = { states: [newState], currentIndex: 0 }; return branchName; }; // Switch to branch switchBranch = (branchName) => { if (!this.branches[branchName]) { throw new Error('Branch ' + branchName + ' does not exist'); } this.currentBranch = branchName; return this.getCurrentData(); }; // Merge branch mergeBranch = (sourceBranch, message = '') => { if (!this.branches[sourceBranch]) { throw new Error('Branch ' + sourceBranch + ' does not exist'); } const sourceBranchObj = this.branches[sourceBranch]; const sourceState = sourceBranchObj.states[sourceBranchObj.currentIndex]; const mergeMessage = message || 'Merged ' + sourceBranch + ' into ' + this.currentBranch; return this.update(sourceState.data, mergeMessage); }; // Get all branch names getBranchList = () => { return Object.keys(this.branches); }; // Get current branch name getCurrentBranchName = () => { return this.currentBranch; }; // Delete branch deleteBranch = (branchName) => { if (branchName === 'main') { throw new Error('Cannot delete main branch'); } if (branchName === this.currentBranch) { throw new Error('Cannot delete current branch'); } if (!this.branches[branchName]) { throw new Error('Branch ' + branchName + ' does not exist'); } delete this.branches[branchName]; return true; }; // Get history getHistory = () => { const branch = this.getCurrentBranch(); const history = []; for (let i = 0; i < branch.states.length; i++) { const state = branch.states[i]; history.push({ id: state.id, message: state.message, timestamp: state.timestamp, isCurrent: (i === branch.currentIndex) }); } return history; }; // Clear all data clear = () => { const emptyState = this.createState({}, 'Cleared all data'); this.branches = { main: { states: [emptyState], currentIndex: 0 } }; this.currentBranch = 'main'; if (this.enableStorage) { this.saveToStorage(); } }; // Save to storage saveToStorage = () => { const dataToSave = { branches: this.branches, currentBranch: this.currentBranch }; saveToStorage(this.storageKey, dataToSave); }; // Load from storage loadFromStorage = () => { const savedData = loadFromStorage(this.storageKey); if (savedData) { if (savedData.branches) { this.branches = savedData.branches; } if (savedData.currentBranch) { this.currentBranch = savedData.currentBranch; } } }; } module.exports = UndoRedoJS;