Merge branch 'beta' into migrate

pull/1/head
Yann Flory 9 years ago
commit 5bb2e12db2

@ -224,10 +224,16 @@ var transform = Patch.transform = function (origToTransform, transformBy, doc, t
var text = doc; var text = doc;
for (var i = toTransform.operations.length-1; i >= 0; i--) { for (var i = toTransform.operations.length-1; i >= 0; i--) {
for (var j = transformBy.operations.length-1; j >= 0; j--) { for (var j = transformBy.operations.length-1; j >= 0; j--) {
toTransform.operations[i] = Operation.transform(text, try {
toTransform.operations[i], toTransform.operations[i] = Operation.transform(text,
transformBy.operations[j], toTransform.operations[i],
transformFunction); 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]) { if (!toTransform.operations[i]) {
break; break;
} }
@ -370,6 +376,9 @@ var random = Patch.random = function (doc, opCount) {
var PARANOIA = module.exports.PARANOIA = true; 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 */ /* throw errors over non-compliant messages which would otherwise be treated as invalid */
var TESTING = module.exports.TESTING = true; var TESTING = module.exports.TESTING = true;
@ -832,7 +841,9 @@ var check = ChainPad.check = function(realtime) {
Common.assert(uiDoc === realtime.userInterfaceContent); 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; var patchMsg = realtime.best;
Common.assert(patchMsg.content.inverseOf.parentHash === realtime.uncommitted.parentHash); Common.assert(patchMsg.content.inverseOf.parentHash === realtime.uncommitted.parentHash);
var patches = []; var patches = [];
@ -844,7 +855,7 @@ var check = ChainPad.check = function(realtime) {
while ((patchMsg = patches.pop())) { while ((patchMsg = patches.pop())) {
doc = Patch.apply(patchMsg.content, doc); doc = Patch.apply(patchMsg.content, doc);
} }
Common.assert(doc === realtime.authDoc);*/ Common.assert(doc === realtime.authDoc);
}; };
var doOperation = ChainPad.doOperation = function (realtime, op) { var doOperation = ChainPad.doOperation = function (realtime, op) {

@ -1,226 +1,291 @@
/*global: WebSocket */ /*global: WebSocket */
define(() => { define(function () {
'use strict'; 'use strict';
const MAX_LAG_BEFORE_PING = 15000;
const MAX_LAG_BEFORE_DISCONNECT = 30000; var MAX_LAG_BEFORE_PING = 15000;
const PING_CYCLE = 5000; var MAX_LAG_BEFORE_DISCONNECT = 30000;
const REQUEST_TIMEOUT = 30000; var PING_CYCLE = 5000;
var REQUEST_TIMEOUT = 30000;
const now = () => new Date().getTime();
var now = function now() {
const networkSendTo = (ctx, peerId, content) => { return new Date().getTime();
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++
}; };
const chan = {
_: internal, var networkSendTo = function networkSendTo(ctx, peerId, content) {
time: now(), var seq = ctx.seq++;
id: id, ctx.ws.send(JSON.stringify([seq, 'MSG', peerId, content]));
members: internal.members, return new Promise(function (res, rej) {
bcast: (msg) => channelBcast(ctx, chan.id, msg), ctx.requests[seq] = { reject: rej, resolve: res, time: now() };
leave: (reason) => 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])); var channelBcast = function channelBcast(ctx, chanId, content) {
var chan = ctx.channels[chanId];
return new Promise((res, rej) => { if (!chan) {
chan._.resolve = res; throw new Error("no such channel " + chanId);
chan._.reject = rej; }
}) var seq = ctx.seq++;
}; ctx.ws.send(JSON.stringify([seq, 'MSG', chanId, content]));
return new Promise(function (res, rej) {
const mkNetwork = (ctx) => { ctx.requests[seq] = { reject: rej, resolve: res, time: now() };
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 })
}; };
network.__defineGetter__("webChannels", () => {
return Object.keys(ctx.channels).map((k) => (ctx.channels[k])); var channelLeave = function channelLeave(ctx, chanId, reason) {
}); var chan = ctx.channels[chanId];
return network; if (!chan) {
}; throw new Error("no such channel " + chanId);
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;
} }
delete ctx.requests[msg[0]]; delete ctx.channels[chanId];
if (msg[1] === 'ACK') { ctx.ws.send(JSON.stringify([ctx.seq++, 'LEAVE', chanId, reason]));
if (req.ping) { // ACK of a PING };
ctx.lag = now() - Number(req.ping);
return; var makeEventHandlers = function makeEventHandlers(ctx, mappings) {
} return function (name, handler) {
req.resolve(); var handlers = mappings[name];
} else if (msg[1] === 'JACK') { if (!handlers) {
if (req._) { throw new Error("no such event " + name);
// 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(); handlers.push(handler);
} else if (msg[1] === 'ERROR') { };
req.reject({ type: msg[2], message: msg[3] }); };
} else {
req.reject({ type: 'UNKNOWN', message: JSON.stringify(msg) }); 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[0] !== 0) {
} var req = ctx.requests[msg[0]];
if (msg[2] === 'IDENT') { if (!req) {
ctx.uid = msg[3]; console.log("error: " + JSON.stringify(msg));
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));
return; 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') { return;
const chan = ctx.channels[msg[3]]; } else if (!ctx.uid) {
if (!chan) { // extranious message, waiting for an ident.
console.log("leaving non-existant chan " + JSON.stringify(msg));
return; return;
} }
chan._.onLeave.forEach((h) => { if (msg[2] === 'PING') {
try { h(msg[1], msg[4]); } catch (e) { console.log(e.stack); } msg[2] = 'PONG';
}); ctx.ws.send(JSON.stringify(msg));
}
if (msg[2] === 'JOIN') {
const chan = ctx.channels[msg[3]];
if (!chan) {
console.log("ERROR: join to non-existant chan " + JSON.stringify(msg));
return; return;
} }
// have we yet fully joined the chan?
const synced = (chan._.members.indexOf(ctx.uid) !== -1); if (msg[2] === 'MSG') {
chan._.members.push(msg[1]); var handlers = void 0;
if (!synced && msg[1] === ctx.uid) { if (msg[3] === ctx.uid) {
// sync the channel join event handlers = ctx.onMessage;
chan.myID = ctx.uid; } else {
chan._.resolve(chan); 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) => { if (msg[2] === 'LEAVE') {
try { h(msg[1]); } catch (e) { console.log(e.stack); } 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);
}
}); });
} }
}
}; if (msg[2] === 'JOIN') {
var _chan2 = ctx.channels[msg[3]];
const connect = (websocketURL) => { if (!_chan2) {
let ctx = { console.log("ERROR: join to non-existant chan " + JSON.stringify(msg));
ws: new WebSocket(websocketURL), return;
seq: 1, }
lag: 0, // have we yet fully joined the chan?
uid: null, var synced = _chan2._.members.indexOf(ctx.uid) !== -1;
network: null, _chan2._.members.push(msg[1]);
channels: {}, if (!synced && msg[1] === ctx.uid) {
onMessage: [], // sync the channel join event
onDisconnect: [], _chan2.myID = ctx.uid;
requests: {} _chan2._.resolve(_chan2);
}; }
setInterval(() => { if (synced) {
for (let id in ctx.requests) { _chan2._.onJoin.forEach(function (h) {
const req = ctx.requests[id]; try {
if (now() - req.time > REQUEST_TIMEOUT) { h(msg[1]);
delete ctx.requests[id]; } catch (e) {
if(typeof req.reject === "function") { req.reject({ type: 'TIMEOUT', message: 'waited ' + now() - req.time + 'ms' }); } console.log(e.stack);
}
});
} }
} }
}, 5000); };
ctx.network = mkNetwork(ctx);
ctx.ws.onmessage = (msg) => (onMessage(ctx, msg)); var connect = function connect(websocketURL) {
ctx.ws.onclose = (evt) => { var ctx = {
ctx.onDisconnect.forEach((h) => { ws: new WebSocket(websocketURL),
try { h(evt.reason); } catch (e) { console.log(e.stack); } 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 };
}); });

@ -11,33 +11,59 @@
overflow: hidden; overflow: hidden;
box-sizing: border-box; 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;
}
</style> </style>
</head> </head>
<body> <body>
<form> <form>
<input type="text" name="text"><br> <input type="radio" name="radio" value="one" checked>One
<input type="password" name="password"><br> <input type="radio" name="radio" value="two">Two
<input type="radio" name="radio" value="one" checked>One<br>
<input type="radio" name="radio" value="two">Two<br>
<input type="radio" name="radio" value="three">Three<br> <input type="radio" name="radio" value="three">Three<br>
<input type="checkbox" name="checkbox1" value="1">Checkbox One<br> <input type="checkbox" name="checkbox1" value="1">Checkbox One
<input type="checkbox" name="checkbox2" value="2">Checkbox Two<br> <input type="checkbox" name="checkbox2" value="2">Checkbox Two<br>
<input type="number" name="number" min="1" max="5">Number<br> <input type="text" name="text" placeholder="Text Input"><br>
<input type="password" name="password" placeholder="Passwords"><br>
<input type="number" name="number" min="1" max="5" placeholder="Numbers">Number<br>
<input type="range" name="range" min="0" max="10">Ranges<br> <input type="range" name="range" min="0" max="100">Ranges<br>
<select> <select name="select">
<option value="one">One</option> <option value="one">One</option>
<option value="two">Two</option> <option value="two">Two</option>
<option value="three">Three</option> <option value="three">Three</option>
<option value="four">Four</option> <option value="four">Four</option>
</select> Dropdowns<br> </select> Dropdowns<br>
<textarea rows="4" cols="50"> </textarea><br> <textarea name="textarea"></textarea><br>
</form> </form>

@ -1,80 +1,201 @@
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
define([ define([
'/api/config?cb=' + Math.random().toString(16).substring(2), '/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/RealtimeTextarea.js', '/common/realtime-input.js',
'/common/messages.js',
'/common/crypto.js', '/common/crypto.js',
'/common/TextPatcher.js', '/common/TextPatcher.js',
'json.sortify',
'/form/ula.js',
'/common/json-ot.js',
'/bower_components/jquery/dist/jquery.min.js', '/bower_components/jquery/dist/jquery.min.js',
'/customize/pad.js' '/customize/pad.js'
], function (Config, Realtime, Messages, Crypto, TextPatcher) { ], function (Config, Realtime, Crypto, TextPatcher, Sortify, Formula, JsonOT) {
var $ = window.jQuery; var $ = window.jQuery;
$(window).on('hashchange', function() {
window.location.reload(); var key;
}); var channel = '';
if (window.location.href.indexOf('#') === -1) { var hash = false;
window.location.href = window.location.href + '#' + Crypto.genKey(); if (!/#/.test(window.location.href)) {
return; key = Crypto.genKey();
} else {
hash = window.location.hash.slice(1);
channel = hash.slice(0,32);
key = hash.slice(32);
} }
var module = window.APP = {}; var module = window.APP = {
var key = Crypto.parseKey(window.location.hash.substring(1)); TextPatcher: TextPatcher,
Sortify: Sortify,
Formula: Formula,
};
var initializing = true; var initializing = true;
/* elements that we need to listen to */ var uid = module.uid = Formula.uid;
/*
* text var getInputType = Formula.getInputType;
* password var $elements = module.elements = $('input, select, textarea')
* radio
* checkbox var eventsByType = Formula.eventsByType;
* number
* range
* select
* textarea
*/
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 = { var config = module.config = {
websocketURL: Config.websocketURL + '_old', initialState: Sortify(Map) || '{}',
websocketURL: Config.websocketURL,
userName: Crypto.rand64(8), userName: Crypto.rand64(8),
channel: key.channel, channel: channel,
cryptKey: key.cryptKey cryptKey: key,
crypto: Crypto,
transformFunction: JsonOT.validate
}; };
var setEditable = function (bool) {/* allow editing */}; var setEditable = module.setEditable = function (bool) {
var canonicalize = function (text) {/* canonicalize all the things */}; /* (dis)allow editing */
$elements.each(function () {
$(this).attr('disabled', !bool);
});
};
setEditable(false); 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) { // create your patcher
if (initializing) { return; } module.patchText = TextPatcher.create({
/* integrate remote changes */ realtime: realtime,
logging: true,
});
}; };
var onLocal = config.onLocal = function () { var onLocal = config.onLocal = function () {
if (initializing) { return; } if (initializing) { return; }
/* serialize local changes */ /* serialize local changes */
readValues();
module.patchText(Sortify(Map));
}; };
var onReady = config.onReady = function (info) { var readValues = function () {
var realtime = module.realtime = info.realtime; UI.each(function (ui, i, list) {
Map[ui.id] = ui.value();
});
};
// create your patcher var updateValues = function () {
module.patchText = TextPatcher.create({ var userDoc = module.realtime.getUserDoc();
realtime: realtime 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); setEditable(true);
initializing = false; initializing = false;
}; };
var onAbort = config.onAbort = function (info) {}; var onAbort = config.onAbort = function (info) {
window.alert("Network Connection Lost");
};
var rt = Realtime.start(config); 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);
});
}); });

@ -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()
*/
```

@ -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;
});

@ -60,10 +60,6 @@ define([
return hj; return hj;
}; };
var stringifyDOM = function (dom) {
return stringify(Hyperjson.fromDOM(dom, isNotMagicLine, brFilter));
};
var andThen = function (Ckeditor) { var andThen = function (Ckeditor) {
/* This is turned off because we prefer that the channel name /* This is turned off because we prefer that the channel name
be chosen by the server, not generated by the client. be chosen by the server, not generated by the client.
@ -232,6 +228,12 @@ define([
(DD).apply(inner, patch); (DD).apply(inner, patch);
}; };
var stringifyDOM = function (dom) {
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter);
hjson[3] = {metadata: userList};
return stringify(hjson);
};
var realtimeOptions = { var realtimeOptions = {
// provide initialstate... // provide initialstate...
initialState: stringifyDOM(inner) || '{}', initialState: stringifyDOM(inner) || '{}',
@ -261,14 +263,13 @@ define([
var updateUserList = function(shjson) { var updateUserList = function(shjson) {
// Extract the user list (metadata) from the hyperjson // Extract the user list (metadata) from the hyperjson
var hjson = JSON.parse(shjson); var hjson = JSON.parse(shjson);
var peerUserList = hjson[hjson.length-1]; var peerUserList = hjson[3];
if(peerUserList.metadata) { if(peerUserList && peerUserList.metadata) {
var userData = peerUserList.metadata; var userData = peerUserList.metadata;
// Update the local user data // Update the local user data
addToUserList(userData); addToUserList(userData);
hjson.pop(); hjson.pop();
} }
return hjson;
} }
var onRemote = realtimeOptions.onRemote = function (info) { var onRemote = realtimeOptions.onRemote = function (info) {
@ -279,15 +280,12 @@ define([
// remember where the cursor is // remember where the cursor is
cursor.update(); cursor.update();
// Extract the user list (metadata) from the hyperjson // Update the user list (metadata) from the hyperjson
var hjson = updateUserList(shjson); updateUserList(shjson);
// build a dom from HJSON, diff, and patch the editor // build a dom from HJSON, diff, and patch the editor
applyHjson(shjson); 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); var shjson2 = stringifyDOM(inner);
if (shjson2 !== shjson) { if (shjson2 !== shjson) {
console.error("shjson2 !== shjson"); console.error("shjson2 !== shjson");
@ -313,7 +311,7 @@ define([
var onReady = realtimeOptions.onReady = function (info) { var onReady = realtimeOptions.onReady = function (info) {
module.patchText = TextPatcher.create({ module.patchText = TextPatcher.create({
realtime: info.realtime, realtime: info.realtime,
logging: false, logging: true,
}); });
module.realtime = info.realtime; module.realtime = info.realtime;
@ -337,15 +335,8 @@ define([
var onLocal = realtimeOptions.onLocal = function () { var onLocal = realtimeOptions.onLocal = function () {
if (initializing) { return; } 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 // stringify the json and send it into chainpad
var shjson = stringify(hjson); var shjson = stringifyDOM(inner);
module.patchText(shjson); module.patchText(shjson);
if (module.realtime.getUserDoc() !== shjson) { if (module.realtime.getUserDoc() !== shjson) {

@ -1,7 +1,6 @@
define([ define([
'/api/config?cb=' + Math.random().toString(16).substring(2), '/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/realtime-input.js', '/common/realtime-input.js',
'/common/messages.js',
'/common/crypto.js', '/common/crypto.js',
'/bower_components/marked/marked.min.js', '/bower_components/marked/marked.min.js',
'/common/convert.js', '/common/convert.js',
@ -15,30 +14,38 @@ define([
Hyperjson = Convert.core.hyperjson, Hyperjson = Convert.core.hyperjson,
Hyperscript = Convert.core.hyperscript; Hyperscript = Convert.core.hyperscript;
window.Vdom = Vdom; var key;
window.Hyperjson = Hyperjson; var channel = '';
window.Hyperscript = Hyperscript; var hash = false;
if (!/#/.test(window.location.href)) {
$(window).on('hashchange', function() { key = Crypto.genKey();
window.location.reload(); } else {
}); hash = window.location.hash.slice(1);
if (window.location.href.indexOf('#') === -1) { channel = hash.slice(0, 32);
window.location.href = window.location.href + '#' + Crypto.genKey(); key = hash.slice(32);
return;
} }
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 // set markdown rendering options :: strip html to prevent XSS
Marked.setOptions({ Marked.setOptions({
sanitize: true 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 draw = window.draw = (function () {
var target = $target[0], var target = $target[0],
inner = $target.find('#inner')[0]; inner = $target.find('#inner')[0];
@ -58,8 +65,7 @@ define([
}; };
}()); }());
// FIXME var colour = module.colour = Rainbow();
var colour = window.colour = Rainbow();
var $inner = $('#inner'); var $inner = $('#inner');
@ -83,31 +89,43 @@ define([
}, 450); }, 450);
}; };
var config = { var initializing = true;
textarea: $textarea[0],
websocketURL: Config.websocketURL, var onInit = config.onInit = function (info) {
userName: Crypto.rand64(8), window.location.hash = info.channel + key;
channel: key.channel, module.realtime = info.realtime;
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 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 () { var userDoc = module.realtime.getUserDoc();
if (redrawTimeout) { clearTimeout(redrawTimeout); } lazyDraw(userDoc);
redrawTimeout = setTimeout(function () {
lazyDraw($textarea.val()); initializing = false;
}, 500); };
});
// 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);
}); });

@ -9,6 +9,9 @@ define([
// TODO consider adding support for less.js // TODO consider adding support for less.js
var $ = window.jQuery; var $ = window.jQuery;
var $style = $('style').first(),
$edit = $('#edit');
var module = window.APP = {}; var module = window.APP = {};
var key; var key;
@ -78,8 +81,6 @@ define([
// nope // nope
}; };
var $style = $('style').first(),
$edit = $('#edit');
$edit.attr('href', '/text/'+ window.location.hash); $edit.attr('href', '/text/'+ window.location.hash);

Loading…
Cancel
Save