all files / model/ DocumentChange.js

96.75% Statements 119/123
82.46% Branches 47/57
89.47% Functions 17/19
96.75% Lines 119/123
3 statements, 1 function Ignored     
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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246                4086× 162×   162×   162×   162×   162× 162×   162× 3924× 3924× 3924× 3924× 3924× 3924× 3924×         4086×   4086×   4086×               3915× 3915× 3915× 3915× 3915×     5016×     3582× 3582× 1391×   3582× 1391×   3582×         1434× 1434× 1422× 28× 1394×     1434×             3915× 5016× 5016× 3445× 3445×   5016× 137× 137×   5016× 1434×   1434×   5016×     3915× 21× 21× 21× 21×       21× 21×           3915× 121× 137× 137× 22×         3915× 3915× 3915×         161× 161×   161× 161× 161× 161× 161× 161× 1246×   161× 161×         214× 214×               217×                                   175×           1293×                 175× 175×   175× 175× 162×   175× 175× 162×   175×                   161× 161×     161× 161× 161×        
import { isPlainObject, clone, cloneDeep, forEach, map, uuid } from '../util'
import OperationSerializer from './OperationSerializer'
import ObjectOperation from './ObjectOperation'
import { fromJSON as selectionFromJSON } from './selectionHelpers'
 
class DocumentChange {
 
  constructor(ops, before, after) {
    if (arguments.length === 1 && isPlainObject(arguments[0])) {
      let data = arguments[0]
      // a unique id for the change
      this.sha = data.sha
      // when the change has been applied
      this.timestamp = data.timestamp
      // application state before the change was applied
      this.before = data.before || {}
      // array of operations
      this.ops = data.ops
      this.info = data.info; // custom change info
      // application state after the change was applied
      this.after = data.after || {}
    } else Eif (arguments.length === 3) {
      this.sha = uuid()
      this.info = {}
      this.timestamp = Date.now()
      this.ops = ops.slice(0)
      this.before = before || {}
      this.after = after || {}
    } else {
      throw new Error('Illegal arguments.')
    }
    // a hash with all updated properties
    this.updated = null
    // a hash with all created nodes
    this.created = null
    // a hash with all deleted nodes
    this.deleted = null
  }
 
  /*
    Extract aggregated information about which nodes and properties have been affected.
    This gets called by Document after applying the change.
  */
  _extractInformation(doc) {
    let ops = this.ops
    let created = {}
    let deleted = {}
    let updated = {}
    let affectedContainerAnnos = []
 
    // TODO: we will introduce a special operation type for coordinates
    function _checkAnnotation(op) {
      switch (op.type) {
        case "create":
        case "delete": {
          let node = op.val
          if (node.hasOwnProperty('start')) {
            updated[node.start.path] = true
          }
          if (node.hasOwnProperty('end')) {
            updated[node.end.path] = true
          }
          break
        }
        case "update":
        case "set": {
          // HACK: detecting annotation changes in an opportunistic way
          let node = doc.get(op.path[0])
          if (node) {
            if (node._isPropertyAnnotation) {
              updated[node.start.path] = true
            } else if (node._isContainerAnnotation) {
              affectedContainerAnnos.push(node)
            }
          }
          break
        }
        default:
          /* istanbul ignore next */
          throw new Error('Illegal state')
      }
    }
 
    for (let i = 0; i < ops.length; i++) {
      let op = ops[i]
      if (op.type === "create") {
        created[op.val.id] = op.val
        delete deleted[op.val.id]
      }
      if (op.type === "delete") {
        delete created[op.val.id]
        deleted[op.val.id] = op.val
      }
      if (op.type === "set" || op.type === "update") {
        updated[op.path] = true
        // also mark the node itself as dirty
        updated[op.path[0]] = true
      }
      _checkAnnotation(op)
    }
 
    affectedContainerAnnos.forEach(function(anno) {
      let container = doc.get(anno.containerId, 'strict')
      let startPos = container.getPosition(anno.start.path[0])
      let endPos = container.getPosition(anno.end.path[0])
      for (let pos = startPos; pos <= endPos; pos++) {
        let node = container.getChildAt(pos)
        let path
        Eif (node.isText()) {
          path = [node.id, 'content']
        } else {
          path = [node.id]
        }
        Eif (!deleted[node.id]) {
          updated[path] = true
        }
      }
    })
 
    // remove all deleted nodes from updated
    if(Object.keys(deleted).length > 0) {
      forEach(updated, function(_, key) {
        let nodeId = key.split(',')[0]
        if (deleted[nodeId]) {
          delete updated[key]
        }
      })
    }
 
    this.created = created
    this.deleted = deleted
    this.updated = updated
  }
 
  invert() {
    // shallow cloning this
    let copy = this.toJSON()
    copy.ops = []
    // swapping before and after
    let tmp = copy.before
    copy.before = copy.after
    copy.after = tmp
    let inverted = DocumentChange.fromJSON(copy)
    let ops = []
    for (let i = this.ops.length - 1; i >= 0; i--) {
      ops.push(this.ops[i].invert())
    }
    inverted.ops = ops
    return inverted
  }
 
  /* istanbul ignore start */
  isAffected(path) {
    console.error('DEPRECATED: use change.hasUpdated() instead')
    return this.hasUpdated(path)
  }
 
  isUpdated(path) {
    console.error('DEPRECATED: use change.hasUpdated() instead')
    return this.hasUpdated(path)
  }
  /* istanbul ignore end */
 
  hasUpdated(path) {
    return this.updated[path]
  }
 
  hasDeleted(id) {
    return this.deleted[id]
  }
 
  serialize() {
    // TODO serializers and deserializers should allow
    // for application data in 'after' and 'before'
 
    let opSerializer = new OperationSerializer()
    let data = this.toJSON()
    data.ops = this.ops.map(function(op) {
      return opSerializer.serialize(op)
    })
    return JSON.stringify(data)
  }
 
  clone() {
    return DocumentChange.fromJSON(this.toJSON())
  }
 
  toJSON() {
    let data = {
      // to identify this change
      sha: this.sha,
      // before state
      before: clone(this.before),
      ops: map(this.ops, function(op) {
        return op.toJSON()
      }),
      info: this.info,
      // after state
      after: clone(this.after),
    }
 
    // Just to make sure rich selection objects don't end up
    // in the JSON result
    data.after.selection = undefined
    data.before.selection = undefined
 
    let sel = this.before.selection
    if (sel && sel._isSelection) {
      data.before.selection = sel.toJSON()
    }
    sel = this.after.selection
    if (sel && sel._isSelection) {
      data.after.selection = sel.toJSON()
    }
    return data
  }
}
 
DocumentChange.deserialize = function(str) {
  let opSerializer = new OperationSerializer()
  let data = JSON.parse(str)
  data.ops = data.ops.map(function(opData) {
    return opSerializer.deserialize(opData)
  })
  Eif (data.before.selection) {
    data.before.selection = selectionFromJSON(data.before.selection)
  }
  Eif (data.after.selection) {
    data.after.selection = selectionFromJSON(data.after.selection)
  }
  return new DocumentChange(data)
}
 
DocumentChange.fromJSON = function(data) {
  // Don't write to original object on deserialization
  let change = cloneDeep(data)
  change.ops = data.ops.map(function(opData) {
    return ObjectOperation.fromJSON(opData)
  })
  change.before.selection = selectionFromJSON(data.before.selection)
  change.after.selection = selectionFromJSON(data.after.selection)
  return new DocumentChange(change)
}
 
export default DocumentChange