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