Merge branch 'production'

pull/1/head
ansuz 9 years ago
commit 491c3644ec

@ -1,6 +1,6 @@
node_modules/
www/bower_components/
www/code/codemirror-5.7/
www/code/codemirror*
www/code/mode/
www/code/codemirror.js
www/pad/rangy.js
@ -12,3 +12,14 @@ storage/kad.js
www/common/otaml.js
www/common/diffDOM.js
www/common/netflux.js
www/padrtc
www/common/netflux-client.js
www/common/es6-promise.min.js
www/_pad
NetFluxWebsocketSrv.js
NetFluxWebsocketServer.js
WebRTCSrv.js
www/assert/hyperscript.js

@ -98,9 +98,8 @@
<p>CryptPad is the <strong>zero knowledge</strong> realtime collaborative editor.
Encryption carried out in your web browser protects the data from the server, the cloud
and the NSA. This project uses the <a href="http://ckeditor.com/">CKEditor</a> Visual Editor
the <a href="https://github.com/xwiki-contrib/chainpad">ChainPad</a> realtime engine and now
<a href="http://visop-dev.com/Project+jQuery.sheet">jQuery.sheet</a> for realtime spreadsheet
editing! The secret encryption key is stored in the URL
the <a href="https://github.com/xwiki-contrib/chainpad">ChainPad</a> realtime engine. The secret
encryption key is stored in the URL
<a href="https://en.wikipedia.org/wiki/Fragment_identifier">fragment identifier</a> which is
never sent to the server but is available to javascript so by sharing the URL, you give
authorization to others who want to participate.</p>

@ -0,0 +1,124 @@
define([], function () {
// this makes recursing a lot simpler
var isArray = function (A) {
return Object.prototype.toString.call(A)==='[object Array]';
};
var parseStyle = function(el){
var style = el.style;
var output = {};
for (var i = 0; i < style.length; ++i) {
var item = style.item(i);
output[item] = style[item];
}
return output;
};
var callOnHyperJSON = function (hj, cb) {
var children;
if (hj && hj[2]) {
children = hj[2].map(function (child) {
if (isArray(child)) {
// if the child is an array, recurse
return callOnHyperJSON(child, cb);
} else if (typeof (child) === 'string') {
// string nodes have leading and trailing quotes
return child.replace(/(^"|"$)/g,"");
} else {
// the above branches should cover all methods
// if we hit this, there is a problem
throw new Error();
}
});
} else {
children = [];
}
// this should return the top level element of your new DOM
return cb(hj[0], hj[1], children);
};
var classify = function (token) {
return '.' + token.trim();
};
var isValidClass = function (x) {
if (x && /\S/.test(x)) {
return true;
}
};
var isTruthy = function (x) {
return x;
};
var DOM2HyperJSON = function(el, predicate, filter){
if(!el.tagName && el.nodeType === Node.TEXT_NODE){
return el.textContent;
}
if(!el.attributes){
return;
}
if (predicate) {
if (!predicate(el)) {
// shortcircuit
return;
}
}
var attributes = {};
var i = 0;
for(;i < el.attributes.length; i++){
var attr = el.attributes[i];
if(attr.name && attr.value){
if(attr.name === "style"){
attributes.style = parseStyle(el);
}
else{
attributes[attr.name] = attr.value;
}
}
}
// this should never be longer than three elements
var result = [];
// get the element type, id, and classes of the element
// and push them to the result array
var sel = el.tagName;
if(attributes.id){
// we don't have to do much to validate IDs because the browser
// will only permit one id to exist
// unless we come across a strange browser in the wild
sel = sel +'#'+ attributes.id;
delete attributes.id;
}
result.push(sel);
// second element of the array is the element attributes
result.push(attributes);
// third element of the array is an array of child nodes
var children = [];
// js hint complains if we use 'var' here
i = 0;
for(; i < el.childNodes.length; i++){
children.push(DOM2HyperJSON(el.childNodes[i], predicate, filter));
}
result.push(children.filter(isTruthy));
if (filter) {
return filter(result);
} else {
return result;
}
};
return {
fromDOM: DOM2HyperJSON,
callOn: callOnHyperJSON
};
});

@ -0,0 +1,400 @@
define([], function () {
var Hyperscript;
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var split = require('browser-split')
var ClassList = require('class-list')
require('html-element')
function context () {
var cleanupFuncs = []
function h() {
var args = [].slice.call(arguments), e = null
function item (l) {
var r
function parseClass (string) {
// Our minimal parser doesnt understand escaping CSS special
// characters like `#`. Dont use them. More reading:
// https://mathiasbynens.be/notes/css-escapes .
var m = split(string, /([\.#]?[^\s#.]+)/)
if(/^\.|#/.test(m[1]))
e = document.createElement('div')
forEach(m, function (v) {
var s = v.substring(1,v.length)
if(!v) return
if(!e)
e = document.createElement(v)
else if (v[0] === '.')
ClassList(e).add(s)
else if (v[0] === '#')
e.setAttribute('id', s)
})
}
if(l == null)
;
else if('string' === typeof l) {
if(!e)
parseClass(l)
else
e.appendChild(r = document.createTextNode(l))
}
else if('number' === typeof l
|| 'boolean' === typeof l
|| l instanceof Date
|| l instanceof RegExp ) {
e.appendChild(r = document.createTextNode(l.toString()))
}
//there might be a better way to handle this...
else if (isArray(l))
forEach(l, item)
else if(isNode(l))
e.appendChild(r = l)
else if(l instanceof Text)
e.appendChild(r = l)
else if ('object' === typeof l) {
for (var k in l) {
if('function' === typeof l[k]) {
if(/^on\w+/.test(k)) {
(function (k, l) { // capture k, l in the closure
if (e.addEventListener){
e.addEventListener(k.substring(2), l[k], false)
cleanupFuncs.push(function(){
e.removeEventListener(k.substring(2), l[k], false)
})
}else{
e.attachEvent(k, l[k])
cleanupFuncs.push(function(){
e.detachEvent(k, l[k])
})
}
})(k, l)
} else {
// observable
e[k] = l[k]()
cleanupFuncs.push(l[k](function (v) {
e[k] = v
}))
}
}
else if(k === 'style') {
if('string' === typeof l[k]) {
e.style.cssText = l[k]
}else{
for (var s in l[k]) (function(s, v) {
if('function' === typeof v) {
// observable
e.style.setProperty(s, v())
cleanupFuncs.push(v(function (val) {
e.style.setProperty(s, val)
}))
} else
e.style.setProperty(s, l[k][s])
})(s, l[k][s])
}
} else if (k.substr(0, 5) === "data-") {
e.setAttribute(k, l[k])
} else {
e.setAttribute(k, l[k])
if (e.getAttribute(k) !== l[k]) {
e[k] = l[k]
}
}
}
} else if ('function' === typeof l) {
//assume it's an observable!
var v = l()
e.appendChild(r = isNode(v) ? v : document.createTextNode(v))
cleanupFuncs.push(l(function (v) {
if(isNode(v) && r.parentElement)
r.parentElement.replaceChild(v, r), r = v
else
r.textContent = v
}))
}
return r
}
while(args.length)
item(args.shift())
return e
}
h.cleanup = function () {
for (var i = 0; i < cleanupFuncs.length; i++){
cleanupFuncs[i]()
}
cleanupFuncs.length = 0
}
return h
}
var h = module.exports = context()
h.context = context
Hyperscript = h;
function isNode (el) {
return el && el.nodeName && el.nodeType
}
function forEach (arr, fn) {
if (arr.forEach) return arr.forEach(fn)
for (var i = 0; i < arr.length; i++) fn(arr[i], i)
}
function isArray (arr) {
return Object.prototype.toString.call(arr) == '[object Array]'
}
},{"browser-split":2,"class-list":3,"html-element":6}],2:[function(require,module,exports){
/*!
* Cross-Browser Split 1.1.1
* Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
* Available under the MIT License
* ECMAScript compliant, uniform cross-browser split method
*/
/**
* Splits a string into an array of strings using a regex or string separator. Matches of the
* separator are not included in the result array. However, if `separator` is a regex that contains
* capturing groups, backreferences are spliced into the result each time `separator` is matched.
* Fixes browser bugs compared to the native `String.prototype.split` and can be used reliably
* cross-browser.
* @param {String} str String to split.
* @param {RegExp|String} separator Regex or string to use for separating the string.
* @param {Number} [limit] Maximum number of items to include in the result array.
* @returns {Array} Array of substrings.
* @example
*
* // Basic use
* split('a b c d', ' ');
* // -> ['a', 'b', 'c', 'd']
*
* // With limit
* split('a b c d', ' ', 2);
* // -> ['a', 'b']
*
* // Backreferences in result array
* split('..word1 word2..', /([a-z]+)(\d+)/i);
* // -> ['..', 'word', '1', ' ', 'word', '2', '..']
*/
module.exports = (function split(undef) {
var nativeSplit = String.prototype.split,
compliantExecNpcg = /()??/.exec("")[1] === undef,
// NPCG: nonparticipating capturing group
self;
self = function(str, separator, limit) {
// If `separator` is not a regex, use `nativeSplit`
if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
return nativeSplit.call(str, separator, limit);
}
var output = [],
flags = (separator.ignoreCase ? "i" : "") + (separator.multiline ? "m" : "") + (separator.extended ? "x" : "") + // Proposed for ES6
(separator.sticky ? "y" : ""),
// Firefox 3+
lastLastIndex = 0,
// Make `global` and avoid `lastIndex` issues by working with a copy
separator = new RegExp(separator.source, flags + "g"),
separator2, match, lastIndex, lastLength;
str += ""; // Type-convert
if (!compliantExecNpcg) {
// Doesn't need flags gy, but they don't hurt
separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
}
/* Values for `limit`, per the spec:
* If undefined: 4294967295 // Math.pow(2, 32) - 1
* If 0, Infinity, or NaN: 0
* If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
* If negative number: 4294967296 - Math.floor(Math.abs(limit))
* If other: Type-convert, then use the above rules
*/
limit = limit === undef ? -1 >>> 0 : // Math.pow(2, 32) - 1
limit >>> 0; // ToUint32(limit)
while (match = separator.exec(str)) {
// `separator.lastIndex` is not reliable cross-browser
lastIndex = match.index + match[0].length;
if (lastIndex > lastLastIndex) {
output.push(str.slice(lastLastIndex, match.index));
// Fix browsers whose `exec` methods don't consistently return `undefined` for
// nonparticipating capturing groups
if (!compliantExecNpcg && match.length > 1) {
match[0].replace(separator2, function() {
for (var i = 1; i < arguments.length - 2; i++) {
if (arguments[i] === undef) {
match[i] = undef;
}
}
});
}
if (match.length > 1 && match.index < str.length) {
Array.prototype.push.apply(output, match.slice(1));
}
lastLength = match[0].length;
lastLastIndex = lastIndex;
if (output.length >= limit) {
break;
}
}
if (separator.lastIndex === match.index) {
separator.lastIndex++; // Avoid an infinite loop
}
}
if (lastLastIndex === str.length) {
if (lastLength || !separator.test("")) {
output.push("");
}
} else {
output.push(str.slice(lastLastIndex));
}
return output.length > limit ? output.slice(0, limit) : output;
};
return self;
})();
},{}],3:[function(require,module,exports){
// contains, add, remove, toggle
var indexof = require('indexof')
module.exports = ClassList
function ClassList(elem) {
var cl = elem.classList
if (cl) {
return cl
}
var classList = {
add: add
, remove: remove
, contains: contains
, toggle: toggle
, toString: $toString
, length: 0
, item: item
}
return classList
function add(token) {
var list = getTokens()
if (indexof(list, token) > -1) {
return
}
list.push(token)
setTokens(list)
}
function remove(token) {
var list = getTokens()
, index = indexof(list, token)
if (index === -1) {
return
}
list.splice(index, 1)
setTokens(list)
}
function contains(token) {
return indexof(getTokens(), token) > -1
}
function toggle(token) {
if (contains(token)) {
remove(token)
return false
} else {
add(token)
return true
}
}
function $toString() {
return elem.className
}
function item(index) {
var tokens = getTokens()
return tokens[index] || null
}
function getTokens() {
var className = elem.className
return filter(className.split(" "), isTruthy)
}
function setTokens(list) {
var length = list.length
elem.className = list.join(" ")
classList.length = length
for (var i = 0; i < list.length; i++) {
classList[i] = list[i]
}
delete list[length]
}
}
function filter (arr, fn) {
var ret = []
for (var i = 0; i < arr.length; i++) {
if (fn(arr[i])) ret.push(arr[i])
}
return ret
}
function isTruthy(value) {
return !!value
}
},{"indexof":4}],4:[function(require,module,exports){
var indexOf = [].indexOf;
module.exports = function(arr, obj){
if (indexOf) return arr.indexOf(obj);
for (var i = 0; i < arr.length; ++i) {
if (arr[i] === obj) return i;
}
return -1;
};
},{}],5:[function(require,module,exports){
var h = require("./index.js");
module.exports = h;
/*
$(function () {
var newDoc = h('p',
h('ul', 'bang bang bang'.split(/\s/).map(function (word) {
return h('li', word);
}))
);
$('body').html(newDoc.outerHTML);
});
*/
},{"./index.js":1}],6:[function(require,module,exports){
},{}]},{},[5]);
return Hyperscript;
});

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script data-main="main" src="/bower_components/requirejs/require.js"></script>
</head>
<body>
<h1>Serialization tests</h1>
<h2>Test 1</h2>
<h3>class strings</h3>
<!-- put in weird HTML that might cause problems -->
<div id="target"><p class=" alice bob charlie has.dot" id="bang">pewpewpew</p></div>
<hr>
<h2>Test 2</h2>
<h3>XWiki Macros</h3>
<!-- Can we serialize XWiki Macros? -->
<div id="widget"><div data-cke-widget-id="0" tabindex="-1" data-cke-widget-wrapper="1" data-cke-filter="off" class="cke_widget_wrapper cke_widget_block" data-cke-display-name="macro:velocity" contenteditable="false"><div class="macro cke_widget_element" data-macro="startmacro:velocity|-||-|Here is a macro" data-cke-widget-data="%7B%22classes%22%3A%7B%22macro%22%3A1%7D%7D" data-cke-widget-upcasted="1" data-cke-widget-keep-attr="0" data-widget="xwiki-macro"><p>Here is a macro</p></div><span style='background: rgba(220, 220, 220, 0.5) url("/common/cryptofist.png") repeat scroll 0% 0%; top: -15px; left: 0px; display: block;' class="cke_reset cke_widget_drag_handler_container"><img title="Click and drag to move" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" data-cke-widget-drag-handler="1" class="cke_reset cke_widget_drag_handler" height="15" width="15"></span></div></div>
<hr>

@ -0,0 +1,50 @@
define([
'/bower_components/jquery/dist/jquery.min.js',
'/assert/hyperjson.js', // serializing classes as an attribute
'/assert/hyperscript.js', // using setAttribute
'/common/TextPatcher.js'
], function (jQuery, Hyperjson, Hyperscript, TextPatcher) {
var $ = window.jQuery;
window.Hyperjson = Hyperjson;
window.Hyperscript = Hyperscript;
window.TextPatcher = TextPatcher;
var assertions = 0;
var assert = function (test, msg) {
if (test()) {
assertions++;
} else {
throw new Error(msg || '');
}
};
var $body = $('body');
var roundTrip = function (target) {
assert(function () {
var hjson = Hyperjson.fromDOM(target);
var cloned = Hyperjson.callOn(hjson, Hyperscript);
var success = cloned.outerHTML === target.outerHTML;
if (!success) {
window.DEBUG = {
error: "Expected equality between A and B",
A: target.outerHTML,
B: cloned.outerHTML,
target: target,
diff: TextPatcher.diff(target.outerHTML, cloned.outerHTML)
};
console.log(JSON.stringify(window.DEBUG, null, 2));
}
return success;
}, "Round trip serialization introduced artifacts.");
};
roundTrip($('#target')[0]);
roundTrip($('#widget')[0]);
console.log("%s test%s passed", assertions, assertions === 1? '':'s');
});

@ -0,0 +1,278 @@
/*
* Copyright 2014 XWiki SAS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define([
'/common/messages.js',
'/bower_components/reconnectingWebsocket/reconnecting-websocket.js',
'/common/crypto.js',
'/common/TextPatcher.js',
'/common/chainpad.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Messages, ReconnectingWebSocket, Crypto, TextPatcher) {
var $ = window.jQuery;
var ChainPad = window.ChainPad;
var PARANOIA = true;
var module = { exports: {} };
/**
* If an error is encountered but it is recoverable, do not immediately fail
* but if it keeps firing errors over and over, do fail.
*/
var MAX_RECOVERABLE_ERRORS = 15;
var recoverableErrors = 0;
/** Maximum number of milliseconds of lag before we fail the connection. */
var MAX_LAG_BEFORE_DISCONNECT = 20000;
var debug = function (x) { console.log(x); };
var warn = function (x) { console.error(x); };
var verbose = function (x) { /*console.log(x);*/ };
var error = function (x) {
console.error(x);
recoverableErrors++;
if (recoverableErrors >= MAX_RECOVERABLE_ERRORS) {
window.alert("FAIL");
}
};
/* websocket stuff */
var isSocketDisconnected = function (socket, realtime) {
var sock = socket._socket;
return sock.readyState === sock.CLOSING
|| sock.readyState === sock.CLOSED
|| (realtime.getLag().waiting && realtime.getLag().lag > MAX_LAG_BEFORE_DISCONNECT);
};
// this differs from other functions with similar names in that
// you are expected to pass a socket into it.
var checkSocket = function (socket) {
if (isSocketDisconnected(socket, socket.realtime) &&
!socket.intentionallyClosing) {
return true;
} else {
return false;
}
};
// TODO before removing websocket implementation
// bind abort to onLeaving
var abort = function (socket, realtime) {
realtime.abort();
try { socket._socket.close(); } catch (e) { warn(e); }
};
var handleError = function (socket, realtime, err, docHTML, allMessages) {
// var internalError = createDebugInfo(err, realtime, docHTML, allMessages);
abort(socket, realtime);
};
var makeWebsocket = function (url) {
var socket = new ReconnectingWebSocket(url);
/* create a set of handlers to use instead of the native socket handler
these handlers will iterate over all of the functions pushed to the
arrays bearing their name.
The first such function to return `false` will prevent subsequent
functions from being executed. */
var out = {
onOpen: [], // takes care of launching the post-open logic
onClose: [], // takes care of cleanup
onError: [], // in case of error, socket will close, and fire this
onMessage: [], // used for the bulk of our logic
send: function (msg) { socket.send(msg); },
close: function () { socket.close(); },
_socket: socket
};
var mkHandler = function (name) {
return function (evt) {
for (var i = 0; i < out[name].length; i++) {
if (out[name][i](evt) === false) {
console.log(name +"Handler");
return;
}
}
};
};
// bind your new handlers to the important listeners on the socket
socket.onopen = mkHandler('onOpen');
socket.onclose = mkHandler('onClose');
socket.onerror = mkHandler('onError');
socket.onmessage = mkHandler('onMessage');
return out;
};
/* end websocket stuff */
var start = module.exports.start = function (config) {
//var textarea = config.textarea;
var websocketUrl = config.websocketURL;
var userName = config.userName;
var channel = config.channel;
var cryptKey = config.cryptKey;
var passwd = 'y';
var doc = config.doc || null;
// wrap up the reconnecting websocket with our additional stack logic
var socket = makeWebsocket(websocketUrl);
var allMessages = window.chainpad_allMessages = [];
var isErrorState = false;
var initializing = true;
var recoverableErrorCount = 0;
var toReturn = { socket: socket };
socket.onOpen.push(function (evt) {
var realtime = toReturn.realtime = socket.realtime =
// everybody has a username, and we assume they don't collide
// usernames are used to determine whether a message is remote
// or local in origin. This could mess with expected behaviour
// if someone spoofed.
ChainPad.create(userName,
passwd, // password, to be deprecated (maybe)
channel, // the channel we're to connect to
/* optional unless your application expects JSON
from getUserDoc */
config.initialState || '',
// transform function (optional), which handles conflicts
{ transformFunction: config.transformFunction });
var onEvent = toReturn.onEvent = function (newText) {
if (isErrorState || initializing) { return; }
// assert things here...
if (realtime.getUserDoc() !== newText) {
// this is a problem
warn("realtime.getUserDoc() !== newText");
}
};
// pass your shiny new realtime into initialization functions
if (config.onInit) {
// extend as you wish
config.onInit({
realtime: realtime
});
}
/* UI hints on userList changes are handled within the toolbar
so we don't actually need to do anything here except confirm
whether we've successfully joined the session, and call our
'onReady' function */
realtime.onUserListChange(function (userList) {
if (!initializing || userList.indexOf(userName) === -1) {
return;
}
// if we spot ourselves being added to the document, we'll switch
// 'initializing' off because it means we're fully synced.
initializing = false;
// execute an onReady callback if one was supplied
// pass an object so we can extend this later
if (config.onReady) {
// extend as you wish
config.onReady({
userList: userList,
realtime: realtime
});
}
});
// when a message is ready to send
// Don't confuse this onMessage with socket.onMessage
realtime.onMessage(function (message) {
if (isErrorState) { return; }
message = Crypto.encrypt(message, cryptKey);
try {
socket.send(message);
} catch (e) {
warn(e);
}
});
realtime.onPatch(function () {
if (config.onRemote) {
config.onRemote({
realtime: realtime
//realtime.getUserDoc()
});
}
});
// when you receive a message...
socket.onMessage.push(function (evt) {
verbose(evt.data);
if (isErrorState) { return; }
var message = Crypto.decrypt(evt.data, cryptKey);
verbose(message);
allMessages.push(message);
if (!initializing) {
if (toReturn.onLocal) {
toReturn.onLocal();
}
}
realtime.message(message);
});
// actual socket bindings
socket.onmessage = function (evt) {
for (var i = 0; i < socket.onMessage.length; i++) {
if (socket.onMessage[i](evt) === false) { return; }
}
};
socket.onclose = function (evt) {
for (var i = 0; i < socket.onMessage.length; i++) {
if (socket.onClose[i](evt) === false) { return; }
}
};
socket.onerror = warn;
var socketChecker = setInterval(function () {
if (checkSocket(socket)) {
warn("Socket disconnected!");
recoverableErrorCount += 1;
if (recoverableErrorCount >= MAX_RECOVERABLE_ERRORS) {
warn("Giving up!");
abort(socket, realtime);
if (config.onAbort) {
config.onAbort({
socket: socket
});
}
if (socketChecker) { clearInterval(socketChecker); }
}
} // it's working as expected, continue
}, 200);
toReturn.patchText = TextPatcher.create({
realtime: realtime,
logging: true
});
realtime.start();
debug('started');
});
return toReturn;
};
return module.exports;
});

@ -0,0 +1,121 @@
define(function () {
/* diff takes two strings, the old content, and the desired content
it returns the difference between these two strings in the form
of an 'Operation' (as defined in chainpad.js).
diff is purely functional.
*/
var diff = function (oldval, newval) {
// Strings are immutable and have reference equality. I think this test is O(1), so its worth doing.
if (oldval === newval) {
return;
}
var commonStart = 0;
while (oldval.charAt(commonStart) === newval.charAt(commonStart)) {
commonStart++;
}
var commonEnd = 0;
while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) &&
commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) {
commonEnd++;
}
var toRemove = 0;
var toInsert = '';
/* throw some assertions in here before dropping patches into the realtime */
if (oldval.length !== commonStart + commonEnd) {
toRemove = oldval.length - commonStart - commonEnd;
}
if (newval.length !== commonStart + commonEnd) {
toInsert = newval.slice(commonStart, newval.length - commonEnd);
}
return {
type: 'Operation',
offset: commonStart,
toInsert: toInsert,
toRemove: toRemove
};
};
/* patch accepts a realtime facade and an operation (which might be falsey)
it applies the operation to the realtime as components (remove/insert)
patch has no return value, and operates solely through side effects on
the realtime facade.
*/
var patch = function (ctx, op) {
if (!op) { return; }
if (op.toRemove) { ctx.remove(op.offset, op.toRemove); }
if (op.toInsert) { ctx.insert(op.offset, op.toInsert); }
};
/* log accepts a string and an operation, and prints an object to the console
the object will display the content which is to be removed, and the content
which will be inserted in its place.
log is useful for debugging, but can otherwise be disabled.
*/
var log = function (text, op) {
if (!op) { return; }
console.log({
insert: op.toInsert,
remove: text.slice(op.offset, op.offset + op.toRemove)
});
};
/* applyChange takes:
ctx: the context (aka the realtime)
oldval: the old value
newval: the new value
it performs a diff on the two values, and generates patches
which are then passed into `ctx.remove` and `ctx.insert`.
Due to its reliance on patch, applyChange has side effects on the supplied
realtime facade.
*/
var applyChange = function(ctx, oldval, newval, logging) {
var op = diff(oldval, newval);
if (logging) { log(oldval, op) }
patch(ctx, op);
};
var create = function(config) {
var ctx = config.realtime;
var logging = config.logging;
// initial state will always fail the !== check in genop.
// because nothing will equal this object
var content = {};
// *** remote -> local changes
ctx.onPatch(function(pos, length) {
content = ctx.getUserDoc();
});
// propogate()
return function (newContent) {
if (newContent !== content) {
applyChange(ctx, ctx.getUserDoc(), newContent, logging);
if (ctx.getUserDoc() !== newContent) {
console.log("Expected that: `ctx.getUserDoc() === newContent`!");
}
return true;
}
return false;
};
};
return {
create: create, // create a TextPatcher object
diff: diff, // diff two strings
patch: patch, // apply an operation to a chainpad's realtime facade
log: log, // print the components of an operation
applyChange: applyChange // a convenient wrapper around diff/log/patch
};
});

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

@ -220,10 +220,9 @@ var transform = Patch.transform = function (origToTransform, transformBy, doc, t
Common.assert(origToTransform.parentHash === transformBy.parentHash);
var resultOfTransformBy = apply(transformBy, doc);
toTransform = clone(origToTransform);
var toTransform = clone(origToTransform);
var text = doc;
for (var i = toTransform.operations.length-1; i >= 0; i--) {
text = Operation.apply(toTransform.operations[i], text);
for (var j = transformBy.operations.length-1; j >= 0; j--) {
toTransform.operations[i] = Operation.transform(text,
toTransform.operations[i],
@ -369,10 +368,10 @@ var random = Patch.random = function (doc, opCount) {
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var PARANOIA = module.exports.PARANOIA = false;
var PARANOIA = module.exports.PARANOIA = true;
/* throw errors over non-compliant messages which would otherwise be treated as invalid */
var TESTING = module.exports.TESTING = false;
var TESTING = module.exports.TESTING = true;
var assert = module.exports.assert = function (expr) {
if (!expr) { throw new Error("Failed assertion"); }
@ -833,7 +832,7 @@ var check = ChainPad.check = function(realtime) {
Common.assert(uiDoc === realtime.userInterfaceContent);
}
var doc = realtime.authDoc;
/*var doc = realtime.authDoc;
var patchMsg = realtime.best;
Common.assert(patchMsg.content.inverseOf.parentHash === realtime.uncommitted.parentHash);
var patches = [];
@ -845,7 +844,7 @@ var check = ChainPad.check = function(realtime) {
while ((patchMsg = patches.pop())) {
doc = Patch.apply(patchMsg.content, doc);
}
Common.assert(doc === realtime.authDoc);
Common.assert(doc === realtime.authDoc);*/
};
var doOperation = ChainPad.doOperation = function (realtime, op) {
@ -1443,7 +1442,13 @@ var rebase = Operation.rebase = function (oldOp, newOp) {
* @param transformBy an existing operation which also has the same base.
* @return toTransform *or* null if the result is a no-op.
*/
var transform0 = Operation.transform0 = function (text, toTransform, transformBy) {
var transform0 = Operation.transform0 = function (text, toTransformOrig, transformByOrig) {
// Cloning the original transformations makes this algorithm such that it
// **DOES NOT MUTATE ANYMORE**
var toTransform = Operation.clone(toTransformOrig);
var transformBy = Operation.clone(transformByOrig);
if (toTransform.offset > transformBy.offset) {
if (toTransform.offset > transformBy.offset + transformBy.toRemove) {
// simple rebase

@ -136,10 +136,9 @@ define([
verbose("cursor.update");
root = root || inner;
sel = sel || Rangy.getSelection(root);
// FIXME under what circumstances are no ranges found?
if (!sel.rangeCount) {
error('[cursor.update] no ranges found');
//return 'no ranges found';
return;
}
var range = sel.getRangeAt(0);
@ -374,6 +373,26 @@ define([
};
};
cursor.brFix = function () {
cursor.update();
var start = Range.start;
var end = Range.end;
if (!start.el) { return; }
if (start.el === end.el && start.offset === end.offset) {
if (start.el.tagName === 'BR') {
// get the parent element, which ought to be a P.
var P = start.el.parentNode;
[cursor.fixStart, cursor.fixEnd].forEach(function (f) {
f(P, 0);
});
cursor.fixSelection(cursor.makeSelection(), cursor.makeRange());
}
}
};
return cursor;
};
});

@ -1,5 +1,4 @@
define([], function () {
// this makes recursing a lot simpler
var isArray = function (A) {
return Object.prototype.toString.call(A)==='[object Array]';
@ -39,13 +38,34 @@ define([], function () {
return cb(hj[0], hj[1], children);
};
var DOM2HyperJSON = function(el){
var classify = function (token) {
return '.' + token.trim();
};
var isValidClass = function (x) {
if (x && /\S/.test(x)) {
return true;
}
};
var isTruthy = function (x) {
return x;
};
var DOM2HyperJSON = function(el, predicate, filter){
if(!el.tagName && el.nodeType === Node.TEXT_NODE){
return el.textContent;
}
if(!el.attributes){
return;
}
if (predicate) {
if (!predicate(el)) {
// shortcircuit
return;
}
}
var attributes = {};
var i = 0;
@ -69,11 +89,25 @@ define([], function () {
var sel = el.tagName;
if(attributes.id){
// we don't have to do much to validate IDs because the browser
// will only permit one id to exist
// unless we come across a strange browser in the wild
sel = sel +'#'+ attributes.id;
delete attributes.id;
}
if(attributes.class){
sel = sel +'.'+ attributes.class.replace(/ /g,".");
// actually parse out classes so that we produce a valid selector
// string. leading or trailing spaces would have caused it to choke
// these are really common in generated html
/* TODO this can be done with RegExps alone, and it will be faster
but this works and is a little less error prone, albeit slower
come back and speed it up when it comes time to optimize */
sel = sel + attributes.class
.split(/\s+/g)
.filter(isValidClass)
.map(classify)
.join('')
.replace(/\.\./g, '.');
delete attributes.class;
}
result.push(sel);
@ -87,11 +121,15 @@ define([], function () {
// js hint complains if we use 'var' here
i = 0;
for(; i < el.childNodes.length; i++){
children.push(DOM2HyperJSON(el.childNodes[i]));
children.push(DOM2HyperJSON(el.childNodes[i], predicate, filter));
}
result.push(children);
result.push(children.filter(isTruthy));
if (filter) {
return filter(result);
} else {
return result;
}
};
return {

@ -4,15 +4,59 @@ define([
var ChainPad = window.ChainPad;
var JsonOT = {};
/* FIXME
resultOp after transform0() might be null, in which case you should return null
because it is simply a transformation which yields a "do nothing" operation */
var validate = JsonOT.validate = function (text, toTransform, transformBy) {
var resultOp = ChainPad.Operation.transform0(text, toTransform, transformBy);
var text2 = ChainPad.Operation.apply(transformBy, text);
var text3 = ChainPad.Operation.apply(resultOp, text2);
var resultOp, text2, text3;
try {
// text = O (mutual common ancestor)
// toTransform = A (the first incoming operation)
// transformBy = B (the second incoming operation)
// threeway merge (0, A, B)
resultOp = ChainPad.Operation.transform0(text, toTransform, transformBy);
/* if after operational transform we find that no op is necessary
return null to ignore this patch */
if (!resultOp) { return null; }
text2 = ChainPad.Operation.apply(transformBy, text);
text3 = ChainPad.Operation.apply(resultOp, text2);
try {
JSON.parse(text3);
return resultOp;
} catch (e) {
console.log(e);
console.error(e);
var info = window.REALTIME_MODULE.ot_parseError = {
type: 'resultParseError',
resultOp: resultOp,
toTransform: toTransform,
transformBy: transformBy,
text1: text,
text2: text2,
text3: text3,
error: e
};
console.log('Debugging info available at `window.REALTIME_MODULE.ot_parseError`');
}
} catch (x) {
console.error(x);
window.REALTIME_MODULE.ot_applyError = {
type: 'resultParseError',
resultOp: resultOp,
toTransform: toTransform,
transformBy: transformBy,
text1: text,
text2: text2,
text3: text3,
error: x
};
console.log('Debugging info available at `window.REALTIME_MODULE.ot_applyError`');
}
// returning **null** breaks out of the loop

@ -12,8 +12,14 @@
box-sizing: border-box;
}
textarea{
position: absolute;
top: 5vh;
left: 0px;
border: 0px;
padding-top: 15px;
width: 100%;
height: 100vh;
height: 95vh;
max-width: 100%;
max-height: 100vh;
@ -32,26 +38,41 @@
color: #637476;
}
#run {
#panel {
position: fixed;
top: 0px;
right: 0px;
z-index: 100;
width: 5vw;
width: 100%;
height: 5vh;
z-index: 95;
background-color: #777;
/* min-height: 75px; */
}
#run {
display: block;
float: right;
height: 100%;
width: 10vw;
z-index: 100;
line-height: 5vw;
font-size: 1.5em;
background-color: #222;
color: #CCC;
display: block;
text-align: center;
border-radius: 5%;
border: 0px;
}
</style>
</head>
<body>
<textarea></textarea>
<div id="panel">
<!-- TODO update this element when new users join -->
<span id="users"></span>
<!-- what else should go in the panel? -->
<a href="#" id="run">RUN</a>
</div>
</body>
</html>

@ -3,9 +3,10 @@ define([
'/common/realtime-input.js',
'/common/messages.js',
'/common/crypto.js',
'/common/cursor.js',
'/bower_components/jquery/dist/jquery.min.js',
'/customize/pad.js'
], function (Config, Realtime, Messages, Crypto) {
], function (Config, Realtime, Messages, Crypto, Cursor) {
var $ = window.jQuery;
$(window).on('hashchange', function() {
window.location.reload();
@ -57,7 +58,75 @@ define([
window.alert("Server Connection Lost");
};
var rt = Realtime.start(config);
var rt = window.rt = Realtime.start(config);
var cursor = Cursor($textarea[0]);
var splice = function (str, index, chars) {
var count = chars.length;
return str.slice(0, index) + chars + str.slice((index -1) + count);
};
var setSelectionRange = function (input, start, end) {
if (input.setSelectionRange) {
input.focus();
input.setSelectionRange(start, end);
} else if (input.createTextRange) {
var range = input.createTextRange();
range.collapse(true);
range.moveEnd('character', end);
range.moveStart('character', start);
range.select();
}
};
var setCursor = function (el, pos) {
setSelectionRange(el, pos, pos);
};
var state = {};
// TODO
$textarea.on('keydown', function (e) {
// track when control keys are pushed down
//switch (e.key) { }
});
// TODO
$textarea.on('keyup', function (e) {
// track when control keys are released
});
$textarea.on('keypress', function (e) {
switch (e.key) {
case 'Tab':
// insert a tab wherever the cursor is...
var start = $textarea.prop('selectionStart');
var end = $textarea.prop('selectionEnd');
if (typeof start !== 'undefined') {
if (start === end) {
$textarea.val(function (i, val) {
return splice(val, start, "\t");
});
setCursor($textarea[0], start +1);
} else {
// indentation?? this ought to be fun.
}
}
// simulate a keypress so the event goes through..
// prevent default behaviour for tab
e.preventDefault();
rt.bumpSharejs();
break;
default:
break;
}
});
$textarea.on('change', function () {
rt.bumpSharejs();
});
$run.click(function (e) {
e.preventDefault();

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script data-main="main" src="/bower_components/requirejs/require.js"></script>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#pad-iframe {
position:fixed;
top:0px;
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" src="inner.html"></iframe>
</body>
</html>

@ -0,0 +1,12 @@
<!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 src="/bower_components/ckeditor/ckeditor.js"></script>
</head>
<body>
<textarea style="display:none" id="editor1" name="editor1"></textarea>
</body>
</html>

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

@ -0,0 +1,233 @@
define([
'/common/messages.js'
], function (Messages) {
/** Id of the element for getting debug info. */
var DEBUG_LINK_CLS = 'rtwysiwyg-debug-link';
/** Id of the div containing the user list. */
var USER_LIST_CLS = 'rtwysiwyg-user-list';
/** Id of the div containing the lag info. */
var LAG_ELEM_CLS = 'rtwysiwyg-lag';
/** The toolbar class which contains the user list, debug link and lag. */
var TOOLBAR_CLS = 'rtwysiwyg-toolbar';
/** Key in the localStore which indicates realtime activity should be disallowed. */
var LOCALSTORAGE_DISALLOW = 'rtwysiwyg-disallow';
var SPINNER_DISAPPEAR_TIME = 3000;
var SPINNER = [ '-', '\\', '|', '/' ];
var uid = function () {
return 'rtwysiwyg-uid-' + String(Math.random()).substring(2);
};
var createRealtimeToolbar = function ($container) {
var id = uid();
$container.prepend(
'<div class="' + TOOLBAR_CLS + '" id="' + id + '">' +
'<div class="rtwysiwyg-toolbar-leftside"></div>' +
'<div class="rtwysiwyg-toolbar-rightside"></div>' +
'</div>'
);
var toolbar = $container.find('#'+id);
toolbar.append([
'<style>',
'.' + TOOLBAR_CLS + ' {',
' color: #666;',
' font-weight: bold;',
// ' background-color: #f0f0ee;',
// ' border-bottom: 1px solid #DDD;',
// ' border-top: 3px solid #CCC;',
// ' border-right: 2px solid #CCC;',
// ' border-left: 2px solid #CCC;',
' height: 26px;',
' margin-bottom: -3px;',
' display: inline-block;',
' width: 100%;',
'}',
'.' + TOOLBAR_CLS + ' a {',
' float: right;',
'}',
'.' + TOOLBAR_CLS + ' div {',
' padding: 0 10px;',
' height: 1.5em;',
// ' background: #f0f0ee;',
' line-height: 25px;',
' height: 22px;',
'}',
'.' + TOOLBAR_CLS + ' div.rtwysiwyg-back {',
' padding: 0;',
' font-weight: bold;',
' cursor: pointer;',
' color: #000;',
'}',
'.rtwysiwyg-toolbar-leftside div {',
' float: left;',
'}',
'.rtwysiwyg-toolbar-leftside {',
' float: left;',
'}',
'.rtwysiwyg-toolbar-rightside {',
' float: right;',
'}',
'.rtwysiwyg-lag {',
' float: right;',
'}',
'.rtwysiwyg-spinner {',
' float: left;',
'}',
'.gwt-TabBar {',
' display:none;',
'}',
'.' + DEBUG_LINK_CLS + ':link { color:transparent; }',
'.' + DEBUG_LINK_CLS + ':link:hover { color:blue; }',
'.gwt-TabPanelBottom { border-top: 0 none; }',
'</style>'
].join('\n'));
return toolbar;
};
var createEscape = function ($container) {
var id = uid();
$container.append('<div class="rtwysiwyg-back" id="' + id + '">&#8656; Back</div>');
var $ret = $container.find('#'+id);
$ret.on('click', function () {
window.location.href = '/';
});
return $ret[0];
};
var createSpinner = function ($container) {
var id = uid();
$container.append('<div class="rtwysiwyg-spinner" id="'+id+'"></div>');
return $container.find('#'+id)[0];
};
var kickSpinner = function (spinnerElement, reversed) {
var txt = spinnerElement.textContent || '-';
var inc = (reversed) ? -1 : 1;
spinnerElement.textContent = SPINNER[(SPINNER.indexOf(txt) + inc) % SPINNER.length];
if (spinnerElement.timeout) { clearTimeout(spinnerElement.timeout); }
spinnerElement.timeout = setTimeout(function () {
spinnerElement.textContent = '';
}, SPINNER_DISAPPEAR_TIME);
};
var createUserList = function ($container) {
var id = uid();
$container.append('<div class="' + USER_LIST_CLS + '" id="'+id+'"></div>');
return $container.find('#'+id)[0];
};
var updateUserList = function (myUserName, listElement, userList) {
var meIdx = userList.indexOf(myUserName);
if (meIdx === -1) {
listElement.textContent = Messages.synchronizing;
return;
}
if (userList.length === 1) {
listElement.textContent = Messages.editingAlone;
} else if (userList.length === 2) {
listElement.textContent = Messages.editingWithOneOtherPerson;
} else {
listElement.textContent = Messages.editingWith + ' ' + (userList.length - 1) + ' ' + Messages.otherPeople;
}
};
var createLagElement = function ($container) {
var id = uid();
$container.append('<div class="' + LAG_ELEM_CLS + '" id="'+id+'"></div>');
return $container.find('#'+id)[0];
};
var checkLag = function (realtime, lagElement) {
var lag = realtime.getLag();
var lagSec = lag.lag/1000;
var lagMsg = Messages.lag + ' ';
if (lag.waiting && lagSec > 1) {
lagMsg += "?? " + Math.floor(lagSec);
} else {
lagMsg += lagSec;
}
lagElement.textContent = lagMsg;
};
// this is a little hack, it should go in it's own file.
// FIXME ok, so let's put it in its own file then
// TODO there should also be a 'clear recent pads' button
var rememberPad = function () {
// FIXME, this is overly complicated, use array methods
var recentPadsStr = localStorage['CryptPad_RECENTPADS'];
var recentPads = [];
if (recentPadsStr) { recentPads = JSON.parse(recentPadsStr); }
// TODO use window.location.hash or something like that
if (window.location.href.indexOf('#') === -1) { return; }
var now = new Date();
var out = [];
for (var i = recentPads.length; i >= 0; i--) {
if (recentPads[i] &&
// TODO precompute this time value, maybe make it configurable?
// FIXME precompute the date too, why getTime every time?
now.getTime() - recentPads[i][1] < (1000*60*60*24*30) &&
recentPads[i][0] !== window.location.href)
{
out.push(recentPads[i]);
}
}
out.push([window.location.href, now.getTime()]);
localStorage['CryptPad_RECENTPADS'] = JSON.stringify(out);
};
var create = function ($container, myUserName, realtime) {
var toolbar = createRealtimeToolbar($container);
createEscape(toolbar.find('.rtwysiwyg-toolbar-leftside'));
var userListElement = createUserList(toolbar.find('.rtwysiwyg-toolbar-leftside'));
var spinner = createSpinner(toolbar.find('.rtwysiwyg-toolbar-rightside'));
var lagElement = createLagElement(toolbar.find('.rtwysiwyg-toolbar-rightside'));
rememberPad();
var connected = false;
realtime.onUserListChange(function (userList) {
if (userList.indexOf(myUserName) !== -1) { connected = true; }
if (!connected) { return; }
updateUserList(myUserName, userListElement, userList);
});
var ks = function () {
if (connected) { kickSpinner(spinner, false); }
};
realtime.onPatch(ks);
// Try to filter out non-patch messages, doesn't have to be perfect this is just the spinner
realtime.onMessage(function (msg) { if (msg.indexOf(':[2,') > -1) { ks(); } });
setInterval(function () {
if (!connected) { return; }
checkLag(realtime, lagElement);
}, 3000);
return {
failed: function () {
connected = false;
userListElement.textContent = '';
lagElement.textContent = '';
},
reconnecting: function () {
connected = false;
userListElement.textContent = Messages.reconnecting;
lagElement.textContent = '';
},
connected: function () {
connected = true;
}
};
};
return { create: create };
});

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

Loading…
Cancel
Save