Update Codepad with the latest improvements

pull/1/head
Yann Flory 9 years ago
parent 692fe24b32
commit c53baab99d

@ -1,483 +0,0 @@
/*
* 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([
'/bower_components/jquery/dist/jquery.min.js',
'/common/otaml.js'
], function () {
var $ = window.jQuery;
var Otaml = window.Otaml;
var module = { exports: {} };
var PARANOIA = true;
var debug = function (x) { };
debug = function (x) { console.log(x); };
var getNextSiblingDeep = function (node, parent)
{
if (node.firstChild) { return node.firstChild; }
do {
if (node.nextSibling) { return node.nextSibling; }
node = node.parentNode;
} while (node && node !== parent);
};
var getOuterHTML = function (node)
{
var html = node.outerHTML;
if (html) { return html; }
if (node.parentNode && node.parentNode.childNodes.length === 1) {
return node.parentNode.innerHTML;
}
var div = document.createElement('div');
div.appendChild(node.cloneNode(true));
return div.innerHTML;
};
var nodeFromHTML = function (html)
{
var e = document.createElement('div');
e.innerHTML = html;
return e.childNodes[0];
};
var getInnerHTML = function (node)
{
var html = node.innerHTML;
if (html) { return html; }
var outerHTML = getOuterHTML(node);
var tw = Otaml.tagWidth(outerHTML);
if (!tw) { return outerHTML; }
return outerHTML.substring(tw, outerHTML.lastIndexOf('</'));
};
var uniqueId = function () { return 'uid-'+(''+Math.random()).slice(2); };
var offsetOfNodeOuterHTML = function (docText, node, dom, ifrWindow)
{
if (PARANOIA && getInnerHTML(dom) !== docText) { throw new Error(); }
if (PARANOIA && !node) { throw new Error(); }
// can't get the index of the outerHTML of the dom in a string with only the innerHTML.
if (node === dom) { throw new Error(); }
var content = getOuterHTML(node);
var idx = docText.lastIndexOf(content);
if (idx === -1) { throw new Error(); }
if (idx !== docText.indexOf(content)) {
var idTag = uniqueId();
var span = ifrWindow.document.createElement('span');
span.setAttribute('id', idTag);
var spanHTML = '<span id="'+idTag+'"></span>';
if (PARANOIA && spanHTML !== span.outerHTML) { throw new Error(); }
node.parentNode.insertBefore(span, node);
var newDocText = getInnerHTML(dom);
idx = newDocText.lastIndexOf(spanHTML);
if (idx === -1 || idx !== newDocText.indexOf(spanHTML)) { throw new Error(); }
node.parentNode.removeChild(span);
if (PARANOIA && getInnerHTML(dom) !== docText) { throw new Error(); }
}
if (PARANOIA && docText.indexOf(content, idx) !== idx) { throw new Error(); }
return idx;
};
var patchString = module.exports.patchString = function (oldString, offset, toRemove, toInsert)
{
return oldString.substring(0, offset) + toInsert + oldString.substring(offset + toRemove);
};
var getNodeAtOffset = function (docText, offset, dom)
{
if (PARANOIA && dom.childNodes.length && docText !== dom.innerHTML) { throw new Error(); }
if (offset < 0) { throw new Error(); }
var idx = 0;
for (var i = 0; i < dom.childNodes.length; i++) {
var childOuterHTML = getOuterHTML(dom.childNodes[i]);
if (PARANOIA && docText.indexOf(childOuterHTML, idx) !== idx) { throw new Error(); }
if (i === 0 && idx >= offset) {
return { node: dom, pos: 0 };
}
if (idx + childOuterHTML.length > offset) {
var childInnerHTML = childOuterHTML;
var tw = Otaml.tagWidth(childOuterHTML);
if (tw) {
childInnerHTML = childOuterHTML.substring(tw, childOuterHTML.lastIndexOf('</'));
}
if (offset - idx - tw < 0) {
if (offset - idx === 0) {
return { node: dom.childNodes[i], pos: 0 };
}
break;
}
return getNodeAtOffset(childInnerHTML, offset - idx - tw, dom.childNodes[i]);
}
idx += childOuterHTML.length;
}
if (dom.nodeName[0] === '#text') {
if (offset > docText.length) { throw new Error(); }
var beforeOffset = docText.substring(0, offset);
if (beforeOffset.indexOf('&') > -1) {
var tn = nodeFromHTML(beforeOffset);
offset = tn.data.length;
}
} else {
offset = 0;
}
return { node: dom, pos: offset };
};
var relocatedPositionInNode = function (newNode, oldNode, offset)
{
if (newNode.nodeName !== '#text' || oldNode.nodeName !== '#text' || offset === 0) {
offset = 0;
} else if (oldNode.data === newNode.data) {
// fallthrough
} else if (offset > newNode.length) {
offset = newNode.length;
} else if (oldNode.data.substring(0, offset) === newNode.data.substring(0, offset)) {
// keep same offset and fall through
} else {
var rOffset = oldNode.length - offset;
if (oldNode.data.substring(offset) ===
newNode.data.substring(newNode.length - rOffset))
{
offset = newNode.length - rOffset;
} else {
offset = 0;
}
}
return { node: newNode, pos: offset };
};
var pushNode = function (list, node) {
if (node.nodeName === '#text') {
list.push.apply(list, node.data.split(''));
} else {
list.push('#' + node.nodeName);
}
};
var getChildPath = function (parent) {
var out = [];
for (var next = parent; next; next = getNextSiblingDeep(next, parent)) {
pushNode(out, next);
}
return out;
};
var tryFromBeginning = function (oldPath, newPath) {
for (var i = 0; i < oldPath.length; i++) {
if (oldPath[i] !== newPath[i]) { return i; }
}
return oldPath.length;
};
var tryFromEnd = function (oldPath, newPath) {
for (var i = 1; i <= oldPath.length; i++) {
if (oldPath[oldPath.length - i] !== newPath[newPath.length - i]) {
return false;
}
}
return true;
};
/**
* returns 2 arrays (before and after).
* before is string representations (see nodeId()) of all nodes before the target
* node and after is representations of all nodes which follow.
*/
var getNodePaths = function (parent, node) {
var before = [];
var next = parent;
for (; next && next !== node; next = getNextSiblingDeep(next, parent)) {
pushNode(before, next);
}
if (next !== node) { throw new Error(); }
var after = [];
next = getNextSiblingDeep(next, parent);
for (; next; next = getNextSiblingDeep(next, parent)) {
pushNode(after, next);
}
return { before: before, after: after };
};
var nodeAtIndex = function (parent, idx) {
var node = parent;
for (var i = 0; i < idx; i++) {
if (node.nodeName === '#text') {
if (i + node.data.length > idx) { return node; }
i += node.data.length - 1;
}
node = getNextSiblingDeep(node);
}
return node;
};
var getRelocatedPosition = function (newParent, oldParent, oldNode, oldOffset, origText, op)
{
var newPath = getChildPath(newParent);
if (newPath.length === 1) {
return { node: null, pos: 0 };
}
var oldPaths = getNodePaths(oldParent, oldNode);
var idx = -1;
var fromBeginning = tryFromBeginning(oldPaths.before, newPath);
if (fromBeginning === oldPaths.before.length) {
idx = oldPaths.before.length;
} else if (tryFromEnd(oldPaths.after, newPath)) {
idx = (newPath.length - oldPaths.after.length - 1);
} else {
idx = fromBeginning;
var id = 'relocate-' + String(Math.random()).substring(2);
$(document.body).append('<textarea id="'+id+'"></textarea>');
$('#'+id).val(JSON.stringify([origText, op, newPath, getChildPath(oldParent), oldPaths]));
}
var out = nodeAtIndex(newParent, idx);
return relocatedPositionInNode(out, oldNode, oldOffset);
};
// We can't create a real range until the new parent is installed in the document
// but we need the old range to be in the document so we can do comparisons
// so create a "pseudo" range instead.
var getRelocatedPseudoRange = function (newParent, oldParent, range, origText, op)
{
if (!range.startContainer) {
throw new Error();
}
if (!newParent) { throw new Error(); }
// Copy because tinkering in the dom messes up the original range.
var startContainer = range.startContainer;
var startOffset = range.startOffset;
var endContainer = range.endContainer;
var endOffset = range.endOffset;
var newStart =
getRelocatedPosition(newParent, oldParent, startContainer, startOffset, origText, op);
if (!newStart.node) {
// there is probably nothing left of the document so just clear the selection.
endContainer = null;
}
var newEnd = { node: newStart.node, pos: newStart.pos };
if (endContainer) {
if (endContainer !== startContainer) {
newEnd = getRelocatedPosition(newParent, oldParent, endContainer, endOffset, origText, op);
} else if (endOffset !== startOffset) {
newEnd = {
node: newStart.node,
pos: relocatedPositionInNode(newStart.node, endContainer, endOffset).pos
};
} else {
newEnd = { node: newStart.node, pos: newStart.pos };
}
}
return { start: newStart, end: newEnd };
};
var replaceAllChildren = function (parent, newParent)
{
var c;
while ((c = parent.firstChild)) {
parent.removeChild(c);
}
while ((c = newParent.firstChild)) {
newParent.removeChild(c);
parent.appendChild(c);
}
};
var isAncestorOf = function (maybeDecendent, maybeAncestor) {
while ((maybeDecendent = maybeDecendent.parentNode)) {
if (maybeDecendent === maybeAncestor) { return true; }
}
return false;
};
var getSelectedRange = function (rangy, ifrWindow, selection) {
selection = selection || rangy.getSelection(ifrWindow);
if (selection.rangeCount === 0) {
return;
}
var range = selection.getRangeAt(0);
range.backward = (selection.rangeCount === 1 && selection.isBackward());
if (!range.startContainer) {
throw new Error();
}
// Occasionally, some browsers *cough* firefox *cough* will attach the range to something
// which has been used in the past but is nolonger part of the dom...
if (range.startContainer &&
isAncestorOf(range.startContainer, ifrWindow.document))
{
return range;
}
return;
};
var applyHTMLOp = function (docText, op, dom, rangy, ifrWindow)
{
var parent = getNodeAtOffset(docText, op.offset, dom).node;
var htmlToRemove = docText.substring(op.offset, op.offset + op.toRemove);
var parentInnerHTML;
var indexOfInnerHTML;
var localOffset;
for (;;) {
for (;;) {
parentInnerHTML = parent.innerHTML;
if (typeof(parentInnerHTML) !== 'undefined'
&& parentInnerHTML.indexOf(htmlToRemove) !== -1)
{
break;
}
if (parent === dom || !(parent = parent.parentNode)) { throw new Error(); }
}
var indexOfOuterHTML = 0;
var tw = 0;
if (parent !== dom) {
indexOfOuterHTML = offsetOfNodeOuterHTML(docText, parent, dom, ifrWindow);
tw = Otaml.tagWidth(docText.substring(indexOfOuterHTML));
}
indexOfInnerHTML = indexOfOuterHTML + tw;
localOffset = op.offset - indexOfInnerHTML;
if (localOffset >= 0 && localOffset + op.toRemove <= parentInnerHTML.length) {
break;
}
parent = parent.parentNode;
if (!parent) { throw new Error(); }
}
if (PARANOIA &&
docText.substr(indexOfInnerHTML, parentInnerHTML.length) !== parentInnerHTML)
{
throw new Error();
}
var newParentInnerHTML =
patchString(parentInnerHTML, localOffset, op.toRemove, op.toInsert);
// Create a temp container for holding the children of the parent node.
// Once we've identified the new range, we'll return the nodes to the
// original parent. This is because parent might be the <body> and we
// don't want to destroy all of our event listeners.
var babysitter = ifrWindow.document.createElement('div');
// give it a uid so that we can prove later that it's not in the document,
// see getSelectedRange()
babysitter.setAttribute('id', uniqueId());
babysitter.innerHTML = newParentInnerHTML;
var range = getSelectedRange(rangy, ifrWindow);
// doesn't intersect at all
if (!range || !range.containsNode(parent, true)) {
replaceAllChildren(parent, babysitter);
return;
}
var pseudoRange = getRelocatedPseudoRange(babysitter, parent, range, rangy);
range.detach();
replaceAllChildren(parent, babysitter);
if (pseudoRange.start.node) {
var selection = rangy.getSelection(ifrWindow);
var newRange = rangy.createRange();
newRange.setStart(pseudoRange.start.node, pseudoRange.start.pos);
newRange.setEnd(pseudoRange.end.node, pseudoRange.end.pos);
selection.setSingleRange(newRange);
}
return;
};
var applyHTMLOpHammer = function (docText, op, dom, rangy, ifrWindow)
{
var newDocText = patchString(docText, op.offset, op.toRemove, op.toInsert);
var babysitter = ifrWindow.document.createElement('body');
// give it a uid so that we can prove later that it's not in the document,
// see getSelectedRange()
babysitter.setAttribute('id', uniqueId());
babysitter.innerHTML = newDocText;
var range = getSelectedRange(rangy, ifrWindow);
// doesn't intersect at all
if (!range) {
replaceAllChildren(dom, babysitter);
return;
}
var pseudoRange = getRelocatedPseudoRange(babysitter, dom, range, docText, op);
range.detach();
replaceAllChildren(dom, babysitter);
if (pseudoRange.start.node) {
var selection = rangy.getSelection(ifrWindow);
var newRange = rangy.createRange();
newRange.setStart(pseudoRange.start.node, pseudoRange.start.pos);
newRange.setEnd(pseudoRange.end.node, pseudoRange.end.pos);
selection.setSingleRange(newRange);
}
return;
};
/* Return whether the selection range has been "dirtied" and needs to be reloaded. */
var applyOp = module.exports.applyOp = function (docText, op, dom, rangy, ifrWindow)
{
if (PARANOIA && docText !== getInnerHTML(dom)) { throw new Error(); }
if (op.offset + op.toRemove > docText.length) {
throw new Error();
}
try {
applyHTMLOpHammer(docText, op, dom, rangy, ifrWindow);
var result = patchString(docText, op.offset, op.toRemove, op.toInsert);
var innerHTML = getInnerHTML(dom);
if (result !== innerHTML) {
$(document.body).append('<textarea id="statebox"></textarea>');
$(document.body).append('<textarea id="errorbox"></textarea>');
var SEP = '\n\n\n\n\n\n\n\n\n\n';
$('#statebox').val(docText + SEP + result + SEP + innerHTML);
var diff = Otaml.makeTextOperation(result, innerHTML);
$('#errorbox').val(JSON.stringify(op) + '\n' + JSON.stringify(diff));
throw new Error();
}
} catch (err) {
if (PARANOIA) { console.log(err.stack); }
// The big hammer
dom.innerHTML = patchString(docText, op.offset, op.toRemove, op.toInsert);
}
};
return module.exports;
});

@ -1,10 +1,10 @@
define([ define([
'/api/config?cb=' + Math.random().toString(16).substring(2), '/api/config?cb=' + Math.random().toString(16).substring(2),
'/code/rtwiki.js', '/code/rt_codemirror.js',
'/common/messages.js', '/common/messages.js',
'/common/crypto.js', '/common/crypto.js',
'/bower_components/jquery/dist/jquery.min.js' '/bower_components/jquery/dist/jquery.min.js'
], function (Config, RTWiki, Messages, Crypto) { ], function (Config, RTCode, Messages, Crypto) {
var $ = window.jQuery; var $ = window.jQuery;
var ifrw = $('#pad-iframe')[0].contentWindow; var ifrw = $('#pad-iframe')[0].contentWindow;
@ -36,7 +36,7 @@ define([
editor.setValue(Messages.codeInitialState); editor.setValue(Messages.codeInitialState);
var rtw = var rtw =
RTWiki.start(ifrw, RTCode.start(ifrw,
Config.websocketURL, Config.websocketURL,
Crypto.rand64(8), Crypto.rand64(8),
key.channel, key.channel,

File diff suppressed because it is too large Load Diff

@ -1,358 +0,0 @@
/*
* 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([
'/code/html-patcher.js',
'/code/errorbox.js',
'/common/messages.js',
'/bower_components/reconnectingWebsocket/reconnecting-websocket.js',
'/common/crypto.js',
'/common/toolbar.js',
'/code/rangy.js',
'/common/chainpad.js',
'/common/otaml.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (HTMLPatcher, ErrorBox, Messages, ReconnectingWebSocket, Crypto, Toolbar) {
window.ErrorBox = ErrorBox;
var $ = window.jQuery;
var Rangy = window.rangy;
Rangy.init();
var ChainPad = window.ChainPad;
var Otaml = window.Otaml;
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;
/** Maximum number of milliseconds of lag before we fail the connection. */
var MAX_LAG_BEFORE_DISCONNECT = 20000;
// ------------------ Trapping Keyboard Events ---------------------- //
var bindEvents = function (element, events, callback, unbind) {
for (var i = 0; i < events.length; i++) {
var e = events[i];
if (element.addEventListener) {
if (unbind) {
element.removeEventListener(e, callback, false);
} else {
element.addEventListener(e, callback, false);
}
} else {
if (unbind) {
element.detachEvent('on' + e, callback);
} else {
element.attachEvent('on' + e, callback);
}
}
}
};
var bindAllEvents = function (cmDiv, onEvent, unbind)
{
bindEvents(cmDiv,
['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste', 'mousedown','mouseup','click'],
onEvent,
unbind);
};
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);
};
var abort = function (socket, realtime) {
realtime.abort();
realtime.toolbar.failed();
try { socket._socket.close(); } catch (e) { }
};
var createDebugInfo = function (cause, realtime, docHTML, allMessages) {
return JSON.stringify({
cause: cause,
realtimeUserDoc: realtime.getUserDoc(),
realtimeAuthDoc: realtime.getAuthDoc(),
docHTML: docHTML,
allMessages: allMessages,
});
};
var handleError = function (socket, realtime, err, docHTML, allMessages) {
var internalError = createDebugInfo(err, realtime, docHTML, allMessages);
abort(socket, realtime);
ErrorBox.show('error', docHTML, internalError);
};
var getDocHTML = function (doc) {
return $(doc).val();
};
var transformCursorCMRemove = function(text, cursor, pos, length) {
var newCursor = cursor;
var textLines = text.substr(0, pos).split("\n");
var removedTextLineNumber = textLines.length-1;
var removedTextColumnIndex = textLines[textLines.length-1].length;
var removedLines = text.substr(pos, length).split("\n").length - 1;
if(cursor.line > (removedTextLineNumber + removedLines)) {
newCursor.line -= removedLines;
}
else if(removedLines > 0 && cursor.line === (removedTextLineNumber+removedLines)) {
var lastLineCharsRemoved = text.substr(pos, length).split("\n")[removedLines].length;
if(cursor.ch >= lastLineCharsRemoved) {
newCursor.line = removedTextLineNumber;
newCursor.ch = removedTextColumnIndex + cursor.ch - lastLineCharsRemoved;
}
else {
newCursor.line -= removedLines;
newCursor.ch = removedTextColumnIndex;
}
}
else if(cursor.line === removedTextLineNumber && cursor.ch > removedTextLineNumber) {
newCursor.ch -= Math.min(length, cursor.ch-removedTextLineNumber);
}
return newCursor;
};
var transformCursorCMInsert = function(oldtext, cursor, pos, text) {
var newCursor = cursor;
var textLines = oldtext.substr(0, pos).split("\n");
var addedTextLineNumber = textLines.length-1;
var addedTextColumnIndex = textLines[textLines.length-1].length;
var addedLines = text.split("\n").length - 1;
if(cursor.line > addedTextLineNumber) {
newCursor.line += addedLines;
}
else if(cursor.line === addedTextLineNumber && cursor.ch > addedTextColumnIndex) {
newCursor.line += addedLines;
if(addedLines > 0) {
newCursor.ch = newCursor.ch - addedTextColumnIndex + text.split("\n")[addedLines].length;
}
else {
newCursor.ch += text.split("\n")[addedLines].length;
}
}
return newCursor;
};
var makeWebsocket = function (url) {
var socket = new ReconnectingWebSocket(url);
var out = {
onOpen: [],
onClose: [],
onError: [],
onMessage: [],
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) { return; }
}
};
};
socket.onopen = mkHandler('onOpen');
socket.onclose = mkHandler('onClose');
socket.onerror = mkHandler('onError');
socket.onmessage = mkHandler('onMessage');
return out;
};
var start = module.exports.start =
function (window, websocketUrl, userName, channel, cryptKey)
{
var passwd = 'y';
//var wysiwygDiv = window.document.getElementById('cke_1_contents');
var doc = window.document.getElementById('editor1');
var cmDiv = window.document.getElementsByClassName('CodeMirror')[0];
var cmEditor = cmDiv.CodeMirror;
//var ifr = wysiwygDiv.getElementsByTagName('iframe')[0];
var socket = makeWebsocket(websocketUrl);
var onEvent = function () { };
var allMessages = [];
var isErrorState = false;
var initializing = true;
var recoverableErrorCount = 0;
var error = function (recoverable, err) {
console.log(new Error().stack);
console.log('error: ' + err.stack);
if (recoverable && recoverableErrorCount++ < MAX_RECOVERABLE_ERRORS) { return; }
var realtime = socket.realtime;
var docHtml = getDocHTML(doc);
isErrorState = true;
handleError(socket, realtime, err, docHtml, allMessages);
};
var attempt = function (func) {
return function () {
var e;
try { return func.apply(func, arguments); } catch (ee) { e = ee; }
if (e) {
console.log(e.stack);
error(true, e);
}
};
};
var checkSocket = function () {
if (isSocketDisconnected(socket, socket.realtime) && !socket.intentionallyClosing) {
//isErrorState = true;
//abort(socket, socket.realtime);
//ErrorBox.show('disconnected', getDocHTML(doc));
return true;
}
return false;
};
socket.onOpen.push(function (evt) {
if (!initializing) {
socket.realtime.start();
return;
}
var realtime = socket.realtime =
ChainPad.create(userName,
passwd,
channel,
getDocHTML(doc),
{ transformFunction: Otaml.transform });
var toolbar = realtime.toolbar =
Toolbar.create(window.$('#cme_toolbox'), userName, realtime);
onEvent = function () {
if (isErrorState) { return; }
if (initializing) { return; }
var oldDocText = realtime.getUserDoc();
var docText = getDocHTML(doc);
var op = attempt(Otaml.makeTextOperation)(oldDocText, docText);
if (!op) { return; }
if (op.toRemove > 0) {
attempt(realtime.remove)(op.offset, op.toRemove);
}
if (op.toInsert.length > 0) {
attempt(realtime.insert)(op.offset, op.toInsert);
}
if (realtime.getUserDoc() !== docText) {
error(false, 'realtime.getUserDoc() !== docText');
}
};
var userDocBeforePatch;
var incomingPatch = function () {
if (isErrorState || initializing) { return; }
userDocBeforePatch = userDocBeforePatch || getDocHTML(doc);
if (PARANOIA && userDocBeforePatch !== getDocHTML(doc)) {
error(false, "userDocBeforePatch != getDocHTML(doc)");
}
var op = attempt(Otaml.makeTextOperation)(userDocBeforePatch, realtime.getUserDoc());
var oldValue = getDocHTML(doc);
var newValue = realtime.getUserDoc();
// Fix cursor and/or selection
var oldCursor = cmEditor.getCursor();
var oldCursorCMStart = cmEditor.getCursor('from');
var oldCursorCMEnd = cmEditor.getCursor('to');
var newCursor;
var newSelection;
if(oldCursorCMStart !== oldCursorCMEnd) { // Selection
if (op.toRemove > 0) {
newSelection = [transformCursorCMRemove(oldValue, oldCursorCMStart, op.offset, op.toRemove), transformCursorCMRemove(oldValue, oldCursorCMEnd, op.offset, op.toRemove)];
}
if (op.toInsert.length > 0) {
newSelection = [transformCursorCMInsert(oldValue, oldCursorCMStart, op.offset, op.toInsert), transformCursorCMInsert(oldValue, oldCursorCMEnd, op.offset, op.toInsert)];
}
}
else { // Cursor
if (op.toRemove > 0) {
newCursor = transformCursorCMRemove(oldValue, oldCursor, op.offset, op.toRemove);
}
if (op.toInsert.length > 0) {
newCursor = transformCursorCMInsert(oldValue, oldCursor, op.offset, op.toInsert);
}
}
$(doc).val(newValue);
cmEditor.setValue(newValue);
if(newCursor) {
cmEditor.setCursor(newCursor);
}
else {
cmEditor.setSelection(newSelection[0], newSelection[1]);
}
};
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;
incomingPatch();
});
socket.onMessage.push(function (evt) {
if (isErrorState) { return; }
var message = Crypto.decrypt(evt.data, cryptKey);
allMessages.push(message);
if (!initializing) {
if (PARANOIA) { onEvent(); }
userDocBeforePatch = realtime.getUserDoc();
}
realtime.message(message);
});
realtime.onMessage(function (message) {
if (isErrorState) { return; }
message = Crypto.encrypt(message, cryptKey);
try {
socket.send(message);
} catch (e) {
error(true, e.stack);
}
});
realtime.onPatch(incomingPatch);
bindAllEvents(cmDiv, onEvent, false);
setInterval(function () {
if (isErrorState || checkSocket()) {
toolbar.reconnecting();
}
}, 200);
realtime.start();
toolbar.connected();
//console.log('started');
});
return {
onEvent: function () { onEvent(); }
};
};
return module.exports;
});

@ -6,7 +6,7 @@ define([
'/common/crypto.js', '/common/crypto.js',
'/code/errorbox.js', '/code/errorbox.js',
'/common/messages.js', '/common/messages.js',
'/common/toolbar.js', '/code/toolbar.js',
'/common/chainpad.js', '/common/chainpad.js',
'/common/otaml.js', '/common/otaml.js',
'/bower_components/jquery/dist/jquery.min.js' '/bower_components/jquery/dist/jquery.min.js'
@ -16,17 +16,6 @@ define([
var Otaml = window.Otaml; var Otaml = window.Otaml;
var module = { exports: {} }; var module = { exports: {} };
var LOCALSTORAGE_DISALLOW = 'rtwiki-disallow';
// Number for a message type which will not interfere with chainpad.
var MESSAGE_TYPE_ISAVED = 5000;
// how often to check if the document has been saved recently
var SAVE_DOC_CHECK_CYCLE = 20000;
// how often to save the document
var SAVE_DOC_TIME = 60000;
// How long to wait before determining that the connection is lost. // How long to wait before determining that the connection is lost.
var MAX_LAG_BEFORE_DISCONNECT = 30000; var MAX_LAG_BEFORE_DISCONNECT = 30000;
@ -36,37 +25,6 @@ define([
var debug = function (x) { }; var debug = function (x) { };
//debug = function (x) { console.log(x) }; //debug = function (x) { console.log(x) };
warn = function (x) { console.log(x); }; warn = function (x) { console.log(x); };
var setStyle = function () {
$('head').append([
'<style>',
'.rtwiki-toolbar {',
' width: 100%;',
' color: #666;',
' font-weight: bold;',
' background-color: #f0f0ee;',
' border: 0, none;',
' height: 24px;',
' float: left;',
'}',
'.rtwiki-toolbar div {',
' padding: 0 10px;',
' height: 1.5em;',
' background: #f0f0ee;',
' line-height: 25px;',
' height: 24px;',
'}',
'.rtwiki-toolbar-leftside {',
' float: left;',
'}',
'.rtwiki-toolbar-rightside {',
' float: right;',
'}',
'.rtwiki-lag {',
' float: right;',
'}',
'</style>'
].join(''));
};
var uid = function () { var uid = function () {
return 'rtwiki-uid-' + String(Math.random()).substring(2); return 'rtwiki-uid-' + String(Math.random()).substring(2);
@ -151,178 +109,6 @@ define([
return lagElement; return lagElement;
}; };
var createRealtimeToolbar = function (container) {
var id = uid();
$(container).prepend(
'<div class="rtwiki-toolbar" id="' + id + '">' +
'<div class="rtwiki-toolbar-leftside"></div>' +
'<div class="rtwiki-toolbar-rightside"></div>' +
'</div>'
);
return $('#'+id);
};
var now = function () { return (new Date()).getTime(); };
var getFormToken = function () {
return $('meta[name="form_token"]').attr('content');
};
var getDocumentSection = function (sectionNum, andThen) {
debug("getting document section...");
$.ajax({
url: window.docediturl,
type: "POST",
async: true,
dataType: 'text',
data: {
xpage: 'editwiki',
section: ''+sectionNum
},
success: function (jqxhr) {
var content = $(jqxhr).find('#content');
if (!content || !content.length) {
andThen(new Error("could not find content"));
} else {
andThen(undefined, content.text());
}
},
error: function (jqxhr, err, cause) {
andThen(new Error(err));
}
});
};
var getIndexOfDocumentSection = function (documentContent, sectionNum, andThen) {
getDocumentSection(sectionNum, function (err, content) {
if (err) {
andThen(err);
return;
}
// This is screwed up, XWiki generates the section by rendering the XDOM back to
// XWiki2.0 syntax so it's not possible to find the actual location of a section.
// See: http://jira.xwiki.org/browse/XWIKI-10430
var idx = documentContent.indexOf(content);
if (idx === -1) {
content = content.split('\n')[0];
idx = documentContent.indexOf(content);
}
if (idx === -1) {
warn("Could not find section content..");
} else if (idx !== documentContent.lastIndexOf(content)) {
warn("Duplicate section content..");
} else {
andThen(undefined, idx);
return;
}
andThen(undefined, 0);
});
};
var seekToSection = function (textArea, andThen) {
var sect = window.location.hash.match(/^#!([\W\w]*&)?section=([0-9]+)/);
if (!sect || !sect[2]) {
andThen();
return;
}
var text = $(textArea).text();
getIndexOfDocumentSection(text, Number(sect[2]), function (err, idx) {
if (err) { andThen(err); return; }
if (idx === 0) {
warn("Attempted to seek to a section which could not be found");
} else {
var heightOne = $(textArea)[0].scrollHeight;
$(textArea).text(text.substring(idx));
var heightTwo = $(textArea)[0].scrollHeight;
$(textArea).text(text);
$(textArea).scrollTop(heightOne - heightTwo);
}
andThen();
});
};
var saveDocument = function (textArea, language, andThen) {
debug("saving document...");
$.ajax({
url: window.docsaveurl,
type: "POST",
async: true,
dataType: 'text',
data: {
xredirect: '',
content: $(textArea).val(),
xeditaction: 'edit',
comment: 'Auto-Saved by Realtime Session',
action_saveandcontinue: 'Save & Continue',
minorEdit: 1,
ajax: true,
form_token: getFormToken(),
language: language
},
success: function () {
andThen();
},
error: function (jqxhr, err, cause) {
warn(err);
// Don't callback, this way in case of error we will keep trying.
//andThen();
}
});
};
/**
* If we are editing a page which does not exist and creating it from a template
* then we should not auto-save the document otherwise it will cause RTWIKI-16
*/
var createPageMode = function () {
return (window.location.href.indexOf('template=') !== -1);
};
var createSaver = function (socket, channel, myUserName, textArea, demoMode, language) {
var timeOfLastSave = now();
socket.onMessage.unshift(function (evt) {
// get the content...
var chanIdx = evt.data.indexOf(channel);
var content = evt.data.substring(evt.data.indexOf(':[', chanIdx + channel.length)+1);
// parse
var json = JSON.parse(content);
// not an isaved message
if (json[0] !== MESSAGE_TYPE_ISAVED) { return; }
timeOfLastSave = now();
return false;
});
var lastSavedState = '';
var to;
var check = function () {
if (to) { clearTimeout(to); }
debug("createSaver.check");
to = setTimeout(check, Math.random() * SAVE_DOC_CHECK_CYCLE);
if (now() - timeOfLastSave < SAVE_DOC_TIME) { return; }
var toSave = $(textArea).val();
if (lastSavedState === toSave) { return; }
if (demoMode) { return; }
saveDocument(textArea, language, function () {
debug("saved document");
timeOfLastSave = now();
lastSavedState = toSave;
var saved = JSON.stringify([MESSAGE_TYPE_ISAVED, 0]);
socket.send('1:x' +
myUserName.length + ':' + myUserName +
channel.length + ':' + channel +
saved.length + ':' + saved
);
});
};
check();
socket.onClose.push(function () {
clearTimeout(to);
});
};
var isSocketDisconnected = function (socket, realtime) { var isSocketDisconnected = function (socket, realtime) {
return socket.readyState === socket.CLOSING || return socket.readyState === socket.CLOSING ||
socket.readyState === socket.CLOSED || socket.readyState === socket.CLOSED ||
@ -338,243 +124,6 @@ define([
} }
}; };
var startWebSocket = function (textArea,
toolbarContainer,
websocketUrl,
userName,
channel,
messages,
demoMode,
language)
{
debug("Opening websocket");
localStorage.removeItem(LOCALSTORAGE_DISALLOW);
var toolbar = createRealtimeToolbar(toolbarContainer);
var socket = new WebSocket(websocketUrl);
socket.onClose = [];
socket.onMessage = [];
var initState = $(textArea).val();
var realtime = socket.realtime = ChainPad.create(userName, 'x', channel, initState);
// for debugging
window.rtwiki_chainpad = realtime;
// http://jira.xwiki.org/browse/RTWIKI-21
var onbeforeunload = window.onbeforeunload || function () { };
window.onbeforeunload = function (ev) {
socket.intentionallyClosing = true;
return onbeforeunload(ev);
};
var isErrorState = false;
var checkSocket = function () {
if (socket.intentionallyClosing || isErrorState) { return false; }
if (isSocketDisconnected(socket, realtime)) {
realtime.abort();
socket.close();
ErrorBox.show('disconnected');
isErrorState = true;
return true;
}
return false;
};
socket.onopen = function (evt) {
var initializing = true;
var userListElement = createUserList(realtime,
userName,
toolbar.find('.rtwiki-toolbar-leftside'),
messages);
userListElement.text(messages.initializing);
createLagElement(socket,
realtime,
toolbar.find('.rtwiki-toolbar-rightside'),
messages);
setAutosaveHiddenState(true);
createSaver(socket, channel, userName, textArea, demoMode, language);
socket.onMessage.push(function (evt) {
debug(evt.data);
realtime.message(evt.data);
});
realtime.onMessage(function (message) { socket.send(message); });
$(textArea).attr("disabled", "disabled");
realtime.onUserListChange(function (userList) {
if (initializing && userList.indexOf(userName) > -1) {
initializing = false;
$(textArea).val(realtime.getUserDoc());
textArea.attach($(textArea)[0], realtime);
$(textArea).removeAttr("disabled");
}
if (!initializing) {
updateUserList(userName, userListElement, userList, messages);
}
});
debug("Bound websocket");
realtime.start();
};
socket.onclose = function (evt) {
for (var i = 0; i < socket.onClose.length; i++) {
if (socket.onClose[i](evt) === false) { return; }
}
};
socket.onmessage = function (evt) {
for (var i = 0; i < socket.onMessage.length; i++) {
if (socket.onMessage[i](evt) === false) { return; }
}
};
socket.onerror = function (err) {
warn(err);
checkSocket(realtime);
};
var to = setInterval(function () {
checkSocket(realtime);
}, 500);
socket.onClose.push(function () {
clearTimeout(to);
if (toolbar && typeof toolbar.remove === 'function') {
toolbar.remove();
} else {
warn("toolbar.remove is not a function"); //why not?
}
setAutosaveHiddenState(false);
});
return socket;
};
var stopWebSocket = function (socket) {
debug("Stopping websocket");
socket.intentionallyClosing = true;
if (!socket) { return; }
if (socket.realtime) { socket.realtime.abort(); }
socket.close();
};
var checkSectionEdit = function () {
var href = window.location.href;
if (href.indexOf('#') === -1) { href += '#!'; }
var si = href.indexOf('section=');
if (si === -1 || si > href.indexOf('#')) { return false; }
var m = href.match(/([&]*section=[0-9]+)/)[1];
href = href.replace(m, '');
if (m[0] === '&') { m = m.substring(1); }
href = href + '&' + m;
window.location.href = href;
return true;
};
var editor = function (websocketUrl, userName, messages, channel, demoMode, language) {
var contentInner = $('#xwikieditcontentinner');
var textArea = contentInner.find('#content');
if (!textArea.length) {
warn("WARNING: Could not find textarea to bind to");
return;
}
if (createPageMode()) { return; }
if (checkSectionEdit()) { return; }
setStyle();
var checked = (localStorage.getItem(LOCALSTORAGE_DISALLOW)) ? "" : 'checked="checked"';
var allowRealtimeCbId = uid();
$('#mainEditArea .buttons').append(
'<div class="rtwiki-allow-outerdiv">' +
'<label class="rtwiki-allow-label" for="' + allowRealtimeCbId + '">' +
'<input type="checkbox" class="rtwiki-allow" id="' + allowRealtimeCbId + '" ' +
checked + '" />' +
' ' + messages.allowRealtime +
'</label>' +
'</div>'
);
var socket;
var checkboxClick = function (checked) {
if (checked || demoMode) {
socket = startWebSocket(textArea,
contentInner,
websocketUrl,
userName,
channel,
messages,
demoMode,
language);
} else if (socket) {
localStorage.setItem(LOCALSTORAGE_DISALLOW, 1);
stopWebSocket(socket);
socket = undefined;
}
};
seekToSection(textArea, function (err) {
if (err) { throw err; }
$('#'+allowRealtimeCbId).click(function () { checkboxClick(this.checked); });
checkboxClick(checked);
});
};
var main = module.exports.main = function (websocketUrl,
userName,
messages,
channel,
demoMode,
language)
{
if (!websocketUrl) {
throw new Error("No WebSocket URL, please ensure Realtime Backend is installed.");
}
// Either we are in edit mode or the document is locked.
// There is no cross-language way that the UI tells us the document is locked
// but we can hunt for the force button.
var forceLink = $('a[href$="&force=1"][href*="/edit/"]');
var hasActiveRealtimeSession = function () {
forceLink.text(messages.joinSession);
forceLink.attr('href', forceLink.attr('href') + '&editor=wiki');
};
if (forceLink.length && !localStorage.getItem(LOCALSTORAGE_DISALLOW)) {
// ok it's locked.
var socket = new WebSocket(websocketUrl);
socket.onopen = function (evt) {
socket.onmessage = function (evt) {
debug("Message! " + evt.data);
var regMsgEnd = '3:[0]';
if (evt.data.indexOf(regMsgEnd) !== evt.data.length - regMsgEnd.length) {
// Not a register message
} else if (evt.data.indexOf(userName.length + ':' + userName) === 0) {
// It's us registering
} else {
// Someone has registered
debug("hasActiveRealtimeSession");
socket.close();
hasActiveRealtimeSession();
}
};
socket.send('1:x' + userName.length + ':' + userName +
channel.length + ':' + channel + '3:[0]');
debug("Bound websocket");
};
} else if (window.XWiki.editor === 'wiki' || demoMode) {
editor(websocketUrl, userName, messages, channel, demoMode, language);
}
};
// CodeMirror/RTWiki // CodeMirror/RTWiki
// Trapping Keyboard Events // Trapping Keyboard Events
var bindEvents = function (element, events, callback, unbind) { var bindEvents = function (element, events, callback, unbind) {
@ -760,12 +309,12 @@ define([
var incomingPatch = function () { var incomingPatch = function () {
if (isErrorState || initializing) { return; } if (isErrorState || initializing) { return; }
var textAreaVal = $(textArea).val(); var textAreaVal = $(textArea).val();
console.log($(textArea).val());
userDocBeforePatch = userDocBeforePatch || textAreaVal; userDocBeforePatch = userDocBeforePatch || textAreaVal;
if (userDocBeforePatch !== textAreaVal) { if (userDocBeforePatch !== textAreaVal) {
//error(false, "userDocBeforePatch !== textAreaVal"); //error(false, "userDocBeforePatch !== textAreaVal");
} }
var op = attempt(Otaml.makeTextOperation)(userDocBeforePatch, realtime.getUserDoc()); var op = attempt(Otaml.makeTextOperation)(userDocBeforePatch, realtime.getUserDoc());
if (typeof op === 'undefined') { if (typeof op === 'undefined') {
warn("TypeError: op is undefined"); warn("TypeError: op is undefined");
return; return;
@ -796,6 +345,7 @@ define([
} }
} }
$(textArea).val(newValue); $(textArea).val(newValue);
userDocBeforePatch = newValue;
cmEditor.setValue(newValue); cmEditor.setValue(newValue);
if(newCursor) { if(newCursor) {
cmEditor.setCursor(newCursor); cmEditor.setCursor(newCursor);
@ -884,7 +434,7 @@ define([
if (!websocketUrl) { if (!websocketUrl) {
throw new Error("No WebSocket URL, please ensure Realtime Backend is installed."); throw new Error("No WebSocket URL, please ensure Realtime Backend is installed.");
} }
var cme = cmEditor(window, websocketUrl, userName, Messages, channel, cryptkey); var cme = cmEditor(window, websocketUrl+'_old', userName, Messages, channel, cryptkey);
return { return {
onEvent: function () { onEvent: function () {
cme.onEvent(); cme.onEvent();

@ -1,263 +0,0 @@
define(function () {
/**
* Licensed under the standard MIT license:
*
* Copyright 2011 Joseph Gentle.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* See: https://github.com/share/ShareJS/blob/master/LICENSE
*/
/* This contains the textarea binding for ShareJS. This binding is really
* simple, and a bit slow on big documents (Its O(N). However, it requires no
* changes to the DOM and no heavy libraries like ace. It works for any kind of
* text input field.
*
* You probably want to use this binding for small fields on forms and such.
* For code editors or rich text editors or whatever, I recommend something
* heavier.
*/
/* applyChange creates the edits to convert oldval -> newval.
*
* This function should be called every time the text element is changed.
* Because changes are always localised, the diffing is quite easy. We simply
* scan in from the start and scan in from the end to isolate the edited range,
* then delete everything that was removed & add everything that was added.
* This wouldn't work for complex changes, but this function should be called
* on keystroke - so the edits will mostly just be single character changes.
* Sometimes they'll paste text over other text, but even then the diff
* generated by this algorithm is correct.
*
* This algorithm is O(N). I suspect you could speed it up somehow using regular expressions.
*/
var applyChange = function(ctx, oldval, newval) {
// Strings are immutable and have reference equality. I think this test is O(1), so its worth doing.
if (oldval === newval) { return; }
var commonStart = 0;
while (oldval.charAt(commonStart) === newval.charAt(commonStart)) {
commonStart++;
}
var commonEnd = 0;
while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) &&
commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) {
commonEnd++;
}
if (oldval.length !== commonStart + commonEnd) {
ctx.remove(commonStart, oldval.length - commonStart - commonEnd);
}
if (newval.length !== commonStart + commonEnd) {
ctx.insert(commonStart, newval.slice(commonStart, newval.length - commonEnd));
}
};
/**
* Fix issues with textarea content which is different per-browser.
*/
var cannonicalize = function (content) {
return content.replace(/\r\n/g, '\n');
};
// Attach a textarea to a document's editing context.
//
// The context is optional, and will be created from the document if its not
// specified.
var attachTextarea = function(elem, ctx, cmElem) {
// initial state will always fail the !== check in genop.
var content = {};
// Replace the content of the text area with newText, and transform the
// current cursor by the specified function.
var replaceText = function(newText, transformCursor, transformCursorCM) {
var newCursor;
var newSelection;
if(cmElem) {
// Fix cursor here?
var cursorCM = cmElem.getCursor();
var cursorCMStart = cmElem.getCursor('from');
var cursorCMEnd = cmElem.getCursor('to');
if(cursorCMStart !== cursorCMEnd) {
newSelection = [transformCursorCM(elem.value, cursorCMStart), transformCursorCM(elem.value, cursorCMEnd)];
}
else {
newCursor = transformCursorCM(elem.value, cursorCM);
}
}
if (transformCursor && !cmElem) {
newSelection = [transformCursor(elem.selectionStart), transformCursor(elem.selectionEnd)];
}
// Fixate the window's scroll while we set the element's value. Otherwise
// the browser scrolls to the element.
var scrollTop = elem.scrollTop;
elem.value = newText;
if(cmElem) {
// Fix cursor here?
cmElem.setValue(newText);
if(newCursor) {
cmElem.setCursor(newCursor);
}
else {
cmElem.setSelection(newSelection[0], newSelection[1]);
}
}
content = elem.value; // Not done on one line so the browser can do newline conversion.
if(!cmElem) {
if (elem.scrollTop !== scrollTop) { elem.scrollTop = scrollTop; }
// Setting the selection moves the cursor. We'll just have to let your
// cursor drift if the element isn't active, though usually users don't
// care.
if (newSelection && window.document.activeElement === elem) {
elem.selectionStart = newSelection[0];
elem.selectionEnd = newSelection[1];
}
}
};
//replaceText(ctx.get());
// *** remote -> local changes
ctx.onRemove(function(pos, length) {
var transformCursor = function(cursor) {
// If the cursor is inside the deleted region, we only want to move back to the start
// of the region. Hence the Math.min.
return pos < cursor ? cursor - Math.min(length, cursor - pos) : cursor;
};
var transformCursorCM = function(text, cursor) {
var newCursor = cursor;
var textLines = text.substr(0, pos).split("\n");
var removedTextLineNumber = textLines.length-1;
var removedTextColumnIndex = textLines[textLines.length-1].length;
var removedLines = text.substr(pos, length).split("\n").length - 1;
if(cursor.line > (removedTextLineNumber + removedLines)) {
newCursor.line -= removedLines;
}
else if(removedLines > 0 && cursor.line === (removedTextLineNumber+removedLines)) {
var lastLineCharsRemoved = text.substr(pos, length).split("\n")[removedLines].length;
if(cursor.ch >= lastLineCharsRemoved) {
newCursor.line = removedTextLineNumber;
newCursor.ch = removedTextColumnIndex + cursor.ch - lastLineCharsRemoved;
}
else {
newCursor.line -= removedLines;
newCursor.ch = removedTextColumnIndex;
}
}
else if(cursor.line === removedTextLineNumber && cursor.ch > removedTextLineNumber) {
newCursor.ch -= Math.min(length, cursor.ch-removedTextLineNumber);
}
return newCursor;
};
replaceText(ctx.getUserDoc(), transformCursor, transformCursorCM);
});
ctx.onInsert(function(pos, text) {
var transformCursor = function(cursor) {
return pos < cursor ? cursor + text.length : cursor;
};
var transformCursorCM = function(oldtext, cursor) {
var newCursor = cursor;
var textLines = oldtext.substr(0, pos).split("\n");
var addedTextLineNumber = textLines.length-1;
var addedTextColumnIndex = textLines[textLines.length-1].length;
var addedLines = text.split("\n").length - 1;
if(cursor.line > addedTextLineNumber) {
newCursor.line += addedLines;
}
else if(cursor.line === addedTextLineNumber && cursor.ch > addedTextColumnIndex) {
newCursor.line += addedLines;
if(addedLines > 0) {
newCursor.ch = newCursor.ch - addedTextColumnIndex + text.split("\n")[addedLines].length;
}
else {
newCursor.ch += text.split("\n")[addedLines].length;
}
}
return newCursor;
};
replaceText(ctx.getUserDoc(), transformCursor, transformCursorCM);
});
// *** local -> remote changes
// This function generates operations from the changed content in the textarea.
var genOp = function() {
// In a timeout so the browser has time to propogate the event's changes to the DOM.
setTimeout(function() {
var val = elem.value;
if (val !== content) {
applyChange(ctx, ctx.getUserDoc(), cannonicalize(val));
}
}, 0);
};
var eventNames = ['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste'];
for (var i = 0; i < eventNames.length; i++) {
var e = eventNames[i];
if (elem.addEventListener) {
elem.addEventListener(e, genOp, false);
} else {
elem.attachEvent('on' + e, genOp);
}
}
window.setTimeout(function() {
if(cmElem) {
var elem2 = cmElem;
elem2.on('change', function() {
elem2.save();
genOp();
});
}
else {
console.log('CM inexistant');
}
}, 500);
ctx.detach = function() {
for (var i = 0; i < eventNames.length; i++) {
var e = eventNames[i];
if (elem.removeEventListener) {
elem.removeEventListener(e, genOp, false);
} else {
elem.detachEvent('on' + e, genOp);
}
}
};
return ctx;
};
return { attach: attachTextarea };
});

@ -0,0 +1,246 @@
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 getOtherUsers = function(myUserName, userList) {
var i = 0;
var list = '';
userList.forEach(function(user) {
if(user !== myUserName) {
if(i === 0) list = ' : ';
list += user + ', ';
i++;
}
});
return (i > 0) ? list.slice(0, -2) : list;
}
var updateUserList = function (myUserName, listElement, userList) {
var meIdx = userList.indexOf(myUserName);
if (meIdx === -1) {
listElement.textContent = Messages.synchronizing;
return;
}
if (userList.length === 1) {
listElement.innerHTML = Messages.editingAlone;
} else if (userList.length === 2) {
listElement.innerHTML = Messages.editingWithOneOtherPerson + getOtherUsers(myUserName, userList);
} else {
listElement.innerHTML = Messages.editingWith + ' ' + (userList.length - 1) + ' ' + Messages.otherPeople + getOtherUsers(myUserName, userList);
}
};
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 };
});
Loading…
Cancel
Save