all files / model/ Transaction.js

87.23% Statements 41/47
78.57% Branches 11/14
66.67% Functions 6/9
91.11% Lines 41/45
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147                                                      206×       206× 206×     206× 206× 206×               550×         550× 550× 379×                             166×                                                                       168× 168× 168× 168× 168× 168× 168× 168× 168× 168×     166× 166× 157× 157× 157×   166×   168×   168× 168×   166×       168× 168× 168× 168× 168×          
import { isFunction } from '../util'
import DocumentChange from './DocumentChange'
import TransactionDocument from './TransactionDocument'
import EditingInterface from './EditingInterface'
 
/*
  A transaction for editing a document in an EditorSession.
 
  Wherever you see `tx`, it is an instance of this class.
 
  The transaction is used to manipulate the document in a 'turtle-graphics' style.
  For that it maintains an internal state consisting of an array of operations, a selection, and
  the current surface.
 
  Usually, at the beginning of a transaction, one Surface is focused. This is used to initialize
  the transaction state. Depending on the type of Surface or the type of the current selection,
  some manipulations are allowed or others are not: it is not possible to create a ContainerAnnotation without
  a ContainerSelection, or pasting a list of nodes into a TextPropertyEditor will strip the structure and just
  the text content.
 
*/
class Transaction extends EditingInterface {
 
  /*
    @param {Document} doc
  */
  constructor(doc, editorSession) {
    super()
 
    // TransactionDocument is essentially a clone of the document used to apply a sequence of document operations
    // without touching the original document
    this._stageDoc = this._document = new TransactionDocument(doc, this)
    this._editorSession = editorSession
 
    // internal state
    this._isTransacting = false
    this._state = 'idle'
    this._surface = null
  }
 
  dispose() {
    this._stageDoc.dispose()
  }
 
  setSelection(sel) {
    super.setSelection(sel)
 
    // NOTE: we might want to remove 'surfaceId' from selection, and instead
    // map surfaces to model paths. For the time being we keep it the old
    // way, but take it from the currently focused surface
    sel = this._selection
    if (!sel.isNull()) {
      if (!sel.surfaceId) {
        // TODO: We could check if the selection is valid within the given surface
        let surface = this._editorSession.getFocusedSurface()
        Eif (surface) {
          sel.surfaceId = surface.id
        } else {
          // TODO: instead of warning we could try to 'find' a suitable surface. However, this would also be a bit 'magical'
          console.warn('No focused surface. Selection will not be rendered.')
        }
      }
    }
  }
 
  // internal API
 
  // NOTE: ops are actually owned by TransactionDocument
  // we use the transaction document internally and not this instance
  get ops() {
    return this._stageDoc.ops
  }
  set ops(ops) {
    this._stageDoc.ops = ops
  }
 
  rollback() {
    this._stageDoc._rollback()
  }
 
  _apply(...args) {
    this._stageDoc._apply(...args)
  }
 
  // _ensureStarted() {
  //   if (this._state !== 'started') throw new Error('Transaction has not been started, or cancelled or saved already.')
  // }
 
  /**
    Start a transaction to manipulate the document
 
    @param {function} transformation a function(tx) that performs actions on the transaction document tx
 
    @example
 
    ```js
    doc.transaction(function(tx, args) {
      tx.update(...)
      ...
      return {
        selection: newSelection
      }
    })
    ```
  */
  _recordChange(transformation, selection) {
    // TODO: we could get rid of isTransacting and use this._state instead
    Iif (this._isTransacting) throw new Error('Nested transactions are not supported.')
    Iif (!isFunction(transformation)) throw new Error('Document.transaction() requires a transformation function.')
    this._isTransacting = true
    this._reset()
    this._state = 'started'
    let change
    try {
      this.setSelection(selection)
      let selBefore = this.getSelection()
      transformation(this, {
        selection: selBefore
      })
      let ops = this.ops
      if (ops.length > 0) {
        change = new DocumentChange(ops, this._before, this._after)
        change.before = { selection: selBefore }
        change.after = { selection: this.getSelection() }
      }
      this._state = 'finished'
    } finally {
      if (this._state !== 'finished') {
        this.rollback()
      }
      this._state = 'idle'
      this._isTransacting = false
    }
    return change
  }
 
  _reset() {
    this._before = {}
    this._after = {}
    this._stageDoc._reset()
    this._info = {}
    this.setSelection(null)
  }
}
 
export default Transaction