diff --git a/www/code/html-patcher.js b/www/code/html-patcher.js
deleted file mode 100644
index d36552d85..000000000
--- a/www/code/html-patcher.js
+++ /dev/null
@@ -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
- element, for example), we need a slightly complicated approach to get the boundary's offset in IE. The
- facts:
-
- - Each line break is represented as \r in the text node's data/nodeValue properties
- - Each line break is represented as \r\n in the TextRange's 'text' property
- - The 'text' property of the TextRange does not contain trailing line breaks
-
- To get round the problem presented by the final fact above, we can use the fact that TextRange's
- moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily
- the same as the number of characters it was instructed to move. The simplest approach is to use this to
- store the characters moved when moving both the start and end of the range to the start of the document
- body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).
- However, this is extremely slow when the document is large and the range is near the end of it. Clearly
- doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same
- problem.
-
- Another approach that works is to use moveStart() to move the start boundary of the range up to the end
- boundary one character at a time and incrementing a counter with the value returned by the moveStart()
- call. However, the check for whether the start boundary has reached the end boundary is expensive, so
- this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of
- the range within the document).
-
- The method below is a hybrid of the two methods above. It uses the fact that a string containing the
- TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the
- text of the TextRange, so the start of the range is moved that length initially and then a character at
- a time to make up for any trailing line breaks not contained in the 'text' property. This has good
- performance in most situations compared to the previous two methods.
- */
- var tempRange = workingRange.duplicate();
- var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
-
- offset = tempRange.moveStart("character", rangeLength);
- while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
- offset++;
- tempRange.moveStart("character", 1);
- }
- } else {
- offset = workingRange.text.length;
- }
- boundaryPosition = new DomPosition(boundaryNode, offset);
- } else {
-
- // If the boundary immediately follows a character data node and this is the end boundary, we should favour
- // a position within that, and likewise for a start boundary preceding a character data node
- previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
- nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
- if (nextNode && isCharacterDataNode(nextNode)) {
- boundaryPosition = new DomPosition(nextNode, 0);
- } else if (previousNode && isCharacterDataNode(previousNode)) {
- boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
- } else {
- boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
- }
- }
-
- // Clean up
- workingNode.parentNode.removeChild(workingNode);
-
- return {
- boundaryPosition: boundaryPosition,
- nodeInfo: {
- nodeIndex: nodeIndex,
- containerElement: containerElement
- }
- };
- };
-
- // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
- // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
- // (http://code.google.com/p/ierange/)
- var createBoundaryTextRange = function(boundaryPosition, isStart) {
- var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
- var doc = dom.getDocument(boundaryPosition.node);
- var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
- var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
-
- if (nodeIsDataNode) {
- boundaryNode = boundaryPosition.node;
- boundaryParent = boundaryNode.parentNode;
- } else {
- childNodes = boundaryPosition.node.childNodes;
- boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
- boundaryParent = boundaryPosition.node;
- }
-
- // Position the range immediately before the node containing the boundary
- workingNode = doc.createElement("span");
-
- // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the
- // element rather than immediately before or after it
- workingNode.innerHTML = "feff;";
-
- // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
- // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
- if (boundaryNode) {
- boundaryParent.insertBefore(workingNode, boundaryNode);
- } else {
- boundaryParent.appendChild(workingNode);
- }
-
- workingRange.moveToElementText(workingNode);
- workingRange.collapse(!isStart);
-
- // Clean up
- boundaryParent.removeChild(workingNode);
-
- // Move the working range to the text offset, if required
- if (nodeIsDataNode) {
- workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
- }
-
- return workingRange;
- };
-
- /*------------------------------------------------------------------------------------------------------------*/
-
- // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
- // prototype
-
- WrappedTextRange = function(textRange) {
- this.textRange = textRange;
- this.refresh();
- };
-
- WrappedTextRange.prototype = new DomRange(document);
-
- WrappedTextRange.prototype.refresh = function() {
- var start, end, startBoundary;
-
- // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
- var rangeContainerElement = getTextRangeContainerElement(this.textRange);
-
- if (textRangeIsCollapsed(this.textRange)) {
- end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
- true).boundaryPosition;
- } else {
- startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
- start = startBoundary.boundaryPosition;
-
- // An optimization used here is that if the start and end boundaries have the same parent element, the
- // search scope for the end boundary can be limited to exclude the portion of the element that precedes
- // the start boundary
- end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
- startBoundary.nodeInfo).boundaryPosition;
- }
-
- this.setStart(start.node, start.offset);
- this.setEnd(end.node, end.offset);
- };
-
- WrappedTextRange.prototype.getName = function() {
- return "WrappedTextRange";
- };
-
- DomRange.copyComparisonConstants(WrappedTextRange);
-
- WrappedTextRange.rangeToTextRange = function(range) {
- if (range.collapsed) {
- return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
- } else {
- var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
- var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
- var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
- textRange.setEndPoint("StartToStart", startRange);
- textRange.setEndPoint("EndToEnd", endRange);
- return textRange;
- }
- };
-
- api.WrappedTextRange = WrappedTextRange;
-
- // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
- // implementation to use by default.
- if (!api.features.implementsDomRange || api.config.preferTextRange) {
- // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
- var globalObj = (function() { return this; })();
- if (typeof globalObj.Range == "undefined") {
- globalObj.Range = WrappedTextRange;
- }
-
- api.createNativeRange = function(doc) {
- doc = getContentDocument(doc, module, "createNativeRange");
- return getBody(doc).createTextRange();
- };
-
- api.WrappedRange = WrappedTextRange;
- }
- }
-
- api.createRange = function(doc) {
- doc = getContentDocument(doc, module, "createRange");
- return new api.WrappedRange(api.createNativeRange(doc));
- };
-
- api.createRangyRange = function(doc) {
- doc = getContentDocument(doc, module, "createRangyRange");
- return new DomRange(doc);
- };
-
- api.createIframeRange = function(iframeEl) {
- module.deprecationNotice("createIframeRange()", "createRange(iframeEl)");
- return api.createRange(iframeEl);
- };
-
- api.createIframeRangyRange = function(iframeEl) {
- module.deprecationNotice("createIframeRangyRange()", "createRangyRange(iframeEl)");
- return api.createRangyRange(iframeEl);
- };
-
- api.addCreateMissingNativeApiListener(function(win) {
- var doc = win.document;
- if (typeof doc.createRange == "undefined") {
- doc.createRange = function() {
- return api.createRange(doc);
- };
- }
- doc = win = null;
- });
-});
-// This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
-// in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
-rangy.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
- api.config.checkSelectionRanges = true;
-
- var BOOLEAN = "boolean";
- var NUMBER = "number";
- var dom = api.dom;
- var util = api.util;
- var isHostMethod = util.isHostMethod;
- var DomRange = api.DomRange;
- var WrappedRange = api.WrappedRange;
- var DOMException = api.DOMException;
- var DomPosition = dom.DomPosition;
- var getNativeSelection;
- var selectionIsCollapsed;
- var features = api.features;
- var CONTROL = "Control";
- var getDocument = dom.getDocument;
- var getBody = dom.getBody;
- var rangesEqual = DomRange.rangesEqual;
-
-
- // Utility function to support direction parameters in the API that may be a string ("backward" or "forward") or a
- // Boolean (true for backwards).
- function isDirectionBackward(dir) {
- return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
- }
-
- function getWindow(win, methodName) {
- if (!win) {
- return window;
- } else if (dom.isWindow(win)) {
- return win;
- } else if (win instanceof WrappedSelection) {
- return win.win;
- } else {
- var doc = dom.getContentDocument(win, module, methodName);
- return dom.getWindow(doc);
- }
- }
-
- function getWinSelection(winParam) {
- return getWindow(winParam, "getWinSelection").getSelection();
- }
-
- function getDocSelection(winParam) {
- return getWindow(winParam, "getDocSelection").document.selection;
- }
-
- function winSelectionIsBackward(sel) {
- var backward = false;
- if (sel.anchorNode) {
- backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
- }
- return backward;
- }
-
- // Test for the Range/TextRange and Selection features required
- // Test for ability to retrieve selection
- var implementsWinGetSelection = isHostMethod(window, "getSelection"),
- implementsDocSelection = util.isHostObject(document, "selection");
-
- features.implementsWinGetSelection = implementsWinGetSelection;
- features.implementsDocSelection = implementsDocSelection;
-
- var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
-
- if (useDocumentSelection) {
- getNativeSelection = getDocSelection;
- api.isSelectionValid = function(winParam) {
- var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
-
- // Check whether the selection TextRange is actually contained within the correct document
- return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
- };
- } else if (implementsWinGetSelection) {
- getNativeSelection = getWinSelection;
- api.isSelectionValid = function() {
- return true;
- };
- } else {
- module.fail("Neither document.selection or window.getSelection() detected.");
- }
-
- api.getNativeSelection = getNativeSelection;
-
- var testSelection = getNativeSelection();
- var testRange = api.createNativeRange(document);
- var body = getBody(document);
-
- // Obtaining a range from a selection
- var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
- ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
-
- features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
-
- // Test for existence of native selection extend() method
- var selectionHasExtend = isHostMethod(testSelection, "extend");
- features.selectionHasExtend = selectionHasExtend;
-
- // Test if rangeCount exists
- var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
- features.selectionHasRangeCount = selectionHasRangeCount;
-
- var selectionSupportsMultipleRanges = false;
- var collapsedNonEditableSelectionsSupported = true;
-
- var addRangeBackwardToNative = selectionHasExtend ?
- function(nativeSelection, range) {
- var doc = DomRange.getRangeDocument(range);
- var endRange = api.createRange(doc);
- endRange.collapseToPoint(range.endContainer, range.endOffset);
- nativeSelection.addRange(getNativeRange(endRange));
- nativeSelection.extend(range.startContainer, range.startOffset);
- } : null;
-
- if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
- typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
-
- (function() {
- // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
- // performed on the current document's selection. See issue 109.
-
- // Note also that if a selection previously existed, it is wiped by these tests. This should usually be fine
- // because initialization usually happens when the document loads, but could be a problem for a script that
- // loads and initializes Rangy later. If anyone complains, code could be added to save and restore the
- // selection.
- var sel = window.getSelection();
- if (sel) {
- // Store the current selection
- var originalSelectionRangeCount = sel.rangeCount;
- var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
- var originalSelectionRanges = [];
- var originalSelectionBackward = winSelectionIsBackward(sel);
- for (var i = 0; i < originalSelectionRangeCount; ++i) {
- originalSelectionRanges[i] = sel.getRangeAt(i);
- }
-
- // Create some test elements
- var body = getBody(document);
- var testEl = body.appendChild( document.createElement("div") );
- testEl.contentEditable = "false";
- var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
-
- // Test whether the native selection will allow a collapsed selection within a non-editable element
- var r1 = document.createRange();
-
- r1.setStart(textNode, 1);
- r1.collapse(true);
- sel.addRange(r1);
- collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
- sel.removeAllRanges();
-
- // Test whether the native selection is capable of supporting multiple ranges
- if (!selectionHasMultipleRanges) {
- var r2 = r1.cloneRange();
- r1.setStart(textNode, 0);
- r2.setEnd(textNode, 3);
- r2.setStart(textNode, 2);
- sel.addRange(r1);
- sel.addRange(r2);
-
- selectionSupportsMultipleRanges = (sel.rangeCount == 2);
- r2.detach();
- }
-
- // Clean up
- body.removeChild(testEl);
- sel.removeAllRanges();
- r1.detach();
-
- for (i = 0; i < originalSelectionRangeCount; ++i) {
- if (i == 0 && originalSelectionBackward) {
- if (addRangeBackwardToNative) {
- addRangeBackwardToNative(sel, originalSelectionRanges[i]);
- } else {
- api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because browser does not support Selection.extend");
- sel.addRange(originalSelectionRanges[i])
- }
- } else {
- sel.addRange(originalSelectionRanges[i])
- }
- }
- }
- })();
- }
-
- features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
- features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
-
- // ControlRanges
- var implementsControlRange = false, testControlRange;
-
- if (body && isHostMethod(body, "createControlRange")) {
- testControlRange = body.createControlRange();
- if (util.areHostProperties(testControlRange, ["item", "add"])) {
- implementsControlRange = true;
- }
- }
- features.implementsControlRange = implementsControlRange;
-
- // Selection collapsedness
- if (selectionHasAnchorAndFocus) {
- selectionIsCollapsed = function(sel) {
- return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
- };
- } else {
- selectionIsCollapsed = function(sel) {
- return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
- };
- }
-
- function updateAnchorAndFocusFromRange(sel, range, backward) {
- var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
- sel.anchorNode = range[anchorPrefix + "Container"];
- sel.anchorOffset = range[anchorPrefix + "Offset"];
- sel.focusNode = range[focusPrefix + "Container"];
- sel.focusOffset = range[focusPrefix + "Offset"];
- }
-
- function updateAnchorAndFocusFromNativeSelection(sel) {
- var nativeSel = sel.nativeSelection;
- sel.anchorNode = nativeSel.anchorNode;
- sel.anchorOffset = nativeSel.anchorOffset;
- sel.focusNode = nativeSel.focusNode;
- sel.focusOffset = nativeSel.focusOffset;
- }
-
- function updateEmptySelection(sel) {
- sel.anchorNode = sel.focusNode = null;
- sel.anchorOffset = sel.focusOffset = 0;
- sel.rangeCount = 0;
- sel.isCollapsed = true;
- sel._ranges.length = 0;
- }
-
- function getNativeRange(range) {
- var nativeRange;
- if (range instanceof DomRange) {
- nativeRange = api.createNativeRange(range.getDocument());
- nativeRange.setEnd(range.endContainer, range.endOffset);
- nativeRange.setStart(range.startContainer, range.startOffset);
- } else if (range instanceof WrappedRange) {
- nativeRange = range.nativeRange;
- } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
- nativeRange = range;
- }
- return nativeRange;
- }
-
- function rangeContainsSingleElement(rangeNodes) {
- if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
- return false;
- }
- for (var i = 1, len = rangeNodes.length; i < len; ++i) {
- if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
- return false;
- }
- }
- return true;
- }
-
- function getSingleElementFromRange(range) {
- var nodes = range.getNodes();
- if (!rangeContainsSingleElement(nodes)) {
- throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
- }
- return nodes[0];
- }
-
- // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
- function isTextRange(range) {
- return !!range && typeof range.text != "undefined";
- }
-
- function updateFromTextRange(sel, range) {
- // Create a Range from the selected TextRange
- var wrappedRange = new WrappedRange(range);
- sel._ranges = [wrappedRange];
-
- updateAnchorAndFocusFromRange(sel, wrappedRange, false);
- sel.rangeCount = 1;
- sel.isCollapsed = wrappedRange.collapsed;
- }
-
- function updateControlSelection(sel) {
- // Update the wrapped selection based on what's now in the native selection
- sel._ranges.length = 0;
- if (sel.docSelection.type == "None") {
- updateEmptySelection(sel);
- } else {
- var controlRange = sel.docSelection.createRange();
- if (isTextRange(controlRange)) {
- // This case (where the selection type is "Control" and calling createRange() on the selection returns
- // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
- // ControlRange have been removed from the ControlRange and removed from the document.
- updateFromTextRange(sel, controlRange);
- } else {
- sel.rangeCount = controlRange.length;
- var range, doc = getDocument(controlRange.item(0));
- for (var i = 0; i < sel.rangeCount; ++i) {
- range = api.createRange(doc);
- range.selectNode(controlRange.item(i));
- sel._ranges.push(range);
- }
- sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
- updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
- }
- }
- }
-
- function addRangeToControlSelection(sel, range) {
- var controlRange = sel.docSelection.createRange();
- var rangeElement = getSingleElementFromRange(range);
-
- // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
- // contained by the supplied range
- var doc = getDocument(controlRange.item(0));
- var newControlRange = getBody(doc).createControlRange();
- for (var i = 0, len = controlRange.length; i < len; ++i) {
- newControlRange.add(controlRange.item(i));
- }
- try {
- newControlRange.add(rangeElement);
- } catch (ex) {
- throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
- }
- newControlRange.select();
-
- // Update the wrapped selection based on what's now in the native selection
- updateControlSelection(sel);
- }
-
- var getSelectionRangeAt;
-
- if (isHostMethod(testSelection, "getRangeAt")) {
- // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
- // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
- // lesson to us all, especially me.
- getSelectionRangeAt = function(sel, index) {
- try {
- return sel.getRangeAt(index);
- } catch (ex) {
- return null;
- }
- };
- } else if (selectionHasAnchorAndFocus) {
- getSelectionRangeAt = function(sel) {
- var doc = getDocument(sel.anchorNode);
- var range = api.createRange(doc);
- range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
-
- // Handle the case when the selection was selected backwards (from the end to the start in the
- // document)
- if (range.collapsed !== this.isCollapsed) {
- range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
- }
-
- return range;
- };
- }
-
- function WrappedSelection(selection, docSelection, win) {
- this.nativeSelection = selection;
- this.docSelection = docSelection;
- this._ranges = [];
- this.win = win;
- this.refresh();
- }
-
- WrappedSelection.prototype = api.selectionPrototype;
-
- function deleteProperties(sel) {
- sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
- sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
- sel.detached = true;
- }
-
- var cachedRangySelections = [];
-
- function actOnCachedSelection(win, action) {
- var i = cachedRangySelections.length, cached, sel;
- while (i--) {
- cached = cachedRangySelections[i];
- sel = cached.selection;
- if (action == "deleteAll") {
- deleteProperties(sel);
- } else if (cached.win == win) {
- if (action == "delete") {
- cachedRangySelections.splice(i, 1);
- return true;
- } else {
- return sel;
- }
- }
- }
- if (action == "deleteAll") {
- cachedRangySelections.length = 0;
- }
- return null;
- }
-
- var getSelection = function(win) {
- // Check if the parameter is a Rangy Selection object
- if (win && win instanceof WrappedSelection) {
- win.refresh();
- return win;
- }
-
- win = getWindow(win, "getNativeSelection");
-
- var sel = actOnCachedSelection(win);
- var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
- if (sel) {
- sel.nativeSelection = nativeSel;
- sel.docSelection = docSel;
- sel.refresh();
- } else {
- sel = new WrappedSelection(nativeSel, docSel, win);
- cachedRangySelections.push( { win: win, selection: sel } );
- }
- return sel;
- };
-
- api.getSelection = getSelection;
-
- api.getIframeSelection = function(iframeEl) {
- module.deprecationNotice("getIframeSelection()", "getSelection(iframeEl)");
- return api.getSelection(dom.getIframeWindow(iframeEl));
- };
-
- var selProto = WrappedSelection.prototype;
-
- function createControlSelection(sel, ranges) {
- // Ensure that the selection becomes of type "Control"
- var doc = getDocument(ranges[0].startContainer);
- var controlRange = getBody(doc).createControlRange();
- for (var i = 0, el, len = ranges.length; i < len; ++i) {
- el = getSingleElementFromRange(ranges[i]);
- try {
- controlRange.add(el);
- } catch (ex) {
- throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
- }
- }
- controlRange.select();
-
- // Update the wrapped selection based on what's now in the native selection
- updateControlSelection(sel);
- }
-
- // Selecting a range
- if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
- selProto.removeAllRanges = function() {
- this.nativeSelection.removeAllRanges();
- updateEmptySelection(this);
- };
-
- var addRangeBackward = function(sel, range) {
- addRangeBackwardToNative(sel.nativeSelection, range);
- sel.refresh();
- };
-
- if (selectionHasRangeCount) {
- selProto.addRange = function(range, direction) {
- if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
- addRangeToControlSelection(this, range);
- } else {
- if (isDirectionBackward(direction) && selectionHasExtend) {
- addRangeBackward(this, range);
- } else {
- var previousRangeCount;
- if (selectionSupportsMultipleRanges) {
- previousRangeCount = this.rangeCount;
- } else {
- this.removeAllRanges();
- previousRangeCount = 0;
- }
- // Clone the native range so that changing the selected range does not affect the selection.
- // This is contrary to the spec but is the only way to achieve consistency between browsers. See
- // issue 80.
- this.nativeSelection.addRange(getNativeRange(range).cloneRange());
-
- // Check whether adding the range was successful
- this.rangeCount = this.nativeSelection.rangeCount;
-
- if (this.rangeCount == previousRangeCount + 1) {
- // The range was added successfully
-
- // Check whether the range that we added to the selection is reflected in the last range extracted from
- // the selection
- if (api.config.checkSelectionRanges) {
- var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
- if (nativeRange && !rangesEqual(nativeRange, range)) {
- // Happens in WebKit with, for example, a selection placed at the start of a text node
- range = new WrappedRange(nativeRange);
- }
- }
- this._ranges[this.rangeCount - 1] = range;
- updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
- this.isCollapsed = selectionIsCollapsed(this);
- } else {
- // The range was not added successfully. The simplest thing is to refresh
- this.refresh();
- }
- }
- }
- };
- } else {
- selProto.addRange = function(range, direction) {
- if (isDirectionBackward(direction) && selectionHasExtend) {
- addRangeBackward(this, range);
- } else {
- this.nativeSelection.addRange(getNativeRange(range));
- this.refresh();
- }
- };
- }
-
- selProto.setRanges = function(ranges) {
- if (implementsControlRange && ranges.length > 1) {
- createControlSelection(this, ranges);
- } else {
- this.removeAllRanges();
- for (var i = 0, len = ranges.length; i < len; ++i) {
- this.addRange(ranges[i]);
- }
- }
- };
- } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
- implementsControlRange && useDocumentSelection) {
-
- selProto.removeAllRanges = function() {
- // Added try/catch as fix for issue #21
- try {
- this.docSelection.empty();
-
- // Check for empty() not working (issue #24)
- if (this.docSelection.type != "None") {
- // Work around failure to empty a control selection by instead selecting a TextRange and then
- // calling empty()
- var doc;
- if (this.anchorNode) {
- doc = getDocument(this.anchorNode);
- } else if (this.docSelection.type == CONTROL) {
- var controlRange = this.docSelection.createRange();
- if (controlRange.length) {
- doc = getDocument( controlRange.item(0) );
- }
- }
- if (doc) {
- var textRange = getBody(doc).createTextRange();
- textRange.select();
- this.docSelection.empty();
- }
- }
- } catch(ex) {}
- updateEmptySelection(this);
- };
-
- selProto.addRange = function(range) {
- if (this.docSelection.type == CONTROL) {
- addRangeToControlSelection(this, range);
- } else {
- api.WrappedTextRange.rangeToTextRange(range).select();
- this._ranges[0] = range;
- this.rangeCount = 1;
- this.isCollapsed = this._ranges[0].collapsed;
- updateAnchorAndFocusFromRange(this, range, false);
- }
- };
-
- selProto.setRanges = function(ranges) {
- this.removeAllRanges();
- var rangeCount = ranges.length;
- if (rangeCount > 1) {
- createControlSelection(this, ranges);
- } else if (rangeCount) {
- this.addRange(ranges[0]);
- }
- };
- } else {
- module.fail("No means of selecting a Range or TextRange was found");
- return false;
- }
-
- selProto.getRangeAt = function(index) {
- if (index < 0 || index >= this.rangeCount) {
- throw new DOMException("INDEX_SIZE_ERR");
- } else {
- // Clone the range to preserve selection-range independence. See issue 80.
- return this._ranges[index].cloneRange();
- }
- };
-
- var refreshSelection;
-
- if (useDocumentSelection) {
- refreshSelection = function(sel) {
- var range;
- if (api.isSelectionValid(sel.win)) {
- range = sel.docSelection.createRange();
- } else {
- range = getBody(sel.win.document).createTextRange();
- range.collapse(true);
- }
-
- if (sel.docSelection.type == CONTROL) {
- updateControlSelection(sel);
- } else if (isTextRange(range)) {
- updateFromTextRange(sel, range);
- } else {
- updateEmptySelection(sel);
- }
- };
- } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
- refreshSelection = function(sel) {
- if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
- updateControlSelection(sel);
- } else {
- sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
- if (sel.rangeCount) {
- for (var i = 0, len = sel.rangeCount; i < len; ++i) {
- sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
- }
- updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
- sel.isCollapsed = selectionIsCollapsed(sel);
- } else {
- updateEmptySelection(sel);
- }
- }
- };
- } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
- refreshSelection = function(sel) {
- var range, nativeSel = sel.nativeSelection;
- if (nativeSel.anchorNode) {
- range = getSelectionRangeAt(nativeSel, 0);
- sel._ranges = [range];
- sel.rangeCount = 1;
- updateAnchorAndFocusFromNativeSelection(sel);
- sel.isCollapsed = selectionIsCollapsed(sel);
- } else {
- updateEmptySelection(sel);
- }
- };
- } else {
- module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
- return false;
- }
-
- selProto.refresh = function(checkForChanges) {
- var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
- var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
-
- refreshSelection(this);
- if (checkForChanges) {
- // Check the range count first
- var i = oldRanges.length;
- if (i != this._ranges.length) {
- return true;
- }
-
- // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
- // ranges after this
- if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
- return true;
- }
-
- // Finally, compare each range in turn
- while (i--) {
- if (!rangesEqual(oldRanges[i], this._ranges[i])) {
- return true;
- }
- }
- return false;
- }
- };
-
- // Removal of a single range
- var removeRangeManually = function(sel, range) {
- var ranges = sel.getAllRanges();
- sel.removeAllRanges();
- for (var i = 0, len = ranges.length; i < len; ++i) {
- if (!rangesEqual(range, ranges[i])) {
- sel.addRange(ranges[i]);
- }
- }
- if (!sel.rangeCount) {
- updateEmptySelection(sel);
- }
- };
-
- if (implementsControlRange) {
- selProto.removeRange = function(range) {
- if (this.docSelection.type == CONTROL) {
- var controlRange = this.docSelection.createRange();
- var rangeElement = getSingleElementFromRange(range);
-
- // Create a new ControlRange containing all the elements in the selected ControlRange minus the
- // element contained by the supplied range
- var doc = getDocument(controlRange.item(0));
- var newControlRange = getBody(doc).createControlRange();
- var el, removed = false;
- for (var i = 0, len = controlRange.length; i < len; ++i) {
- el = controlRange.item(i);
- if (el !== rangeElement || removed) {
- newControlRange.add(controlRange.item(i));
- } else {
- removed = true;
- }
- }
- newControlRange.select();
-
- // Update the wrapped selection based on what's now in the native selection
- updateControlSelection(this);
- } else {
- removeRangeManually(this, range);
- }
- };
- } else {
- selProto.removeRange = function(range) {
- removeRangeManually(this, range);
- };
- }
-
- // Detecting if a selection is backward
- var selectionIsBackward;
- if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
- selectionIsBackward = winSelectionIsBackward;
-
- selProto.isBackward = function() {
- return selectionIsBackward(this);
- };
- } else {
- selectionIsBackward = selProto.isBackward = function() {
- return false;
- };
- }
-
- // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
- selProto.isBackwards = selProto.isBackward;
-
- // Selection stringifier
- // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
- // The current spec does not yet define this method.
- selProto.toString = function() {
- var rangeTexts = [];
- for (var i = 0, len = this.rangeCount; i < len; ++i) {
- rangeTexts[i] = "" + this._ranges[i];
- }
- return rangeTexts.join("");
- };
-
- function assertNodeInSameDocument(sel, node) {
- if (sel.win.document != getDocument(node)) {
- throw new DOMException("WRONG_DOCUMENT_ERR");
- }
- }
-
- // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
- selProto.collapse = function(node, offset) {
- assertNodeInSameDocument(this, node);
- var range = api.createRange(node);
- range.collapseToPoint(node, offset);
- this.setSingleRange(range);
- this.isCollapsed = true;
- };
-
- selProto.collapseToStart = function() {
- if (this.rangeCount) {
- var range = this._ranges[0];
- this.collapse(range.startContainer, range.startOffset);
- } else {
- throw new DOMException("INVALID_STATE_ERR");
- }
- };
-
- selProto.collapseToEnd = function() {
- if (this.rangeCount) {
- var range = this._ranges[this.rangeCount - 1];
- this.collapse(range.endContainer, range.endOffset);
- } else {
- throw new DOMException("INVALID_STATE_ERR");
- }
- };
-
- // The spec is very specific on how selectAllChildren should be implemented so the native implementation is
- // never used by Rangy.
- selProto.selectAllChildren = function(node) {
- assertNodeInSameDocument(this, node);
- var range = api.createRange(node);
- range.selectNodeContents(node);
- this.setSingleRange(range);
- };
-
- selProto.deleteFromDocument = function() {
- // Sepcial behaviour required for IE's control selections
- if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
- var controlRange = this.docSelection.createRange();
- var element;
- while (controlRange.length) {
- element = controlRange.item(0);
- controlRange.remove(element);
- element.parentNode.removeChild(element);
- }
- this.refresh();
- } else if (this.rangeCount) {
- var ranges = this.getAllRanges();
- if (ranges.length) {
- this.removeAllRanges();
- for (var i = 0, len = ranges.length; i < len; ++i) {
- ranges[i].deleteContents();
- }
- // The spec says nothing about what the selection should contain after calling deleteContents on each
- // range. Firefox moves the selection to where the final selected range was, so we emulate that
- this.addRange(ranges[len - 1]);
- }
- }
- };
-
- // The following are non-standard extensions
- selProto.eachRange = function(func, returnValue) {
- for (var i = 0, len = this._ranges.length; i < len; ++i) {
- if ( func( this.getRangeAt(i) ) ) {
- return returnValue;
- }
- }
- };
-
- selProto.getAllRanges = function() {
- var ranges = [];
- this.eachRange(function(range) {
- ranges.push(range);
- });
- return ranges;
- };
-
- selProto.setSingleRange = function(range, direction) {
- this.removeAllRanges();
- this.addRange(range, direction);
- };
-
- selProto.callMethodOnEachRange = function(methodName, params) {
- var results = [];
- this.eachRange( function(range) {
- results.push( range[methodName].apply(range, params) );
- } );
- return results;
- };
-
- function createStartOrEndSetter(isStart) {
- return function(node, offset) {
- var range;
- if (this.rangeCount) {
- range = this.getRangeAt(0);
- range["set" + (isStart ? "Start" : "End")](node, offset);
- } else {
- range = api.createRange(this.win.document);
- range.setStartAndEnd(node, offset);
- }
- this.setSingleRange(range, this.isBackward());
- };
- }
-
- selProto.setStart = createStartOrEndSetter(true);
- selProto.setEnd = createStartOrEndSetter(false);
-
- // Add select() method to Range prototype. Any existing selection will be removed.
- api.rangePrototype.select = function(direction) {
- getSelection( this.getDocument() ).setSingleRange(this, direction);
- };
-
- selProto.changeEachRange = function(func) {
- var ranges = [];
- var backward = this.isBackward();
-
- this.eachRange(function(range) {
- func(range);
- ranges.push(range);
- });
-
- this.removeAllRanges();
- if (backward && ranges.length == 1) {
- this.addRange(ranges[0], "backward");
- } else {
- this.setRanges(ranges);
- }
- };
-
- selProto.containsNode = function(node, allowPartial) {
- return this.eachRange( function(range) {
- return range.containsNode(node, allowPartial);
- }, true );
- };
-
- selProto.getBookmark = function(containerNode) {
- return {
- backward: this.isBackward(),
- rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
- };
- };
-
- selProto.moveToBookmark = function(bookmark) {
- var selRanges = [];
- for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
- range = api.createRange(this.win);
- range.moveToBookmark(rangeBookmark);
- selRanges.push(range);
- }
- if (bookmark.backward) {
- this.setSingleRange(selRanges[0], "backward");
- } else {
- this.setRanges(selRanges);
- }
- };
-
- selProto.toHtml = function() {
- return this.callMethodOnEachRange("toHtml").join("");
- };
-
- function inspect(sel) {
- var rangeInspects = [];
- var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
- var focus = new DomPosition(sel.focusNode, sel.focusOffset);
- var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
-
- if (typeof sel.rangeCount != "undefined") {
- for (var i = 0, len = sel.rangeCount; i < len; ++i) {
- rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
- }
- }
- return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
- ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
- }
-
- selProto.getName = function() {
- return "WrappedSelection";
- };
-
- selProto.inspect = function() {
- return inspect(this);
- };
-
- selProto.detach = function() {
- actOnCachedSelection(this.win, "delete");
- deleteProperties(this);
- };
-
- WrappedSelection.detachAll = function() {
- actOnCachedSelection(null, "deleteAll");
- };
-
- WrappedSelection.inspect = inspect;
- WrappedSelection.isDirectionBackward = isDirectionBackward;
-
- api.Selection = WrappedSelection;
-
- api.selectionPrototype = selProto;
-
- api.addCreateMissingNativeApiListener(function(win) {
- if (typeof win.getSelection == "undefined") {
- win.getSelection = function() {
- return getSelection(win);
- };
- }
- win = null;
- });
-});
diff --git a/www/code/realtime-wysiwyg.js b/www/code/realtime-wysiwyg.js
deleted file mode 100644
index 4363c07f7..000000000
--- a/www/code/realtime-wysiwyg.js
+++ /dev/null
@@ -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 .
- */
-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;
-});
diff --git a/www/code/rtwiki.js b/www/code/rt_codemirror.js
similarity index 50%
rename from www/code/rtwiki.js
rename to www/code/rt_codemirror.js
index c17b98daa..7135651ae 100644
--- a/www/code/rtwiki.js
+++ b/www/code/rt_codemirror.js
@@ -6,7 +6,7 @@ define([
'/common/crypto.js',
'/code/errorbox.js',
'/common/messages.js',
- '/common/toolbar.js',
+ '/code/toolbar.js',
'/common/chainpad.js',
'/common/otaml.js',
'/bower_components/jquery/dist/jquery.min.js'
@@ -16,17 +16,6 @@ define([
var Otaml = window.Otaml;
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.
var MAX_LAG_BEFORE_DISCONNECT = 30000;
@@ -36,37 +25,6 @@ define([
var debug = function (x) { };
//debug = function (x) { console.log(x) };
warn = function (x) { console.log(x); };
- var setStyle = function () {
- $('head').append([
- ''
- ].join(''));
- };
var uid = function () {
return 'rtwiki-uid-' + String(Math.random()).substring(2);
@@ -151,178 +109,6 @@ define([
return lagElement;
};
- var createRealtimeToolbar = function (container) {
- var id = uid();
- $(container).prepend(
- ''
- );
- 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) {
return socket.readyState === socket.CLOSING ||
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(
- '' +
- '' +
- ''
- );
-
- 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
// Trapping Keyboard Events
var bindEvents = function (element, events, callback, unbind) {
@@ -760,12 +309,12 @@ define([
var incomingPatch = function () {
if (isErrorState || initializing) { return; }
var textAreaVal = $(textArea).val();
+ console.log($(textArea).val());
userDocBeforePatch = userDocBeforePatch || textAreaVal;
if (userDocBeforePatch !== textAreaVal) {
//error(false, "userDocBeforePatch !== textAreaVal");
}
var op = attempt(Otaml.makeTextOperation)(userDocBeforePatch, realtime.getUserDoc());
-
if (typeof op === 'undefined') {
warn("TypeError: op is undefined");
return;
@@ -796,6 +345,7 @@ define([
}
}
$(textArea).val(newValue);
+ userDocBeforePatch = newValue;
cmEditor.setValue(newValue);
if(newCursor) {
cmEditor.setCursor(newCursor);
@@ -884,7 +434,7 @@ define([
if (!websocketUrl) {
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 {
onEvent: function () {
cme.onEvent();
diff --git a/www/code/sharejs_textarea.js b/www/code/sharejs_textarea.js
deleted file mode 100644
index 14076033a..000000000
--- a/www/code/sharejs_textarea.js
+++ /dev/null
@@ -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 };
-});
diff --git a/www/code/toolbar.js b/www/code/toolbar.js
new file mode 100644
index 000000000..8871833ff
--- /dev/null
+++ b/www/code/toolbar.js
@@ -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(
+ '' +
+ '' +
+ '' +
+ ''
+ );
+ var toolbar = $container.find('#'+id);
+ toolbar.append([
+ ''
+ ].join('\n'));
+ return toolbar;
+ };
+
+ var createEscape = function ($container) {
+ var id = uid();
+ $container.append('⇐ Back');
+ var $ret = $container.find('#'+id);
+ $ret.on('click', function () {
+ window.location.href = '/';
+ });
+ return $ret[0];
+ };
+
+ var createSpinner = function ($container) {
+ var id = uid();
+ $container.append('');
+ 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('');
+ 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('');
+ 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 };
+});
\ No newline at end of file