Merge branch 'beta' into migrate

This commit is contained in:
Yann Flory 2016-04-27 10:51:51 +02:00
commit 5bb2e12db2
9 changed files with 600 additions and 329 deletions

View File

@ -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) {

View File

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

View File

@ -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="range" name="range" min="0" max="10">Ranges<br>
<select> <input type="number" name="number" min="1" max="5" placeholder="Numbers">Number<br>
<input type="range" name="range" min="0" max="100">Ranges<br>
<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>

View File

@ -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
* password
* radio
* checkbox
* number
* range
* select
* textarea
*/
var $textarea = $('textarea'); var getInputType = Formula.getInputType;
var $elements = module.elements = $('input, select, textarea')
var config = module.config = { var eventsByType = Formula.eventsByType;
websocketURL: Config.websocketURL + '_old',
userName: Crypto.rand64(8), var Map = module.Map = {};
channel: key.channel,
cryptKey: key.cryptKey var UI = module.UI = {
ids: [],
each: function (f) {
UI.ids.forEach(function (id, i, list) {
f(UI[id], i, list);
});
}
}; };
var setEditable = function (bool) {/* allow editing */}; var cursorTypes = ['textarea', 'password', 'text'];
var canonicalize = function (text) {/* canonicalize all the things */};
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 = {
initialState: Sortify(Map) || '{}',
websocketURL: Config.websocketURL,
userName: Crypto.rand64(8),
channel: channel,
cryptKey: key,
crypto: Crypto,
transformFunction: JsonOT.validate
};
var setEditable = module.setEditable = function (bool) {
/* (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 readValues = function () {
UI.each(function (ui, i, list) {
Map[ui.id] = ui.value();
});
};
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];
}
});
};
var onRemote = config.onRemote = function (info) {
if (initializing) { return; }
/* integrate remote changes */
updateValues();
}; };
var onReady = config.onReady = function (info) { var onReady = config.onReady = function (info) {
var realtime = module.realtime = info.realtime; updateValues();
// create your patcher
module.patchText = TextPatcher.create({
realtime: realtime
});
// get ready
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);
});
}); });

14
www/form/types.md Normal file
View File

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

24
www/form/ula.js Normal file
View File

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

View File

@ -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) {

View File

@ -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,
userName: Crypto.rand64(8),
channel: key.channel,
cryptKey: key.cryptKey,
// when remote editors do things... var onInit = config.onInit = function (info) {
onRemote: function () { window.location.hash = info.channel + key;
lazyDraw($textarea.val()); module.realtime = info.realtime;
}, };
// when your editor is ready
onReady: function (info) { // when your editor is ready
if (info.userList) { console.log("Userlist: [%s]", info.userList.join(',')); } var onReady = config.onReady = function (info) {
console.log("Realtime is ready!"); //if (info.userList) { console.log("Userlist: [%s]", info.userList.join(',')); }
$textarea.trigger('keyup'); console.log("Realtime is ready!");
}
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); var rts = Realtime.start(config);
$textarea.on('change keyup keydown', function () {
if (redrawTimeout) { clearTimeout(redrawTimeout); }
redrawTimeout = setTimeout(function () {
lazyDraw($textarea.val());
}, 500);
});
}); });

View File

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