element-web/src/editor/history.ts
David Langley 491f0cd08a
Change license (#13)
* Copyright headers 1

* Licence headers 2

* Copyright Headers 3

* Copyright Headers 4

* Copyright Headers 5

* Copyright Headers 6

* Copyright headers 7

* Add copyright headers for html and config file

* Replace license files and update package.json

* Update with CLA

* lint
2024-09-09 13:57:16 +00:00

146 lines
5.1 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import EditorModel from "./model";
import { IDiff } from "./diff";
import { SerializedPart } from "./parts";
import { Caret } from "./caret";
export interface IHistory {
parts: SerializedPart[];
caret?: Caret;
}
export const MAX_STEP_LENGTH = 10;
export default class HistoryManager {
private stack: IHistory[] = [];
private newlyTypedCharCount = 0;
private currentIndex = -1;
private changedSinceLastPush = false;
private lastCaret?: Caret;
private nonWordBoundarySinceLastPush = false;
private addedSinceLastPush = false;
private removedSinceLastPush = false;
public clear(): void {
this.stack = [];
this.newlyTypedCharCount = 0;
this.currentIndex = -1;
this.changedSinceLastPush = false;
this.lastCaret = undefined;
this.nonWordBoundarySinceLastPush = false;
this.addedSinceLastPush = false;
this.removedSinceLastPush = false;
}
private shouldPush(inputType?: string, diff?: IDiff): boolean {
// right now we can only push a step after
// the input has been applied to the model,
// so we can't push the state before something happened.
// not ideal but changing this would be harder to fit cleanly into
// the editor model.
const isNonBulkInput =
inputType === "insertText" || inputType === "deleteContentForward" || inputType === "deleteContentBackward";
if (diff && isNonBulkInput) {
if (diff.added) {
this.addedSinceLastPush = true;
}
if (diff.removed) {
this.removedSinceLastPush = true;
}
// as long as you've only been adding or removing since the last push
if (this.addedSinceLastPush !== this.removedSinceLastPush) {
// add steps by word boundary, up to MAX_STEP_LENGTH characters
const str = diff.added ? diff.added : diff.removed!;
const isWordBoundary = str === " " || str === "\t" || str === "\n";
if (this.nonWordBoundarySinceLastPush && isWordBoundary) {
return true;
}
if (!isWordBoundary) {
this.nonWordBoundarySinceLastPush = true;
}
this.newlyTypedCharCount += str.length;
return this.newlyTypedCharCount > MAX_STEP_LENGTH;
} else {
// if starting to remove while adding before, or the opposite, push
return true;
}
} else {
// bulk input (paste, ...) should be pushed every time
return true;
}
}
private pushState(model: EditorModel, caret?: Caret): void {
// remove all steps after current step
while (this.currentIndex < this.stack.length - 1) {
this.stack.pop();
}
const parts = model.serializeParts();
this.stack.push({ parts, caret });
this.currentIndex = this.stack.length - 1;
this.lastCaret = undefined;
this.changedSinceLastPush = false;
this.newlyTypedCharCount = 0;
this.nonWordBoundarySinceLastPush = false;
this.addedSinceLastPush = false;
this.removedSinceLastPush = false;
}
// needs to persist parts and caret position
public tryPush(model: EditorModel, caret?: Caret, inputType?: string, diff?: IDiff): boolean {
// ignore state restoration echos.
// these respect the inputType values of the input event,
// but are actually passed in from MessageEditor calling model.reset()
// in the keydown event handler.
if (inputType === "historyUndo" || inputType === "historyRedo") {
return false;
}
const shouldPush = this.shouldPush(inputType, diff);
if (shouldPush) {
this.pushState(model, caret);
} else {
this.lastCaret = caret;
this.changedSinceLastPush = true;
}
return shouldPush;
}
public ensureLastChangesPushed(model: EditorModel): void {
if (this.changedSinceLastPush && this.lastCaret) {
this.pushState(model, this.lastCaret);
}
}
public canUndo(): boolean {
return this.currentIndex >= 1 || this.changedSinceLastPush;
}
public canRedo(): boolean {
return this.currentIndex < this.stack.length - 1;
}
// returns state that should be applied to model
public undo(model: EditorModel): IHistory | void {
if (this.canUndo()) {
this.ensureLastChangesPushed(model);
this.currentIndex -= 1;
return this.stack[this.currentIndex];
}
}
// returns state that should be applied to model
public redo(): IHistory | void {
if (this.canRedo()) {
this.changedSinceLastPush = false;
this.currentIndex += 1;
return this.stack[this.currentIndex];
}
}
}