Files
dees-catalog/ts_web/elements/wysiwyg/wysiwyg.history.ts
2025-06-24 15:52:28 +00:00

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()
};
}
}