mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
add redo functionality to input handler
This commit is contained in:
parent
c31802ae77
commit
9cef20df22
2 changed files with 76 additions and 15 deletions
|
@ -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 } {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue