diff --git a/dist/js/pages/dashboard.js b/dist/js/pages/dashboard.js index ddbdac3dd..4f17d92e4 100644 --- a/dist/js/pages/dashboard.js +++ b/dist/js/pages/dashboard.js @@ -33,13 +33,13 @@ $(function () { { ranges: { 'Today': [moment(), moment()], - 'Yesterday': [moment().subtract('days', 1), moment().subtract('days', 1)], - 'Last 7 Days': [moment().subtract('days', 6), moment()], - 'Last 30 Days': [moment().subtract('days', 29), moment()], + 'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')], + 'Last 7 Days': [moment().subtract(6, 'days'), moment()], + 'Last 30 Days': [moment().subtract(29, 'days'), moment()], 'This Month': [moment().startOf('month'), moment().endOf('month')], - 'Last Month': [moment().subtract('month', 1).startOf('month'), moment().subtract('month', 1).endOf('month')] + 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')] }, - startDate: moment().subtract('days', 29), + startDate: moment().subtract(29, 'days'), endDate: moment() }, function (start, end) { diff --git a/documentation/build/include/plugins.html b/documentation/build/include/plugins.html index c0a04e959..e7e04a267 100644 --- a/documentation/build/include/plugins.html +++ b/documentation/build/include/plugins.html @@ -17,17 +17,17 @@
  • Bootstrap Slider
  • Ion Slider
  • Date Picker
  • -
  • Date Range Picker
  • +
  • Date Range Picker
  • Color Picker
  • Time Picker
  • -
  • iCheck
  • +
  • iCheck
  • Input Mask
  • @@ -35,11 +35,11 @@ diff --git a/documentation/index.html b/documentation/index.html index 6d276aade..df486d5b2 100644 --- a/documentation/index.html +++ b/documentation/index.html @@ -1993,17 +1993,17 @@ AdminLTE/
  • Bootstrap Slider
  • Ion Slider
  • Date Picker
  • -
  • Date Range Picker
  • +
  • Date Range Picker
  • Color Picker
  • Time Picker
  • -
  • iCheck
  • +
  • iCheck
  • Input Mask
  • @@ -2011,11 +2011,11 @@ AdminLTE/ diff --git a/index.html b/index.html index 5589b5157..6d23e5caf 100644 --- a/index.html +++ b/index.html @@ -1068,8 +1068,8 @@ - - + + - - + + diff --git a/pages/forms/advanced.html b/pages/forms/advanced.html index 686e77b7a..a020dd017 100644 --- a/pages/forms/advanced.html +++ b/pages/forms/advanced.html @@ -940,13 +940,13 @@ { ranges: { 'Today': [moment(), moment()], - 'Yesterday': [moment().subtract('days', 1), moment().subtract('days', 1)], - 'Last 7 Days': [moment().subtract('days', 6), moment()], - 'Last 30 Days': [moment().subtract('days', 29), moment()], + 'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')], + 'Last 7 Days': [moment().subtract(6, 'days'), moment()], + 'Last 30 Days': [moment().subtract(29, 'days'), moment()], 'This Month': [moment().startOf('month'), moment().endOf('month')], - 'Last Month': [moment().subtract('month', 1).startOf('month'), moment().subtract('month', 1).endOf('month')] + 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')] }, - startDate: moment().subtract('days', 29), + startDate: moment().subtract(29, 'days'), endDate: moment() }, function (start, end) { diff --git a/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.js b/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.js new file mode 100644 index 000000000..acccf91e0 --- /dev/null +++ b/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.js @@ -0,0 +1,14975 @@ +// TODO: in future try to replace most inline compability checks with polyfills for code readability + +// element.textContent polyfill. +// Unsupporting browsers: IE8 + +if (Object.defineProperty && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(Element.prototype, "textContent") && !Object.getOwnPropertyDescriptor(Element.prototype, "textContent").get) { + (function() { + var innerText = Object.getOwnPropertyDescriptor(Element.prototype, "innerText"); + Object.defineProperty(Element.prototype, "textContent", + { + get: function() { + return innerText.get.call(this); + }, + set: function(s) { + return innerText.set.call(this, s); + } + } + ); + })(); +} + +// isArray polyfill for ie8 +if(!Array.isArray) { + Array.isArray = function(arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; + }; +};/** + * @license wysihtml5x v0.4.15 + * https://github.com/Edicy/wysihtml5 + * + * Author: Christopher Blum (https://github.com/tiff) + * Secondary author of extended features: Oliver Pulges (https://github.com/pulges) + * + * Copyright (C) 2012 XING AG + * Licensed under the MIT license (MIT) + * + */ +var wysihtml5 = { + version: "0.4.15", + + // namespaces + commands: {}, + dom: {}, + quirks: {}, + toolbar: {}, + lang: {}, + selection: {}, + views: {}, + + INVISIBLE_SPACE: "\uFEFF", + + EMPTY_FUNCTION: function() {}, + + ELEMENT_NODE: 1, + TEXT_NODE: 3, + + BACKSPACE_KEY: 8, + ENTER_KEY: 13, + ESCAPE_KEY: 27, + SPACE_KEY: 32, + DELETE_KEY: 46 +}; +;/** + * Rangy, a cross-browser JavaScript range and selection library + * http://code.google.com/p/rangy/ + * + * Copyright 2014, Tim Down + * Licensed under the MIT license. + * Version: 1.3alpha.20140804 + * Build date: 4 August 2014 + */ + +(function(factory, global) { + if (typeof define == "function" && define.amd) { + // AMD. Register as an anonymous module. + define(factory); +/* + TODO: look into this properly. + + } else if (typeof exports == "object") { + // Node/CommonJS style for Browserify + module.exports = factory; +*/ + } else { + // No AMD or CommonJS support so we place Rangy in a global variable + global.rangy = factory(); + } +})(function() { + + 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.20140804", + 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, + autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize + } + }; + + 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 && o !== null && typeof o == "object" && p !== null && typeof p == "object") { + api.util.extend(o, p, true); + } + obj[i] = p; + } + } + // Special case for toString, which does not show up in for...in loops in IE <= 8 + if (props.hasOwnProperty("toString")) { + obj.toString = props.toString; + } + 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; + } + } + + 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 shimListeners = []; + + api.addShimListener = function(listener) { + shimListeners.push(listener); + }; + + function shim(win) { + win = win || window; + init(); + + // Notify listeners + for (var i = 0, len = shimListeners.length; i < len; ++i) { + shimListeners[i](win); + } + } + + api.shim = api.createMissingNativeApi = shim; + + function Module(name, dependencies, initializer) { + this.name = name; + this.dependencies = dependencies; + this.initialized = false; + this.supported = false; + this.initializer = initializer; + } + + Module.prototype = { + init: function() { + 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; + } + + 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]; + } + + var module = createModule(false, name, dependencies, initFunc); + + // Initialize the module immediately if the core is already initialized + if (api.initialized) { + module.init(); + } + }; + + 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 && api.config.autoInitialize) { + 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); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // DOM utility methods used by Rangy + api.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) { + var n; + try { + n = 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 + ">[index:" + getNodeIndex(node) + ",length:" + 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, + INVALID_NODE_TYPE_ERR: 24 + }; + + 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; + }); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Pure JavaScript implementation of DOM Range + api.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(); + } + + 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(); + 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(); + } 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(); + } 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() { + this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; + } + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + 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 DOMException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertValidNodeType(node, invalidTypes) { + if (!arrayContains(invalidTypes, node.nodeType)) { + throw new DOMException("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) { + 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) { + 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); + } + + function rangeToHtml(range) { + assertRangeValid(range); + var container = range.commonAncestorContainer.parentNode.cloneNode(false); + container.appendChild( range.cloneContents() ); + return container.innerHTML; + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + 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 DOMException("INVALID_STATE_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() { + return rangeToHtml(this); + }, + + // 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 DOMException("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); + return this.containsRange(nodeRange); + } else { + return this.containsNodeContents(node); + } + }, + + getNodes: function(nodeTypes, filter) { + assertRangeValid(this); + return getNodesInRange(this, nodeTypes, filter); + }, + + getDocument: function() { + return getRangeDocument(this); + }, + + collapseBefore: function(node) { + this.setEndBefore(node); + this.collapse(false); + }, + + collapseAfter: function(node) { + 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; + } + + 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); + }, + + detach: function() { + // In DOM4, detach() is now a no-op. + } + }); + + 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) { + function createBeforeAfterNodeSetter(isBefore, isStart) { + return function(node) { + 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) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeStart(this, node, offset); + }, + + setEnd: function(node, offset) { + 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() { + 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) { + assertNoDocTypeNotationEntityAncestor(node, true); + + boundaryUpdater(this, node, 0, node, getNodeLength(node)); + }, + + selectNode: function(node) { + 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; + }, + + 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) { + 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 Range(doc) { + this.startContainer = doc; + this.startOffset = 0; + this.endContainer = doc; + this.endOffset = 0; + this.document = doc; + updateCollapsedAndCommonAncestor(this); + } + + createPrototypeRange(Range, updateBoundaries); + + util.extend(Range, { + rangeProperties: rangeProperties, + RangeIterator: RangeIterator, + copyComparisonConstants: copyComparisonConstants, + createPrototypeRange: createPrototypeRange, + inspect: inspect, + toHtml: rangeToHtml, + 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; + }); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Wrappers for the browser's native DOM Range and/or TextRange implementation + api.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); + } + } + + var createBeforeAfterNodeSetter; + + WrappedRange = function(range) { + if (!range) { + throw module.createError("WrappedRange: Range must be specified"); + } + this.nativeRange = range; + updateRangeProperties(this); + }; + + DomRange.createPrototypeRange(WrappedRange, updateNativeRange); + + 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); + + 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 approach used 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);
    +
    +            var 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;
    +                }
    +            };
    +
    +            WrappedTextRange.rangeToTextRange = rangeToTextRange;
    +
    +            WrappedTextRange.prototype.toTextRange = function() {
    +                return rangeToTextRange(this);
    +            };
    +
    +            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.addShimListener(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)
    +    api.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) {
    +                        // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
    +                        // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
    +                        // nothing we can do about this while retaining the feature test so we have to resort to a browser
    +                        // sniff. I'm not happy about it. See
    +                        // https://code.google.com/p/chromium/issues/detail?id=399791
    +                        var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
    +                        if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
    +                            selectionSupportsMultipleRanges = false;
    +                        } else {
    +                            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);
    +                        }
    +                    }
    +
    +                    // Clean up
    +                    body.removeChild(testEl);
    +                    sel.removeAllRanges();
    +
    +                    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 the 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 && implementsDocSelection && 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 && implementsDocSelection) {
    +            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 ) || false;
    +        };
    +
    +        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() {
    +            var rangeHtmls = [];
    +            this.eachRange(function(range) {
    +                rangeHtmls.push( DomRange.toHtml(range) );
    +            });
    +            return rangeHtmls.join("");
    +        };
    +
    +        if (features.implementsTextRange) {
    +            selProto.getNativeTextRange = function() {
    +                var sel, textRange;
    +                if ( (sel = this.docSelection) ) {
    +                    var range = sel.createRange();
    +                    if (isTextRange(range)) {
    +                        return range;
    +                    } else {
    +                        throw module.createError("getNativeTextRange: selection is a control selection"); 
    +                    }
    +                } else if (this.rangeCount > 0) {
    +                    return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
    +                } else {
    +                    throw module.createError("getNativeTextRange: selection contains no range");
    +                }
    +            };
    +        }
    +
    +        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.addShimListener(function(win) {
    +            if (typeof win.getSelection == "undefined") {
    +                win.getSelection = function() {
    +                    return getSelection(win);
    +                };
    +            }
    +            win = null;
    +        });
    +    });
    +    
    +
    +    /*----------------------------------------------------------------------------------------------------------------*/
    +
    +    return api;
    +}, this);;/**
    + * Selection save and restore module for Rangy.
    + * Saves and restores user selections using marker invisible elements in the DOM.
    + *
    + * Part of Rangy, a cross-browser JavaScript range and selection library
    + * http://code.google.com/p/rangy/
    + *
    + * Depends on Rangy core.
    + *
    + * Copyright 2014, Tim Down
    + * Licensed under the MIT license.
    + * Version: 1.3alpha.20140804
    + * Build date: 4 August 2014
    + */
    +(function(factory, global) {
    +    if (typeof define == "function" && define.amd) {
    +        // AMD. Register as an anonymous module with a dependency on Rangy.
    +        define(["rangy"], factory);
    +        /*
    +         } else if (typeof exports == "object") {
    +         // Node/CommonJS style for Browserify
    +         module.exports = factory;
    +         */
    +    } else {
    +        // No AMD or CommonJS support so we use the rangy global variable
    +        factory(global.rangy);
    +    }
    +})(function(rangy) {
    +    rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
    +        var dom = api.dom;
    +
    +        var markerTextChar = "\ufeff";
    +
    +        function gEBI(id, doc) {
    +            return (doc || document).getElementById(id);
    +        }
    +
    +        function insertRangeBoundaryMarker(range, atStart) {
    +            var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
    +            var markerEl;
    +            var doc = dom.getDocument(range.startContainer);
    +
    +            // Clone the Range and collapse to the appropriate boundary point
    +            var boundaryRange = range.cloneRange();
    +            boundaryRange.collapse(atStart);
    +
    +            // Create the marker element containing a single invisible character using DOM methods and insert it
    +            markerEl = doc.createElement("span");
    +            markerEl.id = markerId;
    +            markerEl.style.lineHeight = "0";
    +            markerEl.style.display = "none";
    +            markerEl.className = "rangySelectionBoundary";
    +            markerEl.appendChild(doc.createTextNode(markerTextChar));
    +
    +            boundaryRange.insertNode(markerEl);
    +            return markerEl;
    +        }
    +
    +        function setRangeBoundary(doc, range, markerId, atStart) {
    +            var markerEl = gEBI(markerId, doc);
    +            if (markerEl) {
    +                range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
    +                markerEl.parentNode.removeChild(markerEl);
    +            } else {
    +                module.warn("Marker element has been removed. Cannot restore selection.");
    +            }
    +        }
    +
    +        function compareRanges(r1, r2) {
    +            return r2.compareBoundaryPoints(r1.START_TO_START, r1);
    +        }
    +
    +        function saveRange(range, backward) {
    +            var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
    +
    +            if (range.collapsed) {
    +                endEl = insertRangeBoundaryMarker(range, false);
    +                return {
    +                    document: doc,
    +                    markerId: endEl.id,
    +                    collapsed: true
    +                };
    +            } else {
    +                endEl = insertRangeBoundaryMarker(range, false);
    +                startEl = insertRangeBoundaryMarker(range, true);
    +
    +                return {
    +                    document: doc,
    +                    startMarkerId: startEl.id,
    +                    endMarkerId: endEl.id,
    +                    collapsed: false,
    +                    backward: backward,
    +                    toString: function() {
    +                        return "original text: '" + text + "', new text: '" + range.toString() + "'";
    +                    }
    +                };
    +            }
    +        }
    +
    +        function restoreRange(rangeInfo, normalize) {
    +            var doc = rangeInfo.document;
    +            if (typeof normalize == "undefined") {
    +                normalize = true;
    +            }
    +            var range = api.createRange(doc);
    +            if (rangeInfo.collapsed) {
    +                var markerEl = gEBI(rangeInfo.markerId, doc);
    +                if (markerEl) {
    +                    markerEl.style.display = "inline";
    +                    var previousNode = markerEl.previousSibling;
    +
    +                    // Workaround for issue 17
    +                    if (previousNode && previousNode.nodeType == 3) {
    +                        markerEl.parentNode.removeChild(markerEl);
    +                        range.collapseToPoint(previousNode, previousNode.length);
    +                    } else {
    +                        range.collapseBefore(markerEl);
    +                        markerEl.parentNode.removeChild(markerEl);
    +                    }
    +                } else {
    +                    module.warn("Marker element has been removed. Cannot restore selection.");
    +                }
    +            } else {
    +                setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
    +                setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
    +            }
    +
    +            if (normalize) {
    +                range.normalizeBoundaries();
    +            }
    +
    +            return range;
    +        }
    +
    +        function saveRanges(ranges, backward) {
    +            var rangeInfos = [], range, doc;
    +
    +            // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
    +            ranges = ranges.slice(0);
    +            ranges.sort(compareRanges);
    +
    +            for (var i = 0, len = ranges.length; i < len; ++i) {
    +                rangeInfos[i] = saveRange(ranges[i], backward);
    +            }
    +
    +            // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
    +            // between its markers
    +            for (i = len - 1; i >= 0; --i) {
    +                range = ranges[i];
    +                doc = api.DomRange.getRangeDocument(range);
    +                if (range.collapsed) {
    +                    range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
    +                } else {
    +                    range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
    +                    range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
    +                }
    +            }
    +
    +            return rangeInfos;
    +        }
    +
    +        function saveSelection(win) {
    +            if (!api.isSelectionValid(win)) {
    +                module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
    +                return null;
    +            }
    +            var sel = api.getSelection(win);
    +            var ranges = sel.getAllRanges();
    +            var backward = (ranges.length == 1 && sel.isBackward());
    +
    +            var rangeInfos = saveRanges(ranges, backward);
    +
    +            // Ensure current selection is unaffected
    +            if (backward) {
    +                sel.setSingleRange(ranges[0], "backward");
    +            } else {
    +                sel.setRanges(ranges);
    +            }
    +
    +            return {
    +                win: win,
    +                rangeInfos: rangeInfos,
    +                restored: false
    +            };
    +        }
    +
    +        function restoreRanges(rangeInfos) {
    +            var ranges = [];
    +
    +            // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
    +            // normalization affecting previously restored ranges.
    +            var rangeCount = rangeInfos.length;
    +
    +            for (var i = rangeCount - 1; i >= 0; i--) {
    +                ranges[i] = restoreRange(rangeInfos[i], true);
    +            }
    +
    +            return ranges;
    +        }
    +
    +        function restoreSelection(savedSelection, preserveDirection) {
    +            if (!savedSelection.restored) {
    +                var rangeInfos = savedSelection.rangeInfos;
    +                var sel = api.getSelection(savedSelection.win);
    +                var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
    +
    +                if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
    +                    sel.removeAllRanges();
    +                    sel.addRange(ranges[0], true);
    +                } else {
    +                    sel.setRanges(ranges);
    +                }
    +
    +                savedSelection.restored = true;
    +            }
    +        }
    +
    +        function removeMarkerElement(doc, markerId) {
    +            var markerEl = gEBI(markerId, doc);
    +            if (markerEl) {
    +                markerEl.parentNode.removeChild(markerEl);
    +            }
    +        }
    +
    +        function removeMarkers(savedSelection) {
    +            var rangeInfos = savedSelection.rangeInfos;
    +            for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
    +                rangeInfo = rangeInfos[i];
    +                if (rangeInfo.collapsed) {
    +                    removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
    +                } else {
    +                    removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
    +                    removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
    +                }
    +            }
    +        }
    +
    +        api.util.extend(api, {
    +            saveRange: saveRange,
    +            restoreRange: restoreRange,
    +            saveRanges: saveRanges,
    +            restoreRanges: restoreRanges,
    +            saveSelection: saveSelection,
    +            restoreSelection: restoreSelection,
    +            removeMarkerElement: removeMarkerElement,
    +            removeMarkers: removeMarkers
    +        });
    +    });
    +    
    +}, this);;/*
    +	Base.js, version 1.1a
    +	Copyright 2006-2010, Dean Edwards
    +	License: http://www.opensource.org/licenses/mit-license.php
    +*/
    +
    +var Base = function() {
    +	// dummy
    +};
    +
    +Base.extend = function(_instance, _static) { // subclass
    +	var extend = Base.prototype.extend;
    +	
    +	// build the prototype
    +	Base._prototyping = true;
    +	var proto = new this;
    +	extend.call(proto, _instance);
    +  proto.base = function() {
    +    // call this method from any other method to invoke that method's ancestor
    +  };
    +	delete Base._prototyping;
    +	
    +	// create the wrapper for the constructor function
    +	//var constructor = proto.constructor.valueOf(); //-dean
    +	var constructor = proto.constructor;
    +	var klass = proto.constructor = function() {
    +		if (!Base._prototyping) {
    +			if (this._constructing || this.constructor == klass) { // instantiation
    +				this._constructing = true;
    +				constructor.apply(this, arguments);
    +				delete this._constructing;
    +			} else if (arguments[0] != null) { // casting
    +				return (arguments[0].extend || extend).call(arguments[0], proto);
    +			}
    +		}
    +	};
    +	
    +	// build the class interface
    +	klass.ancestor = this;
    +	klass.extend = this.extend;
    +	klass.forEach = this.forEach;
    +	klass.implement = this.implement;
    +	klass.prototype = proto;
    +	klass.toString = this.toString;
    +	klass.valueOf = function(type) {
    +		//return (type == "object") ? klass : constructor; //-dean
    +		return (type == "object") ? klass : constructor.valueOf();
    +	};
    +	extend.call(klass, _static);
    +	// class initialisation
    +	if (typeof klass.init == "function") klass.init();
    +	return klass;
    +};
    +
    +Base.prototype = {	
    +	extend: function(source, value) {
    +		if (arguments.length > 1) { // extending with a name/value pair
    +			var ancestor = this[source];
    +			if (ancestor && (typeof value == "function") && // overriding a method?
    +				// the valueOf() comparison is to avoid circular references
    +				(!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
    +				/\bbase\b/.test(value)) {
    +				// get the underlying method
    +				var method = value.valueOf();
    +				// override
    +				value = function() {
    +					var previous = this.base || Base.prototype.base;
    +					this.base = ancestor;
    +					var returnValue = method.apply(this, arguments);
    +					this.base = previous;
    +					return returnValue;
    +				};
    +				// point to the underlying method
    +				value.valueOf = function(type) {
    +					return (type == "object") ? value : method;
    +				};
    +				value.toString = Base.toString;
    +			}
    +			this[source] = value;
    +		} else if (source) { // extending with an object literal
    +			var extend = Base.prototype.extend;
    +			// if this object has a customised extend method then use it
    +			if (!Base._prototyping && typeof this != "function") {
    +				extend = this.extend || extend;
    +			}
    +			var proto = {toSource: null};
    +			// do the "toString" and other methods manually
    +			var hidden = ["constructor", "toString", "valueOf"];
    +			// if we are prototyping then include the constructor
    +			var i = Base._prototyping ? 0 : 1;
    +			while (key = hidden[i++]) {
    +				if (source[key] != proto[key]) {
    +					extend.call(this, key, source[key]);
    +
    +				}
    +			}
    +			// copy each of the source object's properties to this object
    +			for (var key in source) {
    +				if (!proto[key]) extend.call(this, key, source[key]);
    +			}
    +		}
    +		return this;
    +	}
    +};
    +
    +// initialise
    +Base = Base.extend({
    +	constructor: function() {
    +		this.extend(arguments[0]);
    +	}
    +}, {
    +	ancestor: Object,
    +	version: "1.1",
    +	
    +	forEach: function(object, block, context) {
    +		for (var key in object) {
    +			if (this.prototype[key] === undefined) {
    +				block.call(context, object[key], key, object);
    +			}
    +		}
    +	},
    +		
    +	implement: function() {
    +		for (var i = 0; i < arguments.length; i++) {
    +			if (typeof arguments[i] == "function") {
    +				// if it's a function, call it
    +				arguments[i](this.prototype);
    +			} else {
    +				// add the interface using the extend method
    +				this.prototype.extend(arguments[i]);
    +			}
    +		}
    +		return this;
    +	},
    +	
    +	toString: function() {
    +		return String(this.valueOf());
    +	}
    +});;/**
    + * Detect browser support for specific features
    + */
    +wysihtml5.browser = (function() {
    +  var userAgent   = navigator.userAgent,
    +      testElement = document.createElement("div"),
    +      // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
    +      isGecko     = userAgent.indexOf("Gecko")        !== -1 && userAgent.indexOf("KHTML") === -1,
    +      isWebKit    = userAgent.indexOf("AppleWebKit/") !== -1,
    +      isChrome    = userAgent.indexOf("Chrome/")      !== -1,
    +      isOpera     = userAgent.indexOf("Opera/")       !== -1;
    +
    +  function iosVersion(userAgent) {
    +    return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1];
    +  }
    +
    +  function androidVersion(userAgent) {
    +    return +(userAgent.match(/android (\d+)/) || [undefined, 0])[1];
    +  }
    +
    +  function isIE(version, equation) {
    +    var rv = -1,
    +        re;
    +
    +    if (navigator.appName == 'Microsoft Internet Explorer') {
    +      re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
    +    } else if (navigator.appName == 'Netscape') {
    +      re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})");
    +    }
    +
    +    if (re && re.exec(navigator.userAgent) != null) {
    +      rv = parseFloat(RegExp.$1);
    +    }
    +
    +    if (rv === -1) { return false; }
    +    if (!version) { return true; }
    +    if (!equation) { return version === rv; }
    +    if (equation === "<") { return version < rv; }
    +    if (equation === ">") { return version > rv; }
    +    if (equation === "<=") { return version <= rv; }
    +    if (equation === ">=") { return version >= rv; }
    +  }
    +
    +  return {
    +    // Static variable needed, publicly accessible, to be able override it in unit tests
    +    USER_AGENT: userAgent,
    +
    +    /**
    +     * Exclude browsers that are not capable of displaying and handling
    +     * contentEditable as desired:
    +     *    - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable
    +     *    - IE < 8 create invalid markup and crash randomly from time to time
    +     *
    +     * @return {Boolean}
    +     */
    +    supported: function() {
    +      var userAgent                   = this.USER_AGENT.toLowerCase(),
    +          // Essential for making html elements editable
    +          hasContentEditableSupport   = "contentEditable" in testElement,
    +          // Following methods are needed in order to interact with the contentEditable area
    +          hasEditingApiSupport        = document.execCommand && document.queryCommandSupported && document.queryCommandState,
    +          // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+
    +          hasQuerySelectorSupport     = document.querySelector && document.querySelectorAll,
    +          // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)
    +          isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || (this.isAndroid() && androidVersion(userAgent) < 4) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;
    +      return hasContentEditableSupport
    +        && hasEditingApiSupport
    +        && hasQuerySelectorSupport
    +        && !isIncompatibleMobileBrowser;
    +    },
    +
    +    isTouchDevice: function() {
    +      return this.supportsEvent("touchmove");
    +    },
    +
    +    isIos: function() {
    +      return (/ipad|iphone|ipod/i).test(this.USER_AGENT);
    +    },
    +
    +    isAndroid: function() {
    +      return this.USER_AGENT.indexOf("Android") !== -1;
    +    },
    +
    +    /**
    +     * Whether the browser supports sandboxed iframes
    +     * Currently only IE 6+ offers such feature