mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 01:06:35 -04:00
make undo/redo work with formatting
This commit is contained in:
parent
9cef20df22
commit
e3e46764d7
2 changed files with 47 additions and 22 deletions
|
@ -47,6 +47,17 @@ export interface InputHandlerAPI {
|
|||
readonly specialKey: HandlerList<SpecialKeyParams>;
|
||||
}
|
||||
|
||||
export function getMaxOffset(node: Node) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
if(!node.textContent) return 0;
|
||||
return node.textContent.length;
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
return node.childNodes.length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
function getCaretPosition(element: Element){
|
||||
const selection = getSelection(element)!;
|
||||
const range = getRange(selection);
|
||||
|
@ -64,17 +75,17 @@ function getCaretPosition(element: Element){
|
|||
let counter = 0;
|
||||
for(const node of element.childNodes){
|
||||
if(node === startNode) break;
|
||||
if(node.textContent) counter += node.textContent.length;
|
||||
if(node.nodeType !== Node.TEXT_NODE) counter++;
|
||||
if(node.textContent && node.nodeType == Node.TEXT_NODE) counter += node.textContent.length;
|
||||
if(node.nodeName === "BR") counter++;
|
||||
}
|
||||
counter += startOffset;
|
||||
return counter;
|
||||
} else {
|
||||
let counter = 0;
|
||||
for(let i = 0; i < startOffset; i++){
|
||||
for(let i = 0; (i < startOffset) && (i < element.childNodes.length); i++){
|
||||
const node = element.childNodes[i];
|
||||
if(node.textContent) counter += node.textContent.length;
|
||||
if(node.nodeType !== Node.TEXT_NODE) counter++;
|
||||
if(node.textContent && node.nodeType == Node.TEXT_NODE) counter += node.textContent.length;
|
||||
if(node.nodeName === "BR") counter++;
|
||||
}
|
||||
return counter;
|
||||
}
|
||||
|
@ -90,7 +101,20 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
|
|||
const beforeInput = new HandlerList<InputEventParams>();
|
||||
const insertText = new HandlerList<InsertTextParams>();
|
||||
const afterInput = new HandlerList<EventParams>();
|
||||
|
||||
const undoManager = new UndoManager();
|
||||
let hasSetupObserver = false;
|
||||
const config = {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true
|
||||
};
|
||||
const observer = new MutationObserver(onMutation);
|
||||
|
||||
function onMutation(mutationsList: MutationRecord[], observer){
|
||||
const element = <Element>mutationsList[0].target;
|
||||
undoManager.register(element.innerHTML, getMaxOffset(element));
|
||||
}
|
||||
|
||||
async function onBeforeInput(this: Element, event: InputEvent): Promise<void> {
|
||||
const selection = getSelection(this)!;
|
||||
|
@ -98,9 +122,6 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
|
|||
|
||||
await beforeInput.dispatch({ event });
|
||||
|
||||
const position = getCaretPosition(this);
|
||||
undoManager.register(this.innerHTML, position);
|
||||
|
||||
if (
|
||||
!range
|
||||
|| !event.inputType.startsWith("insert")
|
||||
|
@ -129,8 +150,13 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
|
|||
}
|
||||
|
||||
async function onInput(this: Element, event: Event): Promise<void> {
|
||||
if(!hasSetupObserver) {
|
||||
observer.observe(this, config);
|
||||
hasSetupObserver = true;
|
||||
}
|
||||
const position = getCaretPosition(this);
|
||||
undoManager.register(this.innerHTML, position);
|
||||
undoManager.register(this.innerHTML, position-1);
|
||||
undoManager.clearRedoStack();
|
||||
await afterInput.dispatch({ event });
|
||||
}
|
||||
|
||||
|
@ -171,13 +197,6 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
|
|||
}
|
||||
}
|
||||
|
||||
async function onPaste(this: Element, event: ClipboardEvent): Promise<void> {
|
||||
const position = getCaretPosition(this);
|
||||
//Wait for paste event to be done
|
||||
setTimeout(() => {}, 0);
|
||||
undoManager.register(this.innerHTML, position);
|
||||
}
|
||||
|
||||
function setupHandler(element: HTMLElement): { destroy(): void } {
|
||||
const destroy = singleCallback(
|
||||
on(element, "beforeinput", onBeforeInput),
|
||||
|
@ -185,7 +204,6 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
|
|||
on(element, "blur", clearInsertText),
|
||||
on(element, "pointerdown", onPointerDown),
|
||||
on(element, "keydown", onKeyDown),
|
||||
on(element, "paste", onPaste),
|
||||
on(document, "selectionchange", clearInsertText),
|
||||
);
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { getRange, getSelection } from "@tslib/cross-browser";
|
||||
import { getMaxOffset } from "./input-handler";
|
||||
|
||||
type State = {
|
||||
content: string;
|
||||
|
@ -10,9 +11,14 @@ export class UndoManager {
|
|||
private redoStack: State[] = [];
|
||||
private isUpdating: boolean = false;
|
||||
private transactionStart: number = 0;
|
||||
public register = this.debounce(this.pushToUndo, 700, (position: number) => this.transactionStart = position);
|
||||
public register = this.debounce(this.pushToUndo, 500, (position: number) => this.transactionStart = position);
|
||||
|
||||
private pushToUndo(content: string): void {
|
||||
public clearRedoStack(){
|
||||
if(this.isUpdating) return;
|
||||
this.redoStack = [];
|
||||
}
|
||||
|
||||
public pushToUndo(content: string): void {
|
||||
if(this.isUpdating) return;
|
||||
if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length-1].content === content) return;
|
||||
|
||||
|
@ -66,6 +72,7 @@ export class UndoManager {
|
|||
return;
|
||||
}
|
||||
let finalOffset = Math.min(nodeOffset, nodeFound.textContent?.length || 0);
|
||||
if(finalOffset > getMaxOffset(nodeFound)) finalOffset = getMaxOffset(nodeFound);
|
||||
range.setStart(nodeFound, finalOffset);
|
||||
range.collapse(true);
|
||||
selection.removeAllRanges()
|
||||
|
@ -97,7 +104,7 @@ export class UndoManager {
|
|||
nodeOffset = counter;
|
||||
break;
|
||||
}
|
||||
if(node.nodeType !== Node.TEXT_NODE) counter--;
|
||||
if(node.nodeName === "BR") counter--;
|
||||
counter -= nodeLength;
|
||||
}
|
||||
if(!range){
|
||||
|
@ -113,6 +120,7 @@ export class UndoManager {
|
|||
return;
|
||||
}
|
||||
let finalOffset = Math.min(nodeOffset, nodeFound.textContent?.length || 0);
|
||||
if(finalOffset > getMaxOffset(nodeFound)) finalOffset = getMaxOffset(nodeFound);
|
||||
range.setStart(nodeFound, finalOffset);
|
||||
range.collapse(true);
|
||||
selection.removeAllRanges()
|
||||
|
@ -127,10 +135,9 @@ export class UndoManager {
|
|||
return (...args) => {
|
||||
const isNewTransaction = timeout === undefined;
|
||||
clearTimeout(timeout);
|
||||
if(isNewTransaction) onTransactionStart.call(this, args[1]);
|
||||
if(isNewTransaction) onTransactionStart.call(this, args[1])
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
this.redoStack = [];
|
||||
func.call(this, args[0]);
|
||||
timeout = undefined;
|
||||
}, delay);
|
||||
|
|
Loading…
Reference in a new issue