add redo functionality to input handler

This commit is contained in:
fcolona 2025-07-14 20:20:48 -03:00
parent c31802ae77
commit 9cef20df22
2 changed files with 76 additions and 15 deletions

View file

@ -99,7 +99,7 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
await beforeInput.dispatch({ event }); await beforeInput.dispatch({ event });
const position = getCaretPosition(this); const position = getCaretPosition(this);
undoManager.register(this, position); undoManager.register(this.innerHTML, position);
if ( if (
!range !range
@ -130,7 +130,7 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
async function onInput(this: Element, event: Event): Promise<void> { async function onInput(this: Element, event: Event): Promise<void> {
const position = getCaretPosition(this); const position = getCaretPosition(this);
undoManager.register(this, position); undoManager.register(this.innerHTML, position);
await afterInput.dispatch({ event }); await afterInput.dispatch({ event });
} }
@ -165,13 +165,17 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
event.preventDefault(); event.preventDefault();
undoManager.undo(this); undoManager.undo(this);
} }
else if((event.ctrlKey || event.metaKey) && event.key == "y"){
event.preventDefault();
undoManager.redo(this);
}
} }
async function onPaste(this: Element, event: ClipboardEvent): Promise<void> { async function onPaste(this: Element, event: ClipboardEvent): Promise<void> {
const position = getCaretPosition(this); const position = getCaretPosition(this);
//Wait for paste event to be done //Wait for paste event to be done
setTimeout(() => {}, 0); setTimeout(() => {}, 0);
undoManager.register(this, position); undoManager.register(this.innerHTML, position);
} }
function setupHandler(element: HTMLElement): { destroy(): void } { function setupHandler(element: HTMLElement): { destroy(): void } {

View file

@ -6,32 +6,41 @@ type State = {
} }
export class UndoManager { export class UndoManager {
private stack: State[] = []; private undoStack: State[] = [];
private redoStack: State[] = [];
private isUpdating: boolean = false; private isUpdating: boolean = false;
private lastPosition: number = 0; private transactionStart: number = 0;
public register = this.debounce(this.push, 700, (position: number) => this.lastPosition = position); public register = this.debounce(this.pushToUndo, 700, (position: number) => this.transactionStart = position);
private push(element: Element): void { private pushToUndo(content: string): void {
if(this.isUpdating) return; if(this.isUpdating) return;
if (this.stack.length > 0 && this.stack[this.stack.length-1].content === element.innerHTML) return; if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length-1].content === content) return;
const state = {content: element.innerHTML, position: this.lastPosition} const state = {content, position: this.transactionStart}
this.stack.push(state); this.undoStack.push(state);
}
private pushToRedo(content: string): void {
if (this.redoStack.length > 0 && this.redoStack[this.redoStack.length-1].content === content) return;
const state = {content: content, position: this.transactionStart}
this.redoStack.push(state);
} }
public undo(element: Element): void{ public undo(element: Element): void{
this.isUpdating = true; this.isUpdating = true;
this.stack.pop(); const undoedState = this.undoStack.pop();
if(undoedState) this.pushToRedo(undoedState.content);
let last: State; let last: State;
if(this.stack.length <= 0) last = {content: "", position: this.lastPosition}; if(this.undoStack.length <= 0) last = {content: "", position: 0};
else last = this.stack[this.stack.length-1]; else last = this.undoStack[this.undoStack.length-1];
element.innerHTML = last.content; element.innerHTML = last.content;
const selection = getSelection(element)!; const selection = getSelection(element)!;
let range = getRange(selection); let range = getRange(selection);
let counter = this.lastPosition; let counter = this.transactionStart;
let nodeFound: Node | null = null; let nodeFound: Node | null = null;
let nodeOffset = 0; let nodeOffset = 0;
for(const node of element.childNodes){ for(const node of element.childNodes){
@ -62,7 +71,54 @@ export class UndoManager {
selection.removeAllRanges() selection.removeAllRanges()
selection.addRange(range); selection.addRange(range);
if(this.stack.length > 0) this.lastPosition = this.stack[this.stack.length-1].position; if(this.undoStack.length > 0) this.transactionStart = this.undoStack[this.undoStack.length-1].position;
this.isUpdating = false;
}
public redo(element: Element): void {
const redoedState = this.redoStack.pop();
if(!redoedState) return;
this.transactionStart = redoedState.position;
this.pushToUndo(redoedState.content);
this.isUpdating = true;
element.innerHTML = redoedState.content;
const selection = getSelection(element)!;
let range = getRange(selection);
let counter = this.transactionStart;
let nodeFound: Node | null = null;
let nodeOffset = 0;
for(const node of element.childNodes){
let nodeLength = node.textContent?.length || 0;
if (counter <= nodeLength) {
nodeFound = node;
nodeOffset = counter;
break;
}
if(node.nodeType !== Node.TEXT_NODE) counter--;
counter -= nodeLength;
}
if(!range){
this.isUpdating = false;
return;
}
if(!nodeFound){
if(element.lastChild) range?.setStart(element.lastChild as Node, (element.lastChild?.textContent?.length || 0))
range.collapse(true);
selection.removeAllRanges()
selection.addRange(range);
this.isUpdating = false;
return;
}
let finalOffset = Math.min(nodeOffset, nodeFound.textContent?.length || 0);
range.setStart(nodeFound, finalOffset);
range.collapse(true);
selection.removeAllRanges()
selection.addRange(range);
if(this.redoStack.length > 0) this.transactionStart = this.redoStack[this.redoStack.length-1].position;
this.isUpdating = false; this.isUpdating = false;
} }
@ -74,6 +130,7 @@ export class UndoManager {
if(isNewTransaction) onTransactionStart.call(this, args[1]); if(isNewTransaction) onTransactionStart.call(this, args[1]);
timeout = setTimeout(() => { timeout = setTimeout(() => {
this.redoStack = [];
func.call(this, args[0]); func.call(this, args[0]);
timeout = undefined; timeout = undefined;
}, delay); }, delay);