import { type IBlock } from './wysiwyg.types.js'; export interface IHistoryState { blocks: IBlock[]; selectedBlockId: string | null; cursorPosition?: { blockId: string; offset: number; }; timestamp: number; } export class WysiwygHistory { private history: IHistoryState[] = []; private currentIndex: number = -1; private maxHistorySize: number = 50; private lastSaveTime: number = 0; private saveDebounceMs: number = 500; // Debounce saves to avoid too many snapshots constructor() { // Initialize with empty state this.history = []; this.currentIndex = -1; } /** * Save current state to history */ saveState(blocks: IBlock[], selectedBlockId: string | null, cursorPosition?: { blockId: string; offset: number }): void { const now = Date.now(); // Debounce rapid changes (like typing) if (now - this.lastSaveTime < this.saveDebounceMs && this.currentIndex >= 0) { // Update the current state instead of creating a new one this.history[this.currentIndex] = { blocks: this.cloneBlocks(blocks), selectedBlockId, cursorPosition: cursorPosition ? { ...cursorPosition } : undefined, timestamp: now }; return; } // Remove any states after current index (when we save after undoing) if (this.currentIndex < this.history.length - 1) { this.history = this.history.slice(0, this.currentIndex + 1); } // Add new state const newState: IHistoryState = { blocks: this.cloneBlocks(blocks), selectedBlockId, cursorPosition: cursorPosition ? { ...cursorPosition } : undefined, timestamp: now }; this.history.push(newState); this.currentIndex++; // Limit history size if (this.history.length > this.maxHistorySize) { this.history.shift(); this.currentIndex--; } this.lastSaveTime = now; } /** * Force save a checkpoint (useful for operations like block deletion) */ saveCheckpoint(blocks: IBlock[], selectedBlockId: string | null, cursorPosition?: { blockId: string; offset: number }): void { this.lastSaveTime = 0; // Reset debounce this.saveState(blocks, selectedBlockId, cursorPosition); } /** * Undo to previous state */ undo(): IHistoryState | null { if (!this.canUndo()) { return null; } this.currentIndex--; return this.cloneState(this.history[this.currentIndex]); } /** * Redo to next state */ redo(): IHistoryState | null { if (!this.canRedo()) { return null; } this.currentIndex++; return this.cloneState(this.history[this.currentIndex]); } /** * Check if undo is available */ canUndo(): boolean { return this.currentIndex > 0; } /** * Check if redo is available */ canRedo(): boolean { return this.currentIndex < this.history.length - 1; } /** * Get current state */ getCurrentState(): IHistoryState | null { if (this.currentIndex >= 0 && this.currentIndex < this.history.length) { return this.cloneState(this.history[this.currentIndex]); } return null; } /** * Clear history */ clear(): void { this.history = []; this.currentIndex = -1; this.lastSaveTime = 0; } /** * Deep clone blocks */ private cloneBlocks(blocks: IBlock[]): IBlock[] { return blocks.map(block => ({ ...block, metadata: block.metadata ? { ...block.metadata } : undefined })); } /** * Clone a history state */ private cloneState(state: IHistoryState): IHistoryState { return { blocks: this.cloneBlocks(state.blocks), selectedBlockId: state.selectedBlockId, cursorPosition: state.cursorPosition ? { ...state.cursorPosition } : undefined, timestamp: state.timestamp }; } /** * Get history info for debugging */ getHistoryInfo(): { size: number; currentIndex: number; canUndo: boolean; canRedo: boolean } { return { size: this.history.length, currentIndex: this.currentIndex, canUndo: this.canUndo(), canRedo: this.canRedo() }; } }