* feat: Add contenteditable tokenizer for badge/code-chip chat input * feat: Add source-space undo/redo history for the rich input * feat: Split text glued to a closing code fence onto its own line * feat: Add ChatFormContenteditable rich input renderer * feat : wire the contenteditable into ChatForm with auto-switch gating
49 lines
1.4 KiB
TypeScript
49 lines
1.4 KiB
TypeScript
/**
|
|
* Source-space undo/redo history for the chat-form contenteditable, whose
|
|
* imperative DOM rebuilds destroy the browser's native undo stack.
|
|
* Entries record the state BEFORE an edit; edits within `groupWindowMs`
|
|
* extend the open group so a typing burst undoes as a unit, while
|
|
* structural edits (paste, mention insert, clear) pass `newGroup`.
|
|
*/
|
|
|
|
export interface SourceHistoryEntry {
|
|
value: string;
|
|
caret: number;
|
|
}
|
|
|
|
export class SourceHistory {
|
|
private undoStack: SourceHistoryEntry[] = [];
|
|
private redoStack: SourceHistoryEntry[] = [];
|
|
private lastPush = 0;
|
|
|
|
constructor(
|
|
private limit = 100,
|
|
private groupWindowMs = 800
|
|
) {}
|
|
|
|
push(entry: SourceHistoryEntry, now: number, newGroup = false): void {
|
|
if (newGroup || now - this.lastPush >= this.groupWindowMs || this.undoStack.length === 0) {
|
|
this.undoStack.push(entry);
|
|
if (this.undoStack.length > this.limit) this.undoStack.shift();
|
|
}
|
|
this.lastPush = now;
|
|
this.redoStack = [];
|
|
}
|
|
|
|
undo(current: SourceHistoryEntry): SourceHistoryEntry | null {
|
|
const entry = this.undoStack.pop();
|
|
if (!entry) return null;
|
|
this.redoStack.push(current);
|
|
this.lastPush = 0; // the next edit after an undo starts a new group
|
|
return entry;
|
|
}
|
|
|
|
redo(current: SourceHistoryEntry): SourceHistoryEntry | null {
|
|
const entry = this.redoStack.pop();
|
|
if (!entry) return null;
|
|
this.undoStack.push(current);
|
|
this.lastPush = 0;
|
|
return entry;
|
|
}
|
|
}
|