mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 09:16:38 -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>;
|
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){
|
function getCaretPosition(element: Element){
|
||||||
const selection = getSelection(element)!;
|
const selection = getSelection(element)!;
|
||||||
const range = getRange(selection);
|
const range = getRange(selection);
|
||||||
|
@ -64,17 +75,17 @@ function getCaretPosition(element: Element){
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
for(const node of element.childNodes){
|
for(const node of element.childNodes){
|
||||||
if(node === startNode) break;
|
if(node === startNode) break;
|
||||||
if(node.textContent) counter += node.textContent.length;
|
if(node.textContent && node.nodeType == Node.TEXT_NODE) counter += node.textContent.length;
|
||||||
if(node.nodeType !== Node.TEXT_NODE) counter++;
|
if(node.nodeName === "BR") counter++;
|
||||||
}
|
}
|
||||||
counter += startOffset;
|
counter += startOffset;
|
||||||
return counter;
|
return counter;
|
||||||
} else {
|
} else {
|
||||||
let counter = 0;
|
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];
|
const node = element.childNodes[i];
|
||||||
if(node.textContent) counter += node.textContent.length;
|
if(node.textContent && node.nodeType == Node.TEXT_NODE) counter += node.textContent.length;
|
||||||
if(node.nodeType !== Node.TEXT_NODE) counter++;
|
if(node.nodeName === "BR") counter++;
|
||||||
}
|
}
|
||||||
return counter;
|
return counter;
|
||||||
}
|
}
|
||||||
|
@ -90,7 +101,20 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
|
||||||
const beforeInput = new HandlerList<InputEventParams>();
|
const beforeInput = new HandlerList<InputEventParams>();
|
||||||
const insertText = new HandlerList<InsertTextParams>();
|
const insertText = new HandlerList<InsertTextParams>();
|
||||||
const afterInput = new HandlerList<EventParams>();
|
const afterInput = new HandlerList<EventParams>();
|
||||||
|
|
||||||
const undoManager = new UndoManager();
|
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> {
|
async function onBeforeInput(this: Element, event: InputEvent): Promise<void> {
|
||||||
const selection = getSelection(this)!;
|
const selection = getSelection(this)!;
|
||||||
|
@ -98,9 +122,6 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
|
||||||
|
|
||||||
await beforeInput.dispatch({ event });
|
await beforeInput.dispatch({ event });
|
||||||
|
|
||||||
const position = getCaretPosition(this);
|
|
||||||
undoManager.register(this.innerHTML, position);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!range
|
!range
|
||||||
|| !event.inputType.startsWith("insert")
|
|| !event.inputType.startsWith("insert")
|
||||||
|
@ -129,8 +150,13 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onInput(this: Element, event: Event): Promise<void> {
|
async function onInput(this: Element, event: Event): Promise<void> {
|
||||||
|
if(!hasSetupObserver) {
|
||||||
|
observer.observe(this, config);
|
||||||
|
hasSetupObserver = true;
|
||||||
|
}
|
||||||
const position = getCaretPosition(this);
|
const position = getCaretPosition(this);
|
||||||
undoManager.register(this.innerHTML, position);
|
undoManager.register(this.innerHTML, position-1);
|
||||||
|
undoManager.clearRedoStack();
|
||||||
await afterInput.dispatch({ event });
|
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 } {
|
function setupHandler(element: HTMLElement): { destroy(): void } {
|
||||||
const destroy = singleCallback(
|
const destroy = singleCallback(
|
||||||
on(element, "beforeinput", onBeforeInput),
|
on(element, "beforeinput", onBeforeInput),
|
||||||
|
@ -185,7 +204,6 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
|
||||||
on(element, "blur", clearInsertText),
|
on(element, "blur", clearInsertText),
|
||||||
on(element, "pointerdown", onPointerDown),
|
on(element, "pointerdown", onPointerDown),
|
||||||
on(element, "keydown", onKeyDown),
|
on(element, "keydown", onKeyDown),
|
||||||
on(element, "paste", onPaste),
|
|
||||||
on(document, "selectionchange", clearInsertText),
|
on(document, "selectionchange", clearInsertText),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { getRange, getSelection } from "@tslib/cross-browser";
|
import { getRange, getSelection } from "@tslib/cross-browser";
|
||||||
|
import { getMaxOffset } from "./input-handler";
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
content: string;
|
content: string;
|
||||||
|
@ -10,9 +11,14 @@ export class UndoManager {
|
||||||
private redoStack: State[] = [];
|
private redoStack: State[] = [];
|
||||||
private isUpdating: boolean = false;
|
private isUpdating: boolean = false;
|
||||||
private transactionStart: number = 0;
|
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.isUpdating) return;
|
||||||
if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length-1].content === content) return;
|
if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length-1].content === content) return;
|
||||||
|
|
||||||
|
@ -66,6 +72,7 @@ export class UndoManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let finalOffset = Math.min(nodeOffset, nodeFound.textContent?.length || 0);
|
let finalOffset = Math.min(nodeOffset, nodeFound.textContent?.length || 0);
|
||||||
|
if(finalOffset > getMaxOffset(nodeFound)) finalOffset = getMaxOffset(nodeFound);
|
||||||
range.setStart(nodeFound, finalOffset);
|
range.setStart(nodeFound, finalOffset);
|
||||||
range.collapse(true);
|
range.collapse(true);
|
||||||
selection.removeAllRanges()
|
selection.removeAllRanges()
|
||||||
|
@ -97,7 +104,7 @@ export class UndoManager {
|
||||||
nodeOffset = counter;
|
nodeOffset = counter;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(node.nodeType !== Node.TEXT_NODE) counter--;
|
if(node.nodeName === "BR") counter--;
|
||||||
counter -= nodeLength;
|
counter -= nodeLength;
|
||||||
}
|
}
|
||||||
if(!range){
|
if(!range){
|
||||||
|
@ -113,6 +120,7 @@ export class UndoManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let finalOffset = Math.min(nodeOffset, nodeFound.textContent?.length || 0);
|
let finalOffset = Math.min(nodeOffset, nodeFound.textContent?.length || 0);
|
||||||
|
if(finalOffset > getMaxOffset(nodeFound)) finalOffset = getMaxOffset(nodeFound);
|
||||||
range.setStart(nodeFound, finalOffset);
|
range.setStart(nodeFound, finalOffset);
|
||||||
range.collapse(true);
|
range.collapse(true);
|
||||||
selection.removeAllRanges()
|
selection.removeAllRanges()
|
||||||
|
@ -127,10 +135,9 @@ export class UndoManager {
|
||||||
return (...args) => {
|
return (...args) => {
|
||||||
const isNewTransaction = timeout === undefined;
|
const isNewTransaction = timeout === undefined;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
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