diff --git a/www/common/chainpad.js b/www/common/chainpad.js index 134d80ce2..e7823bc58 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -224,10 +224,16 @@ var transform = Patch.transform = function (origToTransform, transformBy, doc, t var text = doc; for (var i = toTransform.operations.length-1; i >= 0; i--) { for (var j = transformBy.operations.length-1; j >= 0; j--) { - toTransform.operations[i] = Operation.transform(text, - toTransform.operations[i], - transformBy.operations[j], - transformFunction); + try { + toTransform.operations[i] = Operation.transform(text, + toTransform.operations[i], + transformBy.operations[j], + transformFunction); + } catch (e) { + console.error("The pluggable transform function threw an error, " + + "failing operational transformation"); + return create(Sha.hex_sha256(resultOfTransformBy)); + } if (!toTransform.operations[i]) { break; } @@ -370,6 +376,9 @@ var random = Patch.random = function (doc, opCount) { var PARANOIA = module.exports.PARANOIA = true; +/* Good testing but slooooooooooow */ +var VALIDATE_ENTIRE_CHAIN_EACH_MSG = module.exports.VALIDATE_ENTIRE_CHAIN_EACH_MSG = false; + /* throw errors over non-compliant messages which would otherwise be treated as invalid */ var TESTING = module.exports.TESTING = true; @@ -832,7 +841,9 @@ var check = ChainPad.check = function(realtime) { Common.assert(uiDoc === realtime.userInterfaceContent); } - /*var doc = realtime.authDoc; + if (!Common.VALIDATE_ENTIRE_CHAIN_EACH_MSG) { return; } + + var doc = realtime.authDoc; var patchMsg = realtime.best; Common.assert(patchMsg.content.inverseOf.parentHash === realtime.uncommitted.parentHash); var patches = []; @@ -844,7 +855,7 @@ var check = ChainPad.check = function(realtime) { while ((patchMsg = patches.pop())) { doc = Patch.apply(patchMsg.content, doc); } - Common.assert(doc === realtime.authDoc);*/ + Common.assert(doc === realtime.authDoc); }; var doOperation = ChainPad.doOperation = function (realtime, op) { diff --git a/www/common/netflux-client.js b/www/common/netflux-client.js index 7d436f018..83dadcf5c 100644 --- a/www/common/netflux-client.js +++ b/www/common/netflux-client.js @@ -1,226 +1,291 @@ /*global: WebSocket */ -define(() => { -'use strict'; -const MAX_LAG_BEFORE_PING = 15000; -const MAX_LAG_BEFORE_DISCONNECT = 30000; -const PING_CYCLE = 5000; -const REQUEST_TIMEOUT = 30000; - -const now = () => new Date().getTime(); - -const networkSendTo = (ctx, peerId, content) => { - const seq = ctx.seq++; - ctx.ws.send(JSON.stringify([seq, 'MSG', peerId, content])); - return new Promise((res, rej) => { - ctx.requests[seq] = { reject: rej, resolve: res, time: now() }; - }); -}; - -const channelBcast = (ctx, chanId, content) => { - const chan = ctx.channels[chanId]; - if (!chan) { throw new Error("no such channel " + chanId); } - const seq = ctx.seq++; - ctx.ws.send(JSON.stringify([seq, 'MSG', chanId, content])); - return new Promise((res, rej) => { - ctx.requests[seq] = { reject: rej, resolve: res, time: now() }; - }); -}; - -const channelLeave = (ctx, chanId, reason) => { - const chan = ctx.channels[chanId]; - if (!chan) { throw new Error("no such channel " + chanId); } - delete ctx.channels[chanId]; - ctx.ws.send(JSON.stringify([ctx.seq++, 'LEAVE', chanId, reason])); -}; - -const makeEventHandlers = (ctx, mappings) => { - return (name, handler) => { - const handlers = mappings[name]; - if (!handlers) { throw new Error("no such event " + name); } - handlers.push(handler); - }; -}; - -const mkChannel = (ctx, id) => { - const internal = { - onMessage: [], - onJoin: [], - onLeave: [], - members: [], - jSeq: ctx.seq++ +define(function () { + 'use strict'; + + var MAX_LAG_BEFORE_PING = 15000; + var MAX_LAG_BEFORE_DISCONNECT = 30000; + var PING_CYCLE = 5000; + var REQUEST_TIMEOUT = 30000; + + var now = function now() { + return new Date().getTime(); }; - const chan = { - _: internal, - time: now(), - id: id, - members: internal.members, - bcast: (msg) => channelBcast(ctx, chan.id, msg), - leave: (reason) => channelLeave(ctx, chan.id, reason), - on: makeEventHandlers(ctx, { message: - internal.onMessage, join: internal.onJoin, leave: internal.onLeave }) + + var networkSendTo = function networkSendTo(ctx, peerId, content) { + var seq = ctx.seq++; + ctx.ws.send(JSON.stringify([seq, 'MSG', peerId, content])); + return new Promise(function (res, rej) { + ctx.requests[seq] = { reject: rej, resolve: res, time: now() }; + }); }; - ctx.requests[internal.jSeq] = chan; - ctx.ws.send(JSON.stringify([internal.jSeq, 'JOIN', id])); - - return new Promise((res, rej) => { - chan._.resolve = res; - chan._.reject = rej; - }) -}; - -const mkNetwork = (ctx) => { - const network = { - webChannels: ctx.channels, - getLag: () => (ctx.lag), - sendto: (peerId, content) => (networkSendTo(ctx, peerId, content)), - join: (chanId) => (mkChannel(ctx, chanId)), - on: makeEventHandlers(ctx, { message: ctx.onMessage, disconnect: ctx.onDisconnect }) + + var channelBcast = function channelBcast(ctx, chanId, content) { + var chan = ctx.channels[chanId]; + if (!chan) { + throw new Error("no such channel " + chanId); + } + var seq = ctx.seq++; + ctx.ws.send(JSON.stringify([seq, 'MSG', chanId, content])); + return new Promise(function (res, rej) { + ctx.requests[seq] = { reject: rej, resolve: res, time: now() }; + }); }; - network.__defineGetter__("webChannels", () => { - return Object.keys(ctx.channels).map((k) => (ctx.channels[k])); - }); - return network; -}; - -const onMessage = (ctx, evt) => { - let msg; - try { msg = JSON.parse(evt.data); } catch (e) { console.log(e.stack); return; } - if (msg[0] !== 0) { - const req = ctx.requests[msg[0]]; - if (!req) { - console.log("error: " + JSON.stringify(msg)); - return; + + var channelLeave = function channelLeave(ctx, chanId, reason) { + var chan = ctx.channels[chanId]; + if (!chan) { + throw new Error("no such channel " + chanId); } - delete ctx.requests[msg[0]]; - if (msg[1] === 'ACK') { - if (req.ping) { // ACK of a PING - ctx.lag = now() - Number(req.ping); - return; - } - req.resolve(); - } else if (msg[1] === 'JACK') { - if (req._) { - // Channel join request... - if (!msg[2]) { throw new Error("wrong type of ACK for channel join"); } - req.id = msg[2]; - ctx.channels[req.id] = req; - return; + delete ctx.channels[chanId]; + ctx.ws.send(JSON.stringify([ctx.seq++, 'LEAVE', chanId, reason])); + }; + + var makeEventHandlers = function makeEventHandlers(ctx, mappings) { + return function (name, handler) { + var handlers = mappings[name]; + if (!handlers) { + throw new Error("no such event " + name); } - req.resolve(); - } else if (msg[1] === 'ERROR') { - req.reject({ type: msg[2], message: msg[3] }); - } else { - req.reject({ type: 'UNKNOWN', message: JSON.stringify(msg) }); + handlers.push(handler); + }; + }; + + var mkChannel = function mkChannel(ctx, id) { + var internal = { + onMessage: [], + onJoin: [], + onLeave: [], + members: [], + jSeq: ctx.seq++ + }; + var chan = { + _: internal, + time: now(), + id: id, + members: internal.members, + bcast: function bcast(msg) { + return channelBcast(ctx, chan.id, msg); + }, + leave: function leave(reason) { + return channelLeave(ctx, chan.id, reason); + }, + on: makeEventHandlers(ctx, { message: internal.onMessage, join: internal.onJoin, leave: internal.onLeave }) + }; + ctx.requests[internal.jSeq] = chan; + ctx.ws.send(JSON.stringify([internal.jSeq, 'JOIN', id])); + + return new Promise(function (res, rej) { + chan._.resolve = res; + chan._.reject = rej; + }); + }; + + var mkNetwork = function mkNetwork(ctx) { + var network = { + webChannels: ctx.channels, + getLag: function getLag() { + return ctx.lag; + }, + sendto: function sendto(peerId, content) { + return networkSendTo(ctx, peerId, content); + }, + join: function join(chanId) { + return mkChannel(ctx, chanId); + }, + on: makeEventHandlers(ctx, { message: ctx.onMessage, disconnect: ctx.onDisconnect }) + }; + network.__defineGetter__("webChannels", function () { + return Object.keys(ctx.channels).map(function (k) { + return ctx.channels[k]; + }); + }); + return network; + }; + + var onMessage = function onMessage(ctx, evt) { + var msg = void 0; + try { + msg = JSON.parse(evt.data); + } catch (e) { + console.log(e.stack);return; } - return; - } - if (msg[2] === 'IDENT') { - ctx.uid = msg[3]; - - setInterval(() => { - if (now() - ctx.timeOfLastMessage < MAX_LAG_BEFORE_PING) { return; } - let seq = ctx.seq++; - let currentDate = now(); - ctx.requests[seq] = {time: now(), ping: currentDate}; - ctx.ws.send(JSON.stringify([seq, 'PING', currentDate])); - if (now() - ctx.timeOfLastMessage > MAX_LAG_BEFORE_DISCONNECT) { - ctx.ws.close(); - } - }, PING_CYCLE); - - return; - } else if (!ctx.uid) { - // extranious message, waiting for an ident. - return; - } - if (msg[2] === 'PING') { - msg[2] = 'PONG'; - ctx.ws.send(JSON.stringify(msg)); - return; - } - - if (msg[2] === 'MSG') { - let handlers; - if (msg[3] === ctx.uid) { - handlers = ctx.onMessage; - } else { - const chan = ctx.channels[msg[3]]; - if (!chan) { - console.log("message to non-existant chan " + JSON.stringify(msg)); + if (msg[0] !== 0) { + var req = ctx.requests[msg[0]]; + if (!req) { + console.log("error: " + JSON.stringify(msg)); return; } - handlers = chan._.onMessage; + delete ctx.requests[msg[0]]; + if (msg[1] === 'ACK') { + if (req.ping) { + // ACK of a PING + ctx.lag = now() - Number(req.ping); + return; + } + req.resolve(); + } else if (msg[1] === 'JACK') { + if (req._) { + // Channel join request... + if (!msg[2]) { + throw new Error("wrong type of ACK for channel join"); + } + req.id = msg[2]; + ctx.channels[req.id] = req; + return; + } + req.resolve(); + } else if (msg[1] === 'ERROR') { + req.reject({ type: msg[2], message: msg[3] }); + } else { + req.reject({ type: 'UNKNOWN', message: JSON.stringify(msg) }); + } + return; } - handlers.forEach((h) => { - try { h(msg[4], msg[1]); } catch (e) { console.error(e); } - }); - } + + if (msg[2] === 'IDENT') { + ctx.uid = msg[3]; + + setInterval(function () { + if (now() - ctx.timeOfLastMessage < MAX_LAG_BEFORE_PING) { + return; + } + var seq = ctx.seq++; + var currentDate = now(); + ctx.requests[seq] = { time: now(), ping: currentDate }; + ctx.ws.send(JSON.stringify([seq, 'PING', currentDate])); + if (now() - ctx.timeOfLastMessage > MAX_LAG_BEFORE_DISCONNECT) { + ctx.ws.close(); + } + }, PING_CYCLE); - if (msg[2] === 'LEAVE') { - const chan = ctx.channels[msg[3]]; - if (!chan) { - console.log("leaving non-existant chan " + JSON.stringify(msg)); + return; + } else if (!ctx.uid) { + // extranious message, waiting for an ident. return; } - chan._.onLeave.forEach((h) => { - try { h(msg[1], msg[4]); } catch (e) { console.log(e.stack); } - }); - } - - if (msg[2] === 'JOIN') { - const chan = ctx.channels[msg[3]]; - if (!chan) { - console.log("ERROR: join to non-existant chan " + JSON.stringify(msg)); + if (msg[2] === 'PING') { + msg[2] = 'PONG'; + ctx.ws.send(JSON.stringify(msg)); return; } - // have we yet fully joined the chan? - const synced = (chan._.members.indexOf(ctx.uid) !== -1); - chan._.members.push(msg[1]); - if (!synced && msg[1] === ctx.uid) { - // sync the channel join event - chan.myID = ctx.uid; - chan._.resolve(chan); + + if (msg[2] === 'MSG') { + var handlers = void 0; + if (msg[3] === ctx.uid) { + handlers = ctx.onMessage; + } else { + var chan = ctx.channels[msg[3]]; + if (!chan) { + console.log("message to non-existant chan " + JSON.stringify(msg)); + return; + } + handlers = chan._.onMessage; + } + handlers.forEach(function (h) { + try { + h(msg[4], msg[1]); + } catch (e) { + console.error(e); + } + }); } - if (synced) { - chan._.onJoin.forEach((h) => { - try { h(msg[1]); } catch (e) { console.log(e.stack); } + + if (msg[2] === 'LEAVE') { + var _chan = ctx.channels[msg[3]]; + if (!_chan) { + console.log("leaving non-existant chan " + JSON.stringify(msg)); + return; + } + _chan._.onLeave.forEach(function (h) { + try { + h(msg[1], msg[4]); + } catch (e) { + console.log(e.stack); + } }); } - } -}; - -const connect = (websocketURL) => { - let ctx = { - ws: new WebSocket(websocketURL), - seq: 1, - lag: 0, - uid: null, - network: null, - channels: {}, - onMessage: [], - onDisconnect: [], - requests: {} - }; - setInterval(() => { - for (let id in ctx.requests) { - const req = ctx.requests[id]; - if (now() - req.time > REQUEST_TIMEOUT) { - delete ctx.requests[id]; - if(typeof req.reject === "function") { req.reject({ type: 'TIMEOUT', message: 'waited ' + now() - req.time + 'ms' }); } + + if (msg[2] === 'JOIN') { + var _chan2 = ctx.channels[msg[3]]; + if (!_chan2) { + console.log("ERROR: join to non-existant chan " + JSON.stringify(msg)); + return; + } + // have we yet fully joined the chan? + var synced = _chan2._.members.indexOf(ctx.uid) !== -1; + _chan2._.members.push(msg[1]); + if (!synced && msg[1] === ctx.uid) { + // sync the channel join event + _chan2.myID = ctx.uid; + _chan2._.resolve(_chan2); + } + if (synced) { + _chan2._.onJoin.forEach(function (h) { + try { + h(msg[1]); + } catch (e) { + console.log(e.stack); + } + }); } } - }, 5000); - ctx.network = mkNetwork(ctx); - ctx.ws.onmessage = (msg) => (onMessage(ctx, msg)); - ctx.ws.onclose = (evt) => { - ctx.onDisconnect.forEach((h) => { - try { h(evt.reason); } catch (e) { console.log(e.stack); } + }; + + var connect = function connect(websocketURL) { + var ctx = { + ws: new WebSocket(websocketURL), + seq: 1, + lag: 0, + uid: null, + network: null, + channels: {}, + onMessage: [], + onDisconnect: [], + requests: {} + }; + setInterval(function () { + for (var id in ctx.requests) { + var req = ctx.requests[id]; + if (now() - req.time > REQUEST_TIMEOUT) { + delete ctx.requests[id]; + if (typeof req.reject === "function") { + req.reject({ type: 'TIMEOUT', message: 'waited ' + (now() - req.time) + 'ms' }); + } + } + } + }, 5000); + ctx.network = mkNetwork(ctx); + ctx.ws.onmessage = function (msg) { + return onMessage(ctx, msg); + }; + ctx.ws.onclose = function (evt) { + ctx.onDisconnect.forEach(function (h) { + try { + h(evt.reason); + } catch (e) { + console.log(e.stack); + } + }); + }; + return new Promise(function (resolve, reject) { + ctx.ws.onopen = function () { + var count = 0; + var interval = 100; + var checkIdent = function() { + if(ctx.uid !== null) { + return resolve(ctx.network); + } + else { + if(count * interval > REQUEST_TIMEOUT) { + return reject({ type: 'TIMEOUT', message: 'waited ' + (count * interval) + 'ms' }); + } + setTimeout(checkIdent, 100); + } + } + checkIdent(); + }; }); }; - return new Promise((resolve, reject) => { - ctx.ws.onopen = () => resolve(ctx.network); - }); -}; -return { connect: connect }; -}); + return { connect: connect }; +}); \ No newline at end of file diff --git a/www/form/index.html b/www/form/index.html index aa5a39fc6..97b69f270 100644 --- a/www/form/index.html +++ b/www/form/index.html @@ -11,33 +11,59 @@ overflow: hidden; box-sizing: border-box; } + + form { + border: 3px solid black; + border-radius: 5px; + padding: 15px; + font-weight: bold !important; + font-size: 18px !important; + } + + input[type="text"], + input[type="password"], + input[type="number"], + input[type="range"], + select + { + margin-top: 5px; + margin-bottom: 5px; + width: 80%; + } + textarea { + width: 80%; + height: 40vh; + font-weight: bold; + font-size: 18px; + }
-
-
- - One
- Two
+ One + Two Three
- Checkbox One
+ Checkbox One Checkbox Two
- Number
+
+
+ + + Number
- Ranges
+ Ranges
- Dropdowns
-
+
diff --git a/www/form/main.js b/www/form/main.js index ce756275e..102ed9bf9 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -1,80 +1,201 @@ +require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ '/api/config?cb=' + Math.random().toString(16).substring(2), - '/common/RealtimeTextarea.js', - '/common/messages.js', + '/common/realtime-input.js', '/common/crypto.js', '/common/TextPatcher.js', + 'json.sortify', + '/form/ula.js', + '/common/json-ot.js', '/bower_components/jquery/dist/jquery.min.js', '/customize/pad.js' -], function (Config, Realtime, Messages, Crypto, TextPatcher) { +], function (Config, Realtime, Crypto, TextPatcher, Sortify, Formula, JsonOT) { var $ = window.jQuery; - $(window).on('hashchange', function() { - window.location.reload(); - }); - if (window.location.href.indexOf('#') === -1) { - window.location.href = window.location.href + '#' + Crypto.genKey(); - return; + + var key; + var channel = ''; + var hash = false; + if (!/#/.test(window.location.href)) { + key = Crypto.genKey(); + } else { + hash = window.location.hash.slice(1); + channel = hash.slice(0,32); + key = hash.slice(32); } - var module = window.APP = {}; - var key = Crypto.parseKey(window.location.hash.substring(1)); + var module = window.APP = { + TextPatcher: TextPatcher, + Sortify: Sortify, + Formula: Formula, + }; var initializing = true; - /* elements that we need to listen to */ - /* - * text - * password - * radio - * checkbox - * number - * range - * select - * textarea - */ + var uid = module.uid = Formula.uid; + + var getInputType = Formula.getInputType; + var $elements = module.elements = $('input, select, textarea') + + var eventsByType = Formula.eventsByType; - var $textarea = $('textarea'); + var Map = module.Map = {}; + + var UI = module.UI = { + ids: [], + each: function (f) { + UI.ids.forEach(function (id, i, list) { + f(UI[id], i, list); + }); + } + }; + + var cursorTypes = ['textarea', 'password', 'text']; + + var canonicalize = function (text) { return text.replace(/\r\n/g, '\n'); }; + $elements.each(function (element) { + var $this = $(this); + + var id = uid(); + var type = getInputType($this); + + $this // give each element a uid + .data('rtform-uid', id) + // get its type + .data('rt-ui-type', type); + + UI.ids.push(id); + + var component = UI[id] = { + id: id, + $: $this, + element: element, + type: type, + preserveCursor: cursorTypes.indexOf(type) !== -1, + name: $this.prop('name'), + }; + + component.value = (function () { + var checker = ['radio', 'checkbox'].indexOf(type) !== -1; + + if (checker) { + return function (content) { + return typeof content !== 'undefined'? + $this.prop('checked', !!content): + $this.prop('checked'); + }; + } else { + return function (content) { + return typeof content !== 'undefined' ? + $this.val(content): + canonicalize($this.val()); + }; + } + }()); + + var update = component.update = function () { Map[id] = component.value(); }; + update(); + }); var config = module.config = { - websocketURL: Config.websocketURL + '_old', + initialState: Sortify(Map) || '{}', + websocketURL: Config.websocketURL, userName: Crypto.rand64(8), - channel: key.channel, - cryptKey: key.cryptKey + channel: channel, + cryptKey: key, + crypto: Crypto, + transformFunction: JsonOT.validate }; - var setEditable = function (bool) {/* allow editing */}; - var canonicalize = function (text) {/* canonicalize all the things */}; + var setEditable = module.setEditable = function (bool) { + /* (dis)allow editing */ + $elements.each(function () { + $(this).attr('disabled', !bool); + }); + }; setEditable(false); - var onInit = config.onInit = function (info) { }; + var onInit = config.onInit = function (info) { + var realtime = module.realtime = info.realtime; + window.location.hash = info.channel + key; - var onRemote = config.onRemote = function (info) { - if (initializing) { return; } - /* integrate remote changes */ + // create your patcher + module.patchText = TextPatcher.create({ + realtime: realtime, + logging: true, + }); }; var onLocal = config.onLocal = function () { if (initializing) { return; } /* serialize local changes */ + readValues(); + module.patchText(Sortify(Map)); }; - var onReady = config.onReady = function (info) { - var realtime = module.realtime = info.realtime; + var readValues = function () { + UI.each(function (ui, i, list) { + Map[ui.id] = ui.value(); + }); + }; - // create your patcher - module.patchText = TextPatcher.create({ - realtime: realtime + var updateValues = function () { + var userDoc = module.realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + console.log(userDoc); + + UI.each(function (ui, i, list) { + var newval = parsed[ui.id]; + var oldval = ui.value(); + + if (newval === oldval) { return; } + + var op; + var element = ui.element; + if (ui.preserveCursor) { + op = TextPatcher.diff(oldval, newval); + var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { + var before = element[attr]; + var after = TextPatcher.transformCursor(element[attr], op); + return after; + }); + } + + ui.value(newval); + ui.update(); + + if (op) { + console.log(selects); + element.selectionStart = selects[0]; + element.selectionEnd = selects[1]; + } }); + }; - // get ready + var onRemote = config.onRemote = function (info) { + if (initializing) { return; } + /* integrate remote changes */ + updateValues(); + }; + + var onReady = config.onReady = function (info) { + updateValues(); + console.log("READY"); setEditable(true); initializing = false; }; - var onAbort = config.onAbort = function (info) {}; + var onAbort = config.onAbort = function (info) { + window.alert("Network Connection Lost"); + }; var rt = Realtime.start(config); - // bind to events... + UI.each(function (ui, i, list) { + var type = ui.type; + var events = eventsByType[type]; + ui.$.on(events, onLocal); + }); + }); diff --git a/www/form/types.md b/www/form/types.md new file mode 100644 index 000000000..ab73d3bfa --- /dev/null +++ b/www/form/types.md @@ -0,0 +1,14 @@ + +```Javascript +/* elements that we need to listen to */ +/* + * text => $(text).val() + * password => $(password).val() + * radio => $(radio).prop('checked') + * checkbox => $(checkbox).prop('checked') + * number => $(number).val() // returns string, no default + * range => $(range).val() + * select => $(select).val() + * textarea => $(textarea).val() +*/ +``` diff --git a/www/form/ula.js b/www/form/ula.js new file mode 100644 index 000000000..4591bb5eb --- /dev/null +++ b/www/form/ula.js @@ -0,0 +1,24 @@ +define([], function () { + var ula = {}; + + var uid = ula.uid = (function () { + var i = 0; + var prefix = 'rt_'; + return function () { return prefix + i++; }; + }()); + + ula.getInputType = function ($el) { return $el[0].type; }; + + ula.eventsByType = { + text: 'change keyup', + password: 'change keyup', + radio: 'change click', + checkbox: 'change click', + number: 'change', + range: 'keyup change', + 'select-one': 'change', + textarea: 'change keyup', + }; + + return ula; +}); diff --git a/www/pad/main.js b/www/pad/main.js index 11b45843e..35848477b 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -60,10 +60,6 @@ define([ return hj; }; - var stringifyDOM = function (dom) { - return stringify(Hyperjson.fromDOM(dom, isNotMagicLine, brFilter)); - }; - var andThen = function (Ckeditor) { /* This is turned off because we prefer that the channel name be chosen by the server, not generated by the client. @@ -232,6 +228,12 @@ define([ (DD).apply(inner, patch); }; + var stringifyDOM = function (dom) { + var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter); + hjson[3] = {metadata: userList}; + return stringify(hjson); + }; + var realtimeOptions = { // provide initialstate... initialState: stringifyDOM(inner) || '{}', @@ -261,14 +263,13 @@ define([ var updateUserList = function(shjson) { // Extract the user list (metadata) from the hyperjson var hjson = JSON.parse(shjson); - var peerUserList = hjson[hjson.length-1]; - if(peerUserList.metadata) { + var peerUserList = hjson[3]; + if(peerUserList && peerUserList.metadata) { var userData = peerUserList.metadata; // Update the local user data addToUserList(userData); hjson.pop(); } - return hjson; } var onRemote = realtimeOptions.onRemote = function (info) { @@ -279,15 +280,12 @@ define([ // remember where the cursor is cursor.update(); - // Extract the user list (metadata) from the hyperjson - var hjson = updateUserList(shjson); + // Update the user list (metadata) from the hyperjson + updateUserList(shjson); // build a dom from HJSON, diff, and patch the editor applyHjson(shjson); - // Build a new stringified Chainpad hyperjson without metadata to compare with the one build from the dom - shjson = stringify(hjson); - var shjson2 = stringifyDOM(inner); if (shjson2 !== shjson) { console.error("shjson2 !== shjson"); @@ -313,7 +311,7 @@ define([ var onReady = realtimeOptions.onReady = function (info) { module.patchText = TextPatcher.create({ realtime: info.realtime, - logging: false, + logging: true, }); module.realtime = info.realtime; @@ -337,15 +335,8 @@ define([ var onLocal = realtimeOptions.onLocal = function () { if (initializing) { return; } - // serialize your DOM into an object - var hjson = Hyperjson.fromDOM(inner, isNotMagicLine, brFilter); - - // append the userlist to the hyperjson structure - if(Object.keys(myData).length > 0) { - hjson[hjson.length] = {metadata: userList}; - } // stringify the json and send it into chainpad - var shjson = stringify(hjson); + var shjson = stringifyDOM(inner); module.patchText(shjson); if (module.realtime.getUserDoc() !== shjson) { diff --git a/www/render/main.js b/www/render/main.js index 105514dc7..451728ad0 100644 --- a/www/render/main.js +++ b/www/render/main.js @@ -1,7 +1,6 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), '/common/realtime-input.js', - '/common/messages.js', '/common/crypto.js', '/bower_components/marked/marked.min.js', '/common/convert.js', @@ -15,30 +14,38 @@ define([ Hyperjson = Convert.core.hyperjson, Hyperscript = Convert.core.hyperscript; - window.Vdom = Vdom; - window.Hyperjson = Hyperjson; - window.Hyperscript = Hyperscript; - - $(window).on('hashchange', function() { - window.location.reload(); - }); - if (window.location.href.indexOf('#') === -1) { - window.location.href = window.location.href + '#' + Crypto.genKey(); - return; + var key; + var channel = ''; + var hash = false; + if (!/#/.test(window.location.href)) { + key = Crypto.genKey(); + } else { + hash = window.location.hash.slice(1); + channel = hash.slice(0, 32); + key = hash.slice(32); } - var key = Crypto.parseKey(window.location.hash.substring(1)); - - var $textarea = $('textarea').first(), - $target = $('#target'); - - window.$textarea = $textarea; - // set markdown rendering options :: strip html to prevent XSS Marked.setOptions({ sanitize: true }); + var module = window.APP = { + Vdom: Vdom, + Hyperjson: Hyperjson, + Hyperscript: Hyperscript + }; + + var $target = module.$target = $('#target'); + + var config = { + websocketURL: Config.websocketURL, + userName: Crypto.rand64(8), + channel: channel, + cryptKey: key, + crypto: Crypto + }; + var draw = window.draw = (function () { var target = $target[0], inner = $target.find('#inner')[0]; @@ -58,8 +65,7 @@ define([ }; }()); - // FIXME - var colour = window.colour = Rainbow(); + var colour = module.colour = Rainbow(); var $inner = $('#inner'); @@ -83,31 +89,43 @@ define([ }, 450); }; - var config = { - textarea: $textarea[0], - websocketURL: Config.websocketURL, - userName: Crypto.rand64(8), - channel: key.channel, - cryptKey: key.cryptKey, - - // when remote editors do things... - onRemote: function () { - lazyDraw($textarea.val()); - }, - // when your editor is ready - onReady: function (info) { - if (info.userList) { console.log("Userlist: [%s]", info.userList.join(',')); } - console.log("Realtime is ready!"); - $textarea.trigger('keyup'); - } + var initializing = true; + + var onInit = config.onInit = function (info) { + window.location.hash = info.channel + key; + module.realtime = info.realtime; }; - var rts = Realtime.start(config); + // when your editor is ready + var onReady = config.onReady = function (info) { + //if (info.userList) { console.log("Userlist: [%s]", info.userList.join(',')); } + console.log("Realtime is ready!"); - $textarea.on('change keyup keydown', function () { - if (redrawTimeout) { clearTimeout(redrawTimeout); } - redrawTimeout = setTimeout(function () { - lazyDraw($textarea.val()); - }, 500); - }); + var userDoc = module.realtime.getUserDoc(); + lazyDraw(userDoc); + + initializing = false; + }; + + // when remote editors do things... + var onRemote = config.onRemote = function () { + if (initializing) { return; } + var userDoc = module.realtime.getUserDoc(); + lazyDraw(userDoc); + }; + + var onLocal = config.onLocal = function () { + // we're not really expecting any local events for this editor... + /* but we might add a second pane in the future so that you don't need + a second window to edit your markdown */ + if (initializing) { return; } + var userDoc = module.realtime.getUserDoc(); + lazyDraw(userDoc); + }; + + var onAbort = config.onAbort = function () { + window.alert("Network Connection Lost"); + }; + + var rts = Realtime.start(config); }); diff --git a/www/style/main.js b/www/style/main.js index 7fe78cc34..fa209cf7c 100644 --- a/www/style/main.js +++ b/www/style/main.js @@ -9,6 +9,9 @@ define([ // TODO consider adding support for less.js var $ = window.jQuery; + var $style = $('style').first(), + $edit = $('#edit'); + var module = window.APP = {}; var key; @@ -78,8 +81,6 @@ define([ // nope }; - var $style = $('style').first(), - $edit = $('#edit'); $edit.attr('href', '/text/'+ window.location.hash);