diff --git a/www/canvas/main.js b/www/canvas/main.js index 1a52213ff..212f12bfe 100644 --- a/www/canvas/main.js +++ b/www/canvas/main.js @@ -67,6 +67,7 @@ define([ userName: Crypto.rand64(8), channel: channel, cryptKey: key, + crypto: Crypto, }; var onInit = config.onInit = function (info) { diff --git a/www/common/json-ot.js b/www/common/json-ot.js index a785d524c..f04cf3b75 100644 --- a/www/common/json-ot.js +++ b/www/common/json-ot.js @@ -4,10 +4,9 @@ define([ var ChainPad = window.ChainPad; var JsonOT = {}; -/* FIXME - resultOp after transform0() might be null, in which case you should return null - because it is simply a transformation which yields a "do nothing" operation */ var validate = JsonOT.validate = function (text, toTransform, transformBy) { + var DEBUG = window.REALTIME_DEBUG = window.REALTIME_DEBUG || {}; + var resultOp, text2, text3; try { // text = O (mutual common ancestor) @@ -28,7 +27,7 @@ define([ return resultOp; } catch (e) { console.error(e); - var info = window.REALTIME_MODULE.ot_parseError = { + var info = DEBUG.ot_parseError = { type: 'resultParseError', resultOp: resultOp, @@ -40,11 +39,11 @@ define([ text3: text3, error: e }; - console.log('Debugging info available at `window.REALTIME_MODULE.ot_parseError`'); + console.log('Debugging info available at `window.REALTIME_DEBUG.ot_parseError`'); } } catch (x) { console.error(x); - window.REALTIME_MODULE.ot_applyError = { + window.DEBUG.ot_applyError = { type: 'resultParseError', resultOp: resultOp, @@ -56,7 +55,7 @@ define([ text3: text3, error: x }; - console.log('Debugging info available at `window.REALTIME_MODULE.ot_applyError`'); + console.log('Debugging info available at `window.REALTIME_DEBUG.ot_applyError`'); } // returning **null** breaks out of the loop diff --git a/www/common/netflux-client.js b/www/common/netflux-client.js index e6567229a..83dadcf5c 100644 --- a/www/common/netflux-client.js +++ b/www/common/netflux-client.js @@ -1,225 +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, - 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/common/realtime-input.js b/www/common/realtime-input.js index 6a958e406..5a8726e35 100644 --- a/www/common/realtime-input.js +++ b/www/common/realtime-input.js @@ -15,16 +15,15 @@ * along with this program. If not, see . */ define([ - '/common/messages.js', '/common/netflux-client.js', - '/common/crypto.js', '/common/es6-promise.min.js', '/common/chainpad.js', '/bower_components/jquery/dist/jquery.min.js', -], function (Messages, Netflux, Crypto) { +], function (Netflux) { var $ = window.jQuery; var ChainPad = window.ChainPad; var PARANOIA = true; + var USE_HISTORY = true; var module = { exports: {} }; /** @@ -44,14 +43,14 @@ define([ var websocketUrl = config.websocketURL; var userName = config.userName; var channel = config.channel; - var chanKey = config.cryptKey; + var chanKey = config.cryptKey || ''; + var Crypto = config.crypto; var cryptKey = Crypto.parseKey(chanKey).cryptKey; var passwd = 'y'; // make sure configuration is defined config = config || {}; - var allMessages = []; var initializing = true; var recoverableErrorCount = 0; // unused var toReturn = {}; @@ -99,6 +98,11 @@ define([ }; var onReady = function(wc, network) { + if(config.setMyID) { + config.setMyID({ + myID: wc.myID + }); + } // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced onJoining(wc.myID); @@ -127,11 +131,10 @@ define([ var message = chainpadAdapter.msgIn(peer, msg); verbose(message); - allMessages.push(message); if (!initializing) { - if (toReturn.onLocal) { - toReturn.onLocal(); + if (config.onLocal) { + config.onLocal(); } } // pass the message into Chainpad @@ -205,11 +208,6 @@ define([ wc.on('join', onJoining); wc.on('leave', onLeaving); - if(config.setMyID) { - config.setMyID({ - myID: wc.myID - }); - } // Open a Chainpad session realtime = createRealtime(); @@ -249,14 +247,22 @@ define([ }); // Get the channel history - var hc; - wc.members.forEach(function (p) { - if (p.length === 16) { hc = p; } - }); - wc.history_keeper = hc; - if (hc) { network.sendto(hc, JSON.stringify(['GET_HISTORY', wc.id])); } + if(USE_HISTORY) { + var hc; + + wc.members.forEach(function (p) { + if (p.length === 16) { hc = p; } + }); + wc.history_keeper = hc; + + if (hc) { network.sendto(hc, JSON.stringify(['GET_HISTORY', wc.id])); } + } realtime.start(); + + if(!USE_HISTORY) { + onReady(wc, network); + } }; var findChannelById = function(webChannels, channelId) { 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/hack/main.js b/www/hack/main.js index 8830b9007..fe71ac64d 100644 --- a/www/hack/main.js +++ b/www/hack/main.js @@ -6,10 +6,6 @@ define([ '/bower_components/jquery/dist/jquery.min.js' ], function (Config, Realtime, Crypto, TextPatcher) { var $ = window.jQuery; - /* - $(window).on('hashchange', function() { - window.location.reload(); - });*/ var key; var channel = ''; @@ -44,6 +40,7 @@ define([ userName: userName, channel: channel, cryptKey: key, + crypto: Crypto, }; var initializing = true; @@ -54,6 +51,7 @@ define([ var onInit = config.onInit = function (info) { window.location.hash = info.channel + key; + $(window).on('hashchange', function() { window.location.reload(); }); }; var onRemote = config.onRemote = function (info) { diff --git a/www/p/main.js b/www/p/main.js index 023f9b9f1..64eda9f70 100644 --- a/www/p/main.js +++ b/www/p/main.js @@ -194,9 +194,6 @@ define([ var now = function () { return new Date().getTime(); }; var realtimeOptions = { - // configuration :D - doc: inner, - // provide initialstate... initialState: stringifyDOM(inner) || '{}', @@ -213,7 +210,9 @@ define([ channel: key.channel, // encryption key - cryptKey: key.cryptKey + cryptKey: key.cryptKey, + + crypto: Crypto, }; var DD = new DiffDom(diffOptions); diff --git a/www/pad/main.js b/www/pad/main.js index 09055bc33..508836e38 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -87,7 +87,6 @@ define([ } var fixThings = false; - // var key = Crypto.parseKey(window.location.hash.substring(1)); var editor = window.editor = Ckeditor.replace('editor1', { // https://dev.ckeditor.com/ticket/10907 needsBrFiller: fixThings, @@ -184,7 +183,6 @@ define([ } }; - var now = function () { return new Date().getTime(); }; var initializing = true; var userList = {}; // List of pretty name of all users (mapped with their server ID) @@ -250,12 +248,29 @@ define([ // our encryption key cryptKey: key, + // method which allows us to get the id of the user setMyID: setMyID, + // Crypto object to avoid loading it twice in Cryptpad + crypto: Crypto, + // really basic operational transform transformFunction : JsonOT.validate }; + var updateUserList = function(shjson) { + // Extract the user list (metadata) from the hyperjson + var hjson = JSON.parse(shjson); + 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) { if (initializing) { return; } @@ -265,18 +280,7 @@ define([ cursor.update(); // Extract the user list (metadata) from the hyperjson - var hjson = JSON.parse(shjson); - var peerUserList = hjson[hjson.length-1]; - if(peerUserList.metadata) { - var userData = peerUserList.metadata; - // Update the local user data - userList = userData; - // Send the new data to the toolbar - if(toolbarList && typeof toolbarList.onChange === "function") { - toolbarList.onChange(userList); - } - hjson.pop(); - } + var hjson = updateUserList(shjson); // build a dom from HJSON, diff, and patch the editor applyHjson(shjson); @@ -309,7 +313,7 @@ define([ var onReady = realtimeOptions.onReady = function (info) { module.patchText = TextPatcher.create({ realtime: info.realtime, - logging: false, + logging: true, }); module.realtime = info.realtime; @@ -338,7 +342,7 @@ define([ // append the userlist to the hyperjson structure if(Object.keys(myData).length > 0) { - hjson[hjson.length] = {metadata: userList}; + hjson[3] = {metadata: userList}; } // stringify the json and send it into chainpad var shjson = stringify(hjson); diff --git a/www/padrtc/main.js b/www/padrtc/main.js index ba983e799..233241a86 100644 --- a/www/padrtc/main.js +++ b/www/padrtc/main.js @@ -206,14 +206,12 @@ define([ // our encryption key cryptKey: key, - // configuration :D - doc: inner, - setMyID: setMyID, // really basic operational transform - transformFunction : JsonOT.validate - // pass in websocket/netflux object TODO + transformFunction : JsonOT.validate, + + crypto: Crypto, }; var onRemote = realtimeOptions.onRemote = function (info) { diff --git a/www/style/index.html b/www/style/index.html index e9fdf2855..4ede5c688 100644 --- a/www/style/index.html +++ b/www/style/index.html @@ -8,7 +8,6 @@ Edit this document's style -

HTML Ipsum Presents

diff --git a/www/style/main.js b/www/style/main.js index b230bbb3f..fa209cf7c 100644 --- a/www/style/main.js +++ b/www/style/main.js @@ -1,31 +1,37 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), '/common/realtime-input.js', - '/common/messages.js', '/common/crypto.js', + '/common/TextPatcher.js', '/bower_components/jquery/dist/jquery.min.js', '/customize/pad.js' -], function (Config, Realtime, Messages, Crypto) { +], function (Config, Realtime, Crypto, TextPatcher) { // TODO consider adding support for less.js var $ = window.jQuery; - $(window).on('hashchange', function() { - window.location.reload(); - }); - var userName = Crypto.rand64(8); + var $style = $('style').first(), + $edit = $('#edit'); - if (window.location.href.indexOf('#') === -1) { - window.location.href = window.location.href + '#' + Crypto.genKey(); - return; - } + var module = window.APP = {}; - var key = Crypto.parseKey(window.location.hash.slice(1)); + var key; + var channel = ''; + if (!/#/.test(window.location.href)) { + key = Crypto.genKey(); + } else { + var hash = window.location.hash.slice(1); + channel = hash.slice(0, 32); + key = hash.slice(32); + } - var $style = $('style').first(), - $css = $('#css'), - $edit = $('#edit'); + var config = { + websocketURL: Config.websocketURL, + channel: channel, + cryptKey: key, + crypto: Crypto, + }; - $edit.attr('href', '/text/'+ window.location.hash); + var userName = module.userName = config.userName = Crypto.rand64(8); var lazyDraw = (function () { var to, @@ -38,29 +44,45 @@ define([ }; }()); - var draw = function () { - lazyDraw($css.val()); + var draw = function (content) { lazyDraw(content); }; + + var initializing = true; + + var onInit = config.onInit = function (info) { + window.location.hash = info.channel + key; + var realtime = module.realtime = info.realtime; + module.patchText = TextPatcher.create({ + realtime: realtime, + logging: true, + }); + + $(window).on('hashchange', function() { + window.location.reload(); + }); }; - $css // set the initial value - .val($style.text()) - .on('change', draw); + var onReady = config.onReady = function (info) { + var userDoc = module.realtime.getUserDoc(); + draw(userDoc); + console.log("Ready"); + initializing = false; + }; - var rts = $('textarea').toArray().map(function (e, i) { + var onRemote = config.onRemote = function () { + draw(module.realtime.getUserDoc()); + }; - var config = { - onRemote: draw, - onInit: draw, - onReady: draw, + var onAbort = config.onAbort = function (info) { + // notify the user of the abort + window.alert("Network Connection Lost"); + }; - textarea: e, - websocketURL: Config.websocketURL, - userName: userName, - channel: key.channel, - cryptKey: key.cryptKey - }; + var onLocal = config.onLocal = function () { + // nope + }; + + + $edit.attr('href', '/text/'+ window.location.hash); - var rt = Realtime.start(config); - return rt; - }); + var rt = Realtime.start(config); }); diff --git a/www/text/main.js b/www/text/main.js index 644fa86f6..2b91c446d 100644 --- a/www/text/main.js +++ b/www/text/main.js @@ -1,22 +1,12 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), '/common/realtime-input.js', - '/common/messages.js', '/common/crypto.js', '/common/TextPatcher.js', '/bower_components/jquery/dist/jquery.min.js', '/customize/pad.js' -], function (Config, Realtime, Messages, Crypto, TextPatcher) { +], function (Config, Realtime, Crypto, TextPatcher) { 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 = ''; @@ -42,6 +32,7 @@ define([ userName: userName, channel: channel, cryptKey: key, + crypto: Crypto, }; var setEditable = function (bool) { $textarea.attr('disabled', !bool); }; @@ -51,6 +42,9 @@ define([ var onInit = config.onInit = function (info) { window.location.hash = info.channel + key; + $(window).on('hashchange', function() { + window.location.reload(); + }); }; var onRemote = config.onRemote = function (info) {