Skip to content

Instantly share code, notes, and snippets.

@mattmccray
Last active September 6, 2020 17:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattmccray/7e54fe466ac266416f3f7b2c865f91a8 to your computer and use it in GitHub Desktop.
Save mattmccray/7e54fe466ac266416f3f7b2c865f91a8 to your computer and use it in GitHub Desktop.
Simple Observable Undo Controller
import { observable, computed, runInAction } from 'mobx'
import uid from './uid'
type UndoFn = () => void
type Command = () => UndoFn | Promise<UndoFn>
interface UndoContext {
id: string
label: string
command: Command
revert: UndoFn | Promise<UndoFn>
}
export class ObservableUndoController {
@observable.shallow undoStack: UndoContext[] = []
@observable.shallow redoStack: UndoContext[] = []
@computed get canUndo() { return this.undoStack.length > 0 }
@computed get canRedo() { return this.redoStack.length > 0 }
/**
* Executes and tracks an undoable operation.
*
* @param command Worker function, it should return another function that reverts the operation
* @returns Internal ID of command
*/
execute(command: Command): string;
/**
* Executes and tracks an undoable operation.
*
* @param label Name of operation (good for showing in undo tooltip or menu)
* @param command Worker function, it should return another function that reverts the operation
* @returns Internal ID of command
*/
execute(label: string, command: Command): string;
execute(labelOrCommand?: string | Command, commandOrId?: Command | string, optionalId?: string) {
const command = typeof labelOrCommand === 'function'
? labelOrCommand
: typeof commandOrId === 'function'
? commandOrId
: null
const label = typeof labelOrCommand === 'string'
? labelOrCommand
: 'Undo Operation'
const id = typeof optionalId === 'string'
? optionalId
: typeof commandOrId === 'string'
? commandOrId
: uid.generate()
if (command === null) throw new Error('Invalid undo operation function.')
const revert = command()
runInAction(() => this.undoStack.push({ id, label, command, revert }))
return id
}
undo() {
if (!this.canUndo) return false
runInAction(async () => {
const context = this.undoStack.pop()!
if (typeof context.revert === 'function')
context.revert()
else
(await context.revert)()
this.redoStack.push(context)
})
return true
}
redo() {
if (!this.canRedo) return false
runInAction(() => {
const context = this.redoStack.pop()!
//@ts-expect-error
this.execute(context.label, context.command, context.id)
})
return true
}
}
export default ObservableUndoController
export const uid = {
lastValue: 0,
lastId: '0',
generate(radix = 36) {
let now = Date.now()
// Ensure no duplicates (locally)
while (now <= uid.lastValue) {
now += 1
}
uid.lastValue = now
uid.lastId = now.toString(radix)
return uid.lastId
}
}
export default uid
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment