json-api.coffee 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. # API for JSON OT
  2. json = require './json' if typeof WEB is 'undefined'
  3. if WEB?
  4. extendDoc = exports.extendDoc
  5. exports.extendDoc = (name, fn) ->
  6. SubDoc::[name] = fn
  7. extendDoc name, fn
  8. depath = (path) ->
  9. if path.length == 1 and path[0].constructor == Array
  10. path[0]
  11. else path
  12. class SubDoc
  13. constructor: (@doc, @path) ->
  14. at: (path...) -> @doc.at @path.concat depath path
  15. get: -> @doc.getAt @path
  16. # for objects and lists
  17. set: (value, cb) -> @doc.setAt @path, value, cb
  18. # for strings and lists.
  19. insert: (pos, value, cb) -> @doc.insertAt @path, pos, value, cb
  20. # for strings
  21. del: (pos, length, cb) -> @doc.deleteTextAt @path, length, pos, cb
  22. # for objects and lists
  23. remove: (cb) -> @doc.removeAt @path, cb
  24. push: (value, cb) -> @insert @get().length, value, cb
  25. move: (from, to, cb) -> @doc.moveAt @path, from, to, cb
  26. add: (amount, cb) -> @doc.addAt @path, amount, cb
  27. on: (event, cb) -> @doc.addListener @path, event, cb
  28. removeListener: (l) -> @doc.removeListener l
  29. # text API compatibility
  30. getLength: -> @get().length
  31. getText: -> @get()
  32. traverse = (snapshot, path) ->
  33. container = data:snapshot
  34. key = 'data'
  35. elem = container
  36. for p in path
  37. elem = elem[key]
  38. key = p
  39. throw new Error 'bad path' if typeof elem == 'undefined'
  40. {elem, key}
  41. pathEquals = (p1, p2) ->
  42. return false if p1.length != p2.length
  43. for e,i in p1
  44. return false if e != p2[i]
  45. true
  46. json.api =
  47. provides: {json:true}
  48. at: (path...) -> new SubDoc this, depath path
  49. get: -> @snapshot
  50. set: (value, cb) -> @setAt [], value, cb
  51. getAt: (path) ->
  52. {elem, key} = traverse @snapshot, path
  53. return elem[key]
  54. setAt: (path, value, cb) ->
  55. {elem, key} = traverse @snapshot, path
  56. op = {p:path}
  57. if elem.constructor == Array
  58. op.li = value
  59. op.ld = elem[key] if typeof elem[key] != 'undefined'
  60. else if typeof elem == 'object'
  61. op.oi = value
  62. op.od = elem[key] if typeof elem[key] != 'undefined'
  63. else throw new Error 'bad path'
  64. @submitOp [op], cb
  65. removeAt: (path, cb) ->
  66. {elem, key} = traverse @snapshot, path
  67. throw new Error 'no element at that path' unless typeof elem[key] != 'undefined'
  68. op = {p:path}
  69. if elem.constructor == Array
  70. op.ld = elem[key]
  71. else if typeof elem == 'object'
  72. op.od = elem[key]
  73. else throw new Error 'bad path'
  74. @submitOp [op], cb
  75. insertAt: (path, pos, value, cb) ->
  76. {elem, key} = traverse @snapshot, path
  77. op = {p:path.concat pos}
  78. if elem[key].constructor == Array
  79. op.li = value
  80. else if typeof elem[key] == 'string'
  81. op.si = value
  82. @submitOp [op], cb
  83. moveAt: (path, from, to, cb) ->
  84. op = [{p:path.concat(from), lm:to}]
  85. @submitOp op, cb
  86. addAt: (path, amount, cb) ->
  87. op = [{p:path, na:amount}]
  88. @submitOp op, cb
  89. deleteTextAt: (path, length, pos, cb) ->
  90. {elem, key} = traverse @snapshot, path
  91. op = [{p:path.concat(pos), sd:elem[key][pos...(pos + length)]}]
  92. @submitOp op, cb
  93. addListener: (path, event, cb) ->
  94. l = {path, event, cb}
  95. @_listeners.push l
  96. l
  97. removeListener: (l) ->
  98. i = @_listeners.indexOf l
  99. return false if i < 0
  100. @_listeners.splice i, 1
  101. return true
  102. _register: ->
  103. @_listeners = []
  104. @on 'change', (op) ->
  105. for c in op
  106. if c.na != undefined or c.si != undefined or c.sd != undefined
  107. # no change to structure
  108. continue
  109. to_remove = []
  110. for l, i in @_listeners
  111. # Transform a dummy op by the incoming op to work out what
  112. # should happen to the listener.
  113. dummy = {p:l.path, na:0}
  114. xformed = @type.transformComponent [], dummy, c, 'left'
  115. if xformed.length == 0
  116. # The op was transformed to noop, so we should delete the listener.
  117. to_remove.push i
  118. else if xformed.length == 1
  119. # The op remained, so grab its new path into the listener.
  120. l.path = xformed[0].p
  121. else
  122. throw new Error "Bad assumption in json-api: xforming an 'si' op will always result in 0 or 1 components."
  123. to_remove.sort (a, b) -> b - a
  124. for i in to_remove
  125. @_listeners.splice i, 1
  126. @on 'remoteop', (op) ->
  127. for c in op
  128. match_path = if c.na == undefined then c.p[...c.p.length-1] else c.p
  129. for {path, event, cb} in @_listeners
  130. if pathEquals path, match_path
  131. switch event
  132. when 'insert'
  133. if c.li != undefined and c.ld == undefined
  134. cb(c.p[c.p.length-1], c.li)
  135. else if c.oi != undefined and c.od == undefined
  136. cb(c.p[c.p.length-1], c.oi)
  137. else if c.si != undefined
  138. cb(c.p[c.p.length-1], c.si)
  139. when 'delete'
  140. if c.li == undefined and c.ld != undefined
  141. cb(c.p[c.p.length-1], c.ld)
  142. else if c.oi == undefined and c.od != undefined
  143. cb(c.p[c.p.length-1], c.od)
  144. else if c.sd != undefined
  145. cb(c.p[c.p.length-1], c.sd)
  146. when 'replace'
  147. if c.li != undefined and c.ld != undefined
  148. cb(c.p[c.p.length-1], c.ld, c.li)
  149. else if c.oi != undefined and c.od != undefined
  150. cb(c.p[c.p.length-1], c.od, c.oi)
  151. when 'move'
  152. if c.lm != undefined
  153. cb(c.p[c.p.length-1], c.lm)
  154. when 'add'
  155. if c.na != undefined
  156. cb(c.na)
  157. else if (common = @type.commonPath match_path, path)?
  158. if event == 'child op'
  159. if match_path.length == path.length == common
  160. throw new Error "paths match length and have commonality, but aren't equal?"
  161. child_path = c.p[common+1..]
  162. cb(child_path, c)