167 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			167 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | 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() | ||
|  |     }; | ||
|  |   } | ||
|  | } |