181 lines
5.7 KiB
JavaScript
181 lines
5.7 KiB
JavaScript
|
|
import * as ID from './ID.mjs'
|
|
import { isParentOf } from './isParentOf.mjs'
|
|
|
|
class ReverseOperation {
|
|
constructor (y, transaction, bindingInfos) {
|
|
this.created = new Date()
|
|
const beforeState = transaction.beforeState
|
|
if (beforeState.has(y.userID)) {
|
|
this.toState = ID.createID(y.userID, y.ss.getState(y.userID) - 1)
|
|
this.fromState = ID.createID(y.userID, beforeState.get(y.userID))
|
|
} else {
|
|
this.toState = null
|
|
this.fromState = null
|
|
}
|
|
this.deletedStructs = new Set()
|
|
transaction.deletedStructs.forEach(struct => {
|
|
this.deletedStructs.add({
|
|
from: struct._id,
|
|
len: struct._length
|
|
})
|
|
})
|
|
/**
|
|
* Maps from binding to binding information (e.g. cursor information)
|
|
*/
|
|
this.bindingInfos = bindingInfos
|
|
}
|
|
}
|
|
|
|
function applyReverseOperation (y, scope, reverseBuffer) {
|
|
let performedUndo = false
|
|
let undoOp = null
|
|
y.transact(() => {
|
|
while (!performedUndo && reverseBuffer.length > 0) {
|
|
undoOp = reverseBuffer.pop()
|
|
// make sure that it is possible to iterate {from}-{to}
|
|
if (undoOp.fromState !== null) {
|
|
y.os.getItemCleanStart(undoOp.fromState)
|
|
y.os.getItemCleanEnd(undoOp.toState)
|
|
y.os.iterate(undoOp.fromState, undoOp.toState, op => {
|
|
while (op._deleted && op._redone !== null) {
|
|
op = op._redone
|
|
}
|
|
if (op._deleted === false && isParentOf(scope, op)) {
|
|
performedUndo = true
|
|
op._delete(y)
|
|
}
|
|
})
|
|
}
|
|
const redoitems = new Set()
|
|
for (let del of undoOp.deletedStructs) {
|
|
const fromState = del.from
|
|
const toState = ID.createID(fromState.user, fromState.clock + del.len - 1)
|
|
y.os.getItemCleanStart(fromState)
|
|
y.os.getItemCleanEnd(toState)
|
|
y.os.iterate(fromState, toState, op => {
|
|
if (
|
|
isParentOf(scope, op) &&
|
|
op._parent !== y &&
|
|
(
|
|
op._id.user !== y.userID ||
|
|
undoOp.fromState === null ||
|
|
op._id.clock < undoOp.fromState.clock ||
|
|
op._id.clock > undoOp.toState.clock
|
|
)
|
|
) {
|
|
redoitems.add(op)
|
|
}
|
|
})
|
|
}
|
|
redoitems.forEach(op => {
|
|
const opUndone = op._redo(y, redoitems)
|
|
performedUndo = performedUndo || opUndone
|
|
})
|
|
}
|
|
})
|
|
if (performedUndo && undoOp !== null) {
|
|
// should be performed after the undo transaction
|
|
undoOp.bindingInfos.forEach((info, binding) => {
|
|
binding._restoreUndoStackInfo(info)
|
|
})
|
|
}
|
|
return performedUndo
|
|
}
|
|
|
|
/**
|
|
* Saves a history of locally applied operations. The UndoManager handles the
|
|
* undoing and redoing of locally created changes.
|
|
*/
|
|
export class UndoManager {
|
|
/**
|
|
* @param {YType} scope The scope on which to listen for changes.
|
|
* @param {Object} options Optionally provided configuration.
|
|
*/
|
|
constructor (scope, options = {}) {
|
|
this.options = options
|
|
this._bindings = new Set(options.bindings)
|
|
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
|
|
this._undoBuffer = []
|
|
this._redoBuffer = []
|
|
this._scope = scope
|
|
this._undoing = false
|
|
this._redoing = false
|
|
this._lastTransactionWasUndo = false
|
|
const y = scope._y
|
|
this.y = y
|
|
y._hasUndoManager = true
|
|
let bindingInfos
|
|
y.on('beforeTransaction', (y, transaction, remote) => {
|
|
if (!remote) {
|
|
// Store binding information before transaction is executed
|
|
// By restoring the binding information, we can make sure that the state
|
|
// before the transaction can be recovered
|
|
bindingInfos = new Map()
|
|
this._bindings.forEach(binding => {
|
|
bindingInfos.set(binding, binding._getUndoStackInfo())
|
|
})
|
|
}
|
|
})
|
|
y.on('afterTransaction', (y, transaction, remote) => {
|
|
if (!remote && transaction.changedParentTypes.has(scope)) {
|
|
let reverseOperation = new ReverseOperation(y, transaction, bindingInfos)
|
|
if (!this._undoing) {
|
|
let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null
|
|
if (
|
|
this._redoing === false &&
|
|
this._lastTransactionWasUndo === false &&
|
|
lastUndoOp !== null &&
|
|
(options.captureTimeout < 0 || reverseOperation.created - lastUndoOp.created <= options.captureTimeout)
|
|
) {
|
|
lastUndoOp.created = reverseOperation.created
|
|
if (reverseOperation.toState !== null) {
|
|
lastUndoOp.toState = reverseOperation.toState
|
|
if (lastUndoOp.fromState === null) {
|
|
lastUndoOp.fromState = reverseOperation.fromState
|
|
}
|
|
}
|
|
reverseOperation.deletedStructs.forEach(lastUndoOp.deletedStructs.add, lastUndoOp.deletedStructs)
|
|
} else {
|
|
this._lastTransactionWasUndo = false
|
|
this._undoBuffer.push(reverseOperation)
|
|
}
|
|
if (!this._redoing) {
|
|
this._redoBuffer = []
|
|
}
|
|
} else {
|
|
this._lastTransactionWasUndo = true
|
|
this._redoBuffer.push(reverseOperation)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Enforce that the next change is created as a separate item in the undo stack
|
|
*/
|
|
flushChanges () {
|
|
this._lastTransactionWasUndo = true
|
|
}
|
|
|
|
/**
|
|
* Undo the last locally created change.
|
|
*/
|
|
undo () {
|
|
this._undoing = true
|
|
const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer)
|
|
this._undoing = false
|
|
return performedUndo
|
|
}
|
|
|
|
/**
|
|
* Redo the last locally created change.
|
|
*/
|
|
redo () {
|
|
this._redoing = true
|
|
const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer)
|
|
this._redoing = false
|
|
return performedRedo
|
|
}
|
|
}
|