一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
106 行
2.6 KiB
TypeScript
106 行
2.6 KiB
TypeScript
import AsyncActionQueue from '../AsyncActionQueue';
|
|
import { EventEmitter } from 'events';
|
|
|
|
class UndoQueue {
|
|
|
|
private inner_: unknown[] = [];
|
|
private size_ = 20;
|
|
|
|
public pop() {
|
|
return this.inner_.pop();
|
|
}
|
|
|
|
public push(e: unknown) {
|
|
this.inner_.push(e);
|
|
while (this.length > this.size_) {
|
|
this.inner_.splice(0, 1);
|
|
}
|
|
}
|
|
|
|
public get length(): number {
|
|
return this.inner_.length;
|
|
}
|
|
|
|
public at(index: number): unknown {
|
|
return this.inner_[index];
|
|
}
|
|
|
|
}
|
|
|
|
export default class UndoRedoService {
|
|
|
|
private pushAsyncQueue: AsyncActionQueue = new AsyncActionQueue(700);
|
|
private undoStates: UndoQueue = new UndoQueue();
|
|
private redoStates: UndoQueue = new UndoQueue();
|
|
private eventEmitter: EventEmitter = new EventEmitter();
|
|
private isUndoing = false;
|
|
|
|
public constructor() {
|
|
this.push = this.push.bind(this);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- EventEmitter events carry heterogeneous payloads by name
|
|
public on(eventName: string, callback: (...args: any[])=> void) {
|
|
return this.eventEmitter.on(eventName, callback);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- See on() above
|
|
public off(eventName: string, callback: (...args: any[])=> void) {
|
|
return this.eventEmitter.removeListener(eventName, callback);
|
|
}
|
|
|
|
public push(state: unknown) {
|
|
this.undoStates.push(state);
|
|
this.redoStates = new UndoQueue();
|
|
this.eventEmitter.emit('stackChange');
|
|
}
|
|
|
|
public schedulePush(state: unknown) {
|
|
this.pushAsyncQueue.push(async () => {
|
|
this.push(state);
|
|
});
|
|
}
|
|
|
|
public async undo(redoState: unknown) {
|
|
if (this.isUndoing) return undefined;
|
|
if (!this.canUndo) throw new Error('Nothing to undo');
|
|
this.isUndoing = true;
|
|
await this.pushAsyncQueue.processAllNow();
|
|
const state = this.undoStates.pop();
|
|
this.redoStates.push(redoState);
|
|
this.eventEmitter.emit('stackChange');
|
|
this.isUndoing = false;
|
|
return state;
|
|
}
|
|
|
|
public async redo(undoState: unknown) {
|
|
if (this.isUndoing) return undefined;
|
|
if (!this.canRedo) throw new Error('Nothing to redo');
|
|
this.isUndoing = true;
|
|
await this.pushAsyncQueue.processAllNow();
|
|
const state = this.redoStates.pop();
|
|
this.undoStates.push(undoState);
|
|
this.eventEmitter.emit('stackChange');
|
|
this.isUndoing = false;
|
|
return state;
|
|
}
|
|
|
|
public async reset() {
|
|
this.undoStates = new UndoQueue();
|
|
this.redoStates = new UndoQueue();
|
|
this.isUndoing = false;
|
|
const output = this.pushAsyncQueue.reset();
|
|
this.eventEmitter.emit('stackChange');
|
|
return output;
|
|
}
|
|
|
|
public get canUndo(): boolean {
|
|
return !!this.undoStates.length;
|
|
}
|
|
|
|
public get canRedo(): boolean {
|
|
return !!this.redoStates.length;
|
|
}
|
|
|
|
}
|