mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-29 06:54:07 +00:00
180 lines
5.7 KiB
CoffeeScript
180 lines
5.7 KiB
CoffeeScript
# API for JSON OT
|
|
|
|
json = require './json' if typeof WEB is 'undefined'
|
|
|
|
if WEB?
|
|
extendDoc = exports.extendDoc
|
|
exports.extendDoc = (name, fn) ->
|
|
SubDoc::[name] = fn
|
|
extendDoc name, fn
|
|
|
|
depath = (path) ->
|
|
if path.length == 1 and path[0].constructor == Array
|
|
path[0]
|
|
else path
|
|
|
|
class SubDoc
|
|
constructor: (@doc, @path) ->
|
|
at: (path...) -> @doc.at @path.concat depath path
|
|
get: -> @doc.getAt @path
|
|
# for objects and lists
|
|
set: (value, cb) -> @doc.setAt @path, value, cb
|
|
# for strings and lists.
|
|
insert: (pos, value, cb) -> @doc.insertAt @path, pos, value, cb
|
|
# for strings
|
|
del: (pos, length, cb) -> @doc.deleteTextAt @path, length, pos, cb
|
|
# for objects and lists
|
|
remove: (cb) -> @doc.removeAt @path, cb
|
|
push: (value, cb) -> @insert @get().length, value, cb
|
|
move: (from, to, cb) -> @doc.moveAt @path, from, to, cb
|
|
add: (amount, cb) -> @doc.addAt @path, amount, cb
|
|
on: (event, cb) -> @doc.addListener @path, event, cb
|
|
removeListener: (l) -> @doc.removeListener l
|
|
|
|
# text API compatibility
|
|
getLength: -> @get().length
|
|
getText: -> @get()
|
|
|
|
traverse = (snapshot, path) ->
|
|
container = data:snapshot
|
|
key = 'data'
|
|
elem = container
|
|
for p in path
|
|
elem = elem[key]
|
|
key = p
|
|
throw new Error 'bad path' if typeof elem == 'undefined'
|
|
{elem, key}
|
|
|
|
pathEquals = (p1, p2) ->
|
|
return false if p1.length != p2.length
|
|
for e,i in p1
|
|
return false if e != p2[i]
|
|
true
|
|
|
|
json.api =
|
|
provides: {json:true}
|
|
|
|
at: (path...) -> new SubDoc this, depath path
|
|
|
|
get: -> @snapshot
|
|
set: (value, cb) -> @setAt [], value, cb
|
|
|
|
getAt: (path) ->
|
|
{elem, key} = traverse @snapshot, path
|
|
return elem[key]
|
|
|
|
setAt: (path, value, cb) ->
|
|
{elem, key} = traverse @snapshot, path
|
|
op = {p:path}
|
|
if elem.constructor == Array
|
|
op.li = value
|
|
op.ld = elem[key] if typeof elem[key] != 'undefined'
|
|
else if typeof elem == 'object'
|
|
op.oi = value
|
|
op.od = elem[key] if typeof elem[key] != 'undefined'
|
|
else throw new Error 'bad path'
|
|
@submitOp [op], cb
|
|
|
|
removeAt: (path, cb) ->
|
|
{elem, key} = traverse @snapshot, path
|
|
throw new Error 'no element at that path' unless typeof elem[key] != 'undefined'
|
|
op = {p:path}
|
|
if elem.constructor == Array
|
|
op.ld = elem[key]
|
|
else if typeof elem == 'object'
|
|
op.od = elem[key]
|
|
else throw new Error 'bad path'
|
|
@submitOp [op], cb
|
|
|
|
insertAt: (path, pos, value, cb) ->
|
|
{elem, key} = traverse @snapshot, path
|
|
op = {p:path.concat pos}
|
|
if elem[key].constructor == Array
|
|
op.li = value
|
|
else if typeof elem[key] == 'string'
|
|
op.si = value
|
|
@submitOp [op], cb
|
|
|
|
moveAt: (path, from, to, cb) ->
|
|
op = [{p:path.concat(from), lm:to}]
|
|
@submitOp op, cb
|
|
|
|
addAt: (path, amount, cb) ->
|
|
op = [{p:path, na:amount}]
|
|
@submitOp op, cb
|
|
|
|
deleteTextAt: (path, length, pos, cb) ->
|
|
{elem, key} = traverse @snapshot, path
|
|
op = [{p:path.concat(pos), sd:elem[key][pos...(pos + length)]}]
|
|
@submitOp op, cb
|
|
|
|
addListener: (path, event, cb) ->
|
|
l = {path, event, cb}
|
|
@_listeners.push l
|
|
l
|
|
removeListener: (l) ->
|
|
i = @_listeners.indexOf l
|
|
return false if i < 0
|
|
@_listeners.splice i, 1
|
|
return true
|
|
_register: ->
|
|
@_listeners = []
|
|
@on 'change', (op) ->
|
|
for c in op
|
|
if c.na != undefined or c.si != undefined or c.sd != undefined
|
|
# no change to structure
|
|
continue
|
|
to_remove = []
|
|
for l, i in @_listeners
|
|
# Transform a dummy op by the incoming op to work out what
|
|
# should happen to the listener.
|
|
dummy = {p:l.path, na:0}
|
|
xformed = @type.transformComponent [], dummy, c, 'left'
|
|
if xformed.length == 0
|
|
# The op was transformed to noop, so we should delete the listener.
|
|
to_remove.push i
|
|
else if xformed.length == 1
|
|
# The op remained, so grab its new path into the listener.
|
|
l.path = xformed[0].p
|
|
else
|
|
throw new Error "Bad assumption in json-api: xforming an 'si' op will always result in 0 or 1 components."
|
|
to_remove.sort (a, b) -> b - a
|
|
for i in to_remove
|
|
@_listeners.splice i, 1
|
|
@on 'remoteop', (op) ->
|
|
for c in op
|
|
match_path = if c.na == undefined then c.p[...c.p.length-1] else c.p
|
|
for {path, event, cb} in @_listeners
|
|
if pathEquals path, match_path
|
|
switch event
|
|
when 'insert'
|
|
if c.li != undefined and c.ld == undefined
|
|
cb(c.p[c.p.length-1], c.li)
|
|
else if c.oi != undefined and c.od == undefined
|
|
cb(c.p[c.p.length-1], c.oi)
|
|
else if c.si != undefined
|
|
cb(c.p[c.p.length-1], c.si)
|
|
when 'delete'
|
|
if c.li == undefined and c.ld != undefined
|
|
cb(c.p[c.p.length-1], c.ld)
|
|
else if c.oi == undefined and c.od != undefined
|
|
cb(c.p[c.p.length-1], c.od)
|
|
else if c.sd != undefined
|
|
cb(c.p[c.p.length-1], c.sd)
|
|
when 'replace'
|
|
if c.li != undefined and c.ld != undefined
|
|
cb(c.p[c.p.length-1], c.ld, c.li)
|
|
else if c.oi != undefined and c.od != undefined
|
|
cb(c.p[c.p.length-1], c.od, c.oi)
|
|
when 'move'
|
|
if c.lm != undefined
|
|
cb(c.p[c.p.length-1], c.lm)
|
|
when 'add'
|
|
if c.na != undefined
|
|
cb(c.na)
|
|
else if (common = @type.commonPath match_path, path)?
|
|
if event == 'child op'
|
|
if match_path.length == path.length == common
|
|
throw new Error "paths match length and have commonality, but aren't equal?"
|
|
child_path = c.p[common+1..]
|
|
cb(child_path, c)
|