200 lines
5 KiB
TypeScript
200 lines
5 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
interface UndoCommand {
|
|
checkpoint: number
|
|
undo: () => Promise<void>
|
|
redo: () => Promise<void>
|
|
description?: string
|
|
groupId?: string
|
|
}
|
|
|
|
//
|
|
// General-purpose undo manager
|
|
//
|
|
class UndoManager {
|
|
onStateDidChange?: () => void
|
|
|
|
private commands: UndoCommand[] = []
|
|
private index = -1
|
|
private limit = 0
|
|
private isExecuting = false
|
|
|
|
get currentCheckpoint() {
|
|
if (this.index < 0) {
|
|
return 0
|
|
}
|
|
return this.commands[this.index].checkpoint
|
|
}
|
|
|
|
get undoDescription(): string | undefined {
|
|
const command = this.commands[this.index]
|
|
if (!command) {
|
|
return undefined
|
|
}
|
|
|
|
return command.description
|
|
}
|
|
|
|
get redoDescription(): string | undefined {
|
|
const command = this.commands[this.index + 1]
|
|
if (!command) {
|
|
return undefined
|
|
}
|
|
|
|
return command.description
|
|
}
|
|
|
|
private async execute(command: UndoCommand, action: 'undo' | 'redo') {
|
|
if (!command || typeof command[action] !== 'function') {
|
|
return this
|
|
}
|
|
this.isExecuting = true
|
|
|
|
await command[action]()
|
|
|
|
this.isExecuting = false
|
|
return this
|
|
}
|
|
|
|
async perform(
|
|
redo: () => Promise<void>,
|
|
undo: () => Promise<void>,
|
|
description?: string,
|
|
groupId?: string,
|
|
isDiscardable = false,
|
|
): Promise<UndoManager> {
|
|
await redo()
|
|
return this.registerUndo({undo, redo}, description, groupId, isDiscardable)
|
|
}
|
|
|
|
registerUndo(
|
|
command: {
|
|
undo: () => Promise<void>,
|
|
redo: () => Promise<void>
|
|
},
|
|
description?: string,
|
|
groupId?: string,
|
|
isDiscardable = false,
|
|
): UndoManager {
|
|
if (this.isExecuting) {
|
|
return this
|
|
}
|
|
|
|
// If we are here after having called undo, invalidate items higher on the stack
|
|
this.commands.splice(this.index + 1, this.commands.length - this.index)
|
|
|
|
let checkpoint: number
|
|
if (isDiscardable) {
|
|
checkpoint =
|
|
this.commands.length > 1 ? this.commands[this.commands.length - 1].checkpoint : 0
|
|
} else {
|
|
checkpoint = Date.now()
|
|
}
|
|
|
|
const internalCommand = {
|
|
checkpoint,
|
|
undo: command.undo,
|
|
redo: command.redo,
|
|
description,
|
|
groupId,
|
|
}
|
|
this.commands.push(internalCommand)
|
|
|
|
// If limit is set, remove items from the start
|
|
if (this.limit && this.commands.length > this.limit) {
|
|
this.commands = this.commands.splice(
|
|
0,
|
|
this.commands.length - this.limit,
|
|
)
|
|
}
|
|
|
|
// Set the current index to the end
|
|
this.index = this.commands.length - 1
|
|
|
|
if (this.onStateDidChange) {
|
|
this.onStateDidChange()
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
async undo() {
|
|
if (this.isExecuting) {
|
|
return this
|
|
}
|
|
let command = this.commands[this.index]
|
|
if (!command) {
|
|
return this
|
|
}
|
|
|
|
const currentGroupId = command.groupId
|
|
if (currentGroupId) {
|
|
do {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.execute(command, 'undo')
|
|
this.index -= 1
|
|
command = this.commands[this.index]
|
|
} while (this.index >= 0 && currentGroupId === command.groupId)
|
|
} else {
|
|
await this.execute(command, 'undo')
|
|
this.index -= 1
|
|
}
|
|
|
|
if (this.onStateDidChange) {
|
|
this.onStateDidChange()
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
async redo() {
|
|
if (this.isExecuting) {
|
|
return this
|
|
}
|
|
let command = this.commands[this.index + 1]
|
|
if (!command) {
|
|
return this
|
|
}
|
|
|
|
const currentGroupId = command.groupId
|
|
if (currentGroupId) {
|
|
do {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.execute(command, 'redo')
|
|
this.index += 1
|
|
command = this.commands[this.index + 1]
|
|
} while (this.index < this.commands.length - 1 && currentGroupId === command.groupId)
|
|
} else {
|
|
await this.execute(command, 'redo')
|
|
this.index += 1
|
|
}
|
|
|
|
if (this.onStateDidChange) {
|
|
this.onStateDidChange()
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
clear() {
|
|
const prevSize = this.commands.length
|
|
|
|
this.commands = []
|
|
this.index = -1
|
|
|
|
if (this.onStateDidChange && prevSize > 0) {
|
|
this.onStateDidChange()
|
|
}
|
|
}
|
|
|
|
get canUndo() {
|
|
return this.index !== -1
|
|
}
|
|
|
|
get canRedo() {
|
|
return this.index < this.commands.length - 1
|
|
}
|
|
}
|
|
|
|
const undoManager = new UndoManager()
|
|
export default undoManager
|