diff --git a/Makefile b/Makefile index acfc1591..67c668ef 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,8 @@ CLIENT = \ BUNDLED_TYPES = \ webclient/text.js \ lib/types/text-api.js \ - webclient/json0.js + webclient/json0.js \ + lib/types/json-api.js # Disabled: lib/types/json-api.coffee diff --git a/lib/types/json-api.coffee b/lib/types/json-api.coffee deleted file mode 100644 index a8e9e8d3..00000000 --- a/lib/types/json-api.coffee +++ /dev/null @@ -1,190 +0,0 @@ -# API for JSON OT - -_types = if typeof window == 'undefined' then require('ot-types') else window.ottypes - -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 - parent: -> if @path.length then @doc.at @path[...@path.length-1] else undefined - 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 - -_type = _types['http://sharejs.org/types/JSONv0'] -_type.api = - provides: {json:true} - - _fixComponentPaths: (c) -> - # no change to structure - return unless @_listeners - return if c.na != undefined or c.si != undefined or c.sd != undefined - 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 'na' 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 - - _fixPaths: (op) -> @_fixComponentPaths(c) for c in op - - _submit: (op, callback) -> - @_fixPaths op - @submitOp op, callback - - 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' - @_submit [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' - @_submit [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 - @_submit [op], cb - - moveAt: (path, from, to, cb) -> - op = [{p:path.concat(from), lm:to}] - @_submit op, cb - - addAt: (path, amount, cb) -> - op = [{p:path, na:amount}] - @_submit op, cb - - deleteTextAt: (path, length, pos, cb) -> - {elem, key} = traverse @snapshot, path - op = [{p:path.concat(pos), sd:elem[key][pos...(pos + length)]}] - @_submit op, cb - - addListener: (path, event, cb) -> - @_listeners ||= [] - - l = {path, event, cb} - @_listeners.push l - l - removeListener: (l) -> - return unless @_listeners - - i = @_listeners.indexOf l - return false if i < 0 - @_listeners.splice i, 1 - return true - - _onOp: (op) -> - for c in op - @_fixComponentPaths c - 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 _type.canOpAffectOp path, match_path - if event == 'child op' - child_path = c.p[path.length..] - cb(child_path, c) diff --git a/lib/types/json-api.js b/lib/types/json-api.js new file mode 100644 index 00000000..0da6bd33 --- /dev/null +++ b/lib/types/json-api.js @@ -0,0 +1,515 @@ +// JSON document API for the 'json0' type. + +(function() { + var __slice = [].slice; + var _types = typeof window === 'undefined' ? require('ottypes') : window.ottypes; + var _type = _types['http://sharejs.org/types/JSONv0']; + + // Helpers + + function depath(path) { + if (path.length === 1 && path[0].constructor === Array) { + return path[0]; + } else { + return path; + } + } + + function traverse(snapshot, path) { + var key = 'data'; + var elem = { data: snapshot }; + + for (var i = 0; i < path.length; i++) { + elem = elem[key]; + if (typeof elem === 'undefined') { + throw new Error('bad path'); + } + key = path[i]; + } + + return { + elem: elem, + key: key + }; + } + + function pathEquals(p1, p2) { + if (p1.length !== p2.length) { + return false; + } + for (var i = 0; i < p1.length; ++i) { + if (p1[i] !== p2[i]) { + return false; + } + } + return true; + } + + function containsPath(p1, p2) { + if (p1.length < p2.length) return false; + return pathEquals( p1.slice(0,p2.length), p2); + } + + // does nothing, used as a default callback + function nullFunction(){} + + // helper for creating functions with the method signature func([path],arg1,arg2,...,[cb]) + // populates an array of arguments with a default path and callback + function normalizeArgs(obj,args,func){ + args = Array.prototype.slice.call(args); + var path_prefix = obj.path || []; + + if (func.length > 1 && typeof args[args.length-1] !== 'function') { + args.push(nullFunction); + } + + if (args.length < func.length) { + args.unshift(path_prefix); + } else { + args[0] = path_prefix.concat(args[0]); + } + + return func.apply(obj,args); + }; + + + // SubDoc + // this object is returned from context.createContextAt() + + var SubDoc = function(context, path) { + this.context = context; + this.path = path || []; + }; + + SubDoc.prototype._updatePath = function(op){ + for (var i = 0; i < op.length; i++) { + var c = op[i]; + if(c.lm !== void 0 && containsPath(this.path,c.p)){ + var new_path_prefix = c.p.slice(0,c.p.length-1); + new_path_prefix.push(c.lm); + this.path = new_path_prefix.concat(this.path.slice(new_path_prefix.length)); + } + } + }; + + SubDoc.prototype.createContextAt = function() { + var path = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + return this.context.createContextAt(this.path.concat(depath(path))); + }; + + SubDoc.prototype.get = function(path) { + return normalizeArgs(this,arguments,function(path){ + return this.context.get(path); + }); + }; + + SubDoc.prototype.set = function(path,value,cb) { + return normalizeArgs(this,arguments,function(path,value,cb){ + return this.context.set(path, value, cb); + }); + }; + + SubDoc.prototype.insert = function(path, pos, value, cb) { + return normalizeArgs(this,arguments,function(path, pos, value, cb){ + return this.context.insert(path, pos, value, cb); + }); + }; + + SubDoc.prototype.remove = function(path, cb) { + return normalizeArgs(this,arguments,function(path, cb) { + return this.context.remove(path, cb); + }); + }; + + SubDoc.prototype.push = function(path, value, cb) { + return normalizeArgs(this,arguments,function(path, value, cb) { + return this.context.insert(path, this.get().length, value, cb); + }); + }; + + SubDoc.prototype.move = function(path, from, to, cb) { + return normalizeArgs(this,arguments,function(path, from, to, cb) { + return this.context.move(path, from, to, cb); + }); + }; + + SubDoc.prototype.add = function(path, amount, cb) { + return normalizeArgs(this,arguments,function(path, amount, cb) { + return this.context.add(path, amount, cb); + }); + }; + + SubDoc.prototype.on = function(event, cb) { + return this.context.addListener(this.path, event, cb); + }; + + SubDoc.prototype.removeListener = function(l) { + return this.context.removeListener(l); + }; + + SubDoc.prototype.getLength = function(path) { + return normalizeArgs(this,arguments,function(path) { + return this.context.getLength(path); + }); + }; + + SubDoc.prototype.getText = function(path) { + return normalizeArgs(this,arguments,function(path) { + return this.context.getText(path); + }); + }; + + SubDoc.prototype.deleteText = function(path, pos, length, cb) { + return normalizeArgs(this,arguments,function(path, pos, length, cb) { + return this.context.deleteText(path, length, pos, cb); + }); + }; + + SubDoc.prototype.destroy = function() { + this.context._removeSubDoc(this); + }; + + + // JSON API methods + // these methods are mixed in to the context return from doc.createContext() + + _type.api = { + + provides: { + json: true + }, + + _fixComponentPaths: function(c) { + if (!this._listeners) { + return; + } + if (c.na !== void 0 || c.si !== void 0 || c.sd !== void 0) { + return; + } + + var to_remove = []; + var _ref = this._listeners; + + for (var i = 0; i < _ref.length; i++) { + var l = _ref[i]; + var dummy = { + p: l.path, + na: 0 + }; + var xformed = _type.transformComponent([], dummy, c, 'left'); + if (xformed.length === 0) { + to_remove.push(i); + } else if (xformed.length === 1) { + l.path = xformed[0].p; + } else { + throw new Error("Bad assumption in json-api: xforming an 'na' op will always result in 0 or 1 components."); + } + } + + to_remove.sort(function(a, b) { + return b - a; + }); + + var _results = []; + for (var j = 0; j < to_remove.length; j++) { + i = to_remove[j]; + _results.push(this._listeners.splice(i, 1)); + } + + return _results; + }, + + _fixPaths: function(op) { + var _results = []; + for (var i = 0; i < op.length; i++) { + var c = op[i]; + _results.push(this._fixComponentPaths(c)); + } + return _results; + }, + + _submit: function(op, callback) { + this._fixPaths(op); + return this.submitOp(op, callback); + }, + + _addSubDoc: function(subdoc){ + this._subdocs || (this._subdocs = []); + this._subdocs.push(subdoc); + }, + + _removeSubDoc: function(subdoc){ + this._subdocs || (this._subdocs = []); + for(var i = 0; i < this._subdocs.length; i++){ + if(this._subdocs[i] === subdoc) this._subdocs.splice(i,1); + return; + } + }, + + _updateSubdocPaths: function(op){ + this._subdocs || (this._subdocs = []); + for(var i = 0; i < this._subdocs.length; i++){ + this._subdocs[i]._updatePath(op); + } + }, + + createContextAt: function() { + var path = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + var subdoc = new SubDoc(this, depath(path)); + this._addSubDoc(subdoc); + return subdoc; + }, + + get: function(path) { + if (!path) return this.getSnapshot(); + + var _ref = traverse(this.getSnapshot(), path); + return _ref.elem[_ref.key]; + }, + + set: function(path, value, cb) { + return normalizeArgs(this,arguments,function(path, value, cb) { + var _ref = traverse(this.getSnapshot(), path); + var elem = _ref.elem; + var key = _ref.key; + var op = { + p: path + }; + + if (elem.constructor === Array) { + op.li = value; + if (typeof elem[key] !== 'undefined') { + op.ld = elem[key]; + } + } else if (typeof elem === 'object') { + op.oi = value; + if (typeof elem[key] !== 'undefined') { + op.od = elem[key]; + } + } else { + throw new Error('bad path'); + } + + return this._submit([op], cb); + }); + }, + + remove: function(path, cb) { + return normalizeArgs(this,arguments,function(path, cb) { + var _ref = traverse(this.getSnapshot(), path); + var elem = _ref.elem; + var key = _ref.key; + var op = { + p: path + }; + + if (typeof elem[key] === 'undefined') { + throw new Error('no element at that path'); + } + + if (elem.constructor === Array) { + op.ld = elem[key]; + } else if (typeof elem === 'object') { + op.od = elem[key]; + } else { + throw new Error('bad path'); + } + + return this._submit([op], cb); + }); + }, + + insert: function(path, pos, value, cb) { + return normalizeArgs(this,arguments,function(path, pos, value, cb) { + var _ref = traverse(this.getSnapshot(), path); + var elem = _ref.elem; + var key = _ref.key; + var op = { + p: path.concat(pos) + }; + + if (elem[key].constructor === Array) { + op.li = value; + } else if (typeof elem[key] === 'string') { + op.si = value; + } + + return this._submit([op], cb); + }); + }, + + move: function(path, from, to, cb) { + return normalizeArgs(this,arguments,function(path, from, to, cb) { + var self = this; + var op = [ + { + p: path.concat(from), + lm: to + } + ]; + + return this._submit(op, function(){ + self._updateSubdocPaths(op); + if(cb) cb.apply(cb,arguments); + }); + }); + }, + + push: function(path, value, cb) { + return normalizeArgs(this,arguments,function(path, value, cb) { + return this.insert(path, this.get().length, value, cb); + }); + }, + + add: function(path, amount, cb) { + return normalizeArgs(this,arguments,function(path, value, cb) { + var op = [ + { + p: path, + na: amount + } + ]; + return this._submit(op, cb); + }); + }, + + getLength: function(path) { + return normalizeArgs(this,arguments,function(path) { + return this.get(path).length; + }); + }, + + getText: function(path) { + return normalizeArgs(this,arguments,function(path) { + return this.get(path); + }); + }, + + deleteText: function(path, length, pos, cb) { + return normalizeArgs(this,arguments,function(path, length, pos, cb) { + var _ref = traverse(this.getSnapshot(), path); + var op = [ + { + p: path.concat(pos), + sd: _ref.elem[_ref.key].slice(pos, pos + length) + } + ]; + + return this._submit(op, cb); + }); + }, + + addListener: function(path, event, cb) { + return normalizeArgs(this,arguments,function(path, value, cb) { + var listener = { + path: path, + event: event, + cb: cb + }; + this._listeners || (this._listeners = []); + this._listeners.push(listener); + return listener; + }); + }, + + removeListener: function(listener) { + if (!this._listeners) { + return; + } + var i = this._listeners.indexOf(listener); + if (i < 0) { + return false; + } + this._listeners.splice(i, 1); + return true; + }, + + _onOp: function(op) { + var _results = []; + for (var _i = 0; _i < op.length; _i++) { + var c = op[_i]; + this._fixComponentPaths(c); + + if(c.lm !== void 0 ){ + this._updateSubdocPaths([c]); + } + + var match_path = c.na === void 0 ? c.p.slice(0, c.p.length - 1) : c.p; + + _results.push((function() { + var _ref = this._listeners; + var _results1 = []; + for (var _j = 0; _j < _ref.length; _j++) { + var _ref1 = _ref[_j]; + var path = _ref1.path; + var event = _ref1.event; + var cb = _ref1.cb; + if (pathEquals(path, match_path)) { + switch (event) { + case 'insert': + if (c.li !== void 0 && c.ld === void 0) { + _results1.push(cb(c.p[c.p.length - 1], c.li)); + } else if (c.oi !== void 0 && c.od === void 0) { + _results1.push(cb(c.p[c.p.length - 1], c.oi)); + } else if (c.si !== void 0) { + _results1.push(cb(c.p[c.p.length - 1], c.si)); + } else { + _results1.push(void 0); + } + break; + case 'delete': + if (c.li === void 0 && c.ld !== void 0) { + _results1.push(cb(c.p[c.p.length - 1], c.ld)); + } else if (c.oi === void 0 && c.od !== void 0) { + _results1.push(cb(c.p[c.p.length - 1], c.od)); + } else if (c.sd !== void 0) { + _results1.push(cb(c.p[c.p.length - 1], c.sd)); + } else { + _results1.push(void 0); + } + break; + case 'replace': + if (c.li !== void 0 && c.ld !== void 0) { + _results1.push(cb(c.p[c.p.length - 1], c.ld, c.li)); + } else if (c.oi !== void 0 && c.od !== void 0) { + _results1.push(cb(c.p[c.p.length - 1], c.od, c.oi)); + } else { + _results1.push(void 0); + } + break; + case 'move': + if (c.lm !== void 0) { + _results1.push(cb(c.p[c.p.length - 1], c.lm)); + } else { + _results1.push(void 0); + } + break; + case 'add': + if (c.na !== void 0) { + _results1.push(cb(c.na)); + } else { + _results1.push(void 0); + } + break; + default: + _results1.push(void 0); + } + } else if (_type.canOpAffectOp(path, match_path)) { + if (event === 'child op') { + var child_path = c.p.slice(path.length); + _results1.push(cb(child_path, c)); + } else { + _results1.push(void 0); + } + } else { + _results1.push(void 0); + } + } + return _results1; + }).call(this)); + } + return _results; + } + }; + +}).call(this); \ No newline at end of file diff --git a/prototype/public/json.html b/prototype/public/json.html new file mode 100644 index 00000000..3c717570 --- /dev/null +++ b/prototype/public/json.html @@ -0,0 +1,84 @@ + + + + +

JSON Client API example (check it out in source, and on the wiki)

diff --git a/prototype/public/json_list.html b/prototype/public/json_list.html new file mode 100644 index 00000000..165a4d7f --- /dev/null +++ b/prototype/public/json_list.html @@ -0,0 +1,103 @@ + + + + + + +

Reorder the trains

+