remove old pad entirely
hyperjson version is considered a strict improvementpull/1/head
parent
010566d3c3
commit
5db487db3f
@ -1,93 +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/>.
|
||||
*/
|
||||
require.config({
|
||||
'shim': {
|
||||
'/bower_components/modalBox/modalBox-min.js': [
|
||||
'/bower_components/jquery/dist/jquery.min.js'
|
||||
],
|
||||
}
|
||||
});
|
||||
define([
|
||||
'/common/messages.js',
|
||||
'/bower_components/modalBox/modalBox-min.js'
|
||||
], function (Messages) {
|
||||
var $ = window.jQuery;
|
||||
|
||||
var STYLE = [
|
||||
'<style>',
|
||||
'.modalBox {',
|
||||
' padding:5px;',
|
||||
' border:1px solid #CCC;',
|
||||
' background:#FFF;',
|
||||
' height:500px;',
|
||||
' width:700px;',
|
||||
' display:none;',
|
||||
'}',
|
||||
'img.iw-closeImg {',
|
||||
' width:24px;',
|
||||
' height:24px',
|
||||
'}',
|
||||
'.modalFooter {',
|
||||
' color:#FFF;',
|
||||
' position:absolute;',
|
||||
' bottom:0px',
|
||||
'}',
|
||||
'.modalFooter span {',
|
||||
' cursor:pointer;',
|
||||
'}',
|
||||
'.iw-modalOverlay {',
|
||||
' background:#000;',
|
||||
' opacity:.5',
|
||||
'}',
|
||||
'</style>'
|
||||
].join('');
|
||||
|
||||
var CONTENT = [
|
||||
'<center><h2 class="errorType"></h2></center>',
|
||||
'<br>',
|
||||
'<p class="errorExplanation"></p>'
|
||||
].join('');
|
||||
|
||||
var ERROR_ADDITIONAL = [
|
||||
'<p class="errorMoreExplanation"></p>',
|
||||
'<label for="errorBox_detailsBox" class="errorDetailsLabel"></label>',
|
||||
'<textarea id="errorBox_detailsBox" class="errorData"></textarea>',
|
||||
].join('');
|
||||
|
||||
var showError = function (errorType, docHtml, moreInfo) {
|
||||
$('body').append('<div class="modalBox"></div>');
|
||||
var $modalbox = $('.modalBox');
|
||||
$modalbox.append(CONTENT + STYLE);
|
||||
|
||||
$modalbox.find('.errorType').text(Messages['errorBox_errorType_' + errorType]);
|
||||
$modalbox.find('.errorExplanation').text(Messages['errorBox_errorExplanation_' + errorType]);
|
||||
if (moreInfo) {
|
||||
$modalbox.append(ERROR_ADDITIONAL);
|
||||
$modalbox.find('.errorMoreExplanation').text(Messages.errorBox_moreExplanation);
|
||||
$modalbox.find('.errorData').text(Messages['errorBox_' + errorType]);
|
||||
}
|
||||
|
||||
$modalbox.modalBox({
|
||||
onClose: function () { $('.modalBox').remove(); }
|
||||
});
|
||||
$('.iw-modalOverlay').css({'z-index':10000});
|
||||
};
|
||||
|
||||
return {
|
||||
show: showError
|
||||
};
|
||||
});
|
@ -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,26 +0,0 @@
|
||||
<!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>
|
||||
#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>
|
||||
|
@ -1,12 +0,0 @@
|
||||
<!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>
|
||||
|
@ -1,58 +0,0 @@
|
||||
define([
|
||||
'/api/config?cb=' + Math.random().toString(16).substring(2),
|
||||
'/pad/realtime-wysiwyg.js',
|
||||
'/common/messages.js',
|
||||
'/common/crypto.js',
|
||||
'/bower_components/jquery/dist/jquery.min.js',
|
||||
'/customize/pad.js'
|
||||
], function (Config, RTWysiwyg, Messages, Crypto) {
|
||||
var $ = window.jQuery;
|
||||
var ifrw = $('#pad-iframe')[0].contentWindow;
|
||||
var Ckeditor = ifrw.CKEDITOR;
|
||||
|
||||
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 key = Crypto.parseKey(window.location.hash.substring(1));
|
||||
var editor = Ckeditor.replace('editor1', {
|
||||
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
|
||||
removePlugins: 'magicline,resize'
|
||||
});
|
||||
editor.on('instanceReady', function () {
|
||||
editor.execCommand('maximize');
|
||||
|
||||
// (contenteditable) iframe in an iframe
|
||||
ifrw.$('iframe')[0].contentDocument.body.innerHTML = Messages.initialState;
|
||||
|
||||
var rtw =
|
||||
RTWysiwyg.start(ifrw, // window
|
||||
Config.websocketURL, // websocketUrl
|
||||
Crypto.rand64(8), // userName
|
||||
key.channel, // channel
|
||||
key.cryptKey); // cryptKey
|
||||
editor.on('change', function () { rtw.onEvent(); });
|
||||
});
|
||||
window.editor = editor;
|
||||
window.RTWysiwyg = RTWysiwyg;
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -1,395 +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([
|
||||
'/pad/html-patcher.js',
|
||||
'/pad/errorbox.js',
|
||||
'/common/messages.js',
|
||||
'/bower_components/reconnectingWebsocket/reconnecting-websocket.js',
|
||||
'/common/crypto.js',
|
||||
'/common/toolbar.js',
|
||||
'/pad/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 (wysiwygDiv, docBody, onEvent, unbind)
|
||||
{
|
||||
bindEvents(docBody,
|
||||
['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste'],
|
||||
onEvent,
|
||||
unbind);
|
||||
bindEvents(wysiwygDiv,
|
||||
['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.body.innerHTML;
|
||||
};
|
||||
|
||||
var makeHTMLOperation = function (oldval, newval) {
|
||||
try {
|
||||
var op = Otaml.makeHTMLOperation(oldval, newval);
|
||||
|
||||
if (PARANOIA && op) {
|
||||
// simulate running the patch.
|
||||
var res = HTMLPatcher.patchString(oldval, op.offset, op.toRemove, op.toInsert);
|
||||
if (res !== newval) {
|
||||
console.log(op);
|
||||
console.log(oldval);
|
||||
console.log(newval);
|
||||
console.log(res);
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
// check matching bracket count
|
||||
// TODO(cjd): this can fail even if the patch is valid because of brackets in
|
||||
// html attributes.
|
||||
var removeText = oldval.substring(op.offset, op.offset + op.toRemove);
|
||||
if (((removeText).match(/</g) || []).length !==
|
||||
((removeText).match(/>/g) || []).length)
|
||||
{
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (((op.toInsert).match(/</g) || []).length !==
|
||||
((op.toInsert).match(/>/g) || []).length)
|
||||
{
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
return op;
|
||||
|
||||
} catch (e) {
|
||||
if (PARANOIA) {
|
||||
$(document.body).append('<textarea id="makeOperationErr"></textarea>');
|
||||
$('#makeOperationErr').val(oldval + '\n\n\n\n\n\n\n\n\n\n' + newval);
|
||||
console.log(e.stack);
|
||||
}
|
||||
return {
|
||||
offset: 0,
|
||||
toRemove: oldval.length,
|
||||
toInsert: newval
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// chrome sometimes generates invalid html but it corrects it the next time around.
|
||||
var fixChrome = function (docText, doc, contentWindow) {
|
||||
for (var i = 0; i < 10; i++) {
|
||||
var docElem = doc.createElement('div');
|
||||
docElem.innerHTML = docText;
|
||||
var newDocText = docElem.innerHTML;
|
||||
var fixChromeOp = makeHTMLOperation(docText, newDocText);
|
||||
if (!fixChromeOp) { return docText; }
|
||||
HTMLPatcher.applyOp(docText,
|
||||
fixChromeOp,
|
||||
doc.body,
|
||||
Rangy,
|
||||
contentWindow);
|
||||
docText = getDocHTML(doc);
|
||||
if (newDocText === docText) { return docText; }
|
||||
}
|
||||
throw new Error();
|
||||
};
|
||||
|
||||
var fixSafari_STATE_OUTSIDE = 0;
|
||||
var fixSafari_STATE_IN_TAG = 1;
|
||||
var fixSafari_STATE_IN_ATTR = 2;
|
||||
var fixSafari_HTML_ENTITIES_REGEX = /('|"|<|>|<|>)/g;
|
||||
|
||||
var fixSafari = function (html) {
|
||||
var state = fixSafari_STATE_OUTSIDE;
|
||||
return html.replace(fixSafari_HTML_ENTITIES_REGEX, function (x) {
|
||||
switch (state) {
|
||||
case fixSafari_STATE_OUTSIDE: {
|
||||
if (x === '<') { state = fixSafari_STATE_IN_TAG; }
|
||||
return x;
|
||||
}
|
||||
case fixSafari_STATE_IN_TAG: {
|
||||
switch (x) {
|
||||
case '"': state = fixSafari_STATE_IN_ATTR; break;
|
||||
case '>': state = fixSafari_STATE_OUTSIDE; break;
|
||||
case "'": throw new Error("single quoted attribute");
|
||||
}
|
||||
return x;
|
||||
}
|
||||
case fixSafari_STATE_IN_ATTR: {
|
||||
switch (x) {
|
||||
case '<': return '<';
|
||||
case '>': return '>';
|
||||
case '"': state = fixSafari_STATE_IN_TAG; break;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
}
|
||||
throw new Error();
|
||||
});
|
||||
};
|
||||
|
||||
var getFixedDocText = function (doc, ifrWindow) {
|
||||
var docText = getDocHTML(doc);
|
||||
docText = fixChrome(docText, doc, ifrWindow);
|
||||
docText = fixSafari(docText);
|
||||
return docText;
|
||||
};
|
||||
|
||||
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 ifr = wysiwygDiv.getElementsByTagName('iframe')[0];
|
||||
var doc = ifr.contentWindow.document;
|
||||
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.$('#cke_1_toolbox'), userName, realtime);
|
||||
|
||||
onEvent = function () {
|
||||
if (isErrorState) { return; }
|
||||
if (initializing) { return; }
|
||||
|
||||
var oldDocText = realtime.getUserDoc();
|
||||
var docText = getFixedDocText(doc, ifr.contentWindow);
|
||||
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 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)");
|
||||
}
|
||||
var op = attempt(makeHTMLOperation)(userDocBeforePatch, realtime.getUserDoc());
|
||||
if (!op) { return; }
|
||||
attempt(HTMLPatcher.applyOp)(
|
||||
userDocBeforePatch, op, doc.body, Rangy, ifr.contentWindow);
|
||||
console.log("after patch " + now());
|
||||
};
|
||||
|
||||
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(wysiwygDiv, doc.body, onEvent, false);
|
||||
|
||||
setInterval(function () {
|
||||
if (isErrorState || checkSocket()) {
|
||||
toolbar.reconnecting();
|
||||
}
|
||||
}, 200);
|
||||
|
||||
realtime.start();
|
||||
toolbar.connected();
|
||||
|
||||
//console.log('started');
|
||||
});
|
||||
return {
|
||||
onEvent: function () { onEvent(); }
|
||||
};
|
||||
};
|
||||
|
||||
return module.exports;
|
||||
});
|
Loading…
Reference in New Issue