simple-undo-redo
Version:
Simple undo-redo functionality with branching support for JavaScript applications
263 lines (212 loc) • 6.75 kB
JavaScript
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;