Merge branch 'merge-staging' into staging
commit
812df7de6f
@ -1,751 +0,0 @@
|
|||||||
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
|
|
||||||
define([
|
|
||||||
'/bower_components/chainpad-netflux/chainpad-netflux.js',
|
|
||||||
'/bower_components/chainpad-json-validator/json-ot.js',
|
|
||||||
'json.sortify',
|
|
||||||
'/bower_components/textpatcher/TextPatcher.js',
|
|
||||||
], function (Realtime, JsonOT, Sortify, TextPatcher) {
|
|
||||||
var api = {};
|
|
||||||
// "Proxy" is undefined in Safari : we need to use an normal object and check if there are local
|
|
||||||
// changes regurlarly.
|
|
||||||
var isFakeProxy = typeof window.Proxy === "undefined";
|
|
||||||
|
|
||||||
var DeepProxy = api.DeepProxy = (function () {
|
|
||||||
var deepProxy = {};
|
|
||||||
|
|
||||||
var isArray = deepProxy.isArray = Array.isArray || function (obj) {
|
|
||||||
return Object.toString(obj) === '[object Array]';
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Arrays and nulls both register as 'object' when using native typeof
|
|
||||||
we need to distinguish them as their own types, so use this instead. */
|
|
||||||
var type = deepProxy.type = function (dat) {
|
|
||||||
return dat === null? 'null': isArray(dat)?'array': typeof(dat);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Check if an (sub-)element in an object or an array and should be a proxy.
|
|
||||||
If the browser doesn't support Proxy, return false */
|
|
||||||
var isProxyable = deepProxy.isProxyable = function (obj, forceCheck) {
|
|
||||||
if (typeof forceCheck === "undefined" && isFakeProxy) { return false; }
|
|
||||||
return ['object', 'array'].indexOf(type(obj)) !== -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Any time you set a value, check its type.
|
|
||||||
If that type is proxyable, make a new proxy. */
|
|
||||||
var setter = deepProxy.set = function (cb) {
|
|
||||||
return function (obj, prop, value) {
|
|
||||||
if (prop === 'on') {
|
|
||||||
throw new Error("'on' is a reserved attribute name for realtime lists and maps");
|
|
||||||
}
|
|
||||||
if (isProxyable(value)) {
|
|
||||||
obj[prop] = deepProxy.create(value, cb);
|
|
||||||
} else {
|
|
||||||
obj[prop] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
cb();
|
|
||||||
return obj[prop] || true; // always return truthey or you have problems
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var pathMatches = deepProxy.pathMatches = function (path, pattern) {
|
|
||||||
return !pattern.some(function (x, i) {
|
|
||||||
return x !== path[i];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var lengthDescending = function (a, b) { return b.pattern.length - a.pattern.length; };
|
|
||||||
|
|
||||||
/* TODO implement 'off' as well.
|
|
||||||
change 'setter' to warn users when they attempt to set 'off'
|
|
||||||
*/
|
|
||||||
var on = function(events) {
|
|
||||||
return function (evt, pattern, f) {
|
|
||||||
switch (evt) {
|
|
||||||
case 'change':
|
|
||||||
// pattern needs to be an array
|
|
||||||
pattern = type(pattern) === 'array'? pattern: [pattern];
|
|
||||||
|
|
||||||
events.change.push({
|
|
||||||
cb: function (oldval, newval, path, root) {
|
|
||||||
if (pathMatches(path, pattern)) {
|
|
||||||
return f(oldval, newval, path, root);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pattern: pattern,
|
|
||||||
});
|
|
||||||
// sort into descending order so we evaluate in order of specificity
|
|
||||||
events.change.sort(lengthDescending);
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 'remove':
|
|
||||||
pattern = type(pattern) === 'array'? pattern: [pattern];
|
|
||||||
|
|
||||||
events.remove.push({
|
|
||||||
cb: function (oldval, path, root) {
|
|
||||||
if (pathMatches(path, pattern)) { return f(oldval, path, root); }
|
|
||||||
},
|
|
||||||
pattern: pattern,
|
|
||||||
});
|
|
||||||
|
|
||||||
events.remove.sort(lengthDescending);
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 'ready':
|
|
||||||
events.ready.push({
|
|
||||||
// on('ready' has a different signature than
|
|
||||||
// change and delete, so use 'pattern', not 'f'
|
|
||||||
|
|
||||||
cb: function (info) {
|
|
||||||
pattern(info);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'disconnect':
|
|
||||||
events.disconnect.push({
|
|
||||||
cb: function (info) {
|
|
||||||
// as above
|
|
||||||
pattern(info);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'reconnect':
|
|
||||||
events.reconnect.push({
|
|
||||||
cb: function (info) {
|
|
||||||
// as above
|
|
||||||
pattern(info);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'create':
|
|
||||||
events.create.push({
|
|
||||||
cb: function (info) {
|
|
||||||
pattern(info);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var getter = deepProxy.get = function (/* cb */) {
|
|
||||||
var events = {
|
|
||||||
disconnect: [],
|
|
||||||
reconnect: [],
|
|
||||||
change: [],
|
|
||||||
ready: [],
|
|
||||||
remove: [],
|
|
||||||
create: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
return function (obj, prop) {
|
|
||||||
if (prop === 'on') {
|
|
||||||
return on(events);
|
|
||||||
} else if (prop === '_isProxy') {
|
|
||||||
return true;
|
|
||||||
} else if (prop === '_events') {
|
|
||||||
return events;
|
|
||||||
}
|
|
||||||
return obj[prop];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var deleter = deepProxy.delete = function (cb) {
|
|
||||||
return function (obj, prop) {
|
|
||||||
if (typeof(obj[prop]) === 'undefined') { return true; }
|
|
||||||
delete obj[prop];
|
|
||||||
cb();
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var handlers = deepProxy.handlers = function (cb, isRoot) {
|
|
||||||
if (!isRoot) {
|
|
||||||
return {
|
|
||||||
set: setter(cb),
|
|
||||||
get: function (obj, prop) {
|
|
||||||
if (prop === '_isProxy') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return obj[prop];
|
|
||||||
},
|
|
||||||
deleteProperty: deleter(cb),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
set: setter(cb),
|
|
||||||
get: getter(cb),
|
|
||||||
deleteProperty: deleter(cb),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var remoteChangeFlag = deepProxy.remoteChangeFlag = false;
|
|
||||||
|
|
||||||
var stringifyFakeProxy = deepProxy.stringifyFakeProxy = function (proxy) {
|
|
||||||
var copy = JSON.parse(Sortify(proxy));
|
|
||||||
delete copy._events;
|
|
||||||
delete copy._isProxy;
|
|
||||||
return Sortify(copy);
|
|
||||||
};
|
|
||||||
|
|
||||||
deepProxy.checkLocalChange = function (obj, cb) {
|
|
||||||
if (!isFakeProxy) { return; }
|
|
||||||
var oldObj = stringifyFakeProxy(obj);
|
|
||||||
window.setInterval(function() {
|
|
||||||
var newObj = stringifyFakeProxy(obj);
|
|
||||||
if (newObj !== oldObj) {
|
|
||||||
oldObj = newObj;
|
|
||||||
if (remoteChangeFlag) {
|
|
||||||
remoteChangeFlag = false;
|
|
||||||
} else {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},300);
|
|
||||||
};
|
|
||||||
|
|
||||||
var create = deepProxy.create = function (obj, opt, isRoot) {
|
|
||||||
/* recursively create proxies in case users do:
|
|
||||||
`x.a = {b: {c: 5}};
|
|
||||||
|
|
||||||
otherwise the inner object is not a proxy, which leads to incorrect
|
|
||||||
behaviour on the client that initiated the object (but not for
|
|
||||||
clients that receive the objects) */
|
|
||||||
|
|
||||||
// if the user supplied a callback, use it to create handlers
|
|
||||||
// this saves a bit of work in recursion
|
|
||||||
var methods = type(opt) === 'function'? handlers(opt, isRoot) : opt;
|
|
||||||
switch (type(obj)) {
|
|
||||||
case 'object':
|
|
||||||
var keys = Object.keys(obj);
|
|
||||||
keys.forEach(function (k) {
|
|
||||||
if (isProxyable(obj[k]) && !obj[k]._isProxy) {
|
|
||||||
obj[k] = create(obj[k], opt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'array':
|
|
||||||
obj.forEach(function (o, i) {
|
|
||||||
if (isProxyable(o) && !o._isProxy) {
|
|
||||||
obj[i] = create(obj[i], opt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// if it's not an array or object, you don't need to proxy it
|
|
||||||
throw new Error('attempted to make a proxy of an unproxyable object');
|
|
||||||
}
|
|
||||||
if (!isFakeProxy) {
|
|
||||||
if (obj._isProxy) {
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
return new window.Proxy(obj, methods);
|
|
||||||
}
|
|
||||||
|
|
||||||
var proxy = JSON.parse(JSON.stringify(obj));
|
|
||||||
|
|
||||||
if (isRoot) {
|
|
||||||
var events = {
|
|
||||||
disconnect: [],
|
|
||||||
reconnect: [],
|
|
||||||
change: [],
|
|
||||||
ready: [],
|
|
||||||
remove: [],
|
|
||||||
create: [],
|
|
||||||
};
|
|
||||||
proxy.on = on(events);
|
|
||||||
proxy._events = events;
|
|
||||||
}
|
|
||||||
return proxy;
|
|
||||||
};
|
|
||||||
|
|
||||||
// onChange(path, key, root, oldval, newval)
|
|
||||||
var onChange = function (path, key, root, oldval, newval) {
|
|
||||||
var P = path.slice(0);
|
|
||||||
P.push(key);
|
|
||||||
|
|
||||||
/* returning false in your callback terminates 'bubbling up'
|
|
||||||
we can accomplish this with Array.some because we've presorted
|
|
||||||
listeners by the specificity of their path
|
|
||||||
*/
|
|
||||||
root._events.change.some(function (handler) {
|
|
||||||
return handler.cb(oldval, newval, P, root) === false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var find = deepProxy.find = function (map, path) {
|
|
||||||
/* safely search for nested values in an object via a path */
|
|
||||||
return (map && path.reduce(function (p, n) {
|
|
||||||
return typeof p[n] !== 'undefined' && p[n];
|
|
||||||
}, map)) || undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
var onRemove = function (path, key, root, old, top) {
|
|
||||||
var newpath = path.concat(key);
|
|
||||||
var X = find(root, newpath);
|
|
||||||
|
|
||||||
var t_X = type(X);
|
|
||||||
|
|
||||||
/* TODO 'find' is correct but unnecessarily expensive.
|
|
||||||
optimize it. */
|
|
||||||
|
|
||||||
switch (t_X) {
|
|
||||||
case 'array':
|
|
||||||
|
|
||||||
if (top) {
|
|
||||||
// the top of an onremove should emit an onchange instead
|
|
||||||
onChange(path, key, root, old, undefined);// no newval since it's a deletion
|
|
||||||
} else {
|
|
||||||
root._events.remove.forEach(function (handler) {
|
|
||||||
return handler.cb(X, newpath, root);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// remove all of the array's children
|
|
||||||
X.forEach(function (x, i) {
|
|
||||||
onRemove(newpath, i, root);
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 'object':
|
|
||||||
if (top) {
|
|
||||||
onChange(path, key, root, old, undefined);// no newval since it's a deletion
|
|
||||||
} else {
|
|
||||||
root._events.remove.forEach(function (handler) {
|
|
||||||
return handler.cb(X, newpath, root, old, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// remove all of the object's children
|
|
||||||
Object.keys(X).forEach(function (key) {
|
|
||||||
onRemove(newpath, key, root, X[key], false);
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
root._events.remove.forEach(function (handler) {
|
|
||||||
return handler.cb(X, newpath, root);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* compare a new object 'B' against an existing proxy object 'A'
|
|
||||||
provide a unary function 'f' for the purpose of constructing new
|
|
||||||
deep proxies from regular objects and arrays.
|
|
||||||
|
|
||||||
Supply the path as you recurse, for the purpose of emitting events
|
|
||||||
attached to particular paths within the complete structure.
|
|
||||||
|
|
||||||
Operates entirely via side effects on 'A'
|
|
||||||
*/
|
|
||||||
var objects = deepProxy.objects = function (A, B, f, path, root) {
|
|
||||||
var Akeys = Object.keys(A);
|
|
||||||
var Bkeys = Object.keys(B);
|
|
||||||
|
|
||||||
/* iterating over the keys in B will tell you if a new key exists
|
|
||||||
it will not tell you if a key has been removed.
|
|
||||||
to accomplish that you will need to iterate over A's keys
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* TODO return a truthy or falsey value (in 'objects' and 'arrays')
|
|
||||||
so that we have some measure of whether an object or array changed
|
|
||||||
(from the higher level in the tree, rather than doing everything
|
|
||||||
at the leaf level).
|
|
||||||
|
|
||||||
bonus points if you can defer events until the complete diff has
|
|
||||||
finished (collect them into an array or something, and simplify
|
|
||||||
the event if possible)
|
|
||||||
*/
|
|
||||||
|
|
||||||
Bkeys.forEach(function (b) {
|
|
||||||
var t_b = type(B[b]);
|
|
||||||
var old = A[b];
|
|
||||||
|
|
||||||
if (Akeys.indexOf(b) === -1) {
|
|
||||||
// there was an insertion
|
|
||||||
|
|
||||||
// mind the fallthrough behaviour
|
|
||||||
switch (t_b) {
|
|
||||||
case 'undefined':
|
|
||||||
// umm. this should never happen?
|
|
||||||
throw new Error("undefined type has key. this shouldn't happen?");
|
|
||||||
case 'array':
|
|
||||||
case 'object':
|
|
||||||
A[b] = f(B[b]);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
A[b] = B[b];
|
|
||||||
}
|
|
||||||
|
|
||||||
// insertions are a change
|
|
||||||
|
|
||||||
// onChange(path, key, root, oldval, newval)
|
|
||||||
onChange(path, b, root, old, B[b]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// else the key already existed
|
|
||||||
var t_a = type(A[b]);
|
|
||||||
if (t_a !== t_b) {
|
|
||||||
// its type changed!
|
|
||||||
console.log("type changed from [%s] to [%s]", t_a, t_b);
|
|
||||||
switch (t_b) {
|
|
||||||
case 'undefined':
|
|
||||||
throw new Error("first pass should never reveal undefined keys");
|
|
||||||
case 'array':
|
|
||||||
A[b] = f(B[b]);
|
|
||||||
// make a new proxy
|
|
||||||
break;
|
|
||||||
case 'object':
|
|
||||||
A[b] = f(B[b]);
|
|
||||||
// make a new proxy
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// all other datatypes just require assignment.
|
|
||||||
A[b] = B[b];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// type changes always mean a change happened
|
|
||||||
onChange(path, b, root, old, B[b]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// values might have changed, if not types
|
|
||||||
if (['array', 'object'].indexOf(t_a) === -1) {
|
|
||||||
// it's not an array or object, so we can do deep equality
|
|
||||||
if (A[b] !== B[b]) {
|
|
||||||
// not equal, so assign
|
|
||||||
A[b] = B[b];
|
|
||||||
|
|
||||||
onChange(path, b, root, old, B[b]);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// else it's an array or object
|
|
||||||
var nextPath = path.slice(0).concat(b);
|
|
||||||
if (t_a === 'object') {
|
|
||||||
// it's an object
|
|
||||||
objects.call(root, A[b], B[b], f, nextPath, root);
|
|
||||||
} else {
|
|
||||||
// it's an array
|
|
||||||
deepProxy.arrays.call(root, A[b], B[b], f, nextPath, root);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Akeys.forEach(function (a) {
|
|
||||||
var old = A[a];
|
|
||||||
|
|
||||||
if (a === "on" || a === "_events") { return; }
|
|
||||||
|
|
||||||
// the key was deleted
|
|
||||||
if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') {
|
|
||||||
onRemove(path, a, root, old, true);
|
|
||||||
delete A[a];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
var arrays = deepProxy.arrays = function (A, B, f, path, root) {
|
|
||||||
var l_A = A.length;
|
|
||||||
var l_B = B.length;
|
|
||||||
|
|
||||||
if (l_A !== l_B) {
|
|
||||||
// B is longer than Aj
|
|
||||||
// there has been an insertion
|
|
||||||
|
|
||||||
// OR
|
|
||||||
|
|
||||||
// A is longer than B
|
|
||||||
// there has been a deletion
|
|
||||||
|
|
||||||
B.forEach(function (b, i) {
|
|
||||||
var t_a = type(A[i]);
|
|
||||||
var t_b = type(b);
|
|
||||||
|
|
||||||
var old = A[i];
|
|
||||||
|
|
||||||
if (t_a !== t_b) {
|
|
||||||
// type changes are always destructive
|
|
||||||
// that's good news because destructive is easy
|
|
||||||
switch (t_b) {
|
|
||||||
case 'undefined':
|
|
||||||
throw new Error('this should never happen');
|
|
||||||
case 'object':
|
|
||||||
A[i] = f(b);
|
|
||||||
break;
|
|
||||||
case 'array':
|
|
||||||
A[i] = f(b);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
A[i] = b;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// path, key, root object, oldvalue, newvalue
|
|
||||||
onChange(path, i, root, old, b);
|
|
||||||
} else {
|
|
||||||
// same type
|
|
||||||
var nextPath = path.slice(0).concat(i);
|
|
||||||
|
|
||||||
switch (t_b) {
|
|
||||||
case 'object':
|
|
||||||
objects.call(root, A[i], b, f, nextPath, root);
|
|
||||||
break;
|
|
||||||
case 'array':
|
|
||||||
if (arrays.call(root, A[i], b, f, nextPath, root)) {
|
|
||||||
onChange(path, i, root, old, b);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (b !== A[i]) {
|
|
||||||
A[i] = b;
|
|
||||||
onChange(path, i, root, old, b);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (l_A > l_B) {
|
|
||||||
// A was longer than B, so there have been deletions
|
|
||||||
var i = l_B;
|
|
||||||
//var t_a;
|
|
||||||
var old;
|
|
||||||
|
|
||||||
for (; i <= l_B; i++) {
|
|
||||||
// recursively delete
|
|
||||||
old = A[i];
|
|
||||||
|
|
||||||
onRemove(path, i, root, old, true);
|
|
||||||
}
|
|
||||||
// cool
|
|
||||||
}
|
|
||||||
|
|
||||||
A.length = l_B;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// else they are the same length, iterate over their values
|
|
||||||
A.forEach(function (a, i) {
|
|
||||||
var t_a = type(a);
|
|
||||||
var t_b = type(B[i]);
|
|
||||||
|
|
||||||
var old = a;
|
|
||||||
|
|
||||||
// they have different types
|
|
||||||
if (t_a !== t_b) {
|
|
||||||
switch (t_b) {
|
|
||||||
case 'undefined':
|
|
||||||
onRemove(path, i, root, old, true);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// watch out for fallthrough behaviour
|
|
||||||
// if it's an object or array, create a proxy
|
|
||||||
case 'object':
|
|
||||||
case 'array':
|
|
||||||
A[i] = f(B[i]);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
A[i] = B[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(path, i, root, old, B[i]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// they are the same type, clone the paths array and push to it
|
|
||||||
var nextPath = path.slice(0).concat(i);
|
|
||||||
|
|
||||||
// same type
|
|
||||||
switch (t_b) {
|
|
||||||
case 'undefined':
|
|
||||||
throw new Error('existing key had type `undefined`. this should never happen');
|
|
||||||
case 'object':
|
|
||||||
if (objects.call(root, A[i], B[i], f, nextPath, root)) {
|
|
||||||
onChange(path, i, root, old, B[i]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'array':
|
|
||||||
if (arrays.call(root, A[i], B[i], f, nextPath, root)) {
|
|
||||||
onChange(path, i, root, old, B[i]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (A[i] !== B[i]) {
|
|
||||||
A[i] = B[i];
|
|
||||||
onChange(path, i, root, old, B[i]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
deepProxy.update = function (A, B, cb) {
|
|
||||||
var t_A = type(A);
|
|
||||||
var t_B = type(B);
|
|
||||||
|
|
||||||
if (t_A !== t_B) {
|
|
||||||
throw new Error("Proxy updates can't result in type changes");
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (t_B) {
|
|
||||||
/* use .call so you can supply a different `this` value */
|
|
||||||
case 'array':
|
|
||||||
arrays.call(A, A, B, function (obj) {
|
|
||||||
return create(obj, cb);
|
|
||||||
}, [], A);
|
|
||||||
break;
|
|
||||||
case 'object':
|
|
||||||
// arrays.call(this, A , B , f, path , root)
|
|
||||||
objects.call(A, A, B, function (obj) {
|
|
||||||
return create(obj, cb);
|
|
||||||
}, [], A);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error("unsupported realtime datatype:" + t_B);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return deepProxy;
|
|
||||||
}());
|
|
||||||
|
|
||||||
api.create = function (cfg) {
|
|
||||||
/* validate your inputs before proceeding */
|
|
||||||
|
|
||||||
if (!DeepProxy.isProxyable(cfg.data, true)) {
|
|
||||||
throw new Error('unsupported datatype: '+ DeepProxy.type(cfg.data));
|
|
||||||
}
|
|
||||||
|
|
||||||
var realtimeOptions = {
|
|
||||||
userName: cfg.userName,
|
|
||||||
initialState: Sortify(cfg.data),
|
|
||||||
readOnly: cfg.readOnly,
|
|
||||||
transformFunction: JsonOT.transform || JsonOT.validate,
|
|
||||||
logLevel: typeof(cfg.logLevel) === 'undefined'? 0: cfg.logLevel,
|
|
||||||
validateContent: function (content) {
|
|
||||||
try {
|
|
||||||
JSON.parse(content);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to parse, rejecting patch");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var initializing = true;
|
|
||||||
|
|
||||||
var setterCb = function () {
|
|
||||||
if (!DeepProxy.remoteChangeFlag) { realtimeOptions.onLocal(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
var rt = {};
|
|
||||||
var realtime;
|
|
||||||
|
|
||||||
var proxy;
|
|
||||||
var patchText;
|
|
||||||
|
|
||||||
realtimeOptions.onRemote = function () {
|
|
||||||
if (initializing) { return; }
|
|
||||||
// TODO maybe implement history support here
|
|
||||||
|
|
||||||
var userDoc = realtime.getUserDoc();
|
|
||||||
var parsed = JSON.parse(userDoc);
|
|
||||||
|
|
||||||
DeepProxy.remoteChangeFlag = true;
|
|
||||||
DeepProxy.update(proxy, parsed, setterCb);
|
|
||||||
DeepProxy.remoteChangeFlag = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
var onLocal = realtimeOptions.onLocal = function () {
|
|
||||||
if (initializing) { return; }
|
|
||||||
var strung = isFakeProxy? DeepProxy.stringifyFakeProxy(proxy): Sortify(proxy);
|
|
||||||
patchText(strung);
|
|
||||||
|
|
||||||
// try harder
|
|
||||||
if (realtime.getUserDoc() !== strung) { patchText(strung); }
|
|
||||||
|
|
||||||
// onLocal
|
|
||||||
if (cfg.onLocal) { cfg.onLocal(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
proxy = DeepProxy.create(cfg.data, setterCb, true);
|
|
||||||
console.log(proxy);
|
|
||||||
|
|
||||||
realtimeOptions.onInit = function (info) {
|
|
||||||
proxy._events.create.forEach(function (handler) {
|
|
||||||
handler.cb(info);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var ready = false;
|
|
||||||
realtimeOptions.onReady = function (info) {
|
|
||||||
if (ready) { return; }
|
|
||||||
// create your patcher
|
|
||||||
if (realtime !== info.realtime) {
|
|
||||||
realtime = rt.realtime = info.realtime;
|
|
||||||
patchText = TextPatcher.create({
|
|
||||||
realtime: realtime,
|
|
||||||
logging: cfg.logging || false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error(realtime);
|
|
||||||
}
|
|
||||||
|
|
||||||
var userDoc = realtime.getUserDoc();
|
|
||||||
var parsed = JSON.parse(userDoc);
|
|
||||||
|
|
||||||
DeepProxy.update(proxy, parsed, setterCb);
|
|
||||||
|
|
||||||
proxy._events.ready.forEach(function (handler) {
|
|
||||||
handler.cb(info);
|
|
||||||
});
|
|
||||||
|
|
||||||
DeepProxy.checkLocalChange(proxy, onLocal);
|
|
||||||
|
|
||||||
initializing = false;
|
|
||||||
ready = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
realtimeOptions.onAbort = function (info) {
|
|
||||||
proxy._events.disconnect.forEach(function (handler) {
|
|
||||||
handler.cb(info);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
realtimeOptions.onConnectionChange = function (info) {
|
|
||||||
if (info.state) { // reconnect
|
|
||||||
initializing = true;
|
|
||||||
proxy._events.reconnect.forEach(function (handler) {
|
|
||||||
handler.cb(info);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// disconnected
|
|
||||||
proxy._events.disconnect.forEach(function (handler) {
|
|
||||||
handler.cb(info);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
realtimeOptions.onError = function (info) {
|
|
||||||
proxy._events.disconnect.forEach(function (handler) {
|
|
||||||
handler.cb(info);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
realtime = rt.cpCnInner = cfg.common.startRealtime(realtimeOptions);
|
|
||||||
rt.proxy = proxy;
|
|
||||||
rt.realtime = realtime;
|
|
||||||
|
|
||||||
return rt;
|
|
||||||
};
|
|
||||||
|
|
||||||
return api;
|
|
||||||
});
|
|
@ -0,0 +1,29 @@
|
|||||||
|
define([
|
||||||
|
], function () {
|
||||||
|
var module = { exports: {} };
|
||||||
|
var transformCursor = function (cursor, op) {
|
||||||
|
if (!op) { return cursor; }
|
||||||
|
|
||||||
|
var pos = op.offset;
|
||||||
|
var remove = op.toRemove;
|
||||||
|
var insert = op.toInsert.length;
|
||||||
|
if (typeof cursor === 'undefined') { return; }
|
||||||
|
if (typeof remove === 'number' && pos < cursor) {
|
||||||
|
cursor -= Math.min(remove, cursor - pos);
|
||||||
|
}
|
||||||
|
if (typeof insert === 'number' && pos < cursor) {
|
||||||
|
cursor += insert;
|
||||||
|
}
|
||||||
|
return cursor;
|
||||||
|
};
|
||||||
|
module.exports.transformCursor = function (cursor, ops) {
|
||||||
|
if (Array.isArray(ops)) {
|
||||||
|
for (var i = ops.length - 1; i >= 0; i--) {
|
||||||
|
cursor = transformCursor(cursor, ops[i]);
|
||||||
|
}
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
return transformCursor(ops);
|
||||||
|
};
|
||||||
|
return module.exports;
|
||||||
|
});
|
@ -1,242 +0,0 @@
|
|||||||
define([
|
|
||||||
'jquery'
|
|
||||||
],function ($) {
|
|
||||||
var Board = {};
|
|
||||||
var proxy;
|
|
||||||
|
|
||||||
var Uid = function (prefix) {
|
|
||||||
return function () {
|
|
||||||
return prefix + Number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))
|
|
||||||
.toString(32).replace(/\./g, '');
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var removeUid = function (A, e) {
|
|
||||||
var i = A.indexOf(e);
|
|
||||||
if (i === -1) { return -1; }
|
|
||||||
A.splice(i, 1);
|
|
||||||
return i;
|
|
||||||
};
|
|
||||||
|
|
||||||
var luid = Board.luid = Uid('l-'); // list-uid
|
|
||||||
var cuid = Board.cuid = Uid('c-'); // card uid
|
|
||||||
|
|
||||||
var Input = Board.Input = function (opt) {
|
|
||||||
return $('<input>', opt);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
populate the proxy with all the relevant fields
|
|
||||||
return boolean whether you are the first user
|
|
||||||
*/
|
|
||||||
Board.initialize = function (_proxy) {
|
|
||||||
proxy = _proxy;
|
|
||||||
var first = false;
|
|
||||||
|
|
||||||
['listOrder'].forEach(function (k) {
|
|
||||||
if (typeof(proxy[k]) === 'undefined') {
|
|
||||||
first = true;
|
|
||||||
proxy[k] = [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
['lists', 'cards'].forEach(function (k) {
|
|
||||||
if (typeof(proxy[k]) === 'undefined') {
|
|
||||||
proxy[k] = {};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return first;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* a list is appended to the extant order
|
|
||||||
*/
|
|
||||||
var List = Board.List = function (id) {
|
|
||||||
if (!id) {
|
|
||||||
id = List.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
var $input = Input({
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'list title',
|
|
||||||
})
|
|
||||||
.addClass('list-title')
|
|
||||||
.on('keyup change', function () {
|
|
||||||
var val = $input.val();
|
|
||||||
proxy.lists[id].title = val;
|
|
||||||
});
|
|
||||||
|
|
||||||
var $cards = $('<div>', {
|
|
||||||
|
|
||||||
})
|
|
||||||
.addClass('card-holder');
|
|
||||||
|
|
||||||
var $new = $('<a>', {
|
|
||||||
|
|
||||||
})
|
|
||||||
.addClass('add-card')
|
|
||||||
.text('add new card')
|
|
||||||
.click(function () {
|
|
||||||
// is this correct?
|
|
||||||
$cards.append(Board.Card(id));
|
|
||||||
});
|
|
||||||
|
|
||||||
var $list = $('<div>', {
|
|
||||||
id: id,
|
|
||||||
})
|
|
||||||
.addClass('list-column')
|
|
||||||
.append($input)
|
|
||||||
.append($cards)
|
|
||||||
.append($new);
|
|
||||||
|
|
||||||
return $list;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
*/
|
|
||||||
List.create = function () {
|
|
||||||
var id = luid();
|
|
||||||
proxy.listOrder.push(id);
|
|
||||||
proxy.lists[id] = {
|
|
||||||
title: "",
|
|
||||||
cards: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
return id;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
*/
|
|
||||||
List.remove = function (id) {
|
|
||||||
var i = removeUid(proxy.listOrder, id);
|
|
||||||
if (i === -1) {
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
*/
|
|
||||||
List.move = function () {
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
*/
|
|
||||||
List.insert = function () {
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
List.draw = function ($lists, lid) {
|
|
||||||
if (!lid) {
|
|
||||||
console.log("List Id not supplied");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var $parent = $lists.find('#' + lid);
|
|
||||||
if (!$parent.length) {
|
|
||||||
console.log("Creating new list");
|
|
||||||
// doesn't exist. draw it fresh
|
|
||||||
|
|
||||||
var $list = Board.List(lid);
|
|
||||||
$lists.append($list);
|
|
||||||
|
|
||||||
//console.log("Updating list");
|
|
||||||
|
|
||||||
//var $list = Board.List(lid);
|
|
||||||
var title = proxy.lists[lid].title;
|
|
||||||
|
|
||||||
console.log(title);
|
|
||||||
|
|
||||||
$list.find('input.list-title').val(title);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// else update
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* UI element
|
|
||||||
*/
|
|
||||||
var Card = Board.Card = function (pid) {
|
|
||||||
// pid => parent id
|
|
||||||
|
|
||||||
var id = Card.create(pid);
|
|
||||||
|
|
||||||
var $input = Input({
|
|
||||||
placeholder: 'card description',
|
|
||||||
id: id,
|
|
||||||
})
|
|
||||||
.addClass('card-title');
|
|
||||||
|
|
||||||
var $card = $('<div>', {
|
|
||||||
|
|
||||||
})
|
|
||||||
.addClass('card-container')
|
|
||||||
.append($input);
|
|
||||||
|
|
||||||
return $card;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* a card is instantiated within a parent list
|
|
||||||
* .create(parent) adds the relevant attributes to the data structure
|
|
||||||
* and returns the created id
|
|
||||||
*/
|
|
||||||
Card.create = function (pid) {
|
|
||||||
var id = cuid();
|
|
||||||
|
|
||||||
if (typeof(proxy.lists[pid]) === 'undefined') {
|
|
||||||
console.error("Trying to create card for list which does not exist");
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy.lists[pid].cards.push(id);
|
|
||||||
proxy.cards[id] = {
|
|
||||||
// TODO what goes in a card
|
|
||||||
parent: pid,
|
|
||||||
title: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
return id;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
*/
|
|
||||||
Card.move = function (/*uid, A, B*/) {
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
*/
|
|
||||||
Card.insert = function () {
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
Card.draw = function ($lists, cid) {
|
|
||||||
if (!cid) {
|
|
||||||
console.error("card id not supplied");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!proxy.cards[cid]) {
|
|
||||||
console.error("no such card: ", cid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var card = proxy.cards[cid];
|
|
||||||
card = card; // TODO actually draw
|
|
||||||
};
|
|
||||||
|
|
||||||
Board.Draw = function ($lists) {
|
|
||||||
proxy.listOrder.forEach(function (luid) {
|
|
||||||
List.draw($lists, luid);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return Board;
|
|
||||||
});
|
|
@ -1,97 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html class="cp board">
|
|
||||||
<head>
|
|
||||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
||||||
<title>Zero Knowledge Date Picker</title>
|
|
||||||
<link rel="icon" type="image/png"
|
|
||||||
href="/customize/main-favicon.png"
|
|
||||||
data-main-favicon="/customize/main-favicon.png"
|
|
||||||
data-alt-favicon="/customize/alt-favicon.png"
|
|
||||||
id="favicon" />
|
|
||||||
<link rel="stylesheet" href="/customize/main.css" />
|
|
||||||
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
width: 100;
|
|
||||||
}
|
|
||||||
.clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#adduser, #addoption {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#lists {
|
|
||||||
display: inline-block;
|
|
||||||
border: 1px solid white;
|
|
||||||
height: 80vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#create-list {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
background-color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-column {
|
|
||||||
vertical-align: top;
|
|
||||||
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: inline-block;
|
|
||||||
width: 400px;
|
|
||||||
height: 100%;
|
|
||||||
border: 1px solid white;
|
|
||||||
}
|
|
||||||
/* input */
|
|
||||||
input.list-title {
|
|
||||||
margin: 15px;
|
|
||||||
width: 80%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.card-holder {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
width: 90%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
.add-card {
|
|
||||||
background-color: green;
|
|
||||||
display: block;
|
|
||||||
height: 20px;
|
|
||||||
width: 80%;
|
|
||||||
cursor: pointer;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
border: 5px solid blue;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-container {
|
|
||||||
display: block;
|
|
||||||
height: 50px;
|
|
||||||
width: 95%;
|
|
||||||
margin: auto;
|
|
||||||
padding: 5px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
#board {
|
|
||||||
margin-left: 10vw;
|
|
||||||
overflow-x: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!--<div id="main"> -->
|
|
||||||
|
|
||||||
<div id="toolbar" class="buttons">
|
|
||||||
<sub><a href="/"></a></sub>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="board">
|
|
||||||
<div id="lists"></div>
|
|
||||||
<span id="create-list">Add List</span>
|
|
||||||
</div>
|
|
@ -1,94 +0,0 @@
|
|||||||
define([
|
|
||||||
'jquery',
|
|
||||||
'/api/config',
|
|
||||||
'/customize/messages.js',
|
|
||||||
'board.js',
|
|
||||||
'/bower_components/textpatcher/TextPatcher.js',
|
|
||||||
'/bower_components/chainpad-listmap/chainpad-listmap.js',
|
|
||||||
'/bower_components/chainpad-crypto/crypto.js',
|
|
||||||
'/common/cryptpad-common.js',
|
|
||||||
'/common/common-interface.js',
|
|
||||||
//'/common/visible.js',
|
|
||||||
//'/common/notify.js',
|
|
||||||
'/bower_components/file-saver/FileSaver.min.js'
|
|
||||||
], function ($, Config, Messages, Board, TextPatcher, Listmap, Crypto, Cryptpad, UI /*, Visible, Notify*/) {
|
|
||||||
|
|
||||||
// var saveAs = window.saveAs;
|
|
||||||
|
|
||||||
Cryptpad.styleAlerts();
|
|
||||||
console.log("Initializing your realtime session...");
|
|
||||||
|
|
||||||
var secret = Cryptpad.getSecrets();
|
|
||||||
|
|
||||||
var module = window.APP = {
|
|
||||||
Board: Board,
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
var unnotify = function () {
|
|
||||||
if (!(module.tabNotification &&
|
|
||||||
typeof(module.tabNotification.cancel) === 'function')) { return; }
|
|
||||||
module.tabNotification.cancel();
|
|
||||||
};
|
|
||||||
var notify = function () {
|
|
||||||
if (!(Visible.isSupported() && !Visible.currently())) { return; }
|
|
||||||
unnotify();
|
|
||||||
module.tabNotification = Notify.tab(1000, 10);
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
var setEditable = function (bool) {
|
|
||||||
bool = bool;
|
|
||||||
};
|
|
||||||
|
|
||||||
setEditable(false);
|
|
||||||
|
|
||||||
var $lists = $('#lists');
|
|
||||||
|
|
||||||
$('#create-list').click(function () {
|
|
||||||
Board.List.draw($lists);
|
|
||||||
});
|
|
||||||
|
|
||||||
var firstUser = function () {
|
|
||||||
UI.log("You are the first user to visit this board");
|
|
||||||
};
|
|
||||||
|
|
||||||
var whenReady = function () {
|
|
||||||
var rt = module.rt;
|
|
||||||
var proxy = rt.proxy;
|
|
||||||
|
|
||||||
var first = Board.initialize(proxy);
|
|
||||||
|
|
||||||
//var board = module.board = Board.create(proxy);
|
|
||||||
|
|
||||||
Board.Draw($lists);
|
|
||||||
|
|
||||||
if (first) { firstUser(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
var config = {
|
|
||||||
websocketURL: Config.websocketURL,
|
|
||||||
channel: secret.channel,
|
|
||||||
data: {},
|
|
||||||
crypto: Crypto.createEncryptor(secret.key),
|
|
||||||
};
|
|
||||||
|
|
||||||
Cryptpad.ready(function () {
|
|
||||||
var rt = module.rt = Listmap.create(config);
|
|
||||||
var proxy = rt.proxy;
|
|
||||||
proxy
|
|
||||||
.on('create', function (info) {
|
|
||||||
module.realtime = info.realtime;
|
|
||||||
window.location.hash = info.channel + secret.key;
|
|
||||||
})
|
|
||||||
.on('ready', function () {
|
|
||||||
UI.log("Ready!");
|
|
||||||
whenReady({
|
|
||||||
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.on('disconnect', function () {
|
|
||||||
UI.warn("Disconnected!");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,78 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
||||||
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
|
|
||||||
<style>
|
|
||||||
html, body{
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
border: 3px solid black;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 15px;
|
|
||||||
font-weight: bold !important;
|
|
||||||
font-size: 18px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"],
|
|
||||||
input[type="password"],
|
|
||||||
input[type="number"],
|
|
||||||
input[type="range"],
|
|
||||||
select
|
|
||||||
{
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
textarea {
|
|
||||||
width: 80%;
|
|
||||||
height: 40vh;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<form>
|
|
||||||
<input type="radio" name="radio" value="one" checked>One
|
|
||||||
<input type="radio" name="radio" value="two">Two
|
|
||||||
<input type="radio" name="radio" value="three">Three<br>
|
|
||||||
|
|
||||||
<input type="checkbox" name="checkbox1" value="1">Checkbox One
|
|
||||||
<input type="checkbox" name="checkbox2" value="2">Checkbox Two<br>
|
|
||||||
|
|
||||||
<input type="text" name="text" placeholder="Text Input"><br>
|
|
||||||
<input type="password" name="password" placeholder="Passwords"><br>
|
|
||||||
|
|
||||||
|
|
||||||
<input type="number" name="number" min="1" max="5" placeholder="Numbers">Number<br>
|
|
||||||
|
|
||||||
<input type="range" name="range" min="0" max="100">Ranges<br>
|
|
||||||
|
|
||||||
<select name="select">
|
|
||||||
<option value="one">One</option>
|
|
||||||
<option value="two">Two</option>
|
|
||||||
<option value="three">Three</option>
|
|
||||||
<option value="four">Four</option>
|
|
||||||
</select> Dropdowns<br>
|
|
||||||
|
|
||||||
<select name="select-multiple" multiple>
|
|
||||||
<option value="pew">Pew</option>
|
|
||||||
<option value="bang">Bang</option>
|
|
||||||
<option value="kapow">Kapow</option>
|
|
||||||
<option value="zing">Zing</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<textarea name="textarea"></textarea><br>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,222 +0,0 @@
|
|||||||
define([
|
|
||||||
'jquery',
|
|
||||||
'/api/config',
|
|
||||||
'/bower_components/chainpad-netflux/chainpad-netflux.js',
|
|
||||||
'/bower_components/chainpad-crypto/crypto.js',
|
|
||||||
'/bower_components/textpatcher/TextPatcher.amd.js',
|
|
||||||
'json.sortify',
|
|
||||||
'ula.js',
|
|
||||||
'/bower_components/chainpad-json-validator/json-ot.js',
|
|
||||||
'/common/cryptpad-common.js'
|
|
||||||
], function ($, Config, Realtime, Crypto, TextPatcher, Sortify, Formula, JsonOT, Cryptpad) {
|
|
||||||
|
|
||||||
var secret = Cryptpad.getSecrets();
|
|
||||||
|
|
||||||
var module = window.APP = {
|
|
||||||
TextPatcher: TextPatcher,
|
|
||||||
Sortify: Sortify,
|
|
||||||
Formula: Formula,
|
|
||||||
};
|
|
||||||
var initializing = true;
|
|
||||||
|
|
||||||
var uid = module.uid = Formula.uid;
|
|
||||||
|
|
||||||
var getInputType = Formula.getInputType;
|
|
||||||
var $elements = module.elements = $('input, select, textarea');
|
|
||||||
|
|
||||||
var eventsByType = Formula.eventsByType;
|
|
||||||
|
|
||||||
var Map = module.Map = {};
|
|
||||||
|
|
||||||
var UI = module.UI = {
|
|
||||||
ids: [],
|
|
||||||
each: function (f) {
|
|
||||||
UI.ids.forEach(function (id, i, list) {
|
|
||||||
if (!UI[id]) { return; }
|
|
||||||
f(UI[id], i, list);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
add: function (id, ui) {
|
|
||||||
if (UI.ids.indexOf(id) === -1) {
|
|
||||||
UI.ids.push(id);
|
|
||||||
|
|
||||||
UI[id] = ui;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// it already exists
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
remove: function (id) {
|
|
||||||
delete UI[id];
|
|
||||||
var idx = UI.ids.indexOf(id);
|
|
||||||
if (idx > -1) {
|
|
||||||
UI.ids.splice(idx, 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var cursorTypes = ['textarea', 'password', 'text'];
|
|
||||||
|
|
||||||
var canonicalize = function (text) { return text.replace(/\r\n/g, '\n'); };
|
|
||||||
$elements.each(function (index, element) {
|
|
||||||
var $this = $(this);
|
|
||||||
|
|
||||||
var id = uid();
|
|
||||||
var type = getInputType($this);
|
|
||||||
|
|
||||||
// ignore hidden inputs, submit inputs, and buttons
|
|
||||||
if (['button', 'submit', 'hidden'].indexOf(type) !== -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this // give each element a uid
|
|
||||||
.data('rtform-uid', id)
|
|
||||||
// get its type
|
|
||||||
.data('rt-ui-type', type);
|
|
||||||
|
|
||||||
var component = {
|
|
||||||
id: id,
|
|
||||||
$: $this,
|
|
||||||
element: element,
|
|
||||||
type: type,
|
|
||||||
preserveCursor: cursorTypes.indexOf(type) !== -1,
|
|
||||||
name: $this.prop('name'),
|
|
||||||
};
|
|
||||||
|
|
||||||
UI.add(id, component);
|
|
||||||
|
|
||||||
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):
|
|
||||||
typeof($this.val()) === 'string'? canonicalize($this.val()): $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: secret.channel,
|
|
||||||
crypto: Crypto.createEncryptor(secret.key),
|
|
||||||
transformFunction: JsonOT.validate
|
|
||||||
};
|
|
||||||
|
|
||||||
var setEditable = module.setEditable = function (bool) {
|
|
||||||
/* (dis)allow editing */
|
|
||||||
$elements.each(function () {
|
|
||||||
$(this).attr('disabled', !bool);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setEditable(false);
|
|
||||||
|
|
||||||
config.onInit = function (info) {
|
|
||||||
var realtime = module.realtime = info.realtime;
|
|
||||||
window.location.hash = info.channel + secret.key;
|
|
||||||
|
|
||||||
// create your patcher
|
|
||||||
module.patchText = TextPatcher.create({
|
|
||||||
realtime: realtime,
|
|
||||||
logging: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var readValues = function () {
|
|
||||||
UI.each(function (ui) {
|
|
||||||
Map[ui.id] = ui.value();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var onLocal = config.onLocal = function () {
|
|
||||||
if (initializing) { return; }
|
|
||||||
/* serialize local changes */
|
|
||||||
readValues();
|
|
||||||
module.patchText(Sortify(Map));
|
|
||||||
};
|
|
||||||
|
|
||||||
var updateValues = function () {
|
|
||||||
var userDoc = module.realtime.getUserDoc();
|
|
||||||
var parsed = JSON.parse(userDoc);
|
|
||||||
|
|
||||||
console.log(userDoc);
|
|
||||||
|
|
||||||
// flush received values to the map
|
|
||||||
// but only if you don't have them locally
|
|
||||||
// this *shouldn't* break cursors
|
|
||||||
Object.keys(parsed).forEach(function (key) {
|
|
||||||
if (UI.ids.indexOf(key) === -1) { Map[key] = parsed[key]; }
|
|
||||||
});
|
|
||||||
|
|
||||||
UI.each(function (ui) {
|
|
||||||
var newval = parsed[ui.id];
|
|
||||||
var oldval = ui.value();
|
|
||||||
|
|
||||||
if (typeof(newval) === 'undefined') { return; }
|
|
||||||
if (newval === oldval) { return; }
|
|
||||||
|
|
||||||
var op;
|
|
||||||
var selects;
|
|
||||||
var element = ui.element;
|
|
||||||
if (ui.preserveCursor) {
|
|
||||||
op = TextPatcher.diff(oldval, newval);
|
|
||||||
selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
|
|
||||||
return TextPatcher.transformCursor(element[attr], op);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.value(newval);
|
|
||||||
ui.update();
|
|
||||||
|
|
||||||
if (op && ui.preserveCursor) {
|
|
||||||
//console.log(selects);
|
|
||||||
element.selectionStart = selects[0];
|
|
||||||
element.selectionEnd = selects[1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
config.onRemote = function () {
|
|
||||||
if (initializing) { return; }
|
|
||||||
/* integrate remote changes */
|
|
||||||
updateValues();
|
|
||||||
};
|
|
||||||
|
|
||||||
config.onReady = function () {
|
|
||||||
updateValues();
|
|
||||||
|
|
||||||
console.log("READY");
|
|
||||||
setEditable(true);
|
|
||||||
initializing = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
config.onAbort = function () {
|
|
||||||
window.alert("Network Connection Lost");
|
|
||||||
};
|
|
||||||
|
|
||||||
Realtime.start(config);
|
|
||||||
|
|
||||||
UI.each(function (ui) {
|
|
||||||
var type = ui.type;
|
|
||||||
var events = eventsByType[type];
|
|
||||||
ui.$.on(events, onLocal);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@ -1,14 +0,0 @@
|
|||||||
|
|
||||||
```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()
|
|
||||||
*/
|
|
||||||
```
|
|
@ -1,25 +0,0 @@
|
|||||||
define([], function () {
|
|
||||||
var ula = {};
|
|
||||||
|
|
||||||
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',
|
|
||||||
'select-multiple': 'change',
|
|
||||||
textarea: 'change keyup',
|
|
||||||
};
|
|
||||||
|
|
||||||
return ula;
|
|
||||||
});
|
|
@ -1,60 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
||||||
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
|
|
||||||
<style></style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a id="edit" href="#" target="_blank">Edit this document's style</a>
|
|
||||||
|
|
||||||
<h1>HTML Ipsum Presents</h1>
|
|
||||||
|
|
||||||
<p><strong>Pellentesque habitant morbi tristique</strong> senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. <em>Aenean ultricies mi vitae est.</em> Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, <code>commodo vitae</code>, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. <a href="#">Donec non enim</a> in turpis pulvinar facilisis. Ut felis.</p>
|
|
||||||
|
|
||||||
<h2>Header Level 2</h2>
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li>
|
|
||||||
<li>Aliquam tincidunt mauris eu risus.</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<blockquote><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.</p></blockquote>
|
|
||||||
|
|
||||||
<h3>Header Level 3</h3>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li>
|
|
||||||
<li>Aliquam tincidunt mauris eu risus.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<pre><code>
|
|
||||||
#header h1 a {
|
|
||||||
display: block;
|
|
||||||
width: 300px;
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
</code></pre>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>th 1</th>
|
|
||||||
<th>th 2</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>one</td>
|
|
||||||
<td>two</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>three</td>
|
|
||||||
<td>four</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,75 +0,0 @@
|
|||||||
define([
|
|
||||||
'jquery',
|
|
||||||
'/api/config',
|
|
||||||
'/bower_components/chainpad-netflux/chainpad-netflux.js',
|
|
||||||
'/bower_components/chainpad-crypto/crypto.js',
|
|
||||||
'/bower_components/textpatcher/TextPatcher.amd.js',
|
|
||||||
'/common/cryptpad-common.js'
|
|
||||||
], function ($, Config, Realtime, Crypto, TextPatcher, Cryptpad) {
|
|
||||||
// TODO consider adding support for less.js
|
|
||||||
|
|
||||||
var $style = $('style').first(),
|
|
||||||
$edit = $('#edit');
|
|
||||||
|
|
||||||
var module = window.APP = {};
|
|
||||||
|
|
||||||
var secret = Cryptpad.getSecrets();
|
|
||||||
var config = {
|
|
||||||
websocketURL: Config.websocketURL,
|
|
||||||
channel: secret.channel,
|
|
||||||
crypto: Crypto.createEncryptor(secret.key),
|
|
||||||
};
|
|
||||||
|
|
||||||
var lazyDraw = (function () {
|
|
||||||
var to,
|
|
||||||
delay = 500;
|
|
||||||
return function (content) {
|
|
||||||
if (to) { clearTimeout(to); }
|
|
||||||
to = setTimeout(function () {
|
|
||||||
$style.text(content);
|
|
||||||
},delay);
|
|
||||||
};
|
|
||||||
}());
|
|
||||||
|
|
||||||
var draw = function (content) { lazyDraw(content); };
|
|
||||||
|
|
||||||
var initializing = true;
|
|
||||||
|
|
||||||
config.onInit = function (info) {
|
|
||||||
window.location.hash = info.channel + secret.key;
|
|
||||||
var realtime = module.realtime = info.realtime;
|
|
||||||
module.patchText = TextPatcher.create({
|
|
||||||
realtime: realtime,
|
|
||||||
logging: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
$(window).on('hashchange', function() {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
config.onReady = function () {
|
|
||||||
var userDoc = module.realtime.getUserDoc();
|
|
||||||
draw(userDoc);
|
|
||||||
console.log("Ready");
|
|
||||||
initializing = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
config.onRemote = function () {
|
|
||||||
draw(module.realtime.getUserDoc());
|
|
||||||
};
|
|
||||||
|
|
||||||
config.onAbort = function () {
|
|
||||||
// notify the user of the abort
|
|
||||||
window.alert("Network Connection Lost");
|
|
||||||
};
|
|
||||||
|
|
||||||
config.onLocal = function () {
|
|
||||||
// nope
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
$edit.attr('href', '/examples/text/'+ window.location.hash);
|
|
||||||
|
|
||||||
Realtime.start(config);
|
|
||||||
});
|
|
@ -1,38 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
||||||
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
|
|
||||||
<style>
|
|
||||||
html, body{
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
textarea{
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100vh;
|
|
||||||
|
|
||||||
font-size: 18px;
|
|
||||||
background-color: #073642;
|
|
||||||
color: #839496;
|
|
||||||
|
|
||||||
overflow-x: hidden;
|
|
||||||
|
|
||||||
/* disallow textarea resizes */
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
textarea[disabled] {
|
|
||||||
background-color: #275662;
|
|
||||||
color: #637476;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<textarea></textarea>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,97 +0,0 @@
|
|||||||
define([
|
|
||||||
'jquery',
|
|
||||||
'/api/config',
|
|
||||||
'/bower_components/chainpad-netflux/chainpad-netflux.js',
|
|
||||||
'/bower_components/chainpad-crypto/crypto.js',
|
|
||||||
'/bower_components/textpatcher/TextPatcher.amd.js',
|
|
||||||
'/common/cryptpad-common.js'
|
|
||||||
], function ($, Config, Realtime, Crypto, TextPatcher, Cryptpad) {
|
|
||||||
|
|
||||||
var secret = Cryptpad.getSecrets();
|
|
||||||
if (!secret.keys) {
|
|
||||||
secret.keys = secret.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
var module = window.APP = {
|
|
||||||
TextPatcher: TextPatcher
|
|
||||||
};
|
|
||||||
|
|
||||||
var initializing = true;
|
|
||||||
var $textarea = $('textarea');
|
|
||||||
|
|
||||||
var config = module.config = {
|
|
||||||
initialState: '',
|
|
||||||
websocketURL: Config.websocketURL,
|
|
||||||
validateKey: secret.keys.validateKey || undefined,
|
|
||||||
channel: secret.channel,
|
|
||||||
crypto: Crypto.createEncryptor(secret.keys),
|
|
||||||
};
|
|
||||||
|
|
||||||
var setEditable = function (bool) { $textarea.attr('disabled', !bool); };
|
|
||||||
var canonicalize = function (text) { return text.replace(/\r\n/g, '\n'); };
|
|
||||||
|
|
||||||
setEditable(false);
|
|
||||||
|
|
||||||
config.onInit = function (info) {
|
|
||||||
var editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
|
|
||||||
Cryptpad.replaceHash(editHash);
|
|
||||||
$(window).on('hashchange', function() {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
config.onRemote = function () {
|
|
||||||
if (initializing) { return; }
|
|
||||||
var userDoc = module.realtime.getUserDoc();
|
|
||||||
var content = canonicalize($textarea.val());
|
|
||||||
|
|
||||||
var op = TextPatcher.diff(content, userDoc);
|
|
||||||
var elem = $textarea[0];
|
|
||||||
|
|
||||||
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
|
|
||||||
return TextPatcher.transformCursor(elem[attr], op);
|
|
||||||
});
|
|
||||||
|
|
||||||
$textarea.val(userDoc);
|
|
||||||
elem.selectionStart = selects[0];
|
|
||||||
elem.selectionEnd = selects[1];
|
|
||||||
};
|
|
||||||
|
|
||||||
var onLocal = config.onLocal = function () {
|
|
||||||
if (initializing) { return; }
|
|
||||||
module.patchText(canonicalize($textarea.val()));
|
|
||||||
};
|
|
||||||
|
|
||||||
config.onReady = function (info) {
|
|
||||||
var realtime = module.realtime = info.realtime;
|
|
||||||
module.patchText = TextPatcher.create({
|
|
||||||
realtime: realtime
|
|
||||||
});
|
|
||||||
|
|
||||||
$textarea.val(realtime.getUserDoc());
|
|
||||||
|
|
||||||
setEditable(true);
|
|
||||||
initializing = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
config.onAbort = function () {
|
|
||||||
setEditable(false);
|
|
||||||
window.alert("Server Connection Lost");
|
|
||||||
};
|
|
||||||
|
|
||||||
config.onConnectionChange = function (info) {
|
|
||||||
if (info.state) {
|
|
||||||
initializing = true;
|
|
||||||
} else {
|
|
||||||
setEditable(false);
|
|
||||||
window.alert("Server Connection Lost. Trying to reconnect...");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Realtime.start(config);
|
|
||||||
|
|
||||||
['cut', 'paste', 'change', 'keyup', 'keydown', 'select', 'textInput']
|
|
||||||
.forEach(function (evt) {
|
|
||||||
$textarea.on(evt, onLocal);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in New Issue