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 . - */ -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(''; - 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(' 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(''); - $('#'+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 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(''); - $(document.body).append(''); - 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; -}); diff --git a/www/code/main.js b/www/code/main.js index 12804cbb0..9f5d1fa1c 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -1,10 +1,10 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), - '/code/rtwiki.js', + '/code/rt_codemirror.js', '/common/messages.js', '/common/crypto.js', '/bower_components/jquery/dist/jquery.min.js' -], function (Config, RTWiki, Messages, Crypto) { +], function (Config, RTCode, Messages, Crypto) { var $ = window.jQuery; var ifrw = $('#pad-iframe')[0].contentWindow; @@ -36,7 +36,7 @@ define([ editor.setValue(Messages.codeInitialState); var rtw = - RTWiki.start(ifrw, + RTCode.start(ifrw, Config.websocketURL, Crypto.rand64(8), key.channel, diff --git a/www/code/rangy.js b/www/code/rangy.js deleted file mode 100644 index 787e2b88e..000000000 --- a/www/code/rangy.js +++ /dev/null @@ -1,3743 +0,0 @@ -/** - * Rangy, a cross-browser JavaScript range and selection library - * http://code.google.com/p/rangy/ - * - * Copyright 2013, Tim Down - * Licensed under the MIT license. - * Version: 1.3alpha.804 - * Build date: 8 December 2013 - */ - -/* - TODO FIXME use www/common/rangy if possible... - -*/ - -(function(global) { - var amdSupported = (typeof global.define == "function" && global.define.amd); - - var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; - - // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START - // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113. - var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", - "commonAncestorContainer"]; - - // Minimal set of methods required for DOM Level 2 Range compliance - var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore", - "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents", - "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"]; - - var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; - - // Subset of TextRange's full set of methods that we're interested in - var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select", - "setEndPoint", "getBoundingClientRect"]; - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Trio of functions taken from Peter Michaux's article: - // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting - function isHostMethod(o, p) { - var t = typeof o[p]; - return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown"; - } - - function isHostObject(o, p) { - return !!(typeof o[p] == OBJECT && o[p]); - } - - function isHostProperty(o, p) { - return typeof o[p] != UNDEFINED; - } - - // Creates a convenience function to save verbose repeated calls to tests functions - function createMultiplePropertyTest(testFunc) { - return function(o, props) { - var i = props.length; - while (i--) { - if (!testFunc(o, props[i])) { - return false; - } - } - return true; - }; - } - - // Next trio of functions are a convenience to save verbose repeated calls to previous two functions - var areHostMethods = createMultiplePropertyTest(isHostMethod); - var areHostObjects = createMultiplePropertyTest(isHostObject); - var areHostProperties = createMultiplePropertyTest(isHostProperty); - - function isTextRange(range) { - return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); - } - - function getBody(doc) { - return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; - } - - var modules = {}; - - var api = { - version: "1.3alpha.804", - initialized: false, - supported: true, - - util: { - isHostMethod: isHostMethod, - isHostObject: isHostObject, - isHostProperty: isHostProperty, - areHostMethods: areHostMethods, - areHostObjects: areHostObjects, - areHostProperties: areHostProperties, - isTextRange: isTextRange, - getBody: getBody - }, - - features: {}, - - modules: modules, - config: { - alertOnFail: true, - alertOnWarn: false, - preferTextRange: false - } - }; - - function consoleLog(msg) { - if (isHostObject(window, "console") && isHostMethod(window.console, "log")) { - window.console.log(msg); - } - } - - function alertOrLog(msg, shouldAlert) { - if (shouldAlert) { - window.alert(msg); - } else { - consoleLog(msg); - } - } - - function fail(reason) { - api.initialized = true; - api.supported = false; - alertOrLog("Rangy is not supported on this page in your browser. Reason: " + reason, api.config.alertOnFail); - } - - api.fail = fail; - - function warn(msg) { - alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn); - } - - api.warn = warn; - - // Add utility extend() method - if ({}.hasOwnProperty) { - api.util.extend = function(obj, props, deep) { - var o, p; - for (var i in props) { - if (props.hasOwnProperty(i)) { - o = obj[i]; - p = props[i]; - //if (deep) alert([o !== null, typeof o == "object", p !== null, typeof p == "object"]) - if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") { - api.util.extend(o, p, true); - } - obj[i] = p; - } - } - return obj; - }; - } else { - fail("hasOwnProperty not supported"); - } - - // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not - (function() { - var el = document.createElement("div"); - el.appendChild(document.createElement("span")); - var slice = [].slice; - var toArray; - try { - if (slice.call(el.childNodes, 0)[0].nodeType == 1) { - toArray = function(arrayLike) { - return slice.call(arrayLike, 0); - }; - } - } catch (e) {} - - if (!toArray) { - toArray = function(arrayLike) { - var arr = []; - for (var i = 0, len = arrayLike.length; i < len; ++i) { - arr[i] = arrayLike[i]; - } - return arr; - }; - } - - api.util.toArray = toArray; - })(); - - - // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or - // normalization of event properties - var addListener; - if (isHostMethod(document, "addEventListener")) { - addListener = function(obj, eventType, listener) { - obj.addEventListener(eventType, listener, false); - }; - } else if (isHostMethod(document, "attachEvent")) { - addListener = function(obj, eventType, listener) { - obj.attachEvent("on" + eventType, listener); - }; - } else { - fail("Document does not have required addEventListener or attachEvent method"); - } - - api.util.addListener = addListener; - - var initListeners = []; - - function getErrorDesc(ex) { - return ex.message || ex.description || String(ex); - } - - // Initialization - function init() { - if (api.initialized) { - return; - } - var testRange; - var implementsDomRange = false, implementsTextRange = false; - - // First, perform basic feature tests - - if (isHostMethod(document, "createRange")) { - testRange = document.createRange(); - if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { - implementsDomRange = true; - } - testRange.detach(); - } - - var body = getBody(document); - if (!body || body.nodeName.toLowerCase() != "body") { - fail("No body element found"); - return; - } - - if (body && isHostMethod(body, "createTextRange")) { - testRange = body.createTextRange(); - if (isTextRange(testRange)) { - implementsTextRange = true; - } - } - - if (!implementsDomRange && !implementsTextRange) { - fail("Neither Range nor TextRange are available"); - return; - } - - api.initialized = true; - api.features = { - implementsDomRange: implementsDomRange, - implementsTextRange: implementsTextRange - }; - - // Initialize modules - var module, errorMessage; - for (var moduleName in modules) { - if ( (module = modules[moduleName]) instanceof Module ) { - module.init(module, api); - } - } - - // Call init listeners - for (var i = 0, len = initListeners.length; i < len; ++i) { - try { - initListeners[i](api); - } catch (ex) { - errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex); - consoleLog(errorMessage); - } - } - } - - // Allow external scripts to initialize this library in case it's loaded after the document has loaded - api.init = init; - - // Execute listener immediately if already initialized - api.addInitListener = function(listener) { - if (api.initialized) { - listener(api); - } else { - initListeners.push(listener); - } - }; - - var createMissingNativeApiListeners = []; - - api.addCreateMissingNativeApiListener = function(listener) { - createMissingNativeApiListeners.push(listener); - }; - - function createMissingNativeApi(win) { - win = win || window; - init(); - - // Notify listeners - for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) { - createMissingNativeApiListeners[i](win); - } - } - - api.createMissingNativeApi = createMissingNativeApi; - - function Module(name, dependencies, initializer) { - this.name = name; - this.dependencies = dependencies; - this.initialized = false; - this.supported = false; - this.initializer = initializer; - } - - Module.prototype = { - init: function(api) { - var requiredModuleNames = this.dependencies || []; - for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) { - moduleName = requiredModuleNames[i]; - - requiredModule = modules[moduleName]; - if (!requiredModule || !(requiredModule instanceof Module)) { - throw new Error("required module '" + moduleName + "' not found"); - } - - requiredModule.init(); - - if (!requiredModule.supported) { - throw new Error("required module '" + moduleName + "' not supported"); - } - } - - // Now run initializer - this.initializer(this) - }, - - fail: function(reason) { - this.initialized = true; - this.supported = false; - throw new Error("Module '" + this.name + "' failed to load: " + reason); - }, - - warn: function(msg) { - api.warn("Module " + this.name + ": " + msg); - }, - - deprecationNotice: function(deprecated, replacement) { - api.warn("DEPRECATED: " + deprecated + " in module " + this.name + "is deprecated. Please use " - + replacement + " instead"); - }, - - createError: function(msg) { - return new Error("Error in Rangy " + this.name + " module: " + msg); - } - }; - - function createModule(isCore, name, dependencies, initFunc) { - var newModule = new Module(name, dependencies, function(module) { - if (!module.initialized) { - module.initialized = true; - try { - initFunc(api, module); - module.supported = true; - } catch (ex) { - var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex); - consoleLog(errorMessage); - } - } - }); - modules[name] = newModule; - -/* - // Add module AMD support - if (!isCore && amdSupported) { - global.define(["rangy-core"], function(rangy) { - - }); - } -*/ - } - - api.createModule = function(name) { - // Allow 2 or 3 arguments (second argument is an optional array of dependencies) - var initFunc, dependencies; - if (arguments.length == 2) { - initFunc = arguments[1]; - dependencies = []; - } else { - initFunc = arguments[2]; - dependencies = arguments[1]; - } - createModule(false, name, dependencies, initFunc); - }; - - api.createCoreModule = function(name, dependencies, initFunc) { - createModule(true, name, dependencies, initFunc); - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately - - function RangePrototype() {} - api.RangePrototype = RangePrototype; - api.rangePrototype = new RangePrototype(); - - function SelectionPrototype() {} - api.selectionPrototype = new SelectionPrototype(); - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Wait for document to load before running tests - - var docReady = false; - - var loadHandler = function(e) { - if (!docReady) { - docReady = true; - if (!api.initialized) { - init(); - } - } - }; - - // Test whether we have window and document objects that we will need - if (typeof window == UNDEFINED) { - fail("No window found"); - return; - } - if (typeof document == UNDEFINED) { - fail("No document found"); - return; - } - - if (isHostMethod(document, "addEventListener")) { - document.addEventListener("DOMContentLoaded", loadHandler, false); - } - - // Add a fallback in case the DOMContentLoaded event isn't supported - addListener(window, "load", loadHandler); - - /*----------------------------------------------------------------------------------------------------------------*/ - - // AMD, for those who like this kind of thing - - if (amdSupported) { - // AMD. Register as an anonymous module. - global.define(function() { - api.amd = true; - return api; - }); - } - - // Create a "rangy" property of the global object in any case. Other Rangy modules (which use Rangy's own simple - // module system) rely on the existence of this global property - global.rangy = api; -})(this); - -rangy.createCoreModule("DomUtil", [], function(api, module) { - var UNDEF = "undefined"; - var util = api.util; - - // Perform feature tests - if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { - module.fail("document missing a Node creation method"); - } - - if (!util.isHostMethod(document, "getElementsByTagName")) { - module.fail("document missing getElementsByTagName method"); - } - - var el = document.createElement("div"); - if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || - !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { - module.fail("Incomplete Element implementation"); - } - - // innerHTML is required for Range's createContextualFragment method - if (!util.isHostProperty(el, "innerHTML")) { - module.fail("Element is missing innerHTML property"); - } - - var textNode = document.createTextNode("test"); - if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] || - !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) || - !util.areHostProperties(textNode, ["data"]))) { - module.fail("Incomplete Text Node implementation"); - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been - // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that - // contains just the document as a single element and the value searched for is the document. - var arrayContains = /*Array.prototype.indexOf ? - function(arr, val) { - return arr.indexOf(val) > -1; - }:*/ - - function(arr, val) { - var i = arr.length; - while (i--) { - if (arr[i] === val) { - return true; - } - } - return false; - }; - - // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI - function isHtmlNamespace(node) { - var ns; - return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"); - } - - function parentElement(node) { - var parent = node.parentNode; - return (parent.nodeType == 1) ? parent : null; - } - - function getNodeIndex(node) { - var i = 0; - while( (node = node.previousSibling) ) { - ++i; - } - return i; - } - - function getNodeLength(node) { - switch (node.nodeType) { - case 7: - case 10: - return 0; - case 3: - case 8: - return node.length; - default: - return node.childNodes.length; - } - } - - function getCommonAncestor(node1, node2) { - var ancestors = [], n; - for (n = node1; n; n = n.parentNode) { - ancestors.push(n); - } - - for (n = node2; n; n = n.parentNode) { - if (arrayContains(ancestors, n)) { - return n; - } - } - - return null; - } - - function isAncestorOf(ancestor, descendant, selfIsAncestor) { - var n = selfIsAncestor ? descendant : descendant.parentNode; - while (n) { - if (n === ancestor) { - return true; - } else { - n = n.parentNode; - } - } - return false; - } - - function isOrIsAncestorOf(ancestor, descendant) { - return isAncestorOf(ancestor, descendant, true); - } - - function getClosestAncestorIn(node, ancestor, selfIsAncestor) { - var p, n = selfIsAncestor ? node : node.parentNode; - while (n) { - p = n.parentNode; - if (p === ancestor) { - return n; - } - n = p; - } - return null; - } - - function isCharacterDataNode(node) { - var t = node.nodeType; - return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment - } - - function isTextOrCommentNode(node) { - if (!node) { - return false; - } - var t = node.nodeType; - return t == 3 || t == 8 ; // Text or Comment - } - - function insertAfter(node, precedingNode) { - var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; - if (nextNode) { - parent.insertBefore(node, nextNode); - } else { - parent.appendChild(node); - } - return node; - } - - // Note that we cannot use splitText() because it is bugridden in IE 9. - function splitDataNode(node, index, positionsToPreserve) { - var newNode = node.cloneNode(false); - newNode.deleteData(0, index); - node.deleteData(index, node.length - index); - insertAfter(newNode, node); - - // Preserve positions - if (positionsToPreserve) { - for (var i = 0, position; position = positionsToPreserve[i++]; ) { - // Handle case where position was inside the portion of node after the split point - if (position.node == node && position.offset > index) { - position.node = newNode; - position.offset -= index; - } - // Handle the case where the position is a node offset within node's parent - else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) { - ++position.offset; - } - } - } - return newNode; - } - - function getDocument(node) { - if (node.nodeType == 9) { - return node; - } else if (typeof node.ownerDocument != UNDEF) { - return node.ownerDocument; - } else if (typeof node.document != UNDEF) { - return node.document; - } else if (node.parentNode) { - return getDocument(node.parentNode); - } else { - throw module.createError("getDocument: no document found for node"); - } - } - - function getWindow(node) { - var doc = getDocument(node); - if (typeof doc.defaultView != UNDEF) { - return doc.defaultView; - } else if (typeof doc.parentWindow != UNDEF) { - return doc.parentWindow; - } else { - throw module.createError("Cannot get a window object for node"); - } - } - - function getIframeDocument(iframeEl) { - if (typeof iframeEl.contentDocument != UNDEF) { - return iframeEl.contentDocument; - } else if (typeof iframeEl.contentWindow != UNDEF) { - return iframeEl.contentWindow.document; - } else { - throw module.createError("getIframeDocument: No Document object found for iframe element"); - } - } - - function getIframeWindow(iframeEl) { - if (typeof iframeEl.contentWindow != UNDEF) { - return iframeEl.contentWindow; - } else if (typeof iframeEl.contentDocument != UNDEF) { - return iframeEl.contentDocument.defaultView; - } else { - throw module.createError("getIframeWindow: No Window object found for iframe element"); - } - } - - // This looks bad. Is it worth it? - function isWindow(obj) { - return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document"); - } - - function getContentDocument(obj, module, methodName) { - var doc; - - if (!obj) { - doc = document; - } - - // Test if a DOM node has been passed and obtain a document object for it if so - else if (util.isHostProperty(obj, "nodeType")) { - doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") - ? getIframeDocument(obj) : getDocument(obj); - } - - // Test if the doc parameter appears to be a Window object - else if (isWindow(obj)) { - doc = obj.document; - } - - if (!doc) { - throw module.createError(methodName + "(): Parameter must be a Window object or DOM node"); - } - - return doc; - } - - function getRootContainer(node) { - var parent; - while ( (parent = node.parentNode) ) { - node = parent; - } - return node; - } - - function comparePoints(nodeA, offsetA, nodeB, offsetB) { - // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing - var nodeC, root, childA, childB, n; - if (nodeA == nodeB) { - // Case 1: nodes are the same - return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; - } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { - // Case 2: node C (container B or an ancestor) is a child node of A - return offsetA <= getNodeIndex(nodeC) ? -1 : 1; - } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) { - // Case 3: node C (container A or an ancestor) is a child node of B - return getNodeIndex(nodeC) < offsetB ? -1 : 1; - } else { - root = getCommonAncestor(nodeA, nodeB); - if (!root) { - throw new Error("comparePoints error: nodes have no common ancestor"); - } - - // Case 4: containers are siblings or descendants of siblings - childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); - childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); - - if (childA === childB) { - // This shouldn't be possible - throw module.createError("comparePoints got to case 4 and childA and childB are the same!"); - } else { - n = root.firstChild; - while (n) { - if (n === childA) { - return -1; - } else if (n === childB) { - return 1; - } - n = n.nextSibling; - } - } - } - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried - var crashyTextNodes = false; - - function isBrokenNode(node) { - try { - node.parentNode; - return false; - } catch (e) { - return true; - } - } - - (function() { - var el = document.createElement("b"); - el.innerHTML = "1"; - var textNode = el.firstChild; - el.innerHTML = "
"; - crashyTextNodes = isBrokenNode(textNode); - - api.features.crashyTextNodes = crashyTextNodes; - })(); - - /*----------------------------------------------------------------------------------------------------------------*/ - - function inspectNode(node) { - if (!node) { - return "[No node]"; - } - if (crashyTextNodes && isBrokenNode(node)) { - return "[Broken node]"; - } - if (isCharacterDataNode(node)) { - return '"' + node.data + '"'; - } - if (node.nodeType == 1) { - var idAttr = node.id ? ' id="' + node.id + '"' : ""; - return "<" + node.nodeName + idAttr + ">[" + getNodeIndex(node) + "][" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]"; - } - return node.nodeName; - } - - function fragmentFromNodeChildren(node) { - var fragment = getDocument(node).createDocumentFragment(), child; - while ( (child = node.firstChild) ) { - fragment.appendChild(child); - } - return fragment; - } - - var getComputedStyleProperty; - if (typeof window.getComputedStyle != UNDEF) { - getComputedStyleProperty = function(el, propName) { - return getWindow(el).getComputedStyle(el, null)[propName]; - }; - } else if (typeof document.documentElement.currentStyle != UNDEF) { - getComputedStyleProperty = function(el, propName) { - return el.currentStyle[propName]; - }; - } else { - module.fail("No means of obtaining computed style properties found"); - } - - function NodeIterator(root) { - this.root = root; - this._next = root; - } - - NodeIterator.prototype = { - _current: null, - - hasNext: function() { - return !!this._next; - }, - - next: function() { - var n = this._current = this._next; - var child, next; - if (this._current) { - child = n.firstChild; - if (child) { - this._next = child; - } else { - next = null; - while ((n !== this.root) && !(next = n.nextSibling)) { - n = n.parentNode; - } - this._next = next; - } - } - return this._current; - }, - - detach: function() { - this._current = this._next = this.root = null; - } - }; - - function createIterator(root) { - return new NodeIterator(root); - } - - function DomPosition(node, offset) { - this.node = node; - this.offset = offset; - } - - DomPosition.prototype = { - equals: function(pos) { - return !!pos && this.node === pos.node && this.offset == pos.offset; - }, - - inspect: function() { - return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; - }, - - toString: function() { - return this.inspect(); - } - }; - - function DOMException(codeName) { - this.code = this[codeName]; - this.codeName = codeName; - this.message = "DOMException: " + this.codeName; - } - - DOMException.prototype = { - INDEX_SIZE_ERR: 1, - HIERARCHY_REQUEST_ERR: 3, - WRONG_DOCUMENT_ERR: 4, - NO_MODIFICATION_ALLOWED_ERR: 7, - NOT_FOUND_ERR: 8, - NOT_SUPPORTED_ERR: 9, - INVALID_STATE_ERR: 11 - }; - - DOMException.prototype.toString = function() { - return this.message; - }; - - api.dom = { - arrayContains: arrayContains, - isHtmlNamespace: isHtmlNamespace, - parentElement: parentElement, - getNodeIndex: getNodeIndex, - getNodeLength: getNodeLength, - getCommonAncestor: getCommonAncestor, - isAncestorOf: isAncestorOf, - isOrIsAncestorOf: isOrIsAncestorOf, - getClosestAncestorIn: getClosestAncestorIn, - isCharacterDataNode: isCharacterDataNode, - isTextOrCommentNode: isTextOrCommentNode, - insertAfter: insertAfter, - splitDataNode: splitDataNode, - getDocument: getDocument, - getWindow: getWindow, - getIframeWindow: getIframeWindow, - getIframeDocument: getIframeDocument, - getBody: util.getBody, - isWindow: isWindow, - getContentDocument: getContentDocument, - getRootContainer: getRootContainer, - comparePoints: comparePoints, - isBrokenNode: isBrokenNode, - inspectNode: inspectNode, - getComputedStyleProperty: getComputedStyleProperty, - fragmentFromNodeChildren: fragmentFromNodeChildren, - createIterator: createIterator, - DomPosition: DomPosition - }; - - api.DOMException = DOMException; -}); -rangy.createCoreModule("DomRange", ["DomUtil"], function(api, module) { - var dom = api.dom; - var util = api.util; - var DomPosition = dom.DomPosition; - var DOMException = api.DOMException; - - var isCharacterDataNode = dom.isCharacterDataNode; - var getNodeIndex = dom.getNodeIndex; - var isOrIsAncestorOf = dom.isOrIsAncestorOf; - var getDocument = dom.getDocument; - var comparePoints = dom.comparePoints; - var splitDataNode = dom.splitDataNode; - var getClosestAncestorIn = dom.getClosestAncestorIn; - var getNodeLength = dom.getNodeLength; - var arrayContains = dom.arrayContains; - var getRootContainer = dom.getRootContainer; - var crashyTextNodes = api.features.crashyTextNodes; - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Utility functions - - function isNonTextPartiallySelected(node, range) { - return (node.nodeType != 3) && - (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer)); - } - - function getRangeDocument(range) { - return range.document || getDocument(range.startContainer); - } - - function getBoundaryBeforeNode(node) { - return new DomPosition(node.parentNode, getNodeIndex(node)); - } - - function getBoundaryAfterNode(node) { - return new DomPosition(node.parentNode, getNodeIndex(node) + 1); - } - - function insertNodeAtPosition(node, n, o) { - var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; - if (isCharacterDataNode(n)) { - if (o == n.length) { - dom.insertAfter(node, n); - } else { - n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o)); - } - } else if (o >= n.childNodes.length) { - n.appendChild(node); - } else { - n.insertBefore(node, n.childNodes[o]); - } - return firstNodeInserted; - } - - function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) { - assertRangeValid(rangeA); - assertRangeValid(rangeB); - - if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) { - throw new DOMException("WRONG_DOCUMENT_ERR"); - } - - var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset), - endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset); - - return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; - } - - function cloneSubtree(iterator) { - var partiallySelected; - for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { - partiallySelected = iterator.isPartiallySelectedSubtree(); - node = node.cloneNode(!partiallySelected); - if (partiallySelected) { - subIterator = iterator.getSubtreeIterator(); - node.appendChild(cloneSubtree(subIterator)); - subIterator.detach(true); - } - - if (node.nodeType == 10) { // DocumentType - throw new DOMException("HIERARCHY_REQUEST_ERR"); - } - frag.appendChild(node); - } - return frag; - } - - function iterateSubtree(rangeIterator, func, iteratorState) { - var it, n; - iteratorState = iteratorState || { stop: false }; - for (var node, subRangeIterator; node = rangeIterator.next(); ) { - if (rangeIterator.isPartiallySelectedSubtree()) { - if (func(node) === false) { - iteratorState.stop = true; - return; - } else { - // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of - // the node selected by the Range. - subRangeIterator = rangeIterator.getSubtreeIterator(); - iterateSubtree(subRangeIterator, func, iteratorState); - subRangeIterator.detach(true); - if (iteratorState.stop) { - return; - } - } - } else { - // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its - // descendants - it = dom.createIterator(node); - while ( (n = it.next()) ) { - if (func(n) === false) { - iteratorState.stop = true; - return; - } - } - } - } - } - - function deleteSubtree(iterator) { - var subIterator; - while (iterator.next()) { - if (iterator.isPartiallySelectedSubtree()) { - subIterator = iterator.getSubtreeIterator(); - deleteSubtree(subIterator); - subIterator.detach(true); - } else { - iterator.remove(); - } - } - } - - function extractSubtree(iterator) { - for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { - - if (iterator.isPartiallySelectedSubtree()) { - node = node.cloneNode(false); - subIterator = iterator.getSubtreeIterator(); - node.appendChild(extractSubtree(subIterator)); - subIterator.detach(true); - } else { - iterator.remove(); - } - if (node.nodeType == 10) { // DocumentType - throw new DOMException("HIERARCHY_REQUEST_ERR"); - } - frag.appendChild(node); - } - return frag; - } - - function getNodesInRange(range, nodeTypes, filter) { - var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; - var filterExists = !!filter; - if (filterNodeTypes) { - regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); - } - - var nodes = []; - iterateSubtree(new RangeIterator(range, false), function(node) { - if (filterNodeTypes && !regex.test(node.nodeType)) { - return; - } - if (filterExists && !filter(node)) { - return; - } - // Don't include a boundary container if it is a character data node and the range does not contain any - // of its character data. See issue 190. - var sc = range.startContainer; - if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) { - return; - } - - var ec = range.endContainer; - if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) { - return; - } - - nodes.push(node); - }); - return nodes; - } - - function inspect(range) { - var name = (typeof range.getName == "undefined") ? "Range" : range.getName(); - return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + - dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) - - function RangeIterator(range, clonePartiallySelectedTextNodes) { - this.range = range; - this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; - - - if (!range.collapsed) { - this.sc = range.startContainer; - this.so = range.startOffset; - this.ec = range.endContainer; - this.eo = range.endOffset; - var root = range.commonAncestorContainer; - - if (this.sc === this.ec && isCharacterDataNode(this.sc)) { - this.isSingleCharacterDataNode = true; - this._first = this._last = this._next = this.sc; - } else { - this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ? - this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true); - this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ? - this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true); - } - } - } - - RangeIterator.prototype = { - _current: null, - _next: null, - _first: null, - _last: null, - isSingleCharacterDataNode: false, - - reset: function() { - this._current = null; - this._next = this._first; - }, - - hasNext: function() { - return !!this._next; - }, - - next: function() { - // Move to next node - var current = this._current = this._next; - if (current) { - this._next = (current !== this._last) ? current.nextSibling : null; - - // Check for partially selected text nodes - if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { - if (current === this.ec) { - (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); - } - if (this._current === this.sc) { - (current = current.cloneNode(true)).deleteData(0, this.so); - } - } - } - - return current; - }, - - remove: function() { - var current = this._current, start, end; - - if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { - start = (current === this.sc) ? this.so : 0; - end = (current === this.ec) ? this.eo : current.length; - if (start != end) { - current.deleteData(start, end - start); - } - } else { - if (current.parentNode) { - current.parentNode.removeChild(current); - } else { - } - } - }, - - // Checks if the current node is partially selected - isPartiallySelectedSubtree: function() { - var current = this._current; - return isNonTextPartiallySelected(current, this.range); - }, - - getSubtreeIterator: function() { - var subRange; - if (this.isSingleCharacterDataNode) { - subRange = this.range.cloneRange(); - subRange.collapse(false); - } else { - subRange = new Range(getRangeDocument(this.range)); - var current = this._current; - var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current); - - if (isOrIsAncestorOf(current, this.sc)) { - startContainer = this.sc; - startOffset = this.so; - } - if (isOrIsAncestorOf(current, this.ec)) { - endContainer = this.ec; - endOffset = this.eo; - } - - updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); - } - return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); - }, - - detach: function(detachRange) { - if (detachRange) { - this.range.detach(); - } - this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; - } - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Exceptions - - function RangeException(codeName) { - this.code = this[codeName]; - this.codeName = codeName; - this.message = "RangeException: " + this.codeName; - } - - RangeException.prototype = { - BAD_BOUNDARYPOINTS_ERR: 1, - INVALID_NODE_TYPE_ERR: 2 - }; - - RangeException.prototype.toString = function() { - return this.message; - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; - var rootContainerNodeTypes = [2, 9, 11]; - var readonlyNodeTypes = [5, 6, 10, 12]; - var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; - var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; - - function createAncestorFinder(nodeTypes) { - return function(node, selfIsAncestor) { - var t, n = selfIsAncestor ? node : node.parentNode; - while (n) { - t = n.nodeType; - if (arrayContains(nodeTypes, t)) { - return n; - } - n = n.parentNode; - } - return null; - }; - } - - var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); - var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); - var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); - - function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { - if (getDocTypeNotationEntityAncestor(node, allowSelf)) { - throw new RangeException("INVALID_NODE_TYPE_ERR"); - } - } - - function assertNotDetached(range) { - if (!range.startContainer) { - throw new DOMException("INVALID_STATE_ERR"); - } - } - - function assertValidNodeType(node, invalidTypes) { - if (!arrayContains(invalidTypes, node.nodeType)) { - throw new RangeException("INVALID_NODE_TYPE_ERR"); - } - } - - function assertValidOffset(node, offset) { - if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) { - throw new DOMException("INDEX_SIZE_ERR"); - } - } - - function assertSameDocumentOrFragment(node1, node2) { - if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { - throw new DOMException("WRONG_DOCUMENT_ERR"); - } - } - - function assertNodeNotReadOnly(node) { - if (getReadonlyAncestor(node, true)) { - throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); - } - } - - function assertNode(node, codeName) { - if (!node) { - throw new DOMException(codeName); - } - } - - function isOrphan(node) { - return (crashyTextNodes && dom.isBrokenNode(node)) || - !arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true); - } - - function isValidOffset(node, offset) { - return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length); - } - - function isRangeValid(range) { - return (!!range.startContainer && !!range.endContainer - && !isOrphan(range.startContainer) - && !isOrphan(range.endContainer) - && isValidOffset(range.startContainer, range.startOffset) - && isValidOffset(range.endContainer, range.endOffset)); - } - - function assertRangeValid(range) { - assertNotDetached(range); - if (!isRangeValid(range)) { - throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")"); - } - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Test the browser's innerHTML support to decide how to implement createContextualFragment - var styleEl = document.createElement("style"); - var htmlParsingConforms = false; - try { - styleEl.innerHTML = "x"; - htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node - } catch (e) { - // IE 6 and 7 throw - } - - api.features.htmlParsingConforms = htmlParsingConforms; - - var createContextualFragment = htmlParsingConforms ? - - // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See - // discussion and base code for this implementation at issue 67. - // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface - // Thanks to Aleks Williams. - function(fragmentStr) { - // "Let node the context object's start's node." - var node = this.startContainer; - var doc = getDocument(node); - - // "If the context object's start's node is null, raise an INVALID_STATE_ERR - // exception and abort these steps." - if (!node) { - throw new DOMException("INVALID_STATE_ERR"); - } - - // "Let element be as follows, depending on node's interface:" - // Document, Document Fragment: null - var el = null; - - // "Element: node" - if (node.nodeType == 1) { - el = node; - - // "Text, Comment: node's parentElement" - } else if (isCharacterDataNode(node)) { - el = dom.parentElement(node); - } - - // "If either element is null or element's ownerDocument is an HTML document - // and element's local name is "html" and element's namespace is the HTML - // namespace" - if (el === null || ( - el.nodeName == "HTML" - && dom.isHtmlNamespace(getDocument(el).documentElement) - && dom.isHtmlNamespace(el) - )) { - - // "let element be a new Element with "body" as its local name and the HTML - // namespace as its namespace."" - el = doc.createElement("body"); - } else { - el = el.cloneNode(false); - } - - // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." - // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." - // "In either case, the algorithm must be invoked with fragment as the input - // and element as the context element." - el.innerHTML = fragmentStr; - - // "If this raises an exception, then abort these steps. Otherwise, let new - // children be the nodes returned." - - // "Let fragment be a new DocumentFragment." - // "Append all new children to fragment." - // "Return fragment." - return dom.fragmentFromNodeChildren(el); - } : - - // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that - // previous versions of Rangy used (with the exception of using a body element rather than a div) - function(fragmentStr) { - assertNotDetached(this); - var doc = getRangeDocument(this); - var el = doc.createElement("body"); - el.innerHTML = fragmentStr; - - return dom.fragmentFromNodeChildren(el); - }; - - function splitRangeBoundaries(range, positionsToPreserve) { - assertRangeValid(range); - - var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset; - var startEndSame = (sc === ec); - - if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { - splitDataNode(ec, eo, positionsToPreserve); - } - - if (isCharacterDataNode(sc) && so > 0 && so < sc.length) { - sc = splitDataNode(sc, so, positionsToPreserve); - if (startEndSame) { - eo -= so; - ec = sc; - } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) { - eo++; - } - so = 0; - } - range.setStartAndEnd(sc, so, ec, eo); - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", - "commonAncestorContainer"]; - - var s2s = 0, s2e = 1, e2e = 2, e2s = 3; - var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; - - util.extend(api.rangePrototype, { - compareBoundaryPoints: function(how, range) { - assertRangeValid(this); - assertSameDocumentOrFragment(this.startContainer, range.startContainer); - - var nodeA, offsetA, nodeB, offsetB; - var prefixA = (how == e2s || how == s2s) ? "start" : "end"; - var prefixB = (how == s2e || how == s2s) ? "start" : "end"; - nodeA = this[prefixA + "Container"]; - offsetA = this[prefixA + "Offset"]; - nodeB = range[prefixB + "Container"]; - offsetB = range[prefixB + "Offset"]; - return comparePoints(nodeA, offsetA, nodeB, offsetB); - }, - - insertNode: function(node) { - assertRangeValid(this); - assertValidNodeType(node, insertableNodeTypes); - assertNodeNotReadOnly(this.startContainer); - - if (isOrIsAncestorOf(node, this.startContainer)) { - throw new DOMException("HIERARCHY_REQUEST_ERR"); - } - - // No check for whether the container of the start of the Range is of a type that does not allow - // children of the type of node: the browser's DOM implementation should do this for us when we attempt - // to add the node - - var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); - this.setStartBefore(firstNodeInserted); - }, - - cloneContents: function() { - assertRangeValid(this); - - var clone, frag; - if (this.collapsed) { - return getRangeDocument(this).createDocumentFragment(); - } else { - if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) { - clone = this.startContainer.cloneNode(true); - clone.data = clone.data.slice(this.startOffset, this.endOffset); - frag = getRangeDocument(this).createDocumentFragment(); - frag.appendChild(clone); - return frag; - } else { - var iterator = new RangeIterator(this, true); - clone = cloneSubtree(iterator); - iterator.detach(); - } - return clone; - } - }, - - canSurroundContents: function() { - assertRangeValid(this); - assertNodeNotReadOnly(this.startContainer); - assertNodeNotReadOnly(this.endContainer); - - // Check if the contents can be surrounded. Specifically, this means whether the range partially selects - // no non-text nodes. - var iterator = new RangeIterator(this, true); - var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || - (iterator._last && isNonTextPartiallySelected(iterator._last, this))); - iterator.detach(); - return !boundariesInvalid; - }, - - surroundContents: function(node) { - assertValidNodeType(node, surroundNodeTypes); - - if (!this.canSurroundContents()) { - throw new RangeException("BAD_BOUNDARYPOINTS_ERR"); - } - - // Extract the contents - var content = this.extractContents(); - - // Clear the children of the node - if (node.hasChildNodes()) { - while (node.lastChild) { - node.removeChild(node.lastChild); - } - } - - // Insert the new node and add the extracted contents - insertNodeAtPosition(node, this.startContainer, this.startOffset); - node.appendChild(content); - - this.selectNode(node); - }, - - cloneRange: function() { - assertRangeValid(this); - var range = new Range(getRangeDocument(this)); - var i = rangeProperties.length, prop; - while (i--) { - prop = rangeProperties[i]; - range[prop] = this[prop]; - } - return range; - }, - - toString: function() { - assertRangeValid(this); - var sc = this.startContainer; - if (sc === this.endContainer && isCharacterDataNode(sc)) { - return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; - } else { - var textParts = [], iterator = new RangeIterator(this, true); - iterateSubtree(iterator, function(node) { - // Accept only text or CDATA nodes, not comments - if (node.nodeType == 3 || node.nodeType == 4) { - textParts.push(node.data); - } - }); - iterator.detach(); - return textParts.join(""); - } - }, - - // The methods below are all non-standard. The following batch were introduced by Mozilla but have since - // been removed from Mozilla. - - compareNode: function(node) { - assertRangeValid(this); - - var parent = node.parentNode; - var nodeIndex = getNodeIndex(node); - - if (!parent) { - throw new DOMException("NOT_FOUND_ERR"); - } - - var startComparison = this.comparePoint(parent, nodeIndex), - endComparison = this.comparePoint(parent, nodeIndex + 1); - - if (startComparison < 0) { // Node starts before - return (endComparison > 0) ? n_b_a : n_b; - } else { - return (endComparison > 0) ? n_a : n_i; - } - }, - - comparePoint: function(node, offset) { - assertRangeValid(this); - assertNode(node, "HIERARCHY_REQUEST_ERR"); - assertSameDocumentOrFragment(node, this.startContainer); - - if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { - return -1; - } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { - return 1; - } - return 0; - }, - - createContextualFragment: createContextualFragment, - - toHtml: function() { - assertRangeValid(this); - var container = this.commonAncestorContainer.parentNode.cloneNode(false); - container.appendChild(this.cloneContents()); - return container.innerHTML; - }, - - // touchingIsIntersecting determines whether this method considers a node that borders a range intersects - // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) - intersectsNode: function(node, touchingIsIntersecting) { - assertRangeValid(this); - assertNode(node, "NOT_FOUND_ERR"); - if (getDocument(node) !== getRangeDocument(this)) { - return false; - } - - var parent = node.parentNode, offset = getNodeIndex(node); - assertNode(parent, "NOT_FOUND_ERR"); - - var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset), - endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset); - - return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; - }, - - isPointInRange: function(node, offset) { - assertRangeValid(this); - assertNode(node, "HIERARCHY_REQUEST_ERR"); - assertSameDocumentOrFragment(node, this.startContainer); - - return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && - (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); - }, - - // The methods below are non-standard and invented by me. - - // Sharing a boundary start-to-end or end-to-start does not count as intersection. - intersectsRange: function(range) { - return rangesIntersect(this, range, false); - }, - - // Sharing a boundary start-to-end or end-to-start does count as intersection. - intersectsOrTouchesRange: function(range) { - return rangesIntersect(this, range, true); - }, - - intersection: function(range) { - if (this.intersectsRange(range)) { - var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), - endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); - - var intersectionRange = this.cloneRange(); - if (startComparison == -1) { - intersectionRange.setStart(range.startContainer, range.startOffset); - } - if (endComparison == 1) { - intersectionRange.setEnd(range.endContainer, range.endOffset); - } - return intersectionRange; - } - return null; - }, - - union: function(range) { - if (this.intersectsOrTouchesRange(range)) { - var unionRange = this.cloneRange(); - if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { - unionRange.setStart(range.startContainer, range.startOffset); - } - if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { - unionRange.setEnd(range.endContainer, range.endOffset); - } - return unionRange; - } else { - throw new RangeException("Ranges do not intersect"); - } - }, - - containsNode: function(node, allowPartial) { - if (allowPartial) { - return this.intersectsNode(node, false); - } else { - return this.compareNode(node) == n_i; - } - }, - - containsNodeContents: function(node) { - return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0; - }, - - containsRange: function(range) { - var intersection = this.intersection(range); - return intersection !== null && range.equals(intersection); - }, - - containsNodeText: function(node) { - var nodeRange = this.cloneRange(); - nodeRange.selectNode(node); - var textNodes = nodeRange.getNodes([3]); - if (textNodes.length > 0) { - nodeRange.setStart(textNodes[0], 0); - var lastTextNode = textNodes.pop(); - nodeRange.setEnd(lastTextNode, lastTextNode.length); - var contains = this.containsRange(nodeRange); - nodeRange.detach(); - return contains; - } else { - return this.containsNodeContents(node); - } - }, - - getNodes: function(nodeTypes, filter) { - assertRangeValid(this); - return getNodesInRange(this, nodeTypes, filter); - }, - - getDocument: function() { - return getRangeDocument(this); - }, - - collapseBefore: function(node) { - assertNotDetached(this); - - this.setEndBefore(node); - this.collapse(false); - }, - - collapseAfter: function(node) { - assertNotDetached(this); - - this.setStartAfter(node); - this.collapse(true); - }, - - getBookmark: function(containerNode) { - var doc = getRangeDocument(this); - var preSelectionRange = api.createRange(doc); - containerNode = containerNode || dom.getBody(doc); - preSelectionRange.selectNodeContents(containerNode); - var range = this.intersection(preSelectionRange); - var start = 0, end = 0; - if (range) { - preSelectionRange.setEnd(range.startContainer, range.startOffset); - start = preSelectionRange.toString().length; - end = start + range.toString().length; - preSelectionRange.detach(); - } - - return { - start: start, - end: end, - containerNode: containerNode - }; - }, - - moveToBookmark: function(bookmark) { - var containerNode = bookmark.containerNode; - var charIndex = 0; - this.setStart(containerNode, 0); - this.collapse(true); - var nodeStack = [containerNode], node, foundStart = false, stop = false; - var nextCharIndex, i, childNodes; - - while (!stop && (node = nodeStack.pop())) { - if (node.nodeType == 3) { - nextCharIndex = charIndex + node.length; - if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) { - this.setStart(node, bookmark.start - charIndex); - foundStart = true; - } - if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) { - this.setEnd(node, bookmark.end - charIndex); - stop = true; - } - charIndex = nextCharIndex; - } else { - childNodes = node.childNodes; - i = childNodes.length; - while (i--) { - nodeStack.push(childNodes[i]); - } - } - } - }, - - getName: function() { - return "DomRange"; - }, - - equals: function(range) { - return Range.rangesEqual(this, range); - }, - - isValid: function() { - return isRangeValid(this); - }, - - inspect: function() { - return inspect(this); - } - }); - - function copyComparisonConstantsToObject(obj) { - obj.START_TO_START = s2s; - obj.START_TO_END = s2e; - obj.END_TO_END = e2e; - obj.END_TO_START = e2s; - - obj.NODE_BEFORE = n_b; - obj.NODE_AFTER = n_a; - obj.NODE_BEFORE_AND_AFTER = n_b_a; - obj.NODE_INSIDE = n_i; - } - - function copyComparisonConstants(constructor) { - copyComparisonConstantsToObject(constructor); - copyComparisonConstantsToObject(constructor.prototype); - } - - function createRangeContentRemover(remover, boundaryUpdater) { - return function() { - assertRangeValid(this); - - var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; - - var iterator = new RangeIterator(this, true); - - // Work out where to position the range after content removal - var node, boundary; - if (sc !== root) { - node = getClosestAncestorIn(sc, root, true); - boundary = getBoundaryAfterNode(node); - sc = boundary.node; - so = boundary.offset; - } - - // Check none of the range is read-only - iterateSubtree(iterator, assertNodeNotReadOnly); - - iterator.reset(); - - // Remove the content - var returnValue = remover(iterator); - iterator.detach(); - - // Move to the new position - boundaryUpdater(this, sc, so, sc, so); - - return returnValue; - }; - } - - function createPrototypeRange(constructor, boundaryUpdater, detacher) { - function createBeforeAfterNodeSetter(isBefore, isStart) { - return function(node) { - assertNotDetached(this); - assertValidNodeType(node, beforeAfterNodeTypes); - assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); - - var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); - (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); - }; - } - - function setRangeStart(range, node, offset) { - var ec = range.endContainer, eo = range.endOffset; - if (node !== range.startContainer || offset !== range.startOffset) { - // Check the root containers of the range and the new boundary, and also check whether the new boundary - // is after the current end. In either case, collapse the range to the new position - if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) { - ec = node; - eo = offset; - } - boundaryUpdater(range, node, offset, ec, eo); - } - } - - function setRangeEnd(range, node, offset) { - var sc = range.startContainer, so = range.startOffset; - if (node !== range.endContainer || offset !== range.endOffset) { - // Check the root containers of the range and the new boundary, and also check whether the new boundary - // is after the current end. In either case, collapse the range to the new position - if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) { - sc = node; - so = offset; - } - boundaryUpdater(range, sc, so, node, offset); - } - } - - // Set up inheritance - var F = function() {}; - F.prototype = api.rangePrototype; - constructor.prototype = new F(); - - util.extend(constructor.prototype, { - setStart: function(node, offset) { - assertNotDetached(this); - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); - - setRangeStart(this, node, offset); - }, - - setEnd: function(node, offset) { - assertNotDetached(this); - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); - - setRangeEnd(this, node, offset); - }, - - /** - * Convenience method to set a range's start and end boundaries. Overloaded as follows: - * - Two parameters (node, offset) creates a collapsed range at that position - * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at - * startOffset and ending at endOffset - * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in - * startNode and ending at endOffset in endNode - */ - setStartAndEnd: function() { - assertNotDetached(this); - - var args = arguments; - var sc = args[0], so = args[1], ec = sc, eo = so; - - switch (args.length) { - case 3: - eo = args[2]; - break; - case 4: - ec = args[2]; - eo = args[3]; - break; - } - - boundaryUpdater(this, sc, so, ec, eo); - }, - - setBoundary: function(node, offset, isStart) { - this["set" + (isStart ? "Start" : "End")](node, offset); - }, - - setStartBefore: createBeforeAfterNodeSetter(true, true), - setStartAfter: createBeforeAfterNodeSetter(false, true), - setEndBefore: createBeforeAfterNodeSetter(true, false), - setEndAfter: createBeforeAfterNodeSetter(false, false), - - collapse: function(isStart) { - assertRangeValid(this); - if (isStart) { - boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); - } else { - boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); - } - }, - - selectNodeContents: function(node) { - assertNotDetached(this); - assertNoDocTypeNotationEntityAncestor(node, true); - - boundaryUpdater(this, node, 0, node, getNodeLength(node)); - }, - - selectNode: function(node) { - assertNotDetached(this); - assertNoDocTypeNotationEntityAncestor(node, false); - assertValidNodeType(node, beforeAfterNodeTypes); - - var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); - boundaryUpdater(this, start.node, start.offset, end.node, end.offset); - }, - - extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), - - deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), - - canSurroundContents: function() { - assertRangeValid(this); - assertNodeNotReadOnly(this.startContainer); - assertNodeNotReadOnly(this.endContainer); - - // Check if the contents can be surrounded. Specifically, this means whether the range partially selects - // no non-text nodes. - var iterator = new RangeIterator(this, true); - var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || - (iterator._last && isNonTextPartiallySelected(iterator._last, this))); - iterator.detach(); - return !boundariesInvalid; - }, - - detach: function() { - detacher(this); - }, - - splitBoundaries: function() { - splitRangeBoundaries(this); - }, - - splitBoundariesPreservingPositions: function(positionsToPreserve) { - splitRangeBoundaries(this, positionsToPreserve); - }, - - normalizeBoundaries: function() { - assertRangeValid(this); - - var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; - - var mergeForward = function(node) { - var sibling = node.nextSibling; - if (sibling && sibling.nodeType == node.nodeType) { - ec = node; - eo = node.length; - node.appendData(sibling.data); - sibling.parentNode.removeChild(sibling); - } - }; - - var mergeBackward = function(node) { - var sibling = node.previousSibling; - if (sibling && sibling.nodeType == node.nodeType) { - sc = node; - var nodeLength = node.length; - so = sibling.length; - node.insertData(0, sibling.data); - sibling.parentNode.removeChild(sibling); - if (sc == ec) { - eo += so; - ec = sc; - } else if (ec == node.parentNode) { - var nodeIndex = getNodeIndex(node); - if (eo == nodeIndex) { - ec = node; - eo = nodeLength; - } else if (eo > nodeIndex) { - eo--; - } - } - } - }; - - var normalizeStart = true; - - if (isCharacterDataNode(ec)) { - if (ec.length == eo) { - mergeForward(ec); - } - } else { - if (eo > 0) { - var endNode = ec.childNodes[eo - 1]; - if (endNode && isCharacterDataNode(endNode)) { - mergeForward(endNode); - } - } - normalizeStart = !this.collapsed; - } - - if (normalizeStart) { - if (isCharacterDataNode(sc)) { - if (so == 0) { - mergeBackward(sc); - } - } else { - if (so < sc.childNodes.length) { - var startNode = sc.childNodes[so]; - if (startNode && isCharacterDataNode(startNode)) { - mergeBackward(startNode); - } - } - } - } else { - sc = ec; - so = eo; - } - - boundaryUpdater(this, sc, so, ec, eo); - }, - - collapseToPoint: function(node, offset) { - assertNotDetached(this); - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); - this.setStartAndEnd(node, offset); - } - }); - - copyComparisonConstants(constructor); - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Updates commonAncestorContainer and collapsed after boundary change - function updateCollapsedAndCommonAncestor(range) { - range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); - range.commonAncestorContainer = range.collapsed ? - range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); - } - - function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { - range.startContainer = startContainer; - range.startOffset = startOffset; - range.endContainer = endContainer; - range.endOffset = endOffset; - range.document = dom.getDocument(startContainer); - - updateCollapsedAndCommonAncestor(range); - } - - function detach(range) { - assertNotDetached(range); - range.startContainer = range.startOffset = range.endContainer = range.endOffset = range.document = null; - range.collapsed = range.commonAncestorContainer = null; - } - - function Range(doc) { - this.startContainer = doc; - this.startOffset = 0; - this.endContainer = doc; - this.endOffset = 0; - this.document = doc; - updateCollapsedAndCommonAncestor(this); - } - - createPrototypeRange(Range, updateBoundaries, detach); - - util.extend(Range, { - rangeProperties: rangeProperties, - RangeIterator: RangeIterator, - copyComparisonConstants: copyComparisonConstants, - createPrototypeRange: createPrototypeRange, - inspect: inspect, - getRangeDocument: getRangeDocument, - rangesEqual: function(r1, r2) { - return r1.startContainer === r2.startContainer && - r1.startOffset === r2.startOffset && - r1.endContainer === r2.endContainer && - r1.endOffset === r2.endOffset; - } - }); - - api.DomRange = Range; - api.RangeException = RangeException; -}); -rangy.createCoreModule("WrappedRange", ["DomRange"], function(api, module) { - var WrappedRange, WrappedTextRange; - var dom = api.dom; - var util = api.util; - var DomPosition = dom.DomPosition; - var DomRange = api.DomRange; - var getBody = dom.getBody; - var getContentDocument = dom.getContentDocument; - var isCharacterDataNode = dom.isCharacterDataNode; - - - /*----------------------------------------------------------------------------------------------------------------*/ - - if (api.features.implementsDomRange) { - // This is a wrapper around the browser's native DOM Range. It has two aims: - // - Provide workarounds for specific browser bugs - // - provide convenient extensions, which are inherited from Rangy's DomRange - - (function() { - var rangeProto; - var rangeProperties = DomRange.rangeProperties; - - function updateRangeProperties(range) { - var i = rangeProperties.length, prop; - while (i--) { - prop = rangeProperties[i]; - range[prop] = range.nativeRange[prop]; - } - // Fix for broken collapsed property in IE 9. - range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); - } - - function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) { - var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); - var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); - var nativeRangeDifferent = !range.equals(range.nativeRange); - - // Always set both boundaries for the benefit of IE9 (see issue 35) - if (startMoved || endMoved || nativeRangeDifferent) { - range.setEnd(endContainer, endOffset); - range.setStart(startContainer, startOffset); - } - } - - function detach(range) { - range.nativeRange.detach(); - range.detached = true; - var i = rangeProperties.length; - while (i--) { - range[ rangeProperties[i] ] = null; - } - } - - var createBeforeAfterNodeSetter; - - WrappedRange = function(range) { - if (!range) { - throw module.createError("WrappedRange: Range must be specified"); - } - this.nativeRange = range; - updateRangeProperties(this); - }; - - DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach); - - rangeProto = WrappedRange.prototype; - - rangeProto.selectNode = function(node) { - this.nativeRange.selectNode(node); - updateRangeProperties(this); - }; - - rangeProto.cloneContents = function() { - return this.nativeRange.cloneContents(); - }; - - // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect, - // insertNode() is never delegated to the native range. - - rangeProto.surroundContents = function(node) { - this.nativeRange.surroundContents(node); - updateRangeProperties(this); - }; - - rangeProto.collapse = function(isStart) { - this.nativeRange.collapse(isStart); - updateRangeProperties(this); - }; - - rangeProto.cloneRange = function() { - return new WrappedRange(this.nativeRange.cloneRange()); - }; - - rangeProto.refresh = function() { - updateRangeProperties(this); - }; - - rangeProto.toString = function() { - return this.nativeRange.toString(); - }; - - // Create test range and node for feature detection - - var testTextNode = document.createTextNode("test"); - getBody(document).appendChild(testTextNode); - var range = document.createRange(); - - /*--------------------------------------------------------------------------------------------------------*/ - - // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and - // correct for it - - range.setStart(testTextNode, 0); - range.setEnd(testTextNode, 0); - - try { - range.setStart(testTextNode, 1); - - rangeProto.setStart = function(node, offset) { - this.nativeRange.setStart(node, offset); - updateRangeProperties(this); - }; - - rangeProto.setEnd = function(node, offset) { - this.nativeRange.setEnd(node, offset); - updateRangeProperties(this); - }; - - createBeforeAfterNodeSetter = function(name) { - return function(node) { - this.nativeRange[name](node); - updateRangeProperties(this); - }; - }; - - } catch(ex) { - - rangeProto.setStart = function(node, offset) { - try { - this.nativeRange.setStart(node, offset); - } catch (ex) { - this.nativeRange.setEnd(node, offset); - this.nativeRange.setStart(node, offset); - } - updateRangeProperties(this); - }; - - rangeProto.setEnd = function(node, offset) { - try { - this.nativeRange.setEnd(node, offset); - } catch (ex) { - this.nativeRange.setStart(node, offset); - this.nativeRange.setEnd(node, offset); - } - updateRangeProperties(this); - }; - - createBeforeAfterNodeSetter = function(name, oppositeName) { - return function(node) { - try { - this.nativeRange[name](node); - } catch (ex) { - this.nativeRange[oppositeName](node); - this.nativeRange[name](node); - } - updateRangeProperties(this); - }; - }; - } - - rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); - rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); - rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); - rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); - - /*--------------------------------------------------------------------------------------------------------*/ - - // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing - // whether the native implementation can be trusted - rangeProto.selectNodeContents = function(node) { - this.setStartAndEnd(node, 0, dom.getNodeLength(node)); - }; - - /*--------------------------------------------------------------------------------------------------------*/ - - // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for - // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 - - range.selectNodeContents(testTextNode); - range.setEnd(testTextNode, 3); - - var range2 = document.createRange(); - range2.selectNodeContents(testTextNode); - range2.setEnd(testTextNode, 4); - range2.setStart(testTextNode, 2); - - if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 && - range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { - // This is the wrong way round, so correct for it - - rangeProto.compareBoundaryPoints = function(type, range) { - range = range.nativeRange || range; - if (type == range.START_TO_END) { - type = range.END_TO_START; - } else if (type == range.END_TO_START) { - type = range.START_TO_END; - } - return this.nativeRange.compareBoundaryPoints(type, range); - }; - } else { - rangeProto.compareBoundaryPoints = function(type, range) { - return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); - }; - } - - /*--------------------------------------------------------------------------------------------------------*/ - - // Test for IE 9 deleteContents() and extractContents() bug and correct it. See issue 107. - - var el = document.createElement("div"); - el.innerHTML = "123"; - var textNode = el.firstChild; - var body = getBody(document); - body.appendChild(el); - - range.setStart(textNode, 1); - range.setEnd(textNode, 2); - range.deleteContents(); - - if (textNode.data == "13") { - // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and - // extractContents() - rangeProto.deleteContents = function() { - this.nativeRange.deleteContents(); - updateRangeProperties(this); - }; - - rangeProto.extractContents = function() { - var frag = this.nativeRange.extractContents(); - updateRangeProperties(this); - return frag; - }; - } else { - } - - body.removeChild(el); - body = null; - - /*--------------------------------------------------------------------------------------------------------*/ - - // Test for existence of createContextualFragment and delegate to it if it exists - if (util.isHostMethod(range, "createContextualFragment")) { - rangeProto.createContextualFragment = function(fragmentStr) { - return this.nativeRange.createContextualFragment(fragmentStr); - }; - } - - /*--------------------------------------------------------------------------------------------------------*/ - - // Clean up - getBody(document).removeChild(testTextNode); - range.detach(); - range2.detach(); - - rangeProto.getName = function() { - return "WrappedRange"; - }; - - api.WrappedRange = WrappedRange; - - api.createNativeRange = function(doc) { - doc = getContentDocument(doc, module, "createNativeRange"); - return doc.createRange(); - }; - })(); - } - - if (api.features.implementsTextRange) { - /* - This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() - method. For example, in the following (where pipes denote the selection boundaries): - - - - var range = document.selection.createRange(); - alert(range.parentElement().id); // Should alert "ul" but alerts "b" - - This method returns the common ancestor node of the following: - - the parentElement() of the textRange - - the parentElement() of the textRange after calling collapse(true) - - the parentElement() of the textRange after calling collapse(false) - */ - var getTextRangeContainerElement = function(textRange) { - var parentEl = textRange.parentElement(); - var range = textRange.duplicate(); - range.collapse(true); - var startEl = range.parentElement(); - range = textRange.duplicate(); - range.collapse(false); - var endEl = range.parentElement(); - var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); - - return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); - }; - - var textRangeIsCollapsed = function(textRange) { - return textRange.compareEndPoints("StartToEnd", textRange) == 0; - }; - - // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as - // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has - // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling - // for inputs and images, plus optimizations. - var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) { - var workingRange = textRange.duplicate(); - workingRange.collapse(isStart); - var containerElement = workingRange.parentElement(); - - // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so - // check for that - if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) { - containerElement = wholeRangeContainerElement; - } - - - // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and - // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx - if (!containerElement.canHaveHTML) { - var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); - return { - boundaryPosition: pos, - nodeInfo: { - nodeIndex: pos.offset, - containerElement: pos.node - } - }; - } - - var workingNode = dom.getDocument(containerElement).createElement("span"); - - // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5 - // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64 - if (workingNode.parentNode) { - workingNode.parentNode.removeChild(workingNode); - } - - var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; - var previousNode, nextNode, boundaryPosition, boundaryNode; - var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0; - var childNodeCount = containerElement.childNodes.length; - var end = childNodeCount; - - // Check end first. Code within the loop assumes that the endth child node of the container is definitely - // after the range boundary. - var nodeIndex = end; - - while (true) { - if (nodeIndex == childNodeCount) { - containerElement.appendChild(workingNode); - } else { - containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]); - } - workingRange.moveToElementText(workingNode); - comparison = workingRange.compareEndPoints(workingComparisonType, textRange); - if (comparison == 0 || start == end) { - break; - } else if (comparison == -1) { - if (end == start + 1) { - // We know the endth child node is after the range boundary, so we must be done. - break; - } else { - start = nodeIndex; - } - } else { - end = (end == start + 1) ? start : nodeIndex; - } - nodeIndex = Math.floor((start + end) / 2); - containerElement.removeChild(workingNode); - } - - - // We've now reached or gone past the boundary of the text range we're interested in - // so have identified the node we want - boundaryNode = workingNode.nextSibling; - - if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) { - // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the - // node containing the text range's boundary, so we move the end of the working range to the boundary point - // and measure the length of its text to get the boundary's offset within the node. - workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); - - var offset; - - if (/[\r\n]/.test(boundaryNode.data)) { - /* - For the particular case of a boundary within a text node containing rendered line breaks (within a
-                    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