fixed undo/redo issues and implemented ability to manually flush the UndoManager
This commit is contained in:
		
							parent
							
								
									db5312443e
								
							
						
					
					
						commit
						967903673b
					
				@ -20,7 +20,7 @@ let quill = new Quill('#quill-container', {
 | 
				
			|||||||
      [{ header: [1, 2, false] }],
 | 
					      [{ header: [1, 2, false] }],
 | 
				
			||||||
      ['bold', 'italic', 'underline'],
 | 
					      ['bold', 'italic', 'underline'],
 | 
				
			||||||
      ['image', 'code-block'],
 | 
					      ['image', 'code-block'],
 | 
				
			||||||
      [{ color: [] }, { background: [] }],    // Snow theme fills in values
 | 
					      [{ color: [] }, { background: [] }], // Snow theme fills in values
 | 
				
			||||||
      [{ script: 'sub' }, { script: 'super' }],
 | 
					      [{ script: 'sub' }, { script: 'super' }],
 | 
				
			||||||
      ['link', 'image'],
 | 
					      ['link', 'image'],
 | 
				
			||||||
      ['link', 'code-block'],
 | 
					      ['link', 'code-block'],
 | 
				
			||||||
@ -31,7 +31,7 @@ let quill = new Quill('#quill-container', {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  placeholder: 'Compose an epic...',
 | 
					  placeholder: 'Compose an epic...',
 | 
				
			||||||
  theme: 'snow'  // or 'bubble'
 | 
					  theme: 'snow' // or 'bubble'
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let cursors = quill.getModule('cursors')
 | 
					let cursors = quill.getModule('cursors')
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@ let quill = new Quill('#quill-container', {
 | 
				
			|||||||
      [{ header: [1, 2, false] }],
 | 
					      [{ header: [1, 2, false] }],
 | 
				
			||||||
      ['bold', 'italic', 'underline'],
 | 
					      ['bold', 'italic', 'underline'],
 | 
				
			||||||
      ['image', 'code-block'],
 | 
					      ['image', 'code-block'],
 | 
				
			||||||
      [{ color: [] }, { background: [] }],    // Snow theme fills in values
 | 
					      [{ color: [] }, { background: [] }], // Snow theme fills in values
 | 
				
			||||||
      [{ script: 'sub' }, { script: 'super' }],
 | 
					      [{ script: 'sub' }, { script: 'super' }],
 | 
				
			||||||
      ['link', 'image'],
 | 
					      ['link', 'image'],
 | 
				
			||||||
      ['link', 'code-block'],
 | 
					      ['link', 'code-block'],
 | 
				
			||||||
@ -21,7 +21,7 @@ let quill = new Quill('#quill-container', {
 | 
				
			|||||||
    ]
 | 
					    ]
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  placeholder: 'Compose an epic...',
 | 
					  placeholder: 'Compose an epic...',
 | 
				
			||||||
  theme: 'snow'  // or 'bubble'
 | 
					  theme: 'snow' // or 'bubble'
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let yText = y.define('quill', Y.Text)
 | 
					let yText = y.define('quill', Y.Text)
 | 
				
			||||||
 | 
				
			|||||||
@ -35,7 +35,7 @@ Y({
 | 
				
			|||||||
      toolbar: [
 | 
					      toolbar: [
 | 
				
			||||||
        [{ size: ['small', false, 'large', 'huge'] }],
 | 
					        [{ size: ['small', false, 'large', 'huge'] }],
 | 
				
			||||||
        ['bold', 'italic', 'underline'],
 | 
					        ['bold', 'italic', 'underline'],
 | 
				
			||||||
        [{ color: [] }, { background: [] }],    // Snow theme fills in values
 | 
					        [{ color: [] }, { background: [] }], // Snow theme fills in values
 | 
				
			||||||
        [{ script: 'sub' }, { script: 'super' }],
 | 
					        [{ script: 'sub' }, { script: 'super' }],
 | 
				
			||||||
        ['link', 'image'],
 | 
					        ['link', 'image'],
 | 
				
			||||||
        ['link', 'code-block'],
 | 
					        ['link', 'code-block'],
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,9 @@
 | 
				
			|||||||
/* global MutationObserver */
 | 
					/* global MutationObserver, getSelection */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { fromRelativePosition } from '../../Util/relativePosition.js'
 | 
				
			||||||
import Binding from '../Binding.js'
 | 
					import Binding from '../Binding.js'
 | 
				
			||||||
import { createAssociation, removeAssociation } from './util.js'
 | 
					import { createAssociation, removeAssociation } from './util.js'
 | 
				
			||||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js'
 | 
					import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js'
 | 
				
			||||||
import { defaultFilter, applyFilterOnType } from './filter.js'
 | 
					import { defaultFilter, applyFilterOnType } from './filter.js'
 | 
				
			||||||
import typeObserver from './typeObserver.js'
 | 
					import typeObserver from './typeObserver.js'
 | 
				
			||||||
import domObserver from './domObserver.js'
 | 
					import domObserver from './domObserver.js'
 | 
				
			||||||
@ -67,16 +68,25 @@ export default class DomBinding extends Binding {
 | 
				
			|||||||
      characterData: true,
 | 
					      characterData: true,
 | 
				
			||||||
      subtree: true
 | 
					      subtree: true
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					    this._currentSel = null
 | 
				
			||||||
 | 
					    document.addEventListener('selectionchange', () => {
 | 
				
			||||||
 | 
					      this._currentSel = getCurrentRelativeSelection(this)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
    const y = type._y
 | 
					    const y = type._y
 | 
				
			||||||
 | 
					    this.y = y
 | 
				
			||||||
    // Force flush dom changes before Type changes are applied (they might
 | 
					    // Force flush dom changes before Type changes are applied (they might
 | 
				
			||||||
    // modify the dom)
 | 
					    // modify the dom)
 | 
				
			||||||
    this._beforeTransactionHandler = (y, transaction, remote) => {
 | 
					    this._beforeTransactionHandler = (y, transaction, remote) => {
 | 
				
			||||||
      this._domObserver(this._mutationObserver.takeRecords())
 | 
					      this._domObserver(this._mutationObserver.takeRecords())
 | 
				
			||||||
      beforeTransactionSelectionFixer(y, this, transaction, remote)
 | 
					      this._mutualExclude(() => {
 | 
				
			||||||
 | 
					        beforeTransactionSelectionFixer(this, remote)
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    y.on('beforeTransaction', this._beforeTransactionHandler)
 | 
					    y.on('beforeTransaction', this._beforeTransactionHandler)
 | 
				
			||||||
    this._afterTransactionHandler = (y, transaction, remote) => {
 | 
					    this._afterTransactionHandler = (y, transaction, remote) => {
 | 
				
			||||||
      afterTransactionSelectionFixer(y, this, transaction, remote)
 | 
					      this._mutualExclude(() => {
 | 
				
			||||||
 | 
					        afterTransactionSelectionFixer(this, remote)
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
      // remove associations
 | 
					      // remove associations
 | 
				
			||||||
      // TODO: this could be done more efficiently
 | 
					      // TODO: this could be done more efficiently
 | 
				
			||||||
      // e.g. Always delete using the following approach, or removeAssociation
 | 
					      // e.g. Always delete using the following approach, or removeAssociation
 | 
				
			||||||
@ -115,6 +125,67 @@ export default class DomBinding extends Binding {
 | 
				
			|||||||
    // TODO: apply filter to all elements
 | 
					    // TODO: apply filter to all elements
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  _getUndoStackInfo () {
 | 
				
			||||||
 | 
					    return this.getSelection()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  _restoreUndoStackInfo (info) {
 | 
				
			||||||
 | 
					    this.restoreSelection(info)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getSelection () {
 | 
				
			||||||
 | 
					    return this._currentSel
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  restoreSelection (selection) {
 | 
				
			||||||
 | 
					    if (selection !== null) {
 | 
				
			||||||
 | 
					      const { to, from } = selection
 | 
				
			||||||
 | 
					      let shouldUpdate = false
 | 
				
			||||||
 | 
					      /**
 | 
				
			||||||
 | 
					       * There is little information on the difference between anchor/focus and base/extent.
 | 
				
			||||||
 | 
					       * MDN doesn't even mention base/extent anymore.. though you still have to call
 | 
				
			||||||
 | 
					       * setBaseAndExtent to change the selection..
 | 
				
			||||||
 | 
					       * I can observe that base/extend refer to notes higher up in the xml hierachy.
 | 
				
			||||||
 | 
					       * Espesially for undo/redo this is preferred. If this becomes a problem in the future,
 | 
				
			||||||
 | 
					       * we should probably go back to anchor/focus.
 | 
				
			||||||
 | 
					       */
 | 
				
			||||||
 | 
					      const browserSelection = getSelection()
 | 
				
			||||||
 | 
					      let { baseNode, baseOffset, extentNode, extentOffset } = browserSelection
 | 
				
			||||||
 | 
					      if (from !== null) {
 | 
				
			||||||
 | 
					        let sel = fromRelativePosition(this.y, from)
 | 
				
			||||||
 | 
					        if (sel !== null) {
 | 
				
			||||||
 | 
					          let node = this.typeToDom.get(sel.type)
 | 
				
			||||||
 | 
					          let offset = sel.offset
 | 
				
			||||||
 | 
					          if (node !== baseNode || offset !== baseOffset) {
 | 
				
			||||||
 | 
					            baseNode = node
 | 
				
			||||||
 | 
					            baseOffset = offset
 | 
				
			||||||
 | 
					            shouldUpdate = true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (to !== null) {
 | 
				
			||||||
 | 
					        let sel = fromRelativePosition(this.y, to)
 | 
				
			||||||
 | 
					        if (sel !== null) {
 | 
				
			||||||
 | 
					          let node = this.typeToDom.get(sel.type)
 | 
				
			||||||
 | 
					          let offset = sel.offset
 | 
				
			||||||
 | 
					          if (node !== extentNode || offset !== extentOffset) {
 | 
				
			||||||
 | 
					            extentNode = node
 | 
				
			||||||
 | 
					            extentOffset = offset
 | 
				
			||||||
 | 
					            shouldUpdate = true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (shouldUpdate) {
 | 
				
			||||||
 | 
					        browserSelection.setBaseAndExtent(
 | 
				
			||||||
 | 
					          baseNode,
 | 
				
			||||||
 | 
					          baseOffset,
 | 
				
			||||||
 | 
					          extentNode,
 | 
				
			||||||
 | 
					          extentOffset
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Remove all properties that are handled by this class.
 | 
					   * Remove all properties that are handled by this class.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@ -130,11 +201,10 @@ export default class DomBinding extends Binding {
 | 
				
			|||||||
    super.destroy()
 | 
					    super.destroy()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
  /**
 | 
					 * A filter defines which elements and attributes to share.
 | 
				
			||||||
   * A filter defines which elements and attributes to share.
 | 
					 * Return null if the node should be filtered. Otherwise return the Map of
 | 
				
			||||||
   * Return null if the node should be filtered. Otherwise return the Map of
 | 
					 * accepted attributes.
 | 
				
			||||||
   * accepted attributes.
 | 
					 *
 | 
				
			||||||
   *
 | 
					 * @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction
 | 
				
			||||||
   * @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction
 | 
					 */
 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,84 +1,35 @@
 | 
				
			|||||||
/* globals getSelection */
 | 
					/* globals getSelection */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
 | 
					import { getRelativePosition } from '../../Util/relativePosition.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let browserSelection = null
 | 
					 | 
				
			||||||
let relativeSelection = null
 | 
					let relativeSelection = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					function _getCurrentRelativeSelection (domBinding) {
 | 
				
			||||||
 * @private
 | 
					  const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
 | 
				
			||||||
 */
 | 
					  const baseNodeType = domBinding.domToType.get(baseNode)
 | 
				
			||||||
export let beforeTransactionSelectionFixer
 | 
					  const extentNodeType = domBinding.domToType.get(extentNode)
 | 
				
			||||||
if (typeof getSelection !== 'undefined') {
 | 
					  if (baseNodeType !== undefined && extentNodeType !== undefined) {
 | 
				
			||||||
  beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, domBinding, transaction, remote) {
 | 
					    return {
 | 
				
			||||||
    if (!remote) {
 | 
					      from: getRelativePosition(baseNodeType, baseOffset),
 | 
				
			||||||
      return
 | 
					      to: getRelativePosition(extentNodeType, extentOffset)
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    relativeSelection = { from: null, to: null, fromY: null, toY: null }
 | 
					 | 
				
			||||||
    browserSelection = getSelection()
 | 
					 | 
				
			||||||
    const anchorNode = browserSelection.anchorNode
 | 
					 | 
				
			||||||
    const anchorNodeType = domBinding.domToType.get(anchorNode)
 | 
					 | 
				
			||||||
    if (anchorNode !== null && anchorNodeType !== undefined) {
 | 
					 | 
				
			||||||
      relativeSelection.from = getRelativePosition(anchorNodeType, browserSelection.anchorOffset)
 | 
					 | 
				
			||||||
      relativeSelection.fromY = anchorNodeType._y
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const focusNode = browserSelection.focusNode
 | 
					 | 
				
			||||||
    const focusNodeType = domBinding.domToType.get(focusNode)
 | 
					 | 
				
			||||||
    if (focusNode !== null && focusNodeType !== undefined) {
 | 
					 | 
				
			||||||
      relativeSelection.to = getRelativePosition(focusNodeType, browserSelection.focusOffset)
 | 
					 | 
				
			||||||
      relativeSelection.toY = focusNodeType._y
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
} else {
 | 
					  return null
 | 
				
			||||||
  beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : () => null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function beforeTransactionSelectionFixer (domBinding, remote) {
 | 
				
			||||||
 | 
					  if (remote) {
 | 
				
			||||||
 | 
					    relativeSelection = getCurrentRelativeSelection(domBinding)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function afterTransactionSelectionFixer (y, domBinding, transaction, remote) {
 | 
					export function afterTransactionSelectionFixer (domBinding, remote) {
 | 
				
			||||||
  if (relativeSelection === null || !remote) {
 | 
					  if (relativeSelection !== null && remote) {
 | 
				
			||||||
    return
 | 
					    domBinding.restoreSelection(relativeSelection)
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  const to = relativeSelection.to
 | 
					 | 
				
			||||||
  const from = relativeSelection.from
 | 
					 | 
				
			||||||
  const fromY = relativeSelection.fromY
 | 
					 | 
				
			||||||
  const toY = relativeSelection.toY
 | 
					 | 
				
			||||||
  let shouldUpdate = false
 | 
					 | 
				
			||||||
  let anchorNode = browserSelection.anchorNode
 | 
					 | 
				
			||||||
  let anchorOffset = browserSelection.anchorOffset
 | 
					 | 
				
			||||||
  let focusNode = browserSelection.focusNode
 | 
					 | 
				
			||||||
  let focusOffset = browserSelection.focusOffset
 | 
					 | 
				
			||||||
  if (from !== null) {
 | 
					 | 
				
			||||||
    let sel = fromRelativePosition(fromY, from)
 | 
					 | 
				
			||||||
    if (sel !== null) {
 | 
					 | 
				
			||||||
      let node = domBinding.typeToDom.get(sel.type)
 | 
					 | 
				
			||||||
      let offset = sel.offset
 | 
					 | 
				
			||||||
      if (node !== anchorNode || offset !== anchorOffset) {
 | 
					 | 
				
			||||||
        anchorNode = node
 | 
					 | 
				
			||||||
        anchorOffset = offset
 | 
					 | 
				
			||||||
        shouldUpdate = true
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  if (to !== null) {
 | 
					 | 
				
			||||||
    let sel = fromRelativePosition(toY, to)
 | 
					 | 
				
			||||||
    if (sel !== null) {
 | 
					 | 
				
			||||||
      let node = domBinding.typeToDom.get(sel.type)
 | 
					 | 
				
			||||||
      let offset = sel.offset
 | 
					 | 
				
			||||||
      if (node !== focusNode || offset !== focusOffset) {
 | 
					 | 
				
			||||||
        focusNode = node
 | 
					 | 
				
			||||||
        focusOffset = offset
 | 
					 | 
				
			||||||
        shouldUpdate = true
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  if (shouldUpdate) {
 | 
					 | 
				
			||||||
    browserSelection.setBaseAndExtent(
 | 
					 | 
				
			||||||
      anchorNode,
 | 
					 | 
				
			||||||
      anchorOffset,
 | 
					 | 
				
			||||||
      focusNode,
 | 
					 | 
				
			||||||
      focusOffset
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -120,7 +120,7 @@ export default class Item {
 | 
				
			|||||||
   *
 | 
					   *
 | 
				
			||||||
   * @private
 | 
					   * @private
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  _redo (y) {
 | 
					  _redo (y, redoitems) {
 | 
				
			||||||
    if (this._redone !== null) {
 | 
					    if (this._redone !== null) {
 | 
				
			||||||
      return this._redone
 | 
					      return this._redone
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -130,7 +130,10 @@ export default class Item {
 | 
				
			|||||||
    let parent = this._parent
 | 
					    let parent = this._parent
 | 
				
			||||||
    // make sure that parent is redone
 | 
					    // make sure that parent is redone
 | 
				
			||||||
    if (parent._deleted === true && parent._redone === null) {
 | 
					    if (parent._deleted === true && parent._redone === null) {
 | 
				
			||||||
      parent._redo(y)
 | 
					      // try to undo parent if it will be undone anyway
 | 
				
			||||||
 | 
					      if (!redoitems.has(parent) || !parent._redo(y, redoitems)) {
 | 
				
			||||||
 | 
					        return false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (parent._redone !== null) {
 | 
					    if (parent._redone !== null) {
 | 
				
			||||||
      parent = parent._redone
 | 
					      parent = parent._redone
 | 
				
			||||||
@ -157,7 +160,7 @@ export default class Item {
 | 
				
			|||||||
    struct._parentSub = this._parentSub
 | 
					    struct._parentSub = this._parentSub
 | 
				
			||||||
    struct._integrate(y)
 | 
					    struct._integrate(y)
 | 
				
			||||||
    this._redone = struct
 | 
					    this._redone = struct
 | 
				
			||||||
    return struct
 | 
					    return true
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
 | 
				
			|||||||
@ -254,7 +254,7 @@ function deleteText (y, length, parent, left, right, currentAttributes) {
 | 
				
			|||||||
 * @typedef {Array<Object>} Delta
 | 
					 * @typedef {Array<Object>} Delta
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 /**
 | 
					/**
 | 
				
			||||||
  * Attributes that can be assigned to a selection of text.
 | 
					  * Attributes that can be assigned to a selection of text.
 | 
				
			||||||
  *
 | 
					  *
 | 
				
			||||||
  * @example
 | 
					  * @example
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import ID from './ID/ID.js'
 | 
				
			|||||||
import isParentOf from './isParentOf.js'
 | 
					import isParentOf from './isParentOf.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReverseOperation {
 | 
					class ReverseOperation {
 | 
				
			||||||
  constructor (y, transaction) {
 | 
					  constructor (y, transaction, bindingInfos) {
 | 
				
			||||||
    this.created = new Date()
 | 
					    this.created = new Date()
 | 
				
			||||||
    const beforeState = transaction.beforeState
 | 
					    const beforeState = transaction.beforeState
 | 
				
			||||||
    if (beforeState.has(y.userID)) {
 | 
					    if (beforeState.has(y.userID)) {
 | 
				
			||||||
@ -12,15 +12,26 @@ class ReverseOperation {
 | 
				
			|||||||
      this.toState = null
 | 
					      this.toState = null
 | 
				
			||||||
      this.fromState = null
 | 
					      this.fromState = null
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this.deletedStructs = transaction.deletedStructs
 | 
					    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) {
 | 
					function applyReverseOperation (y, scope, reverseBuffer) {
 | 
				
			||||||
  let performedUndo = false
 | 
					  let performedUndo = false
 | 
				
			||||||
 | 
					  let undoOp
 | 
				
			||||||
  y.transact(() => {
 | 
					  y.transact(() => {
 | 
				
			||||||
    while (!performedUndo && reverseBuffer.length > 0) {
 | 
					    while (!performedUndo && reverseBuffer.length > 0) {
 | 
				
			||||||
      let undoOp = reverseBuffer.pop()
 | 
					      undoOp = reverseBuffer.pop()
 | 
				
			||||||
      // make sure that it is possible to iterate {from}-{to}
 | 
					      // make sure that it is possible to iterate {from}-{to}
 | 
				
			||||||
      if (undoOp.fromState !== null) {
 | 
					      if (undoOp.fromState !== null) {
 | 
				
			||||||
        y.os.getItemCleanStart(undoOp.fromState)
 | 
					        y.os.getItemCleanStart(undoOp.fromState)
 | 
				
			||||||
@ -35,23 +46,39 @@ function applyReverseOperation (y, scope, reverseBuffer) {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      for (let op of undoOp.deletedStructs) {
 | 
					      const redoitems = new Set()
 | 
				
			||||||
        if (
 | 
					      for (let del of undoOp.deletedStructs) {
 | 
				
			||||||
          isParentOf(scope, op) &&
 | 
					        const fromState = del.from
 | 
				
			||||||
          op._parent !== y &&
 | 
					        const toState = new ID(fromState.user, fromState.clock + del.len - 1)
 | 
				
			||||||
          (
 | 
					        y.os.getItemCleanStart(fromState)
 | 
				
			||||||
            op._id.user !== y.userID ||
 | 
					        y.os.getItemCleanEnd(toState)
 | 
				
			||||||
            undoOp.fromState === null ||
 | 
					        y.os.iterate(fromState, toState, op => {
 | 
				
			||||||
            op._id.clock < undoOp.fromState.clock ||
 | 
					          if (
 | 
				
			||||||
            op._id.clock > undoOp.toState.clock
 | 
					            isParentOf(scope, op) &&
 | 
				
			||||||
          )
 | 
					            op._parent !== y &&
 | 
				
			||||||
        ) {
 | 
					            (
 | 
				
			||||||
          performedUndo = true
 | 
					              op._id.user !== y.userID ||
 | 
				
			||||||
          op._redo(y)
 | 
					              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) {
 | 
				
			||||||
 | 
					    // should be performed after the undo transaction
 | 
				
			||||||
 | 
					    undoOp.bindingInfos.forEach((info, binding) => {
 | 
				
			||||||
 | 
					      binding._restoreUndoStackInfo(info)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  return performedUndo
 | 
					  return performedUndo
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -66,6 +93,7 @@ export default class UndoManager {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  constructor (scope, options = {}) {
 | 
					  constructor (scope, options = {}) {
 | 
				
			||||||
    this.options = options
 | 
					    this.options = options
 | 
				
			||||||
 | 
					    this._bindings = new Set(options.bindings)
 | 
				
			||||||
    options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
 | 
					    options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
 | 
				
			||||||
    this._undoBuffer = []
 | 
					    this._undoBuffer = []
 | 
				
			||||||
    this._redoBuffer = []
 | 
					    this._redoBuffer = []
 | 
				
			||||||
@ -76,16 +104,28 @@ export default class UndoManager {
 | 
				
			|||||||
    const y = scope._y
 | 
					    const y = scope._y
 | 
				
			||||||
    this.y = y
 | 
					    this.y = y
 | 
				
			||||||
    y._hasUndoManager = true
 | 
					    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) => {
 | 
					    y.on('afterTransaction', (y, transaction, remote) => {
 | 
				
			||||||
      if (!remote && transaction.changedParentTypes.has(scope)) {
 | 
					      if (!remote && transaction.changedParentTypes.has(scope)) {
 | 
				
			||||||
        let reverseOperation = new ReverseOperation(y, transaction)
 | 
					        let reverseOperation = new ReverseOperation(y, transaction, bindingInfos)
 | 
				
			||||||
        if (!this._undoing) {
 | 
					        if (!this._undoing) {
 | 
				
			||||||
          let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null
 | 
					          let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null
 | 
				
			||||||
          if (
 | 
					          if (
 | 
				
			||||||
            this._redoing === false &&
 | 
					            this._redoing === false &&
 | 
				
			||||||
            this._lastTransactionWasUndo === false &&
 | 
					            this._lastTransactionWasUndo === false &&
 | 
				
			||||||
            lastUndoOp !== null &&
 | 
					            lastUndoOp !== null &&
 | 
				
			||||||
            reverseOperation.created - lastUndoOp.created <= options.captureTimeout
 | 
					            (options.captureTimeout < 0 || reverseOperation.created - lastUndoOp.created <= options.captureTimeout)
 | 
				
			||||||
          ) {
 | 
					          ) {
 | 
				
			||||||
            lastUndoOp.created = reverseOperation.created
 | 
					            lastUndoOp.created = reverseOperation.created
 | 
				
			||||||
            if (reverseOperation.toState !== null) {
 | 
					            if (reverseOperation.toState !== null) {
 | 
				
			||||||
@ -110,6 +150,13 @@ export default class UndoManager {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 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 the last locally created change.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
 | 
				
			|||||||
@ -76,7 +76,10 @@ export function fromRelativePosition (y, rpos) {
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      id = new RootID(rpos[3], rpos[4])
 | 
					      id = new RootID(rpos[3], rpos[4])
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const type = y.os.get(id)
 | 
					    let type = y.os.get(id)
 | 
				
			||||||
 | 
					    while (type._redone !== null) {
 | 
				
			||||||
 | 
					      type = type._redone
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (type === null || type.constructor === GC) {
 | 
					    if (type === null || type.constructor === GC) {
 | 
				
			||||||
      return null
 | 
					      return null
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -87,12 +90,16 @@ export function fromRelativePosition (y, rpos) {
 | 
				
			|||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    let offset = 0
 | 
					    let offset = 0
 | 
				
			||||||
    let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val
 | 
					    let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val
 | 
				
			||||||
 | 
					    const diff = rpos[1] - struct._id.clock
 | 
				
			||||||
 | 
					    while (struct._redone !== null) {
 | 
				
			||||||
 | 
					      struct = struct._redone
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    const parent = struct._parent
 | 
					    const parent = struct._parent
 | 
				
			||||||
    if (struct.constructor === GC || parent._deleted) {
 | 
					    if (struct.constructor === GC || parent._deleted) {
 | 
				
			||||||
      return null
 | 
					      return null
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (!struct._deleted) {
 | 
					    if (!struct._deleted) {
 | 
				
			||||||
      offset = rpos[1] - struct._id.clock
 | 
					      offset = diff
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    struct = struct._left
 | 
					    struct = struct._left
 | 
				
			||||||
    while (struct !== null) {
 | 
					    while (struct !== null) {
 | 
				
			||||||
 | 
				
			|||||||
@ -111,7 +111,7 @@ function compareEvent (t, is, should) {
 | 
				
			|||||||
    t.assert(
 | 
					    t.assert(
 | 
				
			||||||
      should[key] === is[key] ||
 | 
					      should[key] === is[key] ||
 | 
				
			||||||
      JSON.stringify(should[key]) === JSON.stringify(is[key])
 | 
					      JSON.stringify(should[key]) === JSON.stringify(is[key])
 | 
				
			||||||
    , 'event works as expected'
 | 
					      , 'event works as expected'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user