Merge branch 'beta' into migrate
This commit is contained in:
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;
|
|
||||||
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 };
|
||||||
});
|
});
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
209
www/form/main.js
209
www/form/main.js
@ -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
14
www/form/types.md
Normal 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
24
www/form/ula.js
Normal 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;
|
||||||
|
});
|
||||||
@ -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,
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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…
x
Reference in New Issue
Block a user