feat(wysiwyg): implement backspace
This commit is contained in:
167
ts_web/elements/wysiwyg/wysiwyg.history.ts
Normal file
167
ts_web/elements/wysiwyg/wysiwyg.history.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
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()
|
||||
};
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user