resolve merge conflicts
commit
351b95d6a9
@ -0,0 +1,23 @@
|
||||
@import (once) "./unselectable.less";
|
||||
@import (once) "./variables.less";
|
||||
@import (once) "./colortheme.less";
|
||||
|
||||
.leftside-menu_main() {
|
||||
}
|
||||
.leftside-menu-category_main() {
|
||||
.unselectable_make();
|
||||
padding: 5px 20px;
|
||||
margin: 15px 0;
|
||||
cursor: pointer;
|
||||
height: @variables_bar-height;
|
||||
line-height: @variables_bar-height - 10px;
|
||||
.fa {
|
||||
width: 25px;
|
||||
}
|
||||
&:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
}
|
||||
&.cp-leftside-active {
|
||||
background: @colortheme_sidebar-active;
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
@import (once) "./colortheme.less";
|
||||
|
||||
.limit-bar_main () {
|
||||
.cp-limit-container {
|
||||
@colortheme_green: #5cb85c;
|
||||
display: inline-flex;
|
||||
flex-flow: column-reverse;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
.cp-limit-bar {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin: 3px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #999;
|
||||
background: white;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: ~"calc(100% - 6px)";
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
overflow: hidden;
|
||||
.cp-limit-usage {
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
background: blue;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index:1; // .usage
|
||||
&.cp-limit-usage-normal {
|
||||
background: @colortheme_green;
|
||||
}
|
||||
&.cp-limit-usage-warning {
|
||||
background: orange;
|
||||
}
|
||||
&.cp-limit-usage-above {
|
||||
background: red;
|
||||
}
|
||||
}
|
||||
.cp-limit-usage-text {
|
||||
position: relative;
|
||||
color: black;
|
||||
text-shadow: 1px 0 2px white, 0 1px 2px white, -1px 0 2px white, 0 -1px 2px white;
|
||||
z-index: 2; // .usageText
|
||||
font-size: @colortheme_app-font-size;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.cp-limit-upgrade {
|
||||
padding: 0;
|
||||
line-height: 25px;
|
||||
height: 25px;
|
||||
margin: 0 3px;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
.tools_placeholder-color (@color) {
|
||||
&::-webkit-input-placeholder { /* WebKit, Blink, Edge */
|
||||
color: @color;;
|
||||
}
|
||||
&:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
|
||||
color: @color;
|
||||
opacity: 1;
|
||||
}
|
||||
&::-moz-placeholder { /* Mozilla Firefox 19+ */
|
||||
color: @color;
|
||||
opacity: 1;
|
||||
}
|
||||
&:-ms-input-placeholder { /* Internet Explorer 10-11 */
|
||||
color: @color;
|
||||
}
|
||||
&::-ms-input-placeholder { /* Microsoft Edge */
|
||||
color: @color;
|
||||
}
|
||||
}
|
||||
|
||||
.tools_unselectable () {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
.unselectable_make() {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.unselectable_main() {
|
||||
.cp-unselectable {
|
||||
.unselectable_make();
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
// Elements size
|
||||
@variables_bar-height: 32px;
|
||||
|
@ -0,0 +1,748 @@
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
realtimeOptions.onReady = function (info) {
|
||||
// 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;
|
||||
};
|
||||
|
||||
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,820 @@
|
||||
@import (once) "../../customize/src/less2/include/browser.less";
|
||||
@import (once) "../../customize/src/less2/include/toolbar.less";
|
||||
@import (once) "../../customize/src/less2/include/markdown.less";
|
||||
@import (once) '../../customize/src/less2/include/fileupload.less';
|
||||
@import (once) '../../customize/src/less2/include/alertify.less';
|
||||
@import (once) '../../customize/src/less2/include/leftside-menu.less';
|
||||
@import (once) "../../customize/src/less2/include/tools.less";
|
||||
@import (once) "../../customize/src/less2/include/limit-bar.less";
|
||||
|
||||
.toolbar_main();
|
||||
.fileupload_main();
|
||||
.alertify_main();
|
||||
.limit-bar_main();
|
||||
|
||||
@drive_hover: #eee;
|
||||
@drive_hover-light: lighten(@drive_hover, 20%);
|
||||
@drive_info-box-bg: #d2e1f2;
|
||||
@drive_info-box-border: #bbb;
|
||||
@drive_table-header-fg: #555;
|
||||
@drive_table-header-bg: #e8e8e8;
|
||||
@drive_mobile-tree-border-col: #ccc;
|
||||
|
||||
@drive_content-fg: @colortheme_sidebar-right-fg;
|
||||
@drive_content-bg: @colortheme_sidebar-right-bg;
|
||||
@drive_content-bg-ro: darken(@drive_content-bg, 10%);
|
||||
|
||||
|
||||
/* PAGE */
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
max-height: 100%;
|
||||
min-height: auto;
|
||||
|
||||
.cp-unselectable {
|
||||
.tools_unselectable();
|
||||
}
|
||||
|
||||
/* local mixins */
|
||||
.drive_fileIcon {
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 10px 10px;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
|
||||
&:not(.cp-app-drive-element-selected):not(.cp-app-drive-element-selected-tmp) {
|
||||
border: 1px solid #CCC;
|
||||
}
|
||||
.cp-app-drive-element-name {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
margin: 8px 0;
|
||||
display: inline-block;
|
||||
//align-items: center;
|
||||
//justify-content: center;
|
||||
overflow: hidden;
|
||||
//text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.cp-app-drive-element-truncated {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0; right: 0;
|
||||
text-align: center;
|
||||
}
|
||||
img.cp-app-drive-content-icon {
|
||||
height: 48px;
|
||||
max-height: none;
|
||||
max-width: none;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.fa {
|
||||
display: block;
|
||||
margin: auto;
|
||||
font-size: 48px;
|
||||
margin: 8px 0;
|
||||
text-align: center;
|
||||
&.listonly {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img.cp-app-drive-icon {
|
||||
max-width: 20px;
|
||||
max-height: 16px;
|
||||
}
|
||||
|
||||
.cp-app-drive-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
@media screen and (max-width: @browser_media-medium-screen) {
|
||||
display: block;
|
||||
#cp-app-drive-toolbar {
|
||||
.path .element {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
#cp-app-drive-tree {
|
||||
resize: none;
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
max-height: unset;
|
||||
border-bottom: 1px solid @drive_mobile-tree-border-col;
|
||||
.cp-app-drive-tree-category {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.fa {
|
||||
font-family: FontAwesome;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 0px; // Remove the default padding
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 0px 5px;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cp-app-drive-context {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 500;
|
||||
li {
|
||||
padding: 0;
|
||||
font-size: @colortheme_app-font-size;
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cp-app-drive-element-droppable {
|
||||
background-color: #FE9A2E;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.cp-app-drive-element-selected {
|
||||
background: #666 !important;
|
||||
color: #eee;
|
||||
margin: -1px;
|
||||
.fa-minus-square-o, .fa-plus-square-o {
|
||||
color: @colortheme_sidebar-left-fg;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-app-drive-element-selected-tmp {
|
||||
border: 1px dotted #bbb;
|
||||
background: #AAA;
|
||||
color: #ddd;
|
||||
margin: -1px;
|
||||
.fa-minus-square-o, .fa-plus-square-o {
|
||||
color: @colortheme_sidebar-left-fg;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
&.fa-folder, &.fa-folder-open {
|
||||
//color: #FEDE8B;
|
||||
//text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
|
||||
}
|
||||
}
|
||||
|
||||
/* TREE */
|
||||
|
||||
|
||||
#cp-app-drive-tree {
|
||||
font-size: @colortheme_app-font-size;
|
||||
//border-right: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
background: @colortheme_sidebar-left-bg;
|
||||
overflow: auto;
|
||||
resize: horizontal;
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
max-width: 500px;
|
||||
min-width: 200px;
|
||||
padding: 0px;
|
||||
color: @colortheme_sidebar-left-fg;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
max-height: 100%;
|
||||
.cp-app-drive-tree-categories-container {
|
||||
flex: 1;
|
||||
max-width: 500px;
|
||||
overflow: auto;
|
||||
}
|
||||
img.cp-app-drive-icon {
|
||||
margin-bottom: 3px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
.cp-app-drive-tree-docs {
|
||||
margin-top: 20px;
|
||||
//padding: 0 0 0 20px;
|
||||
padding: 0;
|
||||
cursor: auto;
|
||||
&li, li {
|
||||
padding: 0;
|
||||
&.cp-app-drive-element-collapsed ul {
|
||||
display: none;
|
||||
}
|
||||
input {
|
||||
width: ~"calc(100% - 30px)";
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
color: lighten(@colortheme_sidebar-left-fg, 40%);
|
||||
}
|
||||
& > span.cp-app-drive-element-row {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
//min-width: ~"calc(100% + 5px)";
|
||||
.leftside-menu-category_main();
|
||||
width: ~"calc(100% + 5px)";
|
||||
margin: 0;
|
||||
margin-bottom: -6px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin-left: -5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
& > span.cp-app-drive-element-row:not(.cp-app-drive-element-selected):not(.cp-app-drive-element-active):hover {
|
||||
}
|
||||
}
|
||||
}
|
||||
span.cp-app-drive-element {
|
||||
cursor: pointer;
|
||||
}
|
||||
.cp-app-drive-tree-category {
|
||||
margin: 0;
|
||||
margin-top: 15px;
|
||||
.cp-app-drive-tree-root {
|
||||
&> .fa {
|
||||
min-width: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
li {
|
||||
padding: 0;
|
||||
.cp-app-drive-element-row {
|
||||
display: block;
|
||||
padding-left: 20px;
|
||||
.leftside-menu-category_main();
|
||||
margin: 0;
|
||||
.fa {
|
||||
width: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-app-drive-tree-category:last-child {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.limit-container {
|
||||
margin-top: 0;
|
||||
}
|
||||
#cp-app-drive-tree-search {
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
input {
|
||||
background: lighten(@colortheme_drive-bg, 8%);
|
||||
color: @colortheme_drive-color;
|
||||
.tools_placeholder-color(@colortheme_drive-color);
|
||||
outline-width: 0px;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
//border: 1px solid #ccc;
|
||||
border: 0;
|
||||
border-right: 1px solid lighten(@colortheme_drive-bg, 16%);
|
||||
//border-right: 0;
|
||||
height: @variables_bar-height;
|
||||
padding: 0 5px;
|
||||
padding-left: 45px;
|
||||
&:focus {
|
||||
outline-width: 0px;
|
||||
}
|
||||
}
|
||||
.cp-app-drive-tree-search-con {
|
||||
color: @colortheme_drive-color;
|
||||
position: absolute;
|
||||
left: 20px; // TODO align with drive categories
|
||||
top: 8px;
|
||||
}
|
||||
}
|
||||
.fa.cp-app-drive-icon-expcol {
|
||||
margin-left: -10px;
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
top: 10px;
|
||||
width: 11px !important;
|
||||
height: 11px !important;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: white;
|
||||
z-index: 10;
|
||||
cursor: default;
|
||||
&:before {
|
||||
position:relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
.cp-app-drive-tree-docs {
|
||||
.cp-app-drive-tree-root > .cp-app-drive-element-row > .cp-app-drive-icon-expcol {
|
||||
position: relative;
|
||||
top:0;
|
||||
left: -10px;
|
||||
}
|
||||
.cp-app-drive-tree-root > .cp-app-drive-element-row > .cp-app-drive-icon-folder {
|
||||
margin-left: -5px;
|
||||
}
|
||||
.cp-app-drive-tree-root {
|
||||
&> .cp-app-drive-element-row {
|
||||
padding-left: 20px;
|
||||
}
|
||||
&> ul {
|
||||
padding-left: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand/collapse lines
|
||||
.cp-app-drive-tree-docs ul {
|
||||
margin: 0px 0px 0px 10px;
|
||||
list-style: none;
|
||||
padding-left: 10px;
|
||||
li {
|
||||
position: relative;
|
||||
&:before {
|
||||
position: absolute;
|
||||
left: -15px;
|
||||
top: -11px;
|
||||
content: '';
|
||||
display: block;
|
||||
border-left: 1px solid @colortheme_sidebar-left-branch;
|
||||
height: ~"calc(1em + 11px)";
|
||||
border-bottom: 1px solid @colortheme_sidebar-left-branch;
|
||||
width: 15px;
|
||||
}
|
||||
&:after {
|
||||
position: absolute;
|
||||
left: -15px;
|
||||
bottom: -7px;
|
||||
content: '';
|
||||
display: block;
|
||||
border-left: 1px solid @colortheme_sidebar-left-branch;
|
||||
height: 100%;
|
||||
}
|
||||
&.cp-app-drive-tree-root {
|
||||
margin: 0px 0px 0px -10px;
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&:last-child:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* CONTENT */
|
||||
#cp-app-drive-content-container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex: 1;
|
||||
// Needed to avoid the folder's path to overflows
|
||||
// https://stackoverflow.com/questions/38223879/white-space-nowrap-breaks-flexbox-layout
|
||||
min-width: 0;
|
||||
}
|
||||
#cp-app-drive-content {
|
||||
box-sizing: border-box;
|
||||
background: @drive_content-bg;
|
||||
color: @drive_content-fg;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
position: relative;
|
||||
.cp-app-drive-content-select-box {
|
||||
display: none;
|
||||
background-color: rgba(100, 100, 100, 0.7);
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
}
|
||||
&.cp-app-drive-readonly {
|
||||
background: @drive_content-bg-ro;
|
||||
}
|
||||
h1 {
|
||||
padding-left: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.cp-app-drive-content-info-box {
|
||||
line-height: 2em;
|
||||
padding: 0.25em 0.75em;
|
||||
margin: 1em;
|
||||
background: @drive_info-box-bg;
|
||||
span {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
li {
|
||||
cursor: default;
|
||||
&:not(.cp-app-drive-element-header) {
|
||||
&:hover {
|
||||
&:not(.-cp-app-drive-element-selected, .cp-app-drive-element-selected-tmp) {
|
||||
background-color: @drive_hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#cp-app-drive-content-folder {
|
||||
li {
|
||||
&.cp-app-drive-search-result {
|
||||
border-bottom: 1px solid @drive_info-box-border;
|
||||
display: block;
|
||||
&:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
.cp-app-drive-search-label2 {
|
||||
width: 150px;
|
||||
font-size: 15px;
|
||||
text-align: right;
|
||||
padding-right: 15px;
|
||||
}
|
||||
.cp-app-drive-search-opendir {
|
||||
a {
|
||||
cursor: pointer;
|
||||
color: #41b7d8;
|
||||
&:hover {
|
||||
color: #014c8c;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-app-drive-search-path {
|
||||
font-style: italic;
|
||||
direction: rtl;
|
||||
.cp-app-drive-path-element {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
.cp-app-drive-search-title {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: @drive_hover;
|
||||
}
|
||||
}
|
||||
.cp-app-drive-search-col2 {
|
||||
width: 250px;
|
||||
}
|
||||
td.cp-app-drive-search-icon {
|
||||
width: 50px;
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-app-drive-element {
|
||||
.cp-app-drive-element-truncated { display: none; }
|
||||
}
|
||||
div.cp-app-drive-content-grid {
|
||||
padding: 20px;
|
||||
.drive_fileIcon;
|
||||
li {
|
||||
&.cp-app-drive-element {
|
||||
position: relative;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.cp-app-drive-element-state {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
.fa {
|
||||
margin:0;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-app-drive-element-list {
|
||||
display: none;
|
||||
}
|
||||
.cp-app-drive-new-ghost {
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
padding: 0;
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.fa {
|
||||
cursor: pointer;
|
||||
font-size: 90px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cp-app-drive-content-list {
|
||||
.cp-app-drive-element-grid {
|
||||
display: none;
|
||||
}
|
||||
// Make it act as a table!
|
||||
padding-left: 20px;
|
||||
ul {
|
||||
display: table;
|
||||
width: 100%;
|
||||
padding: 0px 10px;
|
||||
}
|
||||
li {
|
||||
display: table-row;
|
||||
&> span {
|
||||
padding: 0 5px;
|
||||
display: table-cell;
|
||||
}
|
||||
&:not(.cp-app-drive-element-header) {
|
||||
height: @variables_bar-height;
|
||||
line-height: @variables_bar-height;
|
||||
}
|
||||
&.cp-app-drive-element-header {
|
||||
cursor: default;
|
||||
color: @drive_table-header-fg;
|
||||
span {
|
||||
&:not(.fa) {
|
||||
text-align: left;
|
||||
}
|
||||
&.sortasc, &.sortdesc {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
&> span {
|
||||
padding: 15px 5px;
|
||||
&.cp-app-drive-sort-active {
|
||||
font-weight: bold;
|
||||
}
|
||||
&.cp-app-drive-sort-clickable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: @drive_table-header-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-app-drive-element {
|
||||
span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
&.cp-app-drive-element-state {
|
||||
.fa:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
&.cp-app-drive-content-icon, &.cp-app-drive-element-state {
|
||||
width: 30px;
|
||||
}
|
||||
&.cp-app-drive-element-type, &.cp-app-drive-element-atime, &.cp-app-drive-element-ctime {
|
||||
width: 175px;
|
||||
}
|
||||
&.cp-app-drive-element-title {
|
||||
width: 250px;
|
||||
@media screen and (max-width: 1200px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&.cp-app-drive-element-folders, &.cp-app-drive-element-files {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#cp-app-drive-content-folder {
|
||||
padding-right: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#cp-app-drive-new-ghost-dialog.cp-modal-container {
|
||||
.drive_fileIcon;
|
||||
|
||||
li:not(.cp-app-drive-element-selected):hover {
|
||||
border: 1px solid white;
|
||||
}
|
||||
.cp-modal {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
li, li .fa {
|
||||
cursor: pointer;
|
||||
}
|
||||
&> p {
|
||||
margin: 50px;
|
||||
}
|
||||
&> div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
overflow-y: auto;
|
||||
.cp-app-drive-new-upload {
|
||||
break-after: always;
|
||||
page-break-after: always;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: @browser_media-not-big) {
|
||||
.cp-modal {
|
||||
& > p {
|
||||
display: none;
|
||||
}
|
||||
& > div {
|
||||
align-content: unset;
|
||||
li {
|
||||
height: 40px;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.fa {
|
||||
font-size: 32px;
|
||||
}
|
||||
.cp-app-drive-new-name {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Toolbar */
|
||||
|
||||
#cp-app-drive-toolbar {
|
||||
background: lighten(@colortheme_drive-bg, 8%);
|
||||
color: @colortheme_drive-color;
|
||||
//height: 30px;
|
||||
//display: flex;
|
||||
//flex-flow: row;
|
||||
z-index: 100;
|
||||
box-sizing: border-box;
|
||||
height: @variables_bar-height;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
|
||||
* {
|
||||
outline-width: 0;
|
||||
&:focus {
|
||||
outline-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.history {
|
||||
float: right;
|
||||
.cp-toolbar-drawer-element {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-app-drive-toolbar-rightside, .cp-app-drive-toolbar-leftside {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
.fa {
|
||||
margin: 0;
|
||||
}
|
||||
button {
|
||||
height: @variables_bar-height;
|
||||
padding: 0 10px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
background: transparent;
|
||||
font-size: @colortheme_app-font-size;
|
||||
color: @colortheme_drive-color;
|
||||
transition: all 0.15s;
|
||||
.drawer {
|
||||
display: none;
|
||||
}
|
||||
.fa, span {
|
||||
font-size: @colortheme_app-font-size;
|
||||
}
|
||||
&:hover {
|
||||
background: @colortheme_drive-bg;
|
||||
}
|
||||
&.cp-app-drive-toolbar-active {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-app-drive-toolbar-rightside {
|
||||
float: right;
|
||||
& > * {
|
||||
float: right;
|
||||
}
|
||||
#cp-app-drive-toolbar-contextbuttons {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
}
|
||||
padding-left: 10px;
|
||||
}
|
||||
.cp-app-drive-toolbar-leftside {
|
||||
& > span {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
button {
|
||||
padding: 0 10px;
|
||||
.fa {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.cp-dropdown-button-title {
|
||||
display: inline-flex;
|
||||
height: @variables_bar-height;
|
||||
align-items: center;
|
||||
span:not(.fa) {
|
||||
line-height: 23px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
font: @colortheme_app-font;
|
||||
span {
|
||||
font: @colortheme_app-font;
|
||||
}
|
||||
.fa, &.fa {
|
||||
font-family: FontAwesome;
|
||||
}
|
||||
}
|
||||
/* The container <div> - needed to position the dropdown content */
|
||||
.cp-dropdown-container {
|
||||
margin: 2px 2px;
|
||||
line-height: 1em;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.cp-dropdown-content {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.cp-app-drive-path {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: @variables_bar-height;
|
||||
line-height: @variables_bar-height;
|
||||
cursor: default;
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
direction: rtl;
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
.cp-app-drive-path-element {
|
||||
display: inline-block;
|
||||
height: @variables_bar-height;
|
||||
line-height: @variables_bar-height;
|
||||
font-size: @colortheme_app-font-size;
|
||||
padding: 0 5px;
|
||||
border: 0;
|
||||
background: darken(@colortheme_drive-bg, 10%);
|
||||
color: @colortheme_drive-color;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.15s;
|
||||
&.cp-app-drive-path-separator {
|
||||
color: #ccc;
|
||||
}
|
||||
&.cp-app-drive-path-lickable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: darken(@colortheme_drive-bg, 15%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="cp">
|
||||
<head>
|
||||
<title>CryptDrive</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
#pad-iframe {
|
||||
position:fixed;
|
||||
top:0;
|
||||
left:0px;
|
||||
bottom:0px;
|
||||
right:0px;
|
||||
width:100%;
|
||||
height:100%;
|
||||
border:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe id="pad-iframe"></iframe><script src="/common/noscriptfix.js"></script>
|
@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<script async data-bootload="/drive/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body style="display: none;">
|
||||
<div id="toolbar" class="toolbar-container"></div>
|
||||
<div class="app-container" tabindex="0">
|
||||
<div id="tree">
|
||||
</div>
|
||||
<div id="rightCol">
|
||||
<div id="driveToolbar"></div>
|
||||
<div id="content" tabindex="2"></div>
|
||||
</div>
|
||||
<div id="treeContextMenu" class="contextMenu dropdown clearfix unselectable">
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
|
||||
<li><a tabindex="-1" data-icon="fa-folder-open" class="open dropdown-item" data-localization="fc_open">Open</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-eye" class="open_ro dropdown-item" data-localization="fc_open_ro">Open (read-only)</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-pencil" class="rename editable dropdown-item" data-localization="fc_rename">Rename</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-trash" class="delete editable dropdown-item" data-localization="fc_delete">Delete</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-folder" class="newfolder editable dropdown-item" data-localization="fc_newfolder">New folder</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-database" class="properties dropdown-item" data-localization="fc_prop">Properties</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="contentContextMenu" class="contextMenu dropdown clearfix unselectable">
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
|
||||
<li><a tabindex="-1" data-icon="fa-folder" class="newfolder editable dropdown-item" data-localization="fc_newfolder">New folder</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-file-word-o" class="newdoc own editable dropdown-item" data-type="pad" data-localization="button_newpad">New pad</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-file-code-o" class="newdoc own editable dropdown-item" data-type="code" data-localization="button_newcode">New code</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-file-powerpoint-o" class="newdoc own editable dropdown-item" data-type="slide" data-localization="button_newslide">New slide</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-calendar" class="newdoc own editable dropdown-item" data-type="poll" data-localization="button_newpoll">New poll</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-paint-brush" class="newdoc own editable dropdown-item" data-type="whiteboard" data-localization="button_newwhiteboard">New whiteboard</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="defaultContextMenu" class="contextMenu dropdown clearfix unselectable">
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
|
||||
<li><a tabindex="-1" data-icon="fa-folder-open" class="open dropdown-item" data-localization="fc_open">Open</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-eye" class="open_ro dropdown-item" data-localization="fc_open_ro">Open (read-only)</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-trash" class="delete dropdown-item" data-localization="fc_delete">Delete</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-database" class="properties dropdown-item" data-localization="fc_prop">Properties</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="trashTreeContextMenu" class="contextMenu dropdown clearfix unselectable">
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
|
||||
<li><a tabindex="-1" data-icon="fa-trash-o" class="empty editable dropdown-item" data-localization="fc_empty">Empty the trash</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="trashContextMenu" class="contextMenu dropdown clearfix unselectable">
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
|
||||
<li><a tabindex="-1" data-icon="fa-eraser" class="remove editable dropdown-item" data-localization="fc_remove">Delete permanently</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-repeat" class="restore editable dropdown-item" data-localization="fc_restore">Restore</a></li>
|
||||
<li><a tabindex="-1" data-icon="fa-database" class="properties dropdown-item" data-localization="fc_prop">Properties</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -0,0 +1,7 @@
|
||||
define([
|
||||
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
|
||||
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
|
||||
'less!/drive/file.less',
|
||||
'less!/customize/src/less/cryptpad.less',
|
||||
'less!/customize/src/less/toolbar.less',
|
||||
], function () {});
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,52 @@
|
||||
define(function () {
|
||||
var padZero = function (str, len) {
|
||||
len = len || 2;
|
||||
var zeros = new Array(len).join('0');
|
||||
return (zeros + str).slice(-len);
|
||||
};
|
||||
var invertColor = function (hex) {
|
||||
if (hex.indexOf('#') === 0) {
|
||||
hex = hex.slice(1);
|
||||
}
|
||||
// convert 3-digit hex to 6-digits.
|
||||
if (hex.length === 3) {
|
||||
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
||||
}
|
||||
if (hex.length !== 6) {
|
||||
console.error(hex);
|
||||
throw new Error('Invalid HEX color.');
|
||||
}
|
||||
// invert color components
|
||||
var r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16),
|
||||
g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16),
|
||||
b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16);
|
||||
// pad each with zeros and return
|
||||
return '#' + padZero(r) + padZero(g) + padZero(b);
|
||||
};
|
||||
var rgb2hex = function (rgb) {
|
||||
if (rgb.indexOf('#') === 0) { return rgb; }
|
||||
rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
|
||||
var hex = function (x) {
|
||||
return ("0" + parseInt(x).toString(16)).slice(-2);
|
||||
};
|
||||
return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
|
||||
};
|
||||
var hex2rgba = function (hex, opacity) {
|
||||
if (hex.indexOf('#') === 0) {
|
||||
hex = hex.slice(1);
|
||||
}
|
||||
if (hex.length === 3) {
|
||||
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
||||
}
|
||||
if (!opacity) { opacity = 1; }
|
||||
var r = parseInt(hex.slice(0,2), 16);
|
||||
var g = parseInt(hex.slice(2,4), 16);
|
||||
var b = parseInt(hex.slice(4,6), 16);
|
||||
return 'rgba('+r+', '+g+', '+b+', '+opacity+')';
|
||||
};
|
||||
return {
|
||||
invert: invertColor,
|
||||
rgb2hex: rgb2hex,
|
||||
hex2rgba: hex2rgba
|
||||
};
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="cp">
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -0,0 +1,511 @@
|
||||
define([
|
||||
'jquery',
|
||||
'/api/config',
|
||||
'/bower_components/chainpad-netflux/chainpad-netflux.js',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/common/toolbar2.js',
|
||||
'/bower_components/textpatcher/TextPatcher.amd.js',
|
||||
'json.sortify',
|
||||
'/bower_components/chainpad-json-validator/json-ot.js',
|
||||
'/common/cryptpad-common.js',
|
||||
'/common/cryptget.js',
|
||||
'/whiteboard/colors.js',
|
||||
'/customize/application_config.js',
|
||||
'/common/common-thumbnail.js',
|
||||
'/bower_components/secure-fabric.js/dist/fabric.min.js',
|
||||
'/bower_components/file-saver/FileSaver.min.js',
|
||||
|
||||
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
|
||||
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
|
||||
'less!/customize/src/less/cryptpad.less',
|
||||
'less!/whiteboard/whiteboard.less',
|
||||
'less!/customize/src/less/toolbar.less',
|
||||
], function ($, Config, Realtime, Crypto, Toolbar, TextPatcher, JSONSortify, JsonOT, Cryptpad, Cryptget, Colors, AppConfig, Thumb) {
|
||||
var saveAs = window.saveAs;
|
||||
var Messages = Cryptpad.Messages;
|
||||
|
||||
var module = window.APP = { $:$ };
|
||||
var Fabric = module.Fabric = window.fabric;
|
||||
|
||||
$(function () {
|
||||
Cryptpad.addLoadingScreen();
|
||||
var onConnectError = function () {
|
||||
Cryptpad.errorLoadingScreen(Messages.websocketError);
|
||||
};
|
||||
var toolbar;
|
||||
|
||||
var secret = Cryptpad.getSecrets();
|
||||
var readOnly = secret.keys && !secret.keys.editKeyStr;
|
||||
if (!secret.keys) {
|
||||
secret.keys = secret.key;
|
||||
}
|
||||
|
||||
var andThen = function () {
|
||||
/* Initialize Fabric */
|
||||
var canvas = module.canvas = new Fabric.Canvas('canvas');
|
||||
var $canvas = $('canvas');
|
||||
var $controls = $('#controls');
|
||||
var $canvasContainer = $('canvas').parents('.canvas-container');
|
||||
var $pickers = $('#pickers');
|
||||
var $colors = $('#colors');
|
||||
var $cursors = $('#cursors');
|
||||
var $deleteButton = $('#delete');
|
||||
|
||||
var brush = {
|
||||
color: '#000000',
|
||||
opacity: 1
|
||||
};
|
||||
|
||||
var $toggle = $('#toggleDraw');
|
||||
var $width = $('#width');
|
||||
var $widthLabel = $('label[for="width"]');
|
||||
var $opacity = $('#opacity');
|
||||
var $opacityLabel = $('label[for="opacity"]');
|
||||
window.canvas = canvas;
|
||||
var createCursor = function () {
|
||||
var w = canvas.freeDrawingBrush.width;
|
||||
var c = canvas.freeDrawingBrush.color;
|
||||
var size = w > 30 ? w+2 : w+32;
|
||||
$cursors.html('<canvas width="'+size+'" height="'+size+'"></canvas>');
|
||||
var $ccanvas = $cursors.find('canvas');
|
||||
var ccanvas = $ccanvas[0];
|
||||
|
||||
var ctx = ccanvas.getContext('2d');
|
||||
var centerX = size / 2;
|
||||
var centerY = size / 2;
|
||||
var radius = w/2;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
|
||||
ctx.fillStyle = c;
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = brush.color;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(size/2, 0); ctx.lineTo(size/2, 10);
|
||||
ctx.moveTo(size/2, size); ctx.lineTo(size/2, size-10);
|
||||
ctx.moveTo(0, size/2); ctx.lineTo(10, size/2);
|
||||
ctx.moveTo(size, size/2); ctx.lineTo(size-10, size/2);
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.stroke();
|
||||
|
||||
var img = ccanvas.toDataURL("image/png");
|
||||
$controls.find('.selected > img').attr('src', img);
|
||||
canvas.freeDrawingCursor = 'url('+img+') '+size/2+' '+size/2+', crosshair';
|
||||
};
|
||||
|
||||
var updateBrushWidth = function () {
|
||||
var val = $width.val();
|
||||
canvas.freeDrawingBrush.width = Number(val);
|
||||
$widthLabel.text(Cryptpad.Messages._getKey("canvas_widthLabel", [val]));
|
||||
$('#width-val').text(val + 'px');
|
||||
createCursor();
|
||||
};
|
||||
updateBrushWidth();
|
||||
|
||||
$width.on('change', updateBrushWidth);
|
||||
|
||||
var updateBrushOpacity = function () {
|
||||
var val = $opacity.val();
|
||||
brush.opacity = Number(val);
|
||||
canvas.freeDrawingBrush.color = Colors.hex2rgba(brush.color, brush.opacity);
|
||||
$opacityLabel.text(Cryptpad.Messages._getKey("canvas_opacityLabel", [val]));
|
||||
$('#opacity-val').text((Number(val) * 100) + '%');
|
||||
createCursor();
|
||||
};
|
||||
updateBrushOpacity();
|
||||
|
||||
$opacity.on('change', updateBrushOpacity);
|
||||
|
||||
var pickColor = function (current, cb) {
|
||||
var $picker = $('<input>', {
|
||||
type: 'color',
|
||||
value: '#FFFFFF',
|
||||
})
|
||||
// TODO confirm that this is safe to remove
|
||||
//.css({ visibility: 'hidden' })
|
||||
.on('change', function () {
|
||||
var color = this.value;
|
||||
cb(color);
|
||||
}).appendTo($pickers);
|
||||
setTimeout(function () {
|
||||
$picker.val(current);
|
||||
$picker.click();
|
||||
});
|
||||
};
|
||||
|
||||
var setColor = function (c) {
|
||||
c = Colors.rgb2hex(c);
|
||||
brush.color = c;
|
||||
canvas.freeDrawingBrush.color = Colors.hex2rgba(brush.color, brush.opacity);
|
||||
module.$color.css({
|
||||
'color': c,
|
||||
});
|
||||
createCursor();
|
||||
};
|
||||
|
||||
|
||||
var palette = AppConfig.whiteboardPalette || [
|
||||
'red', 'blue', 'green', 'white', 'black', 'purple',
|
||||
'gray', 'beige', 'brown', 'cyan', 'darkcyan', 'gold', 'yellow', 'pink'
|
||||
];
|
||||
|
||||
$('.palette-color').on('click', function () {
|
||||
var color = $(this).css('background-color');
|
||||
setColor(color);
|
||||
});
|
||||
|
||||
module.draw = true;
|
||||
var toggleDrawMode = function () {
|
||||
module.draw = !module.draw;
|
||||
canvas.isDrawingMode = module.draw;
|
||||
$toggle.text(module.draw ? Messages.canvas_disable : Messages.canvas_enable);
|
||||
if (module.draw) { $deleteButton.hide(); }
|
||||
else { $deleteButton.show(); }
|
||||
};
|
||||
$toggle.click(toggleDrawMode);
|
||||
|
||||
var deleteSelection = function () {
|
||||
if (canvas.getActiveObject()) {
|
||||
canvas.getActiveObject().remove();
|
||||
}
|
||||
if (canvas.getActiveGroup()) {
|
||||
canvas.getActiveGroup()._objects.forEach(function (el) {
|
||||
el.remove();
|
||||
});
|
||||
canvas.discardActiveGroup();
|
||||
}
|
||||
canvas.renderAll();
|
||||
module.onLocal();
|
||||
};
|
||||
$deleteButton.click(deleteSelection);
|
||||
$(window).on('keyup', function (e) {
|
||||
if (e.which === 46) { deleteSelection (); }
|
||||
});
|
||||
|
||||
var setEditable = function (bool) {
|
||||
if (readOnly && bool) { return; }
|
||||
if (bool) { $controls.css('display', 'flex'); }
|
||||
else { $controls.hide(); }
|
||||
|
||||
canvas.isDrawingMode = bool ? module.draw : false;
|
||||
if (!bool) {
|
||||
canvas.deactivateAll();
|
||||
canvas.renderAll();
|
||||
}
|
||||
canvas.forEachObject(function (object) {
|
||||
object.selectable = bool;
|
||||
});
|
||||
$canvasContainer.find('canvas').css('border-color', bool? 'black': 'red');
|
||||
};
|
||||
|
||||
var saveImage = module.saveImage = function () {
|
||||
var defaultName = "pretty-picture.png";
|
||||
Cryptpad.prompt(Messages.exportPrompt, defaultName, function (filename) {
|
||||
if (!(typeof(filename) === 'string' && filename)) { return; }
|
||||
$canvas[0].toBlob(function (blob) {
|
||||
saveAs(blob, filename);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.FM = Cryptpad.createFileManager({});
|
||||
module.upload = function (title) {
|
||||
var canvas = $canvas[0];
|
||||
var finish = function (thumb) {
|
||||
canvas.toBlob(function (blob) {
|
||||
blob.name = title;
|
||||
module.FM.handleFile(blob, void 0, thumb);
|
||||
});
|
||||
};
|
||||
|
||||
Thumb.fromCanvas(canvas, function (e, blob) {
|
||||
// carry on even if you can't get a thumbnail
|
||||
if (e) { console.error(e); }
|
||||
finish(blob);
|
||||
});
|
||||
};
|
||||
|
||||
var initializing = true;
|
||||
|
||||
var $bar = $('#toolbar');
|
||||
|
||||
var Title;
|
||||
var UserList;
|
||||
var Metadata;
|
||||
|
||||
var config = module.config = {
|
||||
initialState: '{}',
|
||||
websocketURL: Cryptpad.getWebsocketURL(),
|
||||
validateKey: secret.keys.validateKey,
|
||||
readOnly: readOnly,
|
||||
channel: secret.channel,
|
||||
crypto: Crypto.createEncryptor(secret.keys),
|
||||
transformFunction: JsonOT.transform,
|
||||
};
|
||||
|
||||
var addColorToPalette = function (color, i) {
|
||||
if (readOnly) { return; }
|
||||
var $color = $('<span>', {
|
||||
'class': 'palette-color',
|
||||
})
|
||||
.css({
|
||||
'background-color': color,
|
||||
})
|
||||
.click(function () {
|
||||
var c = Colors.rgb2hex($color.css('background-color'));
|
||||
setColor(c);
|
||||
})
|
||||
.on('dblclick', function (e) {
|
||||
e.preventDefault();
|
||||
pickColor(Colors.rgb2hex($color.css('background-color')), function (c) {
|
||||
$color.css({
|
||||
'background-color': c,
|
||||
});
|
||||
palette.splice(i, 1, c);
|
||||
config.onLocal();
|
||||
setColor(c);
|
||||
});
|
||||
});
|
||||
|
||||
$colors.append($color);
|
||||
};
|
||||
|
||||
var metadataCfg = {};
|
||||
var updatePalette = metadataCfg.updatePalette = function (newPalette) {
|
||||
palette = newPalette;
|
||||
$colors.html('<div class="hidden"> </div>');
|
||||
palette.forEach(addColorToPalette);
|
||||
};
|
||||
updatePalette(palette);
|
||||
|
||||
var makeColorButton = function ($container) {
|
||||
var $testColor = $('<input>', { type: 'color', value: '!' });
|
||||
|
||||
// if colors aren't supported, bail out
|
||||
if ($testColor.attr('type') !== 'color' ||
|
||||
$testColor.val() === '!') {
|
||||
console.log("Colors aren't supported. Aborting");
|
||||
return;
|
||||
}
|
||||
|
||||
var $color = module.$color = $('<button>', {
|
||||
id: "color-picker",
|
||||
title: Messages.canvas_chooseColor,
|
||||
'class': "fa fa-square rightside-button",
|
||||
})
|
||||
.on('click', function () {
|
||||
pickColor($color.css('background-color'), function (color) {
|
||||
setColor(color);
|
||||
});
|
||||
});
|
||||
|
||||
setColor('#000');
|
||||
|
||||
$container.append($color);
|
||||
|
||||
return $color;
|
||||
};
|
||||
|
||||
config.onInit = function (info) {
|
||||
UserList = Cryptpad.createUserList(info, config.onLocal, Cryptget, Cryptpad);
|
||||
|
||||
Title = Cryptpad.createTitle({}, config.onLocal, Cryptpad);
|
||||
|
||||
Metadata = Cryptpad.createMetadata(UserList, Title, metadataCfg, Cryptpad);
|
||||
|
||||
var configTb = {
|
||||
displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'],
|
||||
userList: UserList.getToolbarConfig(),
|
||||
share: {
|
||||
secret: secret,
|
||||
channel: info.channel
|
||||
},
|
||||
title: Title.getTitleConfig(),
|
||||
common: Cryptpad,
|
||||
readOnly: readOnly,
|
||||
ifrw: window,
|
||||
realtime: info.realtime,
|
||||
network: info.network,
|
||||
$container: $bar,
|
||||
$contentContainer: $('#canvas-area')
|
||||
};
|
||||
|
||||
toolbar = module.toolbar = Toolbar.create(configTb);
|
||||
|
||||
Title.setToolbar(toolbar);
|
||||
|
||||
var $rightside = toolbar.$rightside;
|
||||
|
||||
/* save as template */
|
||||
if (!Cryptpad.isTemplate(window.location.href)) {
|
||||
var templateObj = {
|
||||
rt: info.realtime,
|
||||
Crypt: Cryptget,
|
||||
getTitle: function () { return document.title; }
|
||||
};
|
||||
var $templateButton = Cryptpad.createButton('template', true, templateObj);
|
||||
$rightside.append($templateButton);
|
||||
}
|
||||
|
||||
var $export = Cryptpad.createButton('export', true, {}, saveImage);
|
||||
$rightside.append($export);
|
||||
|
||||
Cryptpad.createButton('savetodrive', true, {}, function () {})
|
||||
.click(function () {
|
||||
Cryptpad.prompt(Messages.exportPrompt, document.title + '.png',
|
||||
function (name) {
|
||||
if (name === null || !name.trim()) { return; }
|
||||
module.upload(name);
|
||||
});
|
||||
}).appendTo($rightside);
|
||||
|
||||
var $forget = Cryptpad.createButton('forget', true, {}, function (err) {
|
||||
if (err) { return; }
|
||||
setEditable(false);
|
||||
toolbar.failed();
|
||||
});
|
||||
$rightside.append($forget);
|
||||
|
||||
var editHash;
|
||||
|
||||
if (!readOnly) {
|
||||
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
|
||||
makeColorButton($rightside);
|
||||
}
|
||||
if (!readOnly) { Cryptpad.replaceHash(editHash); }
|
||||
};
|
||||
|
||||
// used for debugging, feel free to remove
|
||||
var Catch = function (f) {
|
||||
return function () {
|
||||
try {
|
||||
f();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var onRemote = config.onRemote = Catch(function () {
|
||||
if (initializing) { return; }
|
||||
var userDoc = module.realtime.getUserDoc();
|
||||
|
||||
Metadata.update(userDoc);
|
||||
var json = JSON.parse(userDoc);
|
||||
var remoteDoc = json.content;
|
||||
|
||||
// TODO update palette if it has changed
|
||||
|
||||
canvas.loadFromJSON(remoteDoc);
|
||||
canvas.renderAll();
|
||||
|
||||
var content = canvas.toDatalessJSON();
|
||||
if (content !== remoteDoc) { Cryptpad.notify(); }
|
||||
if (readOnly) { setEditable(false); }
|
||||
});
|
||||
setEditable(false);
|
||||
|
||||
var stringifyInner = function (textValue) {
|
||||
var obj = {
|
||||
content: textValue,
|
||||
metadata: {
|
||||
users: UserList.userData,
|
||||
palette: palette,
|
||||
defaultTitle: Title.defaultTitle,
|
||||
type: 'whiteboard',
|
||||
}
|
||||
};
|
||||
if (!initializing) {
|
||||
obj.metadata.title = Title.title;
|
||||
}
|
||||
// stringify the json and send it into chainpad
|
||||
return JSONSortify(obj);
|
||||
};
|
||||
|
||||
|
||||
var onLocal = module.onLocal = config.onLocal = Catch(function () {
|
||||
if (initializing) { return; }
|
||||
if (readOnly) { return; }
|
||||
|
||||
var content = stringifyInner(canvas.toDatalessJSON());
|
||||
|
||||
module.patchText(content);
|
||||
});
|
||||
|
||||
config.onReady = function (info) {
|
||||
var realtime = module.realtime = info.realtime;
|
||||
module.patchText = TextPatcher.create({
|
||||
realtime: realtime
|
||||
});
|
||||
|
||||
var isNew = false;
|
||||
var userDoc = module.realtime.getUserDoc();
|
||||
if (userDoc === "" || userDoc === "{}") { isNew = true; }
|
||||
else {
|
||||
var hjson = JSON.parse(userDoc);
|
||||
if (typeof(hjson) !== 'object' || Array.isArray(hjson) ||
|
||||
(typeof(hjson.type) !== 'undefined' && hjson.type !== 'whiteboard')) {
|
||||
Cryptpad.errorLoadingScreen(Messages.typeError);
|
||||
throw new Error(Messages.typeError);
|
||||
}
|
||||
}
|
||||
|
||||
Cryptpad.removeLoadingScreen();
|
||||
setEditable(true);
|
||||
initializing = false;
|
||||
onRemote();
|
||||
|
||||
/* TODO: restore palette from metadata.palette */
|
||||
|
||||
if (readOnly) { return; }
|
||||
UserList.getLastName(toolbar.$userNameButton, isNew);
|
||||
};
|
||||
|
||||
config.onAbort = function () {
|
||||
setEditable(false);
|
||||
toolbar.failed();
|
||||
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
|
||||
};
|
||||
|
||||
// TODO onConnectionStateChange
|
||||
config.onConnectionChange = function (info) {
|
||||
setEditable(info.state);
|
||||
toolbar.failed();
|
||||
if (info.state) {
|
||||
initializing = true;
|
||||
toolbar.reconnecting(info.myId);
|
||||
Cryptpad.findOKButton().click();
|
||||
} else {
|
||||
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
|
||||
}
|
||||
};
|
||||
|
||||
module.rt = Realtime.start(config);
|
||||
|
||||
canvas.on('mouse:up', onLocal);
|
||||
|
||||
$('#clear').on('click', function () {
|
||||
canvas.clear();
|
||||
onLocal();
|
||||
});
|
||||
|
||||
$('#save').on('click', function () {
|
||||
saveImage();
|
||||
});
|
||||
};
|
||||
|
||||
Cryptpad.ready(function () {
|
||||
andThen();
|
||||
Cryptpad.reportAppUsage();
|
||||
});
|
||||
Cryptpad.onError(function (info) {
|
||||
if (info) {
|
||||
onConnectError();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@ -0,0 +1,60 @@
|
||||
CKEDITOR.dialog.add('mediatag', function (editor) {
|
||||
var Messages = editor.plugins.mediatag.translations;
|
||||
return {
|
||||
title: Messages.title,
|
||||
minWidth: 400,
|
||||
minHeight: 200,
|
||||
contents: [
|
||||
{
|
||||
id: 'tab-basic',
|
||||
label: Messages.title,
|
||||
elements: [
|
||||
{
|
||||
type: 'text',
|
||||
id: 'width',
|
||||
label: Messages.width,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
id: 'height',
|
||||
label: Messages.height,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
onShow: function () {
|
||||
var el = editor.plugins.mediatag.clicked;
|
||||
var rect = el.getClientRect();
|
||||
var dialog = this.parts.contents.$;
|
||||
var inputs = dialog.querySelectorAll('input');
|
||||
var wInput = inputs[0];
|
||||
var hInput = inputs[1];
|
||||
wInput.value = Math.round(rect.width);
|
||||
hInput.value = Math.round(rect.height);
|
||||
},
|
||||
onOk: function() {
|
||||
var dialog = this;
|
||||
var el = editor.plugins.mediatag.clicked;
|
||||
var dialog = this.parts.contents.$;
|
||||
var inputs = dialog.querySelectorAll('input');
|
||||
var wInput = inputs[0];
|
||||
var hInput = inputs[1];
|
||||
|
||||
window.setTimeout(function () {
|
||||
if (wInput.value === "") {
|
||||
el.removeAttribute('width');
|
||||
el.removeStyle('width');
|
||||
} else {
|
||||
el.setSize('width', parseInt(wInput.value));
|
||||
}
|
||||
if (hInput.value === "") {
|
||||
el.removeAttribute('height');
|
||||
el.removeStyle('height');
|
||||
} else {
|
||||
el.setSize('height', parseInt(hInput.value));
|
||||
}
|
||||
editor.fire( 'change' );
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
@ -0,0 +1,181 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileOverview The Image plugin.
|
||||
*/
|
||||
|
||||
( function() {
|
||||
|
||||
CKEDITOR.plugins.add( 'mediatag', {
|
||||
requires: 'dialog',
|
||||
//icons: 'image',
|
||||
//hidpi: true,
|
||||
onLoad: function () {
|
||||
|
||||
CKEDITOR.addCss(
|
||||
'media-tag{' +
|
||||
'display:inline-block;' +
|
||||
'}' +
|
||||
'media-tag.selected{' +
|
||||
'border: 1px solid black;' +
|
||||
'}' +
|
||||
'media-tag iframe{' +
|
||||
'border: 6px solid #eee;' +
|
||||
'}' +
|
||||
'media-tag img{' +
|
||||
'vertical-align: top;' +
|
||||
'}' +
|
||||
'media-tag *{' +
|
||||
'width:100%; height:100%;' +
|
||||
'}');
|
||||
},
|
||||
init: function( editor ) {
|
||||
var pluginName = 'mediatag';
|
||||
|
||||
// Register the dialog.
|
||||
CKEDITOR.dialog.add( pluginName, this.path + 'mediatag-plugin-dialog.js' );
|
||||
|
||||
var allowed = 'media-tag[!data-crypto-key,!src,contenteditable,width,height]{border-style,border-width,float,height,margin,margin-bottom,margin-left,margin-right,margin-top,width}',
|
||||
required = 'media-tag[data-crypto-key,src]';
|
||||
|
||||
// Register the command.
|
||||
editor.addCommand( pluginName, new CKEDITOR.dialogCommand( pluginName, {
|
||||
allowedContent: allowed,
|
||||
requiredContent: required,
|
||||
contentTransformations: [
|
||||
[ 'media-tag{width}: sizeToStyle', 'media-tag[width]: sizeToAttribute' ],
|
||||
[ 'media-tag{float}: alignmentToStyle', 'media-tag[align]: alignmentToAttribute' ]
|
||||
]
|
||||
} ) );
|
||||
|
||||
var isMediaTag = function (el) {
|
||||
if (el.is('media-tag')) { return el; }
|
||||
var mt = el.getParents().slice().filter(function (p) {
|
||||
return p.is('media-tag');
|
||||
});
|
||||
if (mt.length !== 1) { return; }
|
||||
return mt[0];
|
||||
};
|
||||
editor.on('doubleclick', function (evt) {
|
||||
var element = evt.data.element;
|
||||
var mt = isMediaTag(element);
|
||||
if (mt && !element.data('cke-realelement')) {
|
||||
editor.plugins.mediatag.clicked = mt;
|
||||
evt.data.dialog = 'mediatag';
|
||||
}
|
||||
});
|
||||
|
||||
// If the "contextmenu" plugin is loaded, register the listeners.
|
||||
if (editor.contextMenu) {
|
||||
editor.contextMenu.addListener(function (element) {
|
||||
if (getSelectedMediatag(editor, element)) {
|
||||
return { mediatag: CKEDITOR.TRISTATE_OFF };
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
afterInit: function( editor ) {
|
||||
// Customize the behavior of the alignment commands. (http://dev.ckeditor.com/ticket/7430)
|
||||
setupAlignCommand('left');
|
||||
setupAlignCommand('right');
|
||||
setupAlignCommand('center');
|
||||
setupAlignCommand('block');
|
||||
|
||||
function setupAlignCommand (value) {
|
||||
var command = editor.getCommand('justify' + value);
|
||||
if (command) {
|
||||
if (value === 'left' || value === 'right') {
|
||||
command.on('exec', function (evt) {
|
||||
var img = getSelectedMediatag(editor), align;
|
||||
if (img) {
|
||||
align = getMediatagAlignment(img);
|
||||
if (align === value) {
|
||||
img.removeStyle('float');
|
||||
|
||||
// Remove "align" attribute when necessary.
|
||||
if (value === getMediatagAlignment(img))
|
||||
img.removeAttribute( 'align' );
|
||||
} else {
|
||||
img.setStyle( 'float', value );
|
||||
}
|
||||
|
||||
evt.cancel();
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
command.on('refresh', function (evt) {
|
||||
var img = getSelectedMediatag(editor), align;
|
||||
if (img) {
|
||||
align = getMediatagAlignment(img);
|
||||
|
||||
this.setState(
|
||||
(align === value) ? CKEDITOR.TRISTATE_ON : ( value === 'right' || value === 'left' ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
|
||||
|
||||
evt.cancel();
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
function getSelectedMediatag (editor, element) {
|
||||
if (!element) {
|
||||
var sel = editor.getSelection();
|
||||
element = sel.getSelectedElement();
|
||||
}
|
||||
|
||||
if (element && element.is('media-tag') && !element.data('cke-realelement')
|
||||
&& !element.isReadOnly()) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
function getMediatagAlignment (element) {
|
||||
var align = element.getStyle('float');
|
||||
|
||||
if (align === 'inherit' || align === 'none') {
|
||||
align = 0;
|
||||
}
|
||||
|
||||
if (!align) {
|
||||
align = element.getAttribute('align');
|
||||
}
|
||||
|
||||
return align;
|
||||
}
|
||||
} )();
|
||||
|
||||
/**
|
||||
* Determines whether dimension inputs should be automatically filled when the image URL changes in the Image plugin dialog window.
|
||||
*
|
||||
* config.image_prefillDimensions = false;
|
||||
*
|
||||
* @since 4.5
|
||||
* @cfg {Boolean} [image_prefillDimensions=true]
|
||||
* @member CKEDITOR.config
|
||||
*/
|
||||
|
||||
/**
|
||||
* Whether to remove links when emptying the link URL field in the Image dialog window.
|
||||
*
|
||||
* config.image_removeLinkByEmptyURL = false;
|
||||
*
|
||||
* @cfg {Boolean} [image_removeLinkByEmptyURL=true]
|
||||
* @member CKEDITOR.config
|
||||
*/
|
||||
CKEDITOR.config.mediatag_removeLinkByEmptyURL = true;
|
||||
|
||||
/**
|
||||
* Padding text to set off the image in the preview area.
|
||||
*
|
||||
* config.image_previewText = CKEDITOR.tools.repeat( '___ ', 100 );
|
||||
*
|
||||
* @cfg {String} [image_previewText='Lorem ipsum dolor...' (placeholder text)]
|
||||
* @member CKEDITOR.config
|
||||
*/
|
||||
|
@ -0,0 +1,144 @@
|
||||
@import (once) "../../customize/src/less2/include/browser.less";
|
||||
@import (once) "../../customize/src/less2/include/toolbar.less";
|
||||
@import (once) "../../customize/src/less2/include/markdown.less";
|
||||
@import (once) '../../customize/src/less2/include/fileupload.less';
|
||||
@import (once) '../../customize/src/less2/include/alertify.less';
|
||||
@import (once) '../../customize/src/less2/include/tools.less';
|
||||
|
||||
.toolbar_main();
|
||||
.fileupload_main();
|
||||
.alertify_main();
|
||||
|
||||
// body
|
||||
&.cp-app-whiteboard {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
|
||||
.middle () {
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// created in the html
|
||||
#cp-app-whiteboard-canvas-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
// created by fabricjs. styled so defaults don't break anything
|
||||
.cp-app-whiteboard-canvas-container {
|
||||
margin: auto;
|
||||
background: white;
|
||||
& > canvas {
|
||||
border: 1px solid black;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-app-whiteboard-unselectable {
|
||||
.tools_unselectable();
|
||||
}
|
||||
|
||||
// contains user tools
|
||||
#cp-app-whiteboard-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
position: relative;
|
||||
border-top: 1px solid black;
|
||||
background: white;
|
||||
|
||||
padding: 1em;
|
||||
|
||||
& > * + * {
|
||||
margin: 0;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
#cp-app-whiteboard-width, #cp-app-whiteboard-opacity {
|
||||
.middle;
|
||||
}
|
||||
#cp-app-whiteboard-clear, #cp-app-whiteboard-delete, #cp-app-whiteboard-toggledraw {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.cp-app-whiteboard-selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9001;
|
||||
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.cp-app-whiteboard-range-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
input[type="range"] {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
& > span {
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
.cp-app-whiteboard-range-group:first-of-type {
|
||||
margin-left: 2em;
|
||||
}
|
||||
.cp-app-whiteboard-range-group:last-of-type {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Colors */
|
||||
#cp-app-whiteboard-colors {
|
||||
.middle;
|
||||
z-index: 100;
|
||||
background: white;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: 1em;
|
||||
|
||||
span.cp-app-whiteboard-palette-color {
|
||||
height: 4vw;
|
||||
width: 4vw;
|
||||
display: block;
|
||||
margin: 5px;
|
||||
border: 1px solid black;
|
||||
vertical-align: top;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.1s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// used in the toolbar if supported
|
||||
#cp-app-whiteboard-color-picker {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// input[type=color] must exist in the dom to work correctly
|
||||
// styled so that they don't break layouts
|
||||
|
||||
#cp-app-whiteboard-pickers {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: -5;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="cp">
|
||||
<html>
|
||||
<head>
|
||||
<title>CryptPad</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
#sbox-iframe {
|
||||
position:fixed;
|
||||
top:0px;
|
||||
left:0px;
|
||||
bottom:0px;
|
||||
right:0px;
|
||||
width:100%;
|
||||
height:100%;
|
||||
border:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
#sbox-filePicker-iframe {
|
||||
position: fixed;
|
||||
top:0; left:0;
|
||||
bottom:0; right:0;
|
||||
width:100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<iframe id="sbox-iframe">
|
||||
|
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<script async data-bootload="/whiteboard/inner.js" data-main="/common/sframe-boot.js?ver=1.6" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
.loading-hidden { display: none; }
|
||||
#editor1 { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="cp-app-whiteboard">
|
||||
|
@ -1,511 +1,41 @@
|
||||
// Load #1, load as little as possible because we are in a race to get the loading screen up.
|
||||
define([
|
||||
'jquery',
|
||||
'/bower_components/nthen/index.js',
|
||||
'/api/config',
|
||||
'/bower_components/chainpad-netflux/chainpad-netflux.js',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/common/toolbar2.js',
|
||||
'/bower_components/textpatcher/TextPatcher.amd.js',
|
||||
'json.sortify',
|
||||
'/bower_components/chainpad-json-validator/json-ot.js',
|
||||
'/common/cryptpad-common.js',
|
||||
'/common/cryptget.js',
|
||||
'/whiteboard/colors.js',
|
||||
'/customize/application_config.js',
|
||||
'/common/common-thumbnail.js',
|
||||
'/bower_components/secure-fabric.js/dist/fabric.min.js',
|
||||
'/bower_components/file-saver/FileSaver.min.js',
|
||||
|
||||
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
|
||||
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
|
||||
'less!/customize/src/less/cryptpad.less',
|
||||
'less!/whiteboard/whiteboard.less',
|
||||
'less!/customize/src/less/toolbar.less',
|
||||
], function ($, Config, Realtime, Crypto, Toolbar, TextPatcher, JSONSortify, JsonOT, Cryptpad, Cryptget, Colors, AppConfig, Thumb) {
|
||||
var saveAs = window.saveAs;
|
||||
var Messages = Cryptpad.Messages;
|
||||
|
||||
var module = window.APP = { $:$ };
|
||||
var Fabric = module.Fabric = window.fabric;
|
||||
|
||||
$(function () {
|
||||
Cryptpad.addLoadingScreen();
|
||||
var onConnectError = function () {
|
||||
Cryptpad.errorLoadingScreen(Messages.websocketError);
|
||||
};
|
||||
var toolbar;
|
||||
|
||||
var secret = Cryptpad.getSecrets();
|
||||
var readOnly = secret.keys && !secret.keys.editKeyStr;
|
||||
if (!secret.keys) {
|
||||
secret.keys = secret.key;
|
||||
}
|
||||
|
||||
var andThen = function () {
|
||||
/* Initialize Fabric */
|
||||
var canvas = module.canvas = new Fabric.Canvas('canvas');
|
||||
var $canvas = $('canvas');
|
||||
var $controls = $('#controls');
|
||||
var $canvasContainer = $('canvas').parents('.canvas-container');
|
||||
var $pickers = $('#pickers');
|
||||
var $colors = $('#colors');
|
||||
var $cursors = $('#cursors');
|
||||
var $deleteButton = $('#delete');
|
||||
|
||||
var brush = {
|
||||
color: '#000000',
|
||||
opacity: 1
|
||||
};
|
||||
|
||||
var $toggle = $('#toggleDraw');
|
||||
var $width = $('#width');
|
||||
var $widthLabel = $('label[for="width"]');
|
||||
var $opacity = $('#opacity');
|
||||
var $opacityLabel = $('label[for="opacity"]');
|
||||
window.canvas = canvas;
|
||||
var createCursor = function () {
|
||||
var w = canvas.freeDrawingBrush.width;
|
||||
var c = canvas.freeDrawingBrush.color;
|
||||
var size = w > 30 ? w+2 : w+32;
|
||||
$cursors.html('<canvas width="'+size+'" height="'+size+'"></canvas>');
|
||||
var $ccanvas = $cursors.find('canvas');
|
||||
var ccanvas = $ccanvas[0];
|
||||
|
||||
var ctx = ccanvas.getContext('2d');
|
||||
var centerX = size / 2;
|
||||
var centerY = size / 2;
|
||||
var radius = w/2;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
|
||||
ctx.fillStyle = c;
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = brush.color;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(size/2, 0); ctx.lineTo(size/2, 10);
|
||||
ctx.moveTo(size/2, size); ctx.lineTo(size/2, size-10);
|
||||
ctx.moveTo(0, size/2); ctx.lineTo(10, size/2);
|
||||
ctx.moveTo(size, size/2); ctx.lineTo(size-10, size/2);
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.stroke();
|
||||
|
||||
var img = ccanvas.toDataURL("image/png");
|
||||
$controls.find('.selected > img').attr('src', img);
|
||||
canvas.freeDrawingCursor = 'url('+img+') '+size/2+' '+size/2+', crosshair';
|
||||
};
|
||||
|
||||
var updateBrushWidth = function () {
|
||||
var val = $width.val();
|
||||
canvas.freeDrawingBrush.width = Number(val);
|
||||
$widthLabel.text(Cryptpad.Messages._getKey("canvas_widthLabel", [val]));
|
||||
$('#width-val').text(val + 'px');
|
||||
createCursor();
|
||||
};
|
||||
updateBrushWidth();
|
||||
|
||||
$width.on('change', updateBrushWidth);
|
||||
|
||||
var updateBrushOpacity = function () {
|
||||
var val = $opacity.val();
|
||||
brush.opacity = Number(val);
|
||||
canvas.freeDrawingBrush.color = Colors.hex2rgba(brush.color, brush.opacity);
|
||||
$opacityLabel.text(Cryptpad.Messages._getKey("canvas_opacityLabel", [val]));
|
||||
$('#opacity-val').text((Number(val) * 100) + '%');
|
||||
createCursor();
|
||||
};
|
||||
updateBrushOpacity();
|
||||
|
||||
$opacity.on('change', updateBrushOpacity);
|
||||
|
||||
var pickColor = function (current, cb) {
|
||||
var $picker = $('<input>', {
|
||||
type: 'color',
|
||||
value: '#FFFFFF',
|
||||
})
|
||||
// TODO confirm that this is safe to remove
|
||||
//.css({ visibility: 'hidden' })
|
||||
.on('change', function () {
|
||||
var color = this.value;
|
||||
cb(color);
|
||||
}).appendTo($pickers);
|
||||
setTimeout(function () {
|
||||
$picker.val(current);
|
||||
$picker.click();
|
||||
});
|
||||
};
|
||||
|
||||
var setColor = function (c) {
|
||||
c = Colors.rgb2hex(c);
|
||||
brush.color = c;
|
||||
canvas.freeDrawingBrush.color = Colors.hex2rgba(brush.color, brush.opacity);
|
||||
module.$color.css({
|
||||
'color': c,
|
||||
});
|
||||
createCursor();
|
||||
};
|
||||
|
||||
|
||||
var palette = AppConfig.whiteboardPalette || [
|
||||
'red', 'blue', 'green', 'white', 'black', 'purple',
|
||||
'gray', 'beige', 'brown', 'cyan', 'darkcyan', 'gold', 'yellow', 'pink'
|
||||
];
|
||||
|
||||
$('.palette-color').on('click', function () {
|
||||
var color = $(this).css('background-color');
|
||||
setColor(color);
|
||||
});
|
||||
|
||||
module.draw = true;
|
||||
var toggleDrawMode = function () {
|
||||
module.draw = !module.draw;
|
||||
canvas.isDrawingMode = module.draw;
|
||||
$toggle.text(module.draw ? Messages.canvas_disable : Messages.canvas_enable);
|
||||
if (module.draw) { $deleteButton.hide(); }
|
||||
else { $deleteButton.show(); }
|
||||
};
|
||||
$toggle.click(toggleDrawMode);
|
||||
|
||||
var deleteSelection = function () {
|
||||
if (canvas.getActiveObject()) {
|
||||
canvas.getActiveObject().remove();
|
||||
}
|
||||
if (canvas.getActiveGroup()) {
|
||||
canvas.getActiveGroup()._objects.forEach(function (el) {
|
||||
el.remove();
|
||||
});
|
||||
canvas.discardActiveGroup();
|
||||
}
|
||||
canvas.renderAll();
|
||||
module.onLocal();
|
||||
};
|
||||
$deleteButton.click(deleteSelection);
|
||||
$(window).on('keyup', function (e) {
|
||||
if (e.which === 46) { deleteSelection (); }
|
||||
});
|
||||
|
||||
var setEditable = function (bool) {
|
||||
if (readOnly && bool) { return; }
|
||||
if (bool) { $controls.css('display', 'flex'); }
|
||||
else { $controls.hide(); }
|
||||
|
||||
canvas.isDrawingMode = bool ? module.draw : false;
|
||||
if (!bool) {
|
||||
canvas.deactivateAll();
|
||||
canvas.renderAll();
|
||||
}
|
||||
canvas.forEachObject(function (object) {
|
||||
object.selectable = bool;
|
||||
});
|
||||
$canvasContainer.find('canvas').css('border-color', bool? 'black': 'red');
|
||||
};
|
||||
|
||||
var saveImage = module.saveImage = function () {
|
||||
var defaultName = "pretty-picture.png";
|
||||
Cryptpad.prompt(Messages.exportPrompt, defaultName, function (filename) {
|
||||
if (!(typeof(filename) === 'string' && filename)) { return; }
|
||||
$canvas[0].toBlob(function (blob) {
|
||||
saveAs(blob, filename);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.FM = Cryptpad.createFileManager({});
|
||||
module.upload = function (title) {
|
||||
var canvas = $canvas[0];
|
||||
var finish = function (thumb) {
|
||||
canvas.toBlob(function (blob) {
|
||||
blob.name = title;
|
||||
module.FM.handleFile(blob, void 0, thumb);
|
||||
});
|
||||
};
|
||||
|
||||
Thumb.fromCanvas(canvas, function (e, blob) {
|
||||
// carry on even if you can't get a thumbnail
|
||||
if (e) { console.error(e); }
|
||||
finish(blob);
|
||||
});
|
||||
};
|
||||
|
||||
var initializing = true;
|
||||
|
||||
var $bar = $('#toolbar');
|
||||
|
||||
var Title;
|
||||
var UserList;
|
||||
var Metadata;
|
||||
|
||||
var config = module.config = {
|
||||
initialState: '{}',
|
||||
websocketURL: Cryptpad.getWebsocketURL(),
|
||||
validateKey: secret.keys.validateKey,
|
||||
readOnly: readOnly,
|
||||
channel: secret.channel,
|
||||
crypto: Crypto.createEncryptor(secret.keys),
|
||||
transformFunction: JsonOT.transform,
|
||||
};
|
||||
|
||||
var addColorToPalette = function (color, i) {
|
||||
if (readOnly) { return; }
|
||||
var $color = $('<span>', {
|
||||
'class': 'palette-color',
|
||||
})
|
||||
.css({
|
||||
'background-color': color,
|
||||
})
|
||||
.click(function () {
|
||||
var c = Colors.rgb2hex($color.css('background-color'));
|
||||
setColor(c);
|
||||
})
|
||||
.on('dblclick', function (e) {
|
||||
e.preventDefault();
|
||||
pickColor(Colors.rgb2hex($color.css('background-color')), function (c) {
|
||||
$color.css({
|
||||
'background-color': c,
|
||||
});
|
||||
palette.splice(i, 1, c);
|
||||
config.onLocal();
|
||||
setColor(c);
|
||||
});
|
||||
});
|
||||
|
||||
$colors.append($color);
|
||||
};
|
||||
|
||||
var metadataCfg = {};
|
||||
var updatePalette = metadataCfg.updatePalette = function (newPalette) {
|
||||
palette = newPalette;
|
||||
$colors.html('<div class="hidden"> </div>');
|
||||
palette.forEach(addColorToPalette);
|
||||
};
|
||||
updatePalette(palette);
|
||||
|
||||
var makeColorButton = function ($container) {
|
||||
var $testColor = $('<input>', { type: 'color', value: '!' });
|
||||
|
||||
// if colors aren't supported, bail out
|
||||
if ($testColor.attr('type') !== 'color' ||
|
||||
$testColor.val() === '!') {
|
||||
console.log("Colors aren't supported. Aborting");
|
||||
return;
|
||||
}
|
||||
|
||||
var $color = module.$color = $('<button>', {
|
||||
id: "color-picker",
|
||||
title: Messages.canvas_chooseColor,
|
||||
'class': "fa fa-square rightside-button",
|
||||
})
|
||||
.on('click', function () {
|
||||
pickColor($color.css('background-color'), function (color) {
|
||||
setColor(color);
|
||||
});
|
||||
});
|
||||
|
||||
setColor('#000');
|
||||
|
||||
$container.append($color);
|
||||
|
||||
return $color;
|
||||
};
|
||||
|
||||
config.onInit = function (info) {
|
||||
UserList = Cryptpad.createUserList(info, config.onLocal, Cryptget, Cryptpad);
|
||||
|
||||
Title = Cryptpad.createTitle({}, config.onLocal, Cryptpad);
|
||||
|
||||
Metadata = Cryptpad.createMetadata(UserList, Title, metadataCfg, Cryptpad);
|
||||
|
||||
var configTb = {
|
||||
displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'],
|
||||
userList: UserList.getToolbarConfig(),
|
||||
share: {
|
||||
secret: secret,
|
||||
channel: info.channel
|
||||
},
|
||||
title: Title.getTitleConfig(),
|
||||
common: Cryptpad,
|
||||
readOnly: readOnly,
|
||||
ifrw: window,
|
||||
realtime: info.realtime,
|
||||
network: info.network,
|
||||
$container: $bar,
|
||||
$contentContainer: $('#canvas-area')
|
||||
};
|
||||
|
||||
toolbar = module.toolbar = Toolbar.create(configTb);
|
||||
|
||||
Title.setToolbar(toolbar);
|
||||
|
||||
var $rightside = toolbar.$rightside;
|
||||
|
||||
/* save as template */
|
||||
if (!Cryptpad.isTemplate(window.location.href)) {
|
||||
var templateObj = {
|
||||
rt: info.realtime,
|
||||
Crypt: Cryptget,
|
||||
getTitle: function () { return document.title; }
|
||||
};
|
||||
var $templateButton = Cryptpad.createButton('template', true, templateObj);
|
||||
$rightside.append($templateButton);
|
||||
}
|
||||
|
||||
var $export = Cryptpad.createButton('export', true, {}, saveImage);
|
||||
$rightside.append($export);
|
||||
|
||||
Cryptpad.createButton('savetodrive', true, {}, function () {})
|
||||
.click(function () {
|
||||
Cryptpad.prompt(Messages.exportPrompt, document.title + '.png',
|
||||
function (name) {
|
||||
if (name === null || !name.trim()) { return; }
|
||||
module.upload(name);
|
||||
});
|
||||
}).appendTo($rightside);
|
||||
|
||||
var $forget = Cryptpad.createButton('forget', true, {}, function (err) {
|
||||
if (err) { return; }
|
||||
setEditable(false);
|
||||
toolbar.failed();
|
||||
});
|
||||
$rightside.append($forget);
|
||||
|
||||
var editHash;
|
||||
|
||||
if (!readOnly) {
|
||||
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
|
||||
makeColorButton($rightside);
|
||||
}
|
||||
if (!readOnly) { Cryptpad.replaceHash(editHash); }
|
||||
};
|
||||
|
||||
// used for debugging, feel free to remove
|
||||
var Catch = function (f) {
|
||||
return function () {
|
||||
try {
|
||||
f();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var onRemote = config.onRemote = Catch(function () {
|
||||
if (initializing) { return; }
|
||||
var userDoc = module.realtime.getUserDoc();
|
||||
|
||||
Metadata.update(userDoc);
|
||||
var json = JSON.parse(userDoc);
|
||||
var remoteDoc = json.content;
|
||||
|
||||
// TODO update palette if it has changed
|
||||
|
||||
canvas.loadFromJSON(remoteDoc);
|
||||
canvas.renderAll();
|
||||
|
||||
var content = canvas.toDatalessJSON();
|
||||
if (content !== remoteDoc) { Cryptpad.notify(); }
|
||||
if (readOnly) { setEditable(false); }
|
||||
});
|
||||
setEditable(false);
|
||||
|
||||
var stringifyInner = function (textValue) {
|
||||
var obj = {
|
||||
content: textValue,
|
||||
metadata: {
|
||||
users: UserList.userData,
|
||||
palette: palette,
|
||||
defaultTitle: Title.defaultTitle,
|
||||
type: 'whiteboard',
|
||||
}
|
||||
};
|
||||
if (!initializing) {
|
||||
obj.metadata.title = Title.title;
|
||||
}
|
||||
// stringify the json and send it into chainpad
|
||||
return JSONSortify(obj);
|
||||
};
|
||||
|
||||
|
||||
var onLocal = module.onLocal = config.onLocal = Catch(function () {
|
||||
if (initializing) { return; }
|
||||
if (readOnly) { return; }
|
||||
|
||||
var content = stringifyInner(canvas.toDatalessJSON());
|
||||
|
||||
module.patchText(content);
|
||||
});
|
||||
|
||||
config.onReady = function (info) {
|
||||
var realtime = module.realtime = info.realtime;
|
||||
module.patchText = TextPatcher.create({
|
||||
realtime: realtime
|
||||
});
|
||||
|
||||
var isNew = false;
|
||||
var userDoc = module.realtime.getUserDoc();
|
||||
if (userDoc === "" || userDoc === "{}") { isNew = true; }
|
||||
else {
|
||||
var hjson = JSON.parse(userDoc);
|
||||
if (typeof(hjson) !== 'object' || Array.isArray(hjson) ||
|
||||
(typeof(hjson.type) !== 'undefined' && hjson.type !== 'whiteboard')) {
|
||||
Cryptpad.errorLoadingScreen(Messages.typeError);
|
||||
throw new Error(Messages.typeError);
|
||||
}
|
||||
}
|
||||
|
||||
Cryptpad.removeLoadingScreen();
|
||||
setEditable(true);
|
||||
initializing = false;
|
||||
onRemote();
|
||||
|
||||
/* TODO: restore palette from metadata.palette */
|
||||
|
||||
if (readOnly) { return; }
|
||||
UserList.getLastName(toolbar.$userNameButton, isNew);
|
||||
};
|
||||
|
||||
config.onAbort = function () {
|
||||
setEditable(false);
|
||||
toolbar.failed();
|
||||
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
|
||||
};
|
||||
|
||||
// TODO onConnectionStateChange
|
||||
config.onConnectionChange = function (info) {
|
||||
setEditable(info.state);
|
||||
toolbar.failed();
|
||||
if (info.state) {
|
||||
initializing = true;
|
||||
toolbar.reconnecting(info.myId);
|
||||
Cryptpad.findOKButton().click();
|
||||
} else {
|
||||
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
|
||||
}
|
||||
};
|
||||
|
||||
module.rt = Realtime.start(config);
|
||||
|
||||
canvas.on('mouse:up', onLocal);
|
||||
|
||||
$('#clear').on('click', function () {
|
||||
canvas.clear();
|
||||
onLocal();
|
||||
});
|
||||
|
||||
$('#save').on('click', function () {
|
||||
saveImage();
|
||||
});
|
||||
};
|
||||
|
||||
Cryptpad.ready(function () {
|
||||
andThen();
|
||||
Cryptpad.reportAppUsage();
|
||||
});
|
||||
Cryptpad.onError(function (info) {
|
||||
if (info) {
|
||||
onConnectError();
|
||||
}
|
||||
});
|
||||
|
||||
'jquery',
|
||||
'/common/requireconfig.js',
|
||||
'/common/sframe-common-outer.js'
|
||||
], function (nThen, ApiConfig, $, RequireConfig, SFCommonO) {
|
||||
var requireConfig = RequireConfig();
|
||||
|
||||
// Loaded in load #2
|
||||
nThen(function (waitFor) {
|
||||
$(waitFor());
|
||||
}).nThen(function (waitFor) {
|
||||
var req = {
|
||||
cfg: requireConfig,
|
||||
req: [ '/common/loading.js' ],
|
||||
pfx: window.location.origin
|
||||
};
|
||||
window.rc = requireConfig;
|
||||
window.apiconf = ApiConfig;
|
||||
$('#sbox-iframe').attr('src',
|
||||
ApiConfig.httpSafeOrigin + '/whiteboard/inner.html?' + requireConfig.urlArgs +
|
||||
'#' + encodeURIComponent(JSON.stringify(req)));
|
||||
|
||||
// This is a cheap trick to avoid loading sframe-channel in parallel with the
|
||||
// loading screen setup.
|
||||
var done = waitFor();
|
||||
var onMsg = function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
if (data.q !== 'READY') { return; }
|
||||
window.removeEventListener('message', onMsg);
|
||||
var _done = done;
|
||||
done = function () { };
|
||||
_done();
|
||||
};
|
||||
window.addEventListener('message', onMsg);
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
SFCommonO.start();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue