Merge branch 'cjd-fixed-it-with-hax' into netflux2

This commit is contained in:
Yann Flory 2016-04-01 10:51:27 +02:00
commit b41f0e8c50
14 changed files with 641 additions and 463 deletions

View File

@ -327,10 +327,11 @@ console.log(new Error().stack);
error(false, 'realtime.getUserDoc() !== docText');
}
};
var now = function () { return new Date().getTime(); };
var userDocBeforePatch;
var incomingPatch = function () {
if (isErrorState || initializing) { return; }
console.log("before patch " + now());
userDocBeforePatch = userDocBeforePatch || getFixedDocText(doc, ifr.contentWindow);
if (PARANOIA && userDocBeforePatch !== getFixedDocText(doc, ifr.contentWindow)) {
error(false, "userDocBeforePatch !== getFixedDocText(doc, ifr.contentWindow)");
@ -339,6 +340,7 @@ console.log(new Error().stack);
if (!op) { return; }
attempt(HTMLPatcher.applyOp)(
userDocBeforePatch, op, doc.body, Rangy, ifr.contentWindow);
console.log("after patch " + now());
};
realtime.onUserListChange(function (userList) {

View File

@ -14,28 +14,17 @@
left:0px;
bottom:0px;
right:0px;
width:70%;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#feedback {
position: fixed;
top: 0px;
right: 0px;
border: 0px;
height: 100vh;
width: 30vw;
background-color: #222;
color: #ccc;
}
</style>
</head>
<body>
<iframe id="pad-iframe" src="inner.html"></iframe>
<textarea id="feedback"></textarea>
</body>
</html>

324
www/_socket/main.js Normal file
View File

@ -0,0 +1,324 @@
define([
'/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/messages.js',
'/common/crypto.js',
'/_socket/realtime-input.js',
'/common/hyperjson.js',
'/common/hyperscript.js',
'/_socket/toolbar.js',
'/common/cursor.js',
'/common/json-ot.js',
'/_socket/typingTest.js',
'/bower_components/diff-dom/diffDOM.js',
'/bower_components/jquery/dist/jquery.min.js',
'/customize/pad.js'
], function (Config, Messages, Crypto, realtimeInput, Hyperjson, Hyperscript, Toolbar, Cursor, JsonOT, TypingTest) {
var $ = window.jQuery;
var ifrw = $('#pad-iframe')[0].contentWindow;
var Ckeditor; // to be initialized later...
var DiffDom = window.diffDOM;
window.Hyperjson = Hyperjson;
var hjsonToDom = function (H) {
return Hyperjson.callOn(H, Hyperscript);
};
var userName = Crypto.rand64(8),
toolbar;
var module = window.REALTIME_MODULE = {
localChangeInProgress: 0
};
var isNotMagicLine = function (el) {
// factor as:
// return !(el.tagName === 'SPAN' && el.contentEditable === 'false');
var filter = (el.tagName === 'SPAN' && el.contentEditable === 'false');
if (filter) {
console.log("[hyperjson.serializer] prevented an element" +
"from being serialized:", el);
return false;
}
return true;
};
var andThen = function (Ckeditor) {
$(window).on('hashchange', function() {
window.location.reload();
});
if (window.location.href.indexOf('#') === -1) {
window.location.href = window.location.href + '#' + Crypto.genKey();
return;
}
var fixThings = false;
var key = Crypto.parseKey(window.location.hash.substring(1));
var editor = window.editor = Ckeditor.replace('editor1', {
// https://dev.ckeditor.com/ticket/10907
needsBrFiller: fixThings,
needsNbspFiller: fixThings,
removeButtons: 'Source,Maximize',
// magicline plugin inserts html crap into the document which is not part of the
// document itself and causes problems when it's sent across the wire and reflected back
// but we filter it now, so that's ok.
removePlugins: 'resize'
});
editor.on('instanceReady', function (Ckeditor) {
editor.execCommand('maximize');
var documentBody = ifrw.$('iframe')[0].contentDocument.body;
documentBody.innerHTML = Messages.initialState;
var inner = window.inner = documentBody;
var cursor = window.cursor = Cursor(inner);
var setEditable = function (bool) {
// careful about putting attributes onto the DOM
// they get put into the chain, and you can have trouble
// getting rid of them later
//inner.style.backgroundColor = bool? 'white': 'grey';
inner.setAttribute('contenteditable', bool);
};
// don't let the user edit until the pad is ready
setEditable(false);
var diffOptions = {
preDiffApply: function (info) {
/* DiffDOM will filter out magicline plugin elements
in practice this will make it impossible to use it
while someone else is typing, which could be annoying.
we should check when such an element is going to be
removed, and prevent that from happening. */
if (info.node && info.node.tagName === 'SPAN' &&
info.node.contentEditable === "true") {
// it seems to be a magicline plugin element...
if (info.diff.action === 'removeElement') {
// and you're about to remove it...
// this probably isn't what you want
/*
I have never seen this in the console, but the
magic line is still getting removed on remote
edits. This suggests that it's getting removed
by something other than diffDom.
*/
console.log("preventing removal of the magic line!");
// return true to prevent diff application
return true;
}
}
// no use trying to recover the cursor if it doesn't exist
if (!cursor.exists()) { return; }
/* frame is either 0, 1, 2, or 3, depending on which
cursor frames were affected: none, first, last, or both
*/
var frame = info.frame = cursor.inNode(info.node);
if (!frame) { return; }
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') {
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue);
if (frame & 1) {
// push cursor start if necessary
if (pushes.commonStart < cursor.Range.start.offset) {
cursor.Range.start.offset += pushes.delta;
}
}
if (frame & 2) {
// push cursor end if necessary
if (pushes.commonStart < cursor.Range.end.offset) {
cursor.Range.end.offset += pushes.delta;
}
}
}
},
postDiffApply: function (info) {
if (info.frame) {
if (info.node) {
if (info.frame & 1) { cursor.fixStart(info.node); }
if (info.frame & 2) { cursor.fixEnd(info.node); }
} else { console.error("info.node did not exist"); }
var sel = cursor.makeSelection();
var range = cursor.makeRange();
cursor.fixSelection(sel, range);
}
}
};
var now = function () { return new Date().getTime(); };
var realtimeOptions = {
// configuration :D
doc: inner,
// provide initialstate...
initialState: JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine)),
// really basic operational transform
// reject patch if it results in invalid JSON
transformFunction : JsonOT.validate,
websocketURL: Config.websocketURL,
// username
userName: userName,
// communication channel name
channel: key.channel,
// encryption key
cryptKey: key.cryptKey
};
var DD = new DiffDom(diffOptions);
var localWorkInProgress = function (stage) {
if (module.localChangeInProgress) {
console.error("Applied a change while a local patch was in progress");
alert("local work was interrupted at stage: " + stage);
//module.realtimeInput.onLocal();
return true;
}
return false;
};
// apply patches, and try not to lose the cursor in the process!
var applyHjson = function (shjson) {
localWorkInProgress(1); // check if this would interrupt local work
var userDocStateDom = hjsonToDom(JSON.parse(shjson));
localWorkInProgress(2); // check again
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
localWorkInProgress(3); // check again
var patch = (DD).diff(inner, userDocStateDom);
localWorkInProgress(4); // check again
(DD).apply(inner, patch);
localWorkInProgress(5); // check again
};
var initializing = true;
var onRemote = realtimeOptions.onRemote = function (info) {
if (initializing) { return; }
localWorkInProgress(0);
var shjson = info.realtime.getUserDoc();
// remember where the cursor is
cursor.update();
// build a dom from HJSON, diff, and patch the editor
applyHjson(shjson);
var shjson2 = JSON.stringify(Hyperjson.fromDOM(inner));
if (shjson2 !== shjson) {
console.error("shjson2 !== shjson");
module.realtimeInput.patchText(shjson2);
}
};
var onInit = realtimeOptions.onInit = function (info) {
var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox');
toolbar = info.realtime.toolbar = Toolbar.create($bar, userName, info.realtime);
/* TODO handle disconnects and such*/
};
var onReady = realtimeOptions.onReady = function (info) {
console.log("Unlocking editor");
initializing = false;
setEditable(true);
var shjson = info.realtime.getUserDoc();
applyHjson(shjson);
};
var onAbort = realtimeOptions.onAbort = function (info) {
console.log("Aborting the session!");
// stop the user from continuing to edit
// by setting the editable to false
setEditable(false);
toolbar.failed();
};
var rti = module.realtimeInput = realtimeInput.start(realtimeOptions);
/* catch `type="_moz"` before it goes over the wire */
var brFilter = function (hj) {
if (hj[1].type === '_moz') { hj[1].type = undefined; }
return hj;
};
/* It's incredibly important that you assign 'rti.onLocal'
It's used inside of realtimeInput to make sure that all changes
make it into chainpad.
It's being assigned this way because it can't be passed in, and
and can't be easily returned from realtime input without making
the code less extensible.
*/
var propogate = rti.onLocal = function () {
/* if the problem were a matter of external patches being
applied while a local patch were in progress, then we would
expect to be able to check and find
'module.localChangeInProgress' with a non-zero value while
we were applying a remote change.
*/
module.localChangeInProgress += 1;
var shjson = JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine, brFilter));
if (!rti.patchText(shjson)) {
module.localChangeInProgress -= 1;
return;
}
rti.onEvent(shjson);
module.localChangeInProgress -= 1;
};
/* hitting enter makes a new line, but places the cursor inside
of the <br> instead of the <p>. This makes it such that you
cannot type until you click, which is rather unnacceptable.
If the cursor is ever inside such a <br>, you probably want
to push it out to the parent element, which ought to be a
paragraph tag. This needs to be done on keydown, otherwise
the first such keypress will not be inserted into the P. */
inner.addEventListener('keydown', cursor.brFix);
var easyTest = window.easyTest = function () {
cursor.update();
var start = cursor.Range.start;
var test = TypingTest.testInput(inner, start.el, start.offset, propogate);
propogate();
return test;
};
editor.on('change', propogate);
});
};
var interval = 100;
var first = function () {
Ckeditor = ifrw.CKEDITOR;
if (Ckeditor) {
andThen(Ckeditor);
} else {
console.log("Ckeditor was not defined. Trying again in %sms",interval);
setTimeout(first, interval);
}
};
$(first);
});

View File

@ -18,10 +18,11 @@ define([
'/common/messages.js',
'/bower_components/reconnectingWebsocket/reconnecting-websocket.js',
'/common/crypto.js',
'/common/sharejs_textarea.js',
'/_socket/toolbar.js',
'/_socket/text-patcher.js',
'/common/chainpad.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Messages, ReconnectingWebSocket, Crypto, sharejs) {
], function (Messages,/*FIXME*/ ReconnectingWebSocket, Crypto, Toolbar, TextPatcher) {
var $ = window.jQuery;
var ChainPad = window.ChainPad;
var PARANOIA = true;
@ -33,53 +34,22 @@ define([
*/
var MAX_RECOVERABLE_ERRORS = 15;
var recoverableErrors = 0;
/** Maximum number of milliseconds of lag before we fail the connection. */
var MAX_LAG_BEFORE_DISCONNECT = 20000;
var debug = function (x) { console.log(x); },
warn = function (x) { console.error(x); },
verbose = function (x) { /*console.log(x);*/ };
// ------------------ Trapping Keyboard Events ---------------------- //
var bindEvents = function (element, events, callback, unbind) {
for (var i = 0; i < events.length; i++) {
var e = events[i];
if (element.addEventListener) {
if (unbind) {
element.removeEventListener(e, callback, false);
} else {
element.addEventListener(e, callback, false);
}
} else {
if (unbind) {
element.detachEvent('on' + e, callback);
} else {
element.attachEvent('on' + e, callback);
}
}
var debug = function (x) { console.log(x); };
var warn = function (x) { console.error(x); };
var verbose = function (x) { /*console.log(x);*/ };
var error = function (x) {
console.error(x);
recoverableErrors++;
if (recoverableErrors >= MAX_RECOVERABLE_ERRORS) {
window.alert("FAIL");
}
};
var bindAllEvents = function (textarea, docBody, onEvent, unbind)
{
/*
we use docBody for the purposes of CKEditor.
because otherwise special keybindings like ctrl-b and ctrl-i
would open bookmarks and info instead of applying bold/italic styles
*/
if (docBody) {
bindEvents(docBody,
['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste'],
onEvent,
unbind);
}
bindEvents(textarea,
['mousedown','mouseup','click','change'],
onEvent,
unbind);
};
/* websocket stuff */
var isSocketDisconnected = function (socket, realtime) {
var sock = socket._socket;
@ -113,11 +83,17 @@ define([
var makeWebsocket = function (url) {
var socket = new ReconnectingWebSocket(url);
/* create a set of handlers to use instead of the native socket handler
these handlers will iterate over all of the functions pushed to the
arrays bearing their name.
The first such function to return `false` will prevent subsequent
functions from being executed. */
var out = {
onOpen: [],
onClose: [],
onError: [],
onMessage: [],
onOpen: [], // takes care of launching the post-open logic
onClose: [], // takes care of cleanup
onError: [], // in case of error, socket will close, and fire this
onMessage: [], // used for the bulk of our logic
send: function (msg) { socket.send(msg); },
close: function () { socket.close(); },
_socket: socket
@ -132,6 +108,8 @@ define([
}
};
};
// bind your new handlers to the important listeners on the socket
socket.onopen = mkHandler('onOpen');
socket.onclose = mkHandler('onClose');
socket.onerror = mkHandler('onError');
@ -140,62 +118,52 @@ define([
};
/* end websocket stuff */
var start = module.exports.start =
function (textarea, websocketUrl, userName, channel, cryptKey, config)
{
var start = module.exports.start = function (config) {
//var textarea = config.textarea;
var websocketUrl = config.websocketURL;
var userName = config.userName;
var channel = config.channel;
var cryptKey = config.cryptKey;
var passwd = 'y';
// make sure configuration is defined
config = config || {};
var doc = config.doc || null;
// trying to deprecate onRemote, prefer loading it via the conf
var onRemote = config.onRemote || null;
// wrap up the reconnecting websocket with our additional stack logic
var socket = makeWebsocket(websocketUrl);
var transformFunction = config.transformFunction || null;
var socket;
if (config.socketAdaptor) {
// do netflux stuff
} else {
socket = makeWebsocket(websocketUrl);
}
// define this in case it gets called before the rest of our stuff is ready.
var onEvent = function () { };
var allMessages = [];
var allMessages = window.chainpad_allMessages = [];
var isErrorState = false;
var initializing = true;
var recoverableErrorCount = 0;
var $textarea = $(textarea);
var bump = function () {};
var toReturn = {
socket: socket
};
var toReturn = { socket: socket };
socket.onOpen.push(function (evt) {
if (!initializing) {
console.log("Starting");
// realtime is passed around as an attribute of the socket
// FIXME??
socket.realtime.start();
return;
}
var realtime = toReturn.realtime = socket.realtime =
// everybody has a username, and we assume they don't collide
// usernames are used to determine whether a message is remote
// or local in origin. This could mess with expected behaviour
// if someone spoofed.
ChainPad.create(userName,
passwd, // password, to be deprecated (maybe)
channel, // the channel we're to connect to
var realtime = toReturn.realtime = socket.realtime = ChainPad.create(userName,
passwd,
channel,
$(textarea).val(),
{
transformFunction: config.transformFunction
});
// initialState argument. (optional)
config.initialState || '',
// transform function (optional), which handles conflicts
{ transformFunction: config.transformFunction });
var onEvent = toReturn.onEvent = function (newText) {
if (isErrorState || initializing) { return; }
// assert things here...
if (realtime.getUserDoc() !== newText) {
// this is a problem
warn("realtime.getUserDoc() !== newText");
}
//try{throw new Error();}catch(e){console.log(e.stack);}
};
// pass your shiny new realtime into initialization functions
if (config.onInit) {
// extend as you wish
config.onInit({
@ -203,11 +171,10 @@ define([
});
}
onEvent = function () {
// This looks broken
if (isErrorState || initializing) { return; }
};
/* UI hints on userList changes are handled within the toolbar
so we don't actually need to do anything here except confirm
whether we've successfully joined the session, and call our
'onReady' function */
realtime.onUserListChange(function (userList) {
if (!initializing || userList.indexOf(userName) === -1) {
return;
@ -221,14 +188,32 @@ define([
if (config.onReady) {
// extend as you wish
config.onReady({
userList: userList
userList: userList,
realtime: realtime
});
}
});
var whoami = new RegExp(userName.replace(/[\/\+]/g, function (c) {
return '\\' +c;
}));
// when a message is ready to send
// Don't confuse this onMessage with socket.onMessage
realtime.onMessage(function (message) {
if (isErrorState) { return; }
message = Crypto.encrypt(message, cryptKey);
try {
socket.send(message);
} catch (e) {
warn(e);
}
});
realtime.onPatch(function () {
if (config.onRemote) {
config.onRemote({
realtime: realtime
//realtime.getUserDoc()
});
}
});
// when you receive a message...
socket.onMessage.push(function (evt) {
@ -239,37 +224,11 @@ define([
verbose(message);
allMessages.push(message);
if (!initializing) {
if (PARANOIA) {
// FIXME this is out of sync with the application logic
onEvent();
if (toReturn.onLocal) {
toReturn.onLocal();
}
}
realtime.message(message);
if (/\[5,/.test(message)) { verbose("pong"); }
if (!initializing) {
if (/\[2,/.test(message)) {
//verbose("Got a patch");
if (whoami.test(message)) {
//verbose("Received own message");
} else {
//verbose("Received remote message");
// obviously this is only going to get called if
if (onRemote) { onRemote(realtime.getUserDoc()); }
}
}
}
});
// when a message is ready to send
realtime.onMessage(function (message) {
if (isErrorState) { return; }
message = Crypto.encrypt(message, cryptKey);
try {
socket.send(message);
} catch (e) {
warn(e);
}
});
// actual socket bindings
@ -286,7 +245,6 @@ define([
socket.onerror = warn;
// TODO confirm that we can rely on netflux API
var socketChecker = setInterval(function () {
if (checkSocket(socket)) {
warn("Socket disconnected!");
@ -303,26 +261,17 @@ define([
}
if (socketChecker) { clearInterval(socketChecker); }
}
} else {
// it's working as expected, continue
}
} // it's working as expected, continue
}, 200);
bindAllEvents(textarea, doc, onEvent, false);
// attach textarea
// NOTE: should be able to remove the websocket without damaging this
sharejs.attach(textarea, realtime);
toReturn.patchText = TextPatcher.create({
realtime: realtime
});
realtime.start();
debug('started');
bump = realtime.bumpSharejs;
});
toReturn.onEvent = function () { onEvent(); };
toReturn.bumpSharejs = function () { bump(); };
return toReturn;
};
return module.exports;

View File

@ -0,0 +1,88 @@
define(function () {
/* applyChange takes:
ctx: the context (aka the realtime)
oldval: the old value
newval: the new value
it performs a diff on the two values, and generates patches
which are then passed into `ctx.remove` and `ctx.insert`
*/
var applyChange = function(ctx, oldval, newval) {
// Strings are immutable and have reference equality. I think this test is O(1), so its worth doing.
if (oldval === newval) {
return;
}
var commonStart = 0;
while (oldval.charAt(commonStart) === newval.charAt(commonStart)) {
commonStart++;
}
var commonEnd = 0;
while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) &&
commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) {
commonEnd++;
}
var result;
/* throw some assertions in here before dropping patches into the realtime
*/
if (oldval.length !== commonStart + commonEnd) {
if (ctx.localChange) { ctx.localChange(true); }
result = oldval.length - commonStart - commonEnd;
ctx.remove(commonStart, result);
console.log('removal at position: %s, length: %s', commonStart, result);
console.log("remove: [" + oldval.slice(commonStart, commonStart + result ) + ']');
}
if (newval.length !== commonStart + commonEnd) {
if (ctx.localChange) { ctx.localChange(true); }
result = newval.slice(commonStart, newval.length - commonEnd);
ctx.insert(commonStart, result);
console.log("insert: [" + result + "]");
}
var userDoc;
try {
var userDoc = ctx.getUserDoc();
JSON.parse(userDoc);
} catch (err) {
console.error('[textPatcherParseErr]');
console.error(err);
window.REALTIME_MODULE.textPatcher_parseError = {
error: err,
userDoc: userDoc
};
}
};
var create = function(config) {
var ctx = config.realtime;
// initial state will always fail the !== check in genop.
// because nothing will equal this object
var content = {};
// *** remote -> local changes
ctx.onPatch(function(pos, length) {
content = ctx.getUserDoc()
});
// propogate()
return function (newContent) {
if (newContent !== content) {
applyChange(ctx, ctx.getUserDoc(), newContent);
if (ctx.getUserDoc() !== newContent) {
console.log("Expected that: `ctx.getUserDoc() === newContent`!");
}
return true;
}
return false;
};
};
return { create: create };
});

63
www/_socket/typingTest.js Normal file
View File

@ -0,0 +1,63 @@
define(function () {
var setRandomizedInterval = function (func, target, range) {
var timeout;
var again = function () {
timeout = setTimeout(function () {
again();
func();
}, target - (range / 2) + Math.random() * range);
};
again();
return {
cancel: function () {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
}
};
};
var testInput = function (doc, el, offset, cb) {
var i = 0,
j = offset,
input = " The quick red fox jumps over the lazy brown dog.",
l = input.length,
errors = 0,
max_errors = 15,
interval;
var cancel = function () {
if (interval) { interval.cancel(); }
};
interval = setRandomizedInterval(function () {
cb();
try {
el.replaceData(j, 0, input.charAt(i));
} catch (err) {
errors++;
if (errors >= max_errors) {
console.log("Max error number exceeded");
cancel();
}
console.error(err);
var next = document.createTextNode("");
doc.appendChild(next);
el = next;
j = -1;
}
i = (i + 1) % l;
j++;
}, 200, 50);
return {
cancel: cancel
};
};
return {
testInput: testInput,
setRandomizedInterval: setRandomizedInterval
};
});

View File

@ -372,7 +372,7 @@ var random = Patch.random = function (doc, opCount) {
var PARANOIA = module.exports.PARANOIA = false;
/* throw errors over non-compliant messages which would otherwise be treated as invalid */
var TESTING = module.exports.TESTING = false;
var TESTING = module.exports.TESTING = true;
var assert = module.exports.assert = function (expr) {
if (!expr) { throw new Error("Failed assertion"); }
@ -1443,7 +1443,13 @@ var rebase = Operation.rebase = function (oldOp, newOp) {
* @param transformBy an existing operation which also has the same base.
* @return toTransform *or* null if the result is a no-op.
*/
var transform0 = Operation.transform0 = function (text, toTransform, transformBy) {
var transform0 = Operation.transform0 = function (text, toTransformOrig, transformByOrig) {
// Cloning the original transformations makes this algorithm such that it
// **DOES NOT MUTATE ANYMORE**
var toTransform = Operation.clone(toTransformOrig);
var transformBy = Operation.clone(transformByOrig);
if (toTransform.offset > transformBy.offset) {
if (toTransform.offset > transformBy.offset + transformBy.toRemove) {
// simple rebase

View File

@ -373,6 +373,26 @@ define([
};
};
cursor.brFix = function () {
cursor.update();
var start = Range.start;
var end = Range.end;
if (!start.el) { return; }
if (start.el === end.el && start.offset === end.offset) {
if (start.el.tagName === 'BR') {
// get the parent element, which ought to be a P.
var P = start.el.parentNode;
[cursor.fixStart, cursor.fixEnd].forEach(function (f) {
f(P, 0);
});
cursor.fixSelection(cursor.makeSelection(), cursor.makeRange());
}
}
};
return cursor;
};
});

View File

@ -54,7 +54,7 @@ define([], function () {
The function, if provided, must return true for elements which
should be preserved, and 'false' for elements which should be removed.
*/
var DOM2HyperJSON = function(el, predicate){
var DOM2HyperJSON = function(el, predicate, filter){
if(!el.tagName && el.nodeType === Node.TEXT_NODE){
return el.textContent;
}
@ -118,12 +118,16 @@ define([], function () {
i = 0;
for(; i < el.childNodes.length; i++){
children.push(DOM2HyperJSON(el.childNodes[i], predicate));
children.push(DOM2HyperJSON(el.childNodes[i], predicate, filter));
}
result.push(children.filter(isTruthy));
return result;
if (filter) {
return filter(result);
} else {
return result;
}
};
return {

View File

@ -5,19 +5,45 @@ define([
var JsonOT = {};
var validate = JsonOT.validate = function (text, toTransform, transformBy) {
var resultOp = ChainPad.Operation.transform0(text, toTransform, transformBy);
var text2 = ChainPad.Operation.apply(transformBy, text);
var text3 = ChainPad.Operation.apply(resultOp, text2);
try {
JSON.parse(text3);
return resultOp;
} catch (e) {
var resultOp = ChainPad.Operation.transform0(text, toTransform, transformBy);
var text2 = ChainPad.Operation.apply(transformBy, text);
var text3 = ChainPad.Operation.apply(resultOp, text2);
try {
JSON.parse(text3);
return resultOp;
} catch (e) {
console.error(e);
var info = window.REALTIME_MODULE.ot_parseError = {
type: 'resultParseError',
resultOp: resultOp,
toTransform: toTransform,
transformBy: transformBy,
text1: text,
text2: text2,
text3: text3,
error: e
};
console.log('Debugging info available at `window.REALTIME_MODULE.ot_parseError`');
}
} catch (x) {
console.error(x);
console.error(e);
console.log({
var info = window.REALTIME_MODULE.ot_applyError = {
type: 'resultParseError',
resultOp: resultOp,
toTransform: toTransform,
transformBy: transformBy,
text1: text,
text2: text2,
text3: text3
});
text3: text3,
error: e
};
console.log('Debugging info available at `window.REALTIME_MODULE.ot_applyError`');
}
// returning **null** breaks out of the loop

View File

@ -1,294 +0,0 @@
define([
'/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/messages.js',
'/common/crypto.js',
'/socket/realtime-input.js',
'/common/convert.js',
'/socket/toolbar.js',
'/common/cursor.js',
'/common/json-ot.js',
'/bower_components/diff-dom/diffDOM.js',
'/bower_components/jquery/dist/jquery.min.js',
'/customize/pad.js'
], function (Config, Messages, Crypto, realtimeInput, Convert, Toolbar, Cursor, JsonOT) {
var $ = window.jQuery;
var ifrw = $('#pad-iframe')[0].contentWindow;
var Ckeditor; // to be initialized later...
var DiffDom = window.diffDOM;
window.Convert = Convert;
window.Toolbar = Toolbar;
var userName = Crypto.rand64(8),
toolbar;
var module = {};
var andThen = function (Ckeditor) {
$(window).on('hashchange', function() {
window.location.reload();
});
if (window.location.href.indexOf('#') === -1) {
window.location.href = window.location.href + '#' + Crypto.genKey();
return;
}
var fixThings = false;
var key = Crypto.parseKey(window.location.hash.substring(1));
var editor = window.editor = Ckeditor.replace('editor1', {
// https://dev.ckeditor.com/ticket/10907
needsBrFiller: fixThings,
needsNbspFiller: fixThings,
removeButtons: 'Source,Maximize',
// magicline plugin inserts html crap into the document which is not part of the
// document itself and causes problems when it's sent across the wire and reflected back
// but we filter it now, so that's ok.
removePlugins: 'resize'
});
editor.on('instanceReady', function (Ckeditor) {
editor.execCommand('maximize');
var documentBody = ifrw.$('iframe')[0].contentDocument.body;
documentBody.innerHTML = Messages.initialState;
var inner = window.inner = documentBody;
var cursor = window.cursor = Cursor(inner);
var $textarea = $('#feedback');
var setEditable = function (bool) {
// inner.style.backgroundColor = bool? 'unset': 'grey';
inner.setAttribute('contenteditable', bool);
};
// don't let the user edit until the pad is ready
setEditable(false);
var diffOptions = {
preDiffApply: function (info) {
/* TODO DiffDOM will filter out magicline plugin elements
in practice this will make it impossible to use it
while someone else is typing, which could be annoying
we should check when such an element is going to be
removed, and prevent that from happening. */
// no use trying to recover the cursor if it doesn't exist
if (!cursor.exists()) { return; }
/* frame is either 0, 1, 2, or 3, depending on which
cursor frames were affected: none, first, last, or both
*/
var frame = info.frame = cursor.inNode(info.node);
if (!frame) { return; }
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') {
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue);
if (frame & 1) {
// push cursor start if necessary
if (pushes.commonStart < cursor.Range.start.offset) {
cursor.Range.start.offset += pushes.delta;
}
}
if (frame & 2) {
// push cursor end if necessary
if (pushes.commonStart < cursor.Range.end.offset) {
cursor.Range.end.offset += pushes.delta;
}
}
}
},
postDiffApply: function (info) {
if (info.frame) {
if (info.node) {
if (info.frame & 1) { cursor.fixStart(info.node); }
if (info.frame & 2) { cursor.fixEnd(info.node); }
} else { console.error("info.node did not exist"); }
var sel = cursor.makeSelection();
var range = cursor.makeRange();
cursor.fixSelection(sel, range);
}
}
};
var initializing = true;
var assertStateMatches = function () {
var userDocState = module.realtimeInput.realtime.getUserDoc();
var currentState = $textarea.val();
if (currentState !== userDocState) {
console.log({
userDocState: userDocState,
currentState: currentState
});
throw new Error("currentState !== userDocState");
}
};
// apply patches, and try not to lose the cursor in the process!
var applyHjson = function (shjson) {
setEditable(false);
var userDocStateDom = Convert.hjson.to.dom(JSON.parse(shjson));
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
var DD = new DiffDom(diffOptions);
assertStateMatches();
var patch = (DD).diff(inner, userDocStateDom);
(DD).apply(inner, patch);
// push back to the textarea so we get a userDocState
setEditable(true);
};
var onRemote = function (shjson) {
if (initializing) { return; }
// remember where the cursor is
cursor.update();
// TODO call propogate
// build a dom from HJSON, diff, and patch the editor
applyHjson(shjson);
};
var onInit = function (info) {
var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox');
toolbar = info.realtime.toolbar = Toolbar.create($bar, userName, info.realtime);
/* TODO handle disconnects and such*/
};
var onReady = function (info) {
console.log("Unlocking editor");
initializing = false;
setEditable(true);
applyHjson($textarea.val());
$textarea.trigger('keyup');
};
var onAbort = function (info) {
console.log("Aborting the session!");
// stop the user from continuing to edit
setEditable(false);
// TODO inform them that the session was torn down
toolbar.failed();
};
var realtimeOptions = {
// configuration :D
doc: inner,
// first thing called
onInit: onInit,
onReady: onReady,
// when remote changes occur
onRemote: onRemote,
// handle aborts
onAbort: onAbort,
// really basic operational transform
// reject patch if it results in invalid JSON
transformFunction : JsonOT.validate
};
var rti = module.realtimeInput = window.rti = realtimeInput.start($textarea[0], // synced element
Config.websocketURL, // websocketURL, ofc
userName, // userName
key.channel, // channelName
key.cryptKey, // key
realtimeOptions);
$textarea.val(JSON.stringify(Convert.dom.to.hjson(inner)));
var isNotMagicLine = function (el) {
// factor as:
// return !(el.tagName === 'SPAN' && el.contentEditable === 'false');
var filter = (el.tagName === 'SPAN' && el.contentEditable === 'false');
if (filter) {
console.log("[hyperjson.serializer] prevented an element" +
"from being serialized:", el);
return false;
}
return true;
};
var propogate = function () {
var hjson = Convert.core.hyperjson.fromDOM(inner, isNotMagicLine);
$textarea.val(JSON.stringify(hjson));
rti.bumpSharejs();
};
var testInput = window.testInput = function (el, offset) {
var i = 0,
j = offset,
input = "The quick red fox jumped over the lazy brown dog. ",
l = input.length,
errors = 0,
max_errors = 15,
interval;
var cancel = function () {
if (interval) { window.clearInterval(interval); }
};
interval = window.setInterval(function () {
propogate();
try {
el.replaceData(j, 0, input.charAt(i));
} catch (err) {
errors++;
if (errors >= max_errors) {
console.log("Max error number exceeded");
cancel();
}
console.error(err);
var next = document.createTextNode("");
el.parentNode.appendChild(next);
el = next;
j = 0;
}
i = (i + 1) % l;
j++;
}, 200);
return {
cancel: cancel
};
};
var easyTest = window.easyTest = function () {
cursor.update();
var start = cursor.Range.start;
var test = testInput(start.el, start.offset);
//window.rti.bumpSharejs();
propogate();
return test;
};
editor.on('change', propogate);
});
};
var interval = 100;
var first = function () {
Ckeditor = ifrw.CKEDITOR;
if (Ckeditor) {
andThen(Ckeditor);
} else {
console.log("Ckeditor was not defined. Trying again in %sms",interval);
setTimeout(first, interval);
}
};
$(first);
});

View File

@ -20,6 +20,7 @@ define([
var $textarea = $('textarea');
var config = {
textarea: $textarea[0],
websocketURL: Config.websocketURL,
userName: Crypto.rand64(8),
channel: key.channel,