diff --git a/LICENSE b/LICENSE index 4219df1..0cd4314 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 Tim Down +Copyright (c) 2015 Tim Down Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/bower.json b/bower.json index b653218..cf02a97 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "rangy", - "version": "1.3.0-alpha.20150122", + "version": "1.2.3", "homepage": "https://github.com/timdown/rangy", "authors": [ "Tim Down " @@ -8,16 +8,12 @@ "description": "A cross-browser JavaScript range and selection library.", "main": [ "rangy-core.min.js", - "rangy-classapplier.min.js", - "rangy-highlighter.min.js", + "rangy-cssclassapplier.min.js", "rangy-selectionsaverestore.min.js", - "rangy-serializer.min.js", - "rangy-textrange.min.js" + "rangy-serializer.min.js" ], "moduleType": [ - "amd", - "globals", - "node" + "globals" ], "keywords": [ "range", diff --git a/rangy-classapplier.js b/rangy-classapplier.js deleted file mode 100644 index abf616c..0000000 --- a/rangy-classapplier.js +++ /dev/null @@ -1,1050 +0,0 @@ -/** - * Class Applier module for Rangy. - * Adds, removes and toggles classes on Ranges and Selections - * - * Part of Rangy, a cross-browser JavaScript range and selection library - * https://github.com/timdown/rangy - * - * Depends on Rangy core. - * - * Copyright 2015, Tim Down - * Licensed under the MIT license. - * Version: 1.3.0-alpha.20150122 - * Build date: 22 January 2015 - */ -(function(factory, root) { - if (typeof define == "function" && define.amd) { - // AMD. Register as an anonymous module with a dependency on Rangy. - define(["./rangy-core"], factory); - } else if (typeof module != "undefined" && typeof exports == "object") { - // Node/CommonJS style - module.exports = factory( require("rangy") ); - } else { - // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) - factory(root.rangy); - } -})(function(rangy) { - rangy.createModule("ClassApplier", ["WrappedSelection"], function(api, module) { - var dom = api.dom; - var DomPosition = dom.DomPosition; - var contains = dom.arrayContains; - var isHtmlNamespace = dom.isHtmlNamespace; - - - var defaultTagName = "span"; - - function each(obj, func) { - for (var i in obj) { - if (obj.hasOwnProperty(i)) { - if (func(i, obj[i]) === false) { - return false; - } - } - } - return true; - } - - function trim(str) { - return str.replace(/^\s\s*/, "").replace(/\s\s*$/, ""); - } - - var hasClass, addClass, removeClass; - if (api.util.isHostObject(document.createElement("div"), "classList")) { - hasClass = function(el, className) { - return el.classList.contains(className); - }; - - addClass = function(el, className) { - return el.classList.add(className); - }; - - removeClass = function(el, className) { - return el.classList.remove(className); - }; - } else { - hasClass = function(el, className) { - return el.className && new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(el.className); - }; - - addClass = function(el, className) { - if (el.className) { - if (!hasClass(el, className)) { - el.className += " " + className; - } - } else { - el.className = className; - } - }; - - removeClass = (function() { - function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) { - return (whiteSpaceBefore && whiteSpaceAfter) ? " " : ""; - } - - return function(el, className) { - if (el.className) { - el.className = el.className.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer); - } - }; - })(); - } - - function sortClassName(className) { - return className && className.split(/\s+/).sort().join(" "); - } - - function getSortedClassName(el) { - return sortClassName(el.className); - } - - function haveSameClasses(el1, el2) { - return getSortedClassName(el1) == getSortedClassName(el2); - } - - function movePosition(position, oldParent, oldIndex, newParent, newIndex) { - var posNode = position.node, posOffset = position.offset; - var newNode = posNode, newOffset = posOffset; - - if (posNode == newParent && posOffset > newIndex) { - ++newOffset; - } - - if (posNode == oldParent && (posOffset == oldIndex || posOffset == oldIndex + 1)) { - newNode = newParent; - newOffset += newIndex - oldIndex; - } - - if (posNode == oldParent && posOffset > oldIndex + 1) { - --newOffset; - } - - position.node = newNode; - position.offset = newOffset; - } - - function movePositionWhenRemovingNode(position, parentNode, index) { - if (position.node == parentNode && position.offset > index) { - --position.offset; - } - } - - function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) { - // For convenience, allow newIndex to be -1 to mean "insert at the end". - if (newIndex == -1) { - newIndex = newParent.childNodes.length; - } - - var oldParent = node.parentNode; - var oldIndex = dom.getNodeIndex(node); - - for (var i = 0, position; position = positionsToPreserve[i++]; ) { - movePosition(position, oldParent, oldIndex, newParent, newIndex); - } - - // Now actually move the node. - if (newParent.childNodes.length == newIndex) { - newParent.appendChild(node); - } else { - newParent.insertBefore(node, newParent.childNodes[newIndex]); - } - } - - function removePreservingPositions(node, positionsToPreserve) { - - var oldParent = node.parentNode; - var oldIndex = dom.getNodeIndex(node); - - for (var i = 0, position; position = positionsToPreserve[i++]; ) { - movePositionWhenRemovingNode(position, oldParent, oldIndex); - } - - node.parentNode.removeChild(node); - } - - function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) { - var child, children = []; - while ( (child = node.firstChild) ) { - movePreservingPositions(child, newParent, newIndex++, positionsToPreserve); - children.push(child); - } - if (removeNode) { - removePreservingPositions(node, positionsToPreserve); - } - return children; - } - - function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) { - return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve); - } - - function rangeSelectsAnyText(range, textNode) { - var textNodeRange = range.cloneRange(); - textNodeRange.selectNodeContents(textNode); - - var intersectionRange = textNodeRange.intersection(range); - var text = intersectionRange ? intersectionRange.toString() : ""; - - return text != ""; - } - - function getEffectiveTextNodes(range) { - var nodes = range.getNodes([3]); - - // Optimization as per issue 145 - - // Remove non-intersecting text nodes from the start of the range - var start = 0, node; - while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) { - ++start; - } - - // Remove non-intersecting text nodes from the start of the range - var end = nodes.length - 1; - while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) { - --end; - } - - return nodes.slice(start, end + 1); - } - - function elementsHaveSameNonClassAttributes(el1, el2) { - if (el1.attributes.length != el2.attributes.length) return false; - for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) { - attr1 = el1.attributes[i]; - name = attr1.name; - if (name != "class") { - attr2 = el2.attributes.getNamedItem(name); - if ( (attr1 === null) != (attr2 === null) ) return false; - if (attr1.specified != attr2.specified) return false; - if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false; - } - } - return true; - } - - function elementHasNonClassAttributes(el, exceptions) { - for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) { - attrName = el.attributes[i].name; - if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") { - return true; - } - } - return false; - } - - var getComputedStyleProperty = dom.getComputedStyleProperty; - var isEditableElement = (function() { - var testEl = document.createElement("div"); - return typeof testEl.isContentEditable == "boolean" ? - function (node) { - return node && node.nodeType == 1 && node.isContentEditable; - } : - function (node) { - if (!node || node.nodeType != 1 || node.contentEditable == "false") { - return false; - } - return node.contentEditable == "true" || isEditableElement(node.parentNode); - }; - })(); - - function isEditingHost(node) { - var parent; - return node && node.nodeType == 1 && - (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") || - (isEditableElement(node) && !isEditableElement(node.parentNode))); - } - - function isEditable(node) { - return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node); - } - - var inlineDisplayRegex = /^inline(-block|-table)?$/i; - - function isNonInlineElement(node) { - return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display")); - } - - // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html) - var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/; - - function isUnrenderedWhiteSpaceNode(node) { - if (node.data.length == 0) { - return true; - } - if (htmlNonWhiteSpaceRegex.test(node.data)) { - return false; - } - var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace"); - switch (cssWhiteSpace) { - case "pre": - case "pre-wrap": - case "-moz-pre-wrap": - return false; - case "pre-line": - if (/[\r\n]/.test(node.data)) { - return false; - } - } - - // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a - // non-inline element, it will not be rendered. This seems to be a good enough definition. - return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling); - } - - function getRangeBoundaries(ranges) { - var positions = [], i, range; - for (i = 0; range = ranges[i++]; ) { - positions.push( - new DomPosition(range.startContainer, range.startOffset), - new DomPosition(range.endContainer, range.endOffset) - ); - } - return positions; - } - - function updateRangesFromBoundaries(ranges, positions) { - for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) { - range = ranges[i]; - start = positions[i * 2]; - end = positions[i * 2 + 1]; - range.setStartAndEnd(start.node, start.offset, end.node, end.offset); - } - } - - function isSplitPoint(node, offset) { - if (dom.isCharacterDataNode(node)) { - if (offset == 0) { - return !!node.previousSibling; - } else if (offset == node.length) { - return !!node.nextSibling; - } else { - return true; - } - } - - return offset > 0 && offset < node.childNodes.length; - } - - function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) { - var newNode, parentNode; - var splitAtStart = (descendantOffset == 0); - - if (dom.isAncestorOf(descendantNode, node)) { - return node; - } - - if (dom.isCharacterDataNode(descendantNode)) { - var descendantIndex = dom.getNodeIndex(descendantNode); - if (descendantOffset == 0) { - descendantOffset = descendantIndex; - } else if (descendantOffset == descendantNode.length) { - descendantOffset = descendantIndex + 1; - } else { - throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node (" + - descendantOffset + " in " + descendantNode.data); - } - descendantNode = descendantNode.parentNode; - } - - if (isSplitPoint(descendantNode, descendantOffset)) { - // descendantNode is now guaranteed not to be a text or other character node - newNode = descendantNode.cloneNode(false); - parentNode = descendantNode.parentNode; - if (newNode.id) { - newNode.removeAttribute("id"); - } - var child, newChildIndex = 0; - - while ( (child = descendantNode.childNodes[descendantOffset]) ) { - movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve); - } - movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve); - return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve); - } else if (node != descendantNode) { - newNode = descendantNode.parentNode; - - // Work out a new split point in the parent node - var newNodeIndex = dom.getNodeIndex(descendantNode); - - if (!splitAtStart) { - newNodeIndex++; - } - return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve); - } - return node; - } - - function areElementsMergeable(el1, el2) { - return el1.namespaceURI == el2.namespaceURI && - el1.tagName.toLowerCase() == el2.tagName.toLowerCase() && - haveSameClasses(el1, el2) && - elementsHaveSameNonClassAttributes(el1, el2) && - getComputedStyleProperty(el1, "display") == "inline" && - getComputedStyleProperty(el2, "display") == "inline"; - } - - function createAdjacentMergeableTextNodeGetter(forward) { - var siblingPropName = forward ? "nextSibling" : "previousSibling"; - - return function(textNode, checkParentElement) { - var el = textNode.parentNode; - var adjacentNode = textNode[siblingPropName]; - if (adjacentNode) { - // Can merge if the node's previous/next sibling is a text node - if (adjacentNode && adjacentNode.nodeType == 3) { - return adjacentNode; - } - } else if (checkParentElement) { - // Compare text node parent element with its sibling - adjacentNode = el[siblingPropName]; - if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) { - var adjacentNodeChild = adjacentNode[forward ? "firstChild" : "lastChild"]; - if (adjacentNodeChild && adjacentNodeChild.nodeType == 3) { - return adjacentNodeChild; - } - } - } - return null; - }; - } - - var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false), - getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true); - - - function Merge(firstNode) { - this.isElementMerge = (firstNode.nodeType == 1); - this.textNodes = []; - var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; - if (firstTextNode) { - this.textNodes[0] = firstTextNode; - } - } - - Merge.prototype = { - doMerge: function(positionsToPreserve) { - var textNodes = this.textNodes; - var firstTextNode = textNodes[0]; - if (textNodes.length > 1) { - var firstTextNodeIndex = dom.getNodeIndex(firstTextNode); - var textParts = [], combinedTextLength = 0, textNode, parent; - for (var i = 0, len = textNodes.length, j, position; i < len; ++i) { - textNode = textNodes[i]; - parent = textNode.parentNode; - if (i > 0) { - parent.removeChild(textNode); - if (!parent.hasChildNodes()) { - parent.parentNode.removeChild(parent); - } - if (positionsToPreserve) { - for (j = 0; position = positionsToPreserve[j++]; ) { - // Handle case where position is inside the text node being merged into a preceding node - if (position.node == textNode) { - position.node = firstTextNode; - position.offset += combinedTextLength; - } - // Handle case where both text nodes precede the position within the same parent node - if (position.node == parent && position.offset > firstTextNodeIndex) { - --position.offset; - if (position.offset == firstTextNodeIndex + 1 && i < len - 1) { - position.node = firstTextNode; - position.offset = combinedTextLength; - } - } - } - } - } - textParts[i] = textNode.data; - combinedTextLength += textNode.data.length; - } - firstTextNode.data = textParts.join(""); - } - return firstTextNode.data; - }, - - getLength: function() { - var i = this.textNodes.length, len = 0; - while (i--) { - len += this.textNodes[i].length; - } - return len; - }, - - toString: function() { - var textParts = []; - for (var i = 0, len = this.textNodes.length; i < len; ++i) { - textParts[i] = "'" + this.textNodes[i].data + "'"; - } - return "[Merge(" + textParts.join(",") + ")]"; - } - }; - - var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements", - "removeEmptyElements", "onElementCreate"]; - - // TODO: Populate this with every attribute name that corresponds to a property with a different name. Really?? - var attrNamesForProperties = {}; - - function ClassApplier(className, options, tagNames) { - var normalize, i, len, propName, applier = this; - applier.cssClass = applier.className = className; // cssClass property is for backward compatibility - - var elementPropertiesFromOptions = null, elementAttributes = {}; - - // Initialize from options object - if (typeof options == "object" && options !== null) { - if (typeof options.elementTagName !== "undefined") { - options.elementTagName = options.elementTagName.toLowerCase(); - } - tagNames = options.tagNames; - elementPropertiesFromOptions = options.elementProperties; - elementAttributes = options.elementAttributes; - - for (i = 0; propName = optionProperties[i++]; ) { - if (options.hasOwnProperty(propName)) { - applier[propName] = options[propName]; - } - } - normalize = options.normalize; - } else { - normalize = options; - } - - // Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying - applier.normalize = (typeof normalize == "undefined") ? true : normalize; - - // Initialize element properties and attribute exceptions - applier.attrExceptions = []; - var el = document.createElement(applier.elementTagName); - applier.elementProperties = applier.copyPropertiesToElement(elementPropertiesFromOptions, el, true); - each(elementAttributes, function(attrName) { - applier.attrExceptions.push(attrName); - }); - applier.elementAttributes = elementAttributes; - - applier.elementSortedClassName = applier.elementProperties.hasOwnProperty("className") ? - sortClassName(applier.elementProperties.className + " " + className) : className; - - // Initialize tag names - applier.applyToAnyTagName = false; - var type = typeof tagNames; - if (type == "string") { - if (tagNames == "*") { - applier.applyToAnyTagName = true; - } else { - applier.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/); - } - } else if (type == "object" && typeof tagNames.length == "number") { - applier.tagNames = []; - for (i = 0, len = tagNames.length; i < len; ++i) { - if (tagNames[i] == "*") { - applier.applyToAnyTagName = true; - } else { - applier.tagNames.push(tagNames[i].toLowerCase()); - } - } - } else { - applier.tagNames = [applier.elementTagName]; - } - } - - ClassApplier.prototype = { - elementTagName: defaultTagName, - elementProperties: {}, - elementAttributes: {}, - ignoreWhiteSpace: true, - applyToEditableOnly: false, - useExistingElements: true, - removeEmptyElements: true, - onElementCreate: null, - - copyPropertiesToElement: function(props, el, createCopy) { - var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName; - - for (var p in props) { - if (props.hasOwnProperty(p)) { - propValue = props[p]; - elPropValue = el[p]; - - // Special case for class. The copied properties object has the applier's CSS class as well as its - // own to simplify checks when removing styling elements - if (p == "className") { - addClass(el, propValue); - addClass(el, this.className); - el[p] = sortClassName(el[p]); - if (createCopy) { - elProps[p] = propValue; - } - } - - // Special case for style - else if (p == "style") { - elStyle = elPropValue; - if (createCopy) { - elProps[p] = elPropsStyle = {}; - } - for (s in props[p]) { - if (props[p].hasOwnProperty(s)) { - elStyle[s] = propValue[s]; - if (createCopy) { - elPropsStyle[s] = elStyle[s]; - } - } - } - this.attrExceptions.push(p); - } else { - el[p] = propValue; - // Copy the property back from the dummy element so that later comparisons to check whether - // elements may be removed are checking against the right value. For example, the href property - // of an element returns a fully qualified URL even if it was previously assigned a relative - // URL. - if (createCopy) { - elProps[p] = el[p]; - - // Not all properties map to identically-named attributes - attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p; - this.attrExceptions.push(attrName); - } - } - } - } - - return createCopy ? elProps : ""; - }, - - copyAttributesToElement: function(attrs, el) { - for (var attrName in attrs) { - if (attrs.hasOwnProperty(attrName)) { - el.setAttribute(attrName, attrs[attrName]); - } - } - }, - - hasClass: function(node) { - return node.nodeType == 1 && - (this.applyToAnyTagName || contains(this.tagNames, node.tagName.toLowerCase())) && - hasClass(node, this.className); - }, - - getSelfOrAncestorWithClass: function(node) { - while (node) { - if (this.hasClass(node)) { - return node; - } - node = node.parentNode; - } - return null; - }, - - isModifiable: function(node) { - return !this.applyToEditableOnly || isEditable(node); - }, - - // White space adjacent to an unwrappable node can be ignored for wrapping - isIgnorableWhiteSpaceNode: function(node) { - return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node); - }, - - // Normalizes nodes after applying a CSS class to a Range. - postApply: function(textNodes, range, positionsToPreserve, isUndo) { - var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; - - var merges = [], currentMerge; - - var rangeStartNode = firstNode, rangeEndNode = lastNode; - var rangeStartOffset = 0, rangeEndOffset = lastNode.length; - - var textNode, precedingTextNode; - - // Check for every required merge and create a Merge object for each - for (var i = 0, len = textNodes.length; i < len; ++i) { - textNode = textNodes[i]; - precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo); - if (precedingTextNode) { - if (!currentMerge) { - currentMerge = new Merge(precedingTextNode); - merges.push(currentMerge); - } - currentMerge.textNodes.push(textNode); - if (textNode === firstNode) { - rangeStartNode = currentMerge.textNodes[0]; - rangeStartOffset = rangeStartNode.length; - } - if (textNode === lastNode) { - rangeEndNode = currentMerge.textNodes[0]; - rangeEndOffset = currentMerge.getLength(); - } - } else { - currentMerge = null; - } - } - - // Test whether the first node after the range needs merging - var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo); - - if (nextTextNode) { - if (!currentMerge) { - currentMerge = new Merge(lastNode); - merges.push(currentMerge); - } - currentMerge.textNodes.push(nextTextNode); - } - - // Apply the merges - if (merges.length) { - for (i = 0, len = merges.length; i < len; ++i) { - merges[i].doMerge(positionsToPreserve); - } - - // Set the range boundaries - range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset); - } - }, - - createContainer: function(doc) { - var el = doc.createElement(this.elementTagName); - this.copyPropertiesToElement(this.elementProperties, el, false); - this.copyAttributesToElement(this.elementAttributes, el); - addClass(el, this.className); - if (this.onElementCreate) { - this.onElementCreate(el, this); - } - return el; - }, - - elementHasProperties: function(el, props) { - var applier = this; - return each(props, function(p, propValue) { - if (p == "className") { - return sortClassName(el.className) == applier.elementSortedClassName; - } else if (typeof propValue == "object") { - if (!applier.elementHasProperties(el[p], propValue)) { - return false; - } - } else if (el[p] !== propValue) { - return false; - } - }); - }, - - applyToTextNode: function(textNode, positionsToPreserve) { - var parent = textNode.parentNode; - if (parent.childNodes.length == 1 && - this.useExistingElements && - isHtmlNamespace(parent) && - contains(this.tagNames, parent.tagName.toLowerCase()) && - this.elementHasProperties(parent, this.elementProperties)) { - - addClass(parent, this.className); - } else { - var el = this.createContainer(dom.getDocument(textNode)); - textNode.parentNode.insertBefore(el, textNode); - el.appendChild(textNode); - } - }, - - isRemovable: function(el) { - return isHtmlNamespace(el) && - el.tagName.toLowerCase() == this.elementTagName && - getSortedClassName(el) == this.elementSortedClassName && - this.elementHasProperties(el, this.elementProperties) && - !elementHasNonClassAttributes(el, this.attrExceptions) && - this.isModifiable(el); - }, - - isEmptyContainer: function(el) { - var childNodeCount = el.childNodes.length; - return el.nodeType == 1 && - this.isRemovable(el) && - (childNodeCount == 0 || (childNodeCount == 1 && this.isEmptyContainer(el.firstChild))); - }, - - removeEmptyContainers: function(range) { - var applier = this; - var nodesToRemove = range.getNodes([1], function(el) { - return applier.isEmptyContainer(el); - }); - - var rangesToPreserve = [range]; - var positionsToPreserve = getRangeBoundaries(rangesToPreserve); - - for (var i = 0, node; node = nodesToRemove[i++]; ) { - removePreservingPositions(node, positionsToPreserve); - } - - // Update the range from the preserved boundary positions - updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve); - }, - - undoToTextNode: function(textNode, range, ancestorWithClass, positionsToPreserve) { - if (!range.containsNode(ancestorWithClass)) { - // Split out the portion of the ancestor from which we can remove the CSS class - //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass); - var ancestorRange = range.cloneRange(); - ancestorRange.selectNode(ancestorWithClass); - if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)) { - splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, positionsToPreserve); - range.setEndAfter(ancestorWithClass); - } - if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)) { - ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, positionsToPreserve); - } - } - - if (this.isRemovable(ancestorWithClass)) { - replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve); - } else { - removeClass(ancestorWithClass, this.className); - } - }, - - splitAncestorWithClass: function(container, offset, positionsToPreserve) { - var ancestorWithClass = this.getSelfOrAncestorWithClass(container); - if (ancestorWithClass) { - splitNodeAt(ancestorWithClass, container, offset, positionsToPreserve); - } - }, - - undoToAncestor: function(ancestorWithClass, positionsToPreserve) { - if (this.isRemovable(ancestorWithClass)) { - replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve); - } else { - removeClass(ancestorWithClass, this.className); - } - }, - - applyToRange: function(range, rangesToPreserve) { - rangesToPreserve = rangesToPreserve || []; - - // Create an array of range boundaries to preserve - var positionsToPreserve = getRangeBoundaries(rangesToPreserve || []); - - range.splitBoundariesPreservingPositions(positionsToPreserve); - - // Tidy up the DOM by removing empty containers - if (this.removeEmptyElements) { - this.removeEmptyContainers(range); - } - - var textNodes = getEffectiveTextNodes(range); - - if (textNodes.length) { - for (var i = 0, textNode; textNode = textNodes[i++]; ) { - if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode) && - this.isModifiable(textNode)) { - this.applyToTextNode(textNode, positionsToPreserve); - } - } - textNode = textNodes[textNodes.length - 1]; - range.setStartAndEnd(textNodes[0], 0, textNode, textNode.length); - if (this.normalize) { - this.postApply(textNodes, range, positionsToPreserve, false); - } - - // Update the ranges from the preserved boundary positions - updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve); - } - }, - - applyToRanges: function(ranges) { - - var i = ranges.length; - while (i--) { - this.applyToRange(ranges[i], ranges); - } - - - return ranges; - }, - - applyToSelection: function(win) { - var sel = api.getSelection(win); - sel.setRanges( this.applyToRanges(sel.getAllRanges()) ); - }, - - undoToRange: function(range, rangesToPreserve) { - // Create an array of range boundaries to preserve - rangesToPreserve = rangesToPreserve || []; - var positionsToPreserve = getRangeBoundaries(rangesToPreserve); - - - range.splitBoundariesPreservingPositions(positionsToPreserve); - - // Tidy up the DOM by removing empty containers - if (this.removeEmptyElements) { - this.removeEmptyContainers(range, positionsToPreserve); - } - - var textNodes = getEffectiveTextNodes(range); - var textNode, ancestorWithClass; - var lastTextNode = textNodes[textNodes.length - 1]; - - if (textNodes.length) { - this.splitAncestorWithClass(range.endContainer, range.endOffset, positionsToPreserve); - this.splitAncestorWithClass(range.startContainer, range.startOffset, positionsToPreserve); - for (var i = 0, len = textNodes.length; i < len; ++i) { - textNode = textNodes[i]; - ancestorWithClass = this.getSelfOrAncestorWithClass(textNode); - if (ancestorWithClass && this.isModifiable(textNode)) { - this.undoToAncestor(ancestorWithClass, positionsToPreserve); - } - } - // Ensure the range is still valid - range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length); - - - if (this.normalize) { - this.postApply(textNodes, range, positionsToPreserve, true); - } - - // Update the ranges from the preserved boundary positions - updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve); - } - }, - - undoToRanges: function(ranges) { - // Get ranges returned in document order - var i = ranges.length; - - while (i--) { - this.undoToRange(ranges[i], ranges); - } - - return ranges; - }, - - undoToSelection: function(win) { - var sel = api.getSelection(win); - var ranges = api.getSelection(win).getAllRanges(); - this.undoToRanges(ranges); - sel.setRanges(ranges); - }, - - /* - getTextSelectedByRange: function(textNode, range) { - var textRange = range.cloneRange(); - textRange.selectNodeContents(textNode); - - var intersectionRange = textRange.intersection(range); - var text = intersectionRange ? intersectionRange.toString() : ""; - textRange.detach(); - - return text; - }, - */ - - isAppliedToRange: function(range) { - if (range.collapsed || range.toString() == "") { - return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer); - } else { - var textNodes = range.getNodes( [3] ); - if (textNodes.length) - for (var i = 0, textNode; textNode = textNodes[i++]; ) { - if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) && - this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) { - return false; - } - } - return true; - } - }, - - isAppliedToRanges: function(ranges) { - var i = ranges.length; - if (i == 0) { - return false; - } - while (i--) { - if (!this.isAppliedToRange(ranges[i])) { - return false; - } - } - return true; - }, - - isAppliedToSelection: function(win) { - var sel = api.getSelection(win); - return this.isAppliedToRanges(sel.getAllRanges()); - }, - - toggleRange: function(range) { - if (this.isAppliedToRange(range)) { - this.undoToRange(range); - } else { - this.applyToRange(range); - } - }, - - /* - toggleRanges: function(ranges) { - if (this.isAppliedToRanges(ranges)) { - this.undoToRanges(ranges); - } else { - this.applyToRanges(ranges); - } - }, - */ - - toggleSelection: function(win) { - if (this.isAppliedToSelection(win)) { - this.undoToSelection(win); - } else { - this.applyToSelection(win); - } - }, - - getElementsWithClassIntersectingRange: function(range) { - var elements = []; - var applier = this; - range.getNodes([3], function(textNode) { - var el = applier.getSelfOrAncestorWithClass(textNode); - if (el && !contains(elements, el)) { - elements.push(el); - } - }); - return elements; - }, - - /* - getElementsWithClassIntersectingSelection: function(win) { - var sel = api.getSelection(win); - var elements = []; - var applier = this; - sel.eachRange(function(range) { - var rangeElements = applier.getElementsWithClassIntersectingRange(range); - for (var i = 0, el; el = rangeElements[i++]; ) { - if (!contains(elements, el)) { - elements.push(el); - } - } - }); - return elements; - }, - */ - - detach: function() {} - }; - - function createClassApplier(className, options, tagNames) { - return new ClassApplier(className, options, tagNames); - } - - ClassApplier.util = { - hasClass: hasClass, - addClass: addClass, - removeClass: removeClass, - hasSameClasses: haveSameClasses, - replaceWithOwnChildren: replaceWithOwnChildrenPreservingPositions, - elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes, - elementHasNonClassAttributes: elementHasNonClassAttributes, - splitNodeAt: splitNodeAt, - isEditableElement: isEditableElement, - isEditingHost: isEditingHost, - isEditable: isEditable - }; - - api.CssClassApplier = api.ClassApplier = ClassApplier; - api.createCssClassApplier = api.createClassApplier = createClassApplier; - }); - -}, this); diff --git a/rangy-classapplier.min.js b/rangy-classapplier.min.js deleted file mode 100644 index 542f514..0000000 --- a/rangy-classapplier.min.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Class Applier module for Rangy. - * Adds, removes and toggles classes on Ranges and Selections - * - * Part of Rangy, a cross-browser JavaScript range and selection library - * https://github.com/timdown/rangy - * - * Depends on Rangy core. - * - * Copyright 2015, Tim Down - * Licensed under the MIT license. - * Version: 1.3.0-alpha.20150122 - * Build date: 22 January 2015 - */ -!function(e,t){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(t.rangy)}(function(e){e.createModule("ClassApplier",["WrappedSelection"],function(e,t){function n(e,t){for(var n in e)if(e.hasOwnProperty(n)&&t(n,e[n])===!1)return!1;return!0}function s(e){return e.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}function i(e){return e&&e.split(/\s+/).sort().join(" ")}function r(e){return i(e.className)}function o(e,t){return r(e)==r(t)}function a(e,t,n,s,i){var r=e.node,o=e.offset,a=r,l=o;r==s&&o>i&&++l,r!=t||o!=n&&o!=n+1||(a=s,l+=i-n),r==t&&o>n+1&&--l,e.node=a,e.offset=l}function l(e,t,n){e.node==t&&e.offset>n&&--e.offset}function f(e,t,n,s){-1==n&&(n=t.childNodes.length);for(var i,r=e.parentNode,o=L.getNodeIndex(e),l=0;i=s[l++];)a(i,r,o,t,n);t.childNodes.length==n?t.appendChild(e):t.insertBefore(e,t.childNodes[n])}function u(e,t){for(var n,s=e.parentNode,i=L.getNodeIndex(e),r=0;n=t[r++];)l(n,s,i);e.parentNode.removeChild(e)}function d(e,t,n,s,i){for(var r,o=[];r=e.firstChild;)f(r,t,n++,i),o.push(r);return s&&u(e,i),o}function p(e,t){return d(e,e.parentNode,L.getNodeIndex(e),!0,t)}function c(e,t){var n=e.cloneRange();n.selectNodeContents(t);var s=n.intersection(e),i=s?s.toString():"";return""!=i}function h(e){for(var t,n=e.getNodes([3]),s=0;(t=n[s])&&!c(e,t);)++s;for(var i=n.length-1;(t=n[i])&&!c(e,t);)--i;return n.slice(s,i+1)}function g(e,t){if(e.attributes.length!=t.attributes.length)return!1;for(var n,s,i,r=0,o=e.attributes.length;o>r;++r)if(n=e.attributes[r],i=n.name,"class"!=i){if(s=t.attributes.getNamedItem(i),null===n!=(null===s))return!1;if(n.specified!=s.specified)return!1;if(n.specified&&n.nodeValue!==s.nodeValue)return!1}return!0}function m(e,t){for(var n,s=0,i=e.attributes.length;i>s;++s)if(n=e.attributes[s].name,(!t||!H(t,n))&&e.attributes[s].specified&&"class"!=n)return!0;return!1}function N(e){var t;return e&&1==e.nodeType&&((t=e.parentNode)&&9==t.nodeType&&"on"==t.designMode||D(e)&&!D(e.parentNode))}function v(e){return(D(e)||1!=e.nodeType&&D(e.parentNode))&&!N(e)}function y(e){return e&&1==e.nodeType&&!$.test(B(e,"display"))}function C(e){if(0==e.data.length)return!0;if(U.test(e.data))return!1;var t=B(e.parentNode,"whiteSpace");switch(t){case"pre":case"pre-wrap":case"-moz-pre-wrap":return!1;case"pre-line":if(/[\r\n]/.test(e.data))return!1}return y(e.previousSibling)||y(e.nextSibling)}function T(e){var t,n,s=[];for(t=0;n=e[t++];)s.push(new M(n.startContainer,n.startOffset),new M(n.endContainer,n.endOffset));return s}function E(e,t){for(var n,s,i,r=0,o=e.length;o>r;++r)n=e[r],s=t[2*r],i=t[2*r+1],n.setStartAndEnd(s.node,s.offset,i.node,i.offset)}function b(e,t){return L.isCharacterDataNode(e)?0==t?!!e.previousSibling:t==e.length?!!e.nextSibling:!0:t>0&&ta;++a)"*"==r[a]?u.applyToAnyTagName=!0:u.tagNames.push(r[a].toLowerCase());else u.tagNames=[u.elementTagName]}function w(e,t,n){return new P(e,t,n)}var O,W,I,L=e.dom,M=L.DomPosition,H=L.arrayContains,j=L.isHtmlNamespace,z="span";e.util.isHostObject(document.createElement("div"),"classList")?(O=function(e,t){return e.classList.contains(t)},W=function(e,t){return e.classList.add(t)},I=function(e,t){return e.classList.remove(t)}):(O=function(e,t){return e.className&&new RegExp("(?:^|\\s)"+t+"(?:\\s|$)").test(e.className)},W=function(e,t){e.className?O(e,t)||(e.className+=" "+t):e.className=t},I=function(){function e(e,t,n){return t&&n?" ":""}return function(t,n){t.className&&(t.className=t.className.replace(new RegExp("(^|\\s)"+n+"(\\s|$)"),e))}}());var B=L.getComputedStyleProperty,D=function(){var e=document.createElement("div");return"boolean"==typeof e.isContentEditable?function(e){return e&&1==e.nodeType&&e.isContentEditable}:function(e){return e&&1==e.nodeType&&"false"!=e.contentEditable?"true"==e.contentEditable||D(e.parentNode):!1}}(),$=/^inline(-block|-table)?$/i,U=/[^\r\n\t\f \u200B]/,V=x(!1),k=x(!0);R.prototype={doMerge:function(e){var t=this.textNodes,n=t[0];if(t.length>1){for(var s,i,r,o,a=L.getNodeIndex(n),l=[],f=0,u=0,d=t.length;d>u;++u){if(s=t[u],i=s.parentNode,u>0&&(i.removeChild(s),i.hasChildNodes()||i.parentNode.removeChild(i),e))for(r=0;o=e[r++];)o.node==s&&(o.node=n,o.offset+=f),o.node==i&&o.offset>a&&(--o.offset,o.offset==a+1&&d-1>u&&(o.node=n,o.offset=f));l[u]=s.data,f+=s.data.length}n.data=l.join("")}return n.data},getLength:function(){for(var e=this.textNodes.length,t=0;e--;)t+=this.textNodes[e].length;return t},toString:function(){for(var e=[],t=0,n=this.textNodes.length;n>t;++t)e[t]="'"+this.textNodes[t].data+"'";return"[Merge("+e.join(",")+")]"}};var q=["elementTagName","ignoreWhiteSpace","applyToEditableOnly","useExistingElements","removeEmptyElements","onElementCreate"],F={};P.prototype={elementTagName:z,elementProperties:{},elementAttributes:{},ignoreWhiteSpace:!0,applyToEditableOnly:!1,useExistingElements:!0,removeEmptyElements:!0,onElementCreate:null,copyPropertiesToElement:function(e,t,n){var s,r,o,a,l,f,u={};for(var d in e)if(e.hasOwnProperty(d))if(a=e[d],l=t[d],"className"==d)W(t,a),W(t,this.className),t[d]=i(t[d]),n&&(u[d]=a);else if("style"==d){r=l,n&&(u[d]=o={});for(s in e[d])e[d].hasOwnProperty(s)&&(r[s]=a[s],n&&(o[s]=r[s]));this.attrExceptions.push(d)}else t[d]=a,n&&(u[d]=t[d],f=F.hasOwnProperty(d)?F[d]:d,this.attrExceptions.push(f));return n?u:""},copyAttributesToElement:function(e,t){for(var n in e)e.hasOwnProperty(n)&&t.setAttribute(n,e[n])},hasClass:function(e){return 1==e.nodeType&&(this.applyToAnyTagName||H(this.tagNames,e.tagName.toLowerCase()))&&O(e,this.className)},getSelfOrAncestorWithClass:function(e){for(;e;){if(this.hasClass(e))return e;e=e.parentNode}return null},isModifiable:function(e){return!this.applyToEditableOnly||v(e)},isIgnorableWhiteSpaceNode:function(e){return this.ignoreWhiteSpace&&e&&3==e.nodeType&&C(e)},postApply:function(e,t,n,s){for(var i,r,o,a=e[0],l=e[e.length-1],f=[],u=a,d=l,p=0,c=l.length,h=0,g=e.length;g>h;++h)r=e[h],o=V(r,!s),o?(i||(i=new R(o),f.push(i)),i.textNodes.push(r),r===a&&(u=i.textNodes[0],p=u.length),r===l&&(d=i.textNodes[0],c=i.getLength())):i=null;var m=k(l,!s);if(m&&(i||(i=new R(l),f.push(i)),i.textNodes.push(m)),f.length){for(h=0,g=f.length;g>h;++h)f[h].doMerge(n);t.setStartAndEnd(u,p,d,c)}},createContainer:function(e){var t=e.createElement(this.elementTagName);return this.copyPropertiesToElement(this.elementProperties,t,!1),this.copyAttributesToElement(this.elementAttributes,t),W(t,this.className),this.onElementCreate&&this.onElementCreate(t,this),t},elementHasProperties:function(e,t){var s=this;return n(t,function(t,n){if("className"==t)return i(e.className)==s.elementSortedClassName;if("object"==typeof n){if(!s.elementHasProperties(e[t],n))return!1}else if(e[t]!==n)return!1})},applyToTextNode:function(e){var t=e.parentNode;if(1==t.childNodes.length&&this.useExistingElements&&j(t)&&H(this.tagNames,t.tagName.toLowerCase())&&this.elementHasProperties(t,this.elementProperties))W(t,this.className);else{var n=this.createContainer(L.getDocument(e));e.parentNode.insertBefore(n,e),n.appendChild(e)}},isRemovable:function(e){return j(e)&&e.tagName.toLowerCase()==this.elementTagName&&r(e)==this.elementSortedClassName&&this.elementHasProperties(e,this.elementProperties)&&!m(e,this.attrExceptions)&&this.isModifiable(e)},isEmptyContainer:function(e){var t=e.childNodes.length;return 1==e.nodeType&&this.isRemovable(e)&&(0==t||1==t&&this.isEmptyContainer(e.firstChild))},removeEmptyContainers:function(e){for(var t,n=this,s=e.getNodes([1],function(e){return n.isEmptyContainer(e)}),i=[e],r=T(i),o=0;t=s[o++];)u(t,r);E(i,r)},undoToTextNode:function(e,t,n,s){if(!t.containsNode(n)){var i=t.cloneRange();i.selectNode(n),i.isPointInRange(t.endContainer,t.endOffset)&&(A(n,t.endContainer,t.endOffset,s),t.setEndAfter(n)),i.isPointInRange(t.startContainer,t.startOffset)&&(n=A(n,t.startContainer,t.startOffset,s))}this.isRemovable(n)?p(n,s):I(n,this.className)},splitAncestorWithClass:function(e,t,n){var s=this.getSelfOrAncestorWithClass(e);s&&A(s,e,t,n)},undoToAncestor:function(e,t){this.isRemovable(e)?p(e,t):I(e,this.className)},applyToRange:function(e,t){t=t||[];var n=T(t||[]);e.splitBoundariesPreservingPositions(n),this.removeEmptyElements&&this.removeEmptyContainers(e);var s=h(e);if(s.length){for(var i,r=0;i=s[r++];)this.isIgnorableWhiteSpaceNode(i)||this.getSelfOrAncestorWithClass(i)||!this.isModifiable(i)||this.applyToTextNode(i,n);i=s[s.length-1],e.setStartAndEnd(s[0],0,i,i.length),this.normalize&&this.postApply(s,e,n,!1),E(t,n)}},applyToRanges:function(e){for(var t=e.length;t--;)this.applyToRange(e[t],e);return e},applyToSelection:function(t){var n=e.getSelection(t);n.setRanges(this.applyToRanges(n.getAllRanges()))},undoToRange:function(e,t){t=t||[];var n=T(t);e.splitBoundariesPreservingPositions(n),this.removeEmptyElements&&this.removeEmptyContainers(e,n);var s,i,r=h(e),o=r[r.length-1];if(r.length){this.splitAncestorWithClass(e.endContainer,e.endOffset,n),this.splitAncestorWithClass(e.startContainer,e.startOffset,n);for(var a=0,l=r.length;l>a;++a)s=r[a],i=this.getSelfOrAncestorWithClass(s),i&&this.isModifiable(s)&&this.undoToAncestor(i,n);e.setStartAndEnd(r[0],0,o,o.length),this.normalize&&this.postApply(r,e,n,!0),E(t,n)}},undoToRanges:function(e){for(var t=e.length;t--;)this.undoToRange(e[t],e);return e},undoToSelection:function(t){var n=e.getSelection(t),s=e.getSelection(t).getAllRanges();this.undoToRanges(s),n.setRanges(s)},isAppliedToRange:function(e){if(e.collapsed||""==e.toString())return!!this.getSelfOrAncestorWithClass(e.commonAncestorContainer);var t=e.getNodes([3]);if(t.length)for(var n,s=0;n=t[s++];)if(!this.isIgnorableWhiteSpaceNode(n)&&c(e,n)&&this.isModifiable(n)&&!this.getSelfOrAncestorWithClass(n))return!1;return!0},isAppliedToRanges:function(e){var t=e.length;if(0==t)return!1;for(;t--;)if(!this.isAppliedToRange(e[t]))return!1;return!0},isAppliedToSelection:function(t){var n=e.getSelection(t);return this.isAppliedToRanges(n.getAllRanges())},toggleRange:function(e){this.isAppliedToRange(e)?this.undoToRange(e):this.applyToRange(e)},toggleSelection:function(e){this.isAppliedToSelection(e)?this.undoToSelection(e):this.applyToSelection(e)},getElementsWithClassIntersectingRange:function(e){var t=[],n=this;return e.getNodes([3],function(e){var s=n.getSelfOrAncestorWithClass(e);s&&!H(t,s)&&t.push(s)}),t},detach:function(){}},P.util={hasClass:O,addClass:W,removeClass:I,hasSameClasses:o,replaceWithOwnChildren:p,elementsHaveSameNonClassAttributes:g,elementHasNonClassAttributes:m,splitNodeAt:A,isEditableElement:D,isEditingHost:N,isEditable:v},e.CssClassApplier=e.ClassApplier=P,e.createCssClassApplier=e.createClassApplier=w})},this); \ No newline at end of file diff --git a/rangy-core.js b/rangy-core.js index 33e0254..8cda5f6 100644 --- a/rangy-core.js +++ b/rangy-core.js @@ -1,34 +1,20 @@ /** - * Rangy, a cross-browser JavaScript range and selection library - * https://github.com/timdown/rangy + * @license Rangy, a cross-browser JavaScript range and selection library + * http://code.google.com/p/rangy/ * - * Copyright 2015, Tim Down + * Copyright 2012, Tim Down * Licensed under the MIT license. - * Version: 1.3.0-alpha.20150122 - * Build date: 22 January 2015 + * Version: 1.2.3 + * Build date: 26 February 2012 */ +window['rangy'] = (function() { -(function(factory, root) { - if (typeof define == "function" && define.amd) { - // AMD. Register as an anonymous module. - define(factory); - } else if (typeof module != "undefined" && typeof exports == "object") { - // Node/CommonJS style - module.exports = factory(); - } else { - // No AMD or CommonJS support so we place Rangy in (probably) the global variable - root.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"]; + "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"]; - // 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"]; @@ -36,8 +22,8 @@ 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"]; + var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark", + "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"]; /*----------------------------------------------------------------------------------------------------------------*/ @@ -78,166 +64,67 @@ return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); } - function getBody(doc) { - return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; - } - - var modules = {}; - - var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED); - - var util = { - isHostMethod: isHostMethod, - isHostObject: isHostObject, - isHostProperty: isHostProperty, - areHostMethods: areHostMethods, - areHostObjects: areHostObjects, - areHostProperties: areHostProperties, - isTextRange: isTextRange, - getBody: getBody - }; - var api = { - version: "1.3.0-alpha.20150122", + version: "1.2.3", initialized: false, - isBrowser: isBrowser, supported: true, - util: util, + + util: { + isHostMethod: isHostMethod, + isHostObject: isHostObject, + isHostProperty: isHostProperty, + areHostMethods: areHostMethods, + areHostObjects: areHostObjects, + areHostProperties: areHostProperties, + isTextRange: isTextRange + }, + features: {}, - modules: modules, + + modules: {}, config: { - alertOnFail: true, alertOnWarn: false, - preferTextRange: false, - autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize + preferTextRange: false } }; - function consoleLog(msg) { - if (typeof console != UNDEFINED && isHostMethod(console, "log")) { - console.log(msg); - } - } - - function alertOrLog(msg, shouldAlert) { - if (isBrowser && shouldAlert) { - alert(msg); - } else { - consoleLog(msg); - } - } - function fail(reason) { + window.alert("Rangy not supported in your browser. Reason: " + reason); api.initialized = true; api.supported = false; - alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail); } api.fail = fail; function warn(msg) { - alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn); + var warningMessage = "Rangy warning: " + msg; + if (api.config.alertOnWarn) { + window.alert(warningMessage); + } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) { + window.console.log(warningMessage); + } } api.warn = warn; - // Add utility extend() method - var extend; if ({}.hasOwnProperty) { - util.extend = extend = function(obj, props, deep) { - var o, p; + api.util.extend = function(o, props) { 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") { - extend(o, p, true); - } - obj[i] = p; + o[i] = props[i]; } } - // 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; - }; - - util.createOptions = function(optionsParam, defaults) { - var options = {}; - extend(options, defaults); - if (optionsParam) { - extend(options, optionsParam); - } - return options; }; } else { fail("hasOwnProperty not supported"); } - - // Test whether we're in a browser and bail out if not - if (!isBrowser) { - fail("Rangy can only run in a browser"); - } - - // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not - (function() { - var toArray; - - if (isBrowser) { - var el = document.createElement("div"); - el.appendChild(document.createElement("span")); - var slice = [].slice; - 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; - }; - } - - 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 (isBrowser) { - 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"); - } - - util.addListener = addListener; - } var initListeners = []; - - function getErrorDesc(ex) { - return ex.message || ex.description || String(ex); - } + var moduleInitializers = []; // Initialization function init() { - if (!isBrowser || api.initialized) { + if (api.initialized) { return; } var testRange; @@ -250,13 +137,10 @@ if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { implementsDomRange = true; } + testRange.detach(); } - var body = getBody(document); - if (!body || body.nodeName.toLowerCase() != "body") { - fail("No body element found"); - return; - } + var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0]; if (body && isHostMethod(body, "createTextRange")) { testRange = body.createTextRange(); @@ -266,8 +150,7 @@ } if (!implementsDomRange && !implementsTextRange) { - fail("Neither Range nor TextRange are available"); - return; + fail("Neither Range nor TextRange are implemented"); } api.initialized = true; @@ -276,21 +159,16 @@ 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) { + // Initialize modules and call init listeners + var allListeners = moduleInitializers.concat(initListeners); + for (var i = 0, len = allListeners.length; i < len; ++i) { try { - initListeners[i](api); + allListeners[i](api); } catch (ex) { - errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex); - consoleLog(errorMessage); + if (isHostObject(window, "console") && isHostMethod(window.console, "log")) { + window.console.log("Init listener threw an exception. Continuing.", ex); + } + } } } @@ -307,3449 +185,3040 @@ } }; - var shimListeners = []; + var createMissingNativeApiListeners = []; - api.addShimListener = function(listener) { - shimListeners.push(listener); + api.addCreateMissingNativeApiListener = function(listener) { + createMissingNativeApiListeners.push(listener); }; - function shim(win) { + function createMissingNativeApi(win) { win = win || window; init(); // Notify listeners - for (var i = 0, len = shimListeners.length; i < len; ++i) { - shimListeners[i](win); + for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) { + createMissingNativeApiListeners[i](win); } } - if (isBrowser) { - api.shim = api.createMissingNativeApi = shim; - } + api.createMissingNativeApi = createMissingNativeApi; - function Module(name, dependencies, initializer) { + /** + * @constructor + */ + function Module(name) { 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]; + Module.prototype.fail = function(reason) { + this.initialized = true; + this.supported = false; - requiredModule = modules[moduleName]; - if (!requiredModule || !(requiredModule instanceof Module)) { - throw new Error("required module '" + moduleName + "' not found"); - } + throw new Error("Module '" + this.name + "' failed to load: " + reason); + }; - requiredModule.init(); + Module.prototype.warn = function(msg) { + api.warn("Module " + this.name + ": " + msg); + }; - 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); - }, + Module.prototype.createError = function(msg) { + return new Error("Error in Rangy " + this.name + " module: " + msg); + }; - warn: function(msg) { - api.warn("Module " + this.name + ": " + msg); - }, + api.createModule = function(name, initFunc) { + var module = new Module(name); + api.modules[name] = module; - deprecationNotice: function(deprecated, replacement) { - api.warn("DEPRECATED: " + deprecated + " in module " + this.name + "is deprecated. Please use " + - replacement + " instead"); - }, + moduleInitializers.push(function(api) { + initFunc(api, module); + module.initialized = true; + module.supported = true; + }); + }; - createError: function(msg) { - return new Error("Error in Rangy " + this.name + " module: " + msg); + api.requireModules = function(modules) { + for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) { + moduleName = modules[i]; + module = api.modules[moduleName]; + if (!module || !(module instanceof Module)) { + throw new Error("Module '" + moduleName + "' not found"); + } + if (!module.supported) { + throw new Error("Module '" + moduleName + "' not supported"); + } } }; - - function createModule(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); - if (ex.stack) { - consoleLog(ex.stack); - } - } - } - }); - modules[name] = newModule; - return 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]; - } + /*----------------------------------------------------------------------------------------------------------------*/ + + // Wait for document to load before running tests + + var docReady = false; - var module = createModule(name, dependencies, initFunc); + var loadHandler = function(e) { - // Initialize the module immediately if the core is already initialized - if (api.initialized && api.supported) { - module.init(); + if (!docReady) { + docReady = true; + if (!api.initialized) { + init(); + } } }; - api.createCoreModule = function(name, dependencies, initFunc) { - createModule(name, dependencies, initFunc); - }; + // 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); + } - // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately + // Add a fallback in case the DOMContentLoaded event isn't supported + if (isHostMethod(window, "addEventListener")) { + window.addEventListener("load", loadHandler, false); + } else if (isHostMethod(window, "attachEvent")) { + window.attachEvent("onload", loadHandler); + } else { + fail("Window does not have required addEventListener or attachEvent method"); + } - function RangePrototype() {} - api.RangePrototype = RangePrototype; - api.rangePrototype = new RangePrototype(); + return api; +})(); +rangy.createModule("DomUtil", function(api, module) { - function SelectionPrototype() {} - api.selectionPrototype = new SelectionPrototype(); + 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"); + } - // DOM utility methods used by Rangy - api.createCoreModule("DomUtil", [], function(api, module) { - var UNDEF = "undefined"; - var util = api.util; + if (!util.isHostMethod(document, "getElementsByTagName")) { + module.fail("document missing getElementsByTagName method"); + } - // Perform feature tests - if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { - module.fail("document missing a Node creation 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"); + } - if (!util.isHostMethod(document, "getElementsByTagName")) { - module.fail("document missing getElementsByTagName method"); - } + // innerHTML is required for Range's createContextualFragment method + if (!util.isHostProperty(el, "innerHTML")) { + module.fail("Element is missing innerHTML property"); + } - var el = document.createElement("div"); - if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || - !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { - module.fail("Incomplete Element implementation"); - } + 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"); + } - // 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; + }; - // 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; - }:*/ + // 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(arr, val) { - var i = arr.length; - while (i--) { - if (arr[i] === val) { - return true; - } - } - return false; - }; + function parentElement(node) { + var parent = node.parentNode; + return (parent.nodeType == 1) ? parent : null; + } - // 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 getNodeIndex(node) { + var i = 0; + while( (node = node.previousSibling) ) { + i++; } + return i; + } - function parentElement(node) { - var parent = node.parentNode; - return (parent.nodeType == 1) ? parent : null; - } + function getNodeLength(node) { + var childNodes; + return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0); + } - function getNodeIndex(node) { - var i = 0; - while( (node = node.previousSibling) ) { - ++i; - } - return i; + function getCommonAncestor(node1, node2) { + var ancestors = [], n; + for (n = node1; n; n = n.parentNode) { + ancestors.push(n); } - function getNodeLength(node) { - switch (node.nodeType) { - case 7: - case 10: - return 0; - case 3: - case 8: - return node.length; - default: - return node.childNodes.length; + for (n = node2; n; n = n.parentNode) { + if (arrayContains(ancestors, n)) { + return n; } } - function getCommonAncestor(node1, node2) { - var ancestors = [], n; - for (n = node1; n; n = n.parentNode) { - ancestors.push(n); - } + return null; + } - for (n = node2; n; n = n.parentNode) { - if (arrayContains(ancestors, n)) { - return n; - } + function isAncestorOf(ancestor, descendant, selfIsAncestor) { + var n = selfIsAncestor ? descendant : descendant.parentNode; + while (n) { + if (n === ancestor) { + return true; + } else { + n = n.parentNode; } - - return null; } + return false; + } - function isAncestorOf(ancestor, descendant, selfIsAncestor) { - var n = selfIsAncestor ? descendant : descendant.parentNode; - while (n) { - if (n === ancestor) { - return true; - } else { - n = n.parentNode; - } + function getClosestAncestorIn(node, ancestor, selfIsAncestor) { + var p, n = selfIsAncestor ? node : node.parentNode; + while (n) { + p = n.parentNode; + if (p === ancestor) { + return n; } - return false; - } - - function isOrIsAncestorOf(ancestor, descendant) { - return isAncestorOf(ancestor, descendant, true); + n = p; } + return null; + } - 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 isCharacterDataNode(node) { - var t = node.nodeType; - return t == 3 || t == 4 || t == 8 ; // Text, CDataSection 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; + } - function isTextOrCommentNode(node) { - if (!node) { - return false; - } - var t = node.nodeType; - return t == 3 || t == 8 ; // Text or Comment - } + // Note that we cannot use splitText() because it is bugridden in IE 9. + function splitDataNode(node, index) { + var newNode = node.cloneNode(false); + newNode.deleteData(0, index); + node.deleteData(index, node.length - index); + insertAfter(newNode, node); + return newNode; + } - function insertAfter(node, precedingNode) { - var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; - if (nextNode) { - parent.insertBefore(node, nextNode); - } else { - parent.appendChild(node); - } + 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 new Error("getDocument: no document found for 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 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 new Error("Cannot get a window object for node"); } + } - 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 getIframeDocument(iframeEl) { + if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument; + } else if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow.document; + } else { + throw new Error("getIframeWindow: No Document object found for iframe element"); } + } - 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 getIframeWindow(iframeEl) { + if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow; + } else if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument.defaultView; + } else { + throw new Error("getIframeWindow: No Window object found for iframe element"); } + } - 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 getBody(doc) { + return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; + } - 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"); - } + function getRootContainer(node) { + var parent; + while ( (parent = node.parentNode) ) { + node = parent; } + return node; + } - // This looks bad. Is it worth it? - function isWindow(obj) { - return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document"); - } + 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) { - function getContentDocument(obj, module, methodName) { - var doc; + // Case 1: nodes are the same + return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { - if (!obj) { - doc = document; - } + // 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)) ) { - // 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); - } + // Case 3: node C (container A or an ancestor) is a child node of B + return getNodeIndex(nodeC) < offsetB ? -1 : 1; + } else { - // Test if the doc parameter appears to be a Window object - else if (isWindow(obj)) { - doc = obj.document; - } + // Case 4: containers are siblings or descendants of siblings + root = getCommonAncestor(nodeA, nodeB); + childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); + childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); + + if (childA === childB) { + // This shouldn't be possible - if (!doc) { - throw module.createError(methodName + "(): Parameter must be a Window object or DOM node"); + throw new Error("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; + } + throw new Error("Should not be here!"); } + } + } - return doc; + function fragmentFromNodeChildren(node) { + var fragment = getDocument(node).createDocumentFragment(), child; + while ( (child = node.firstChild) ) { + fragment.appendChild(child); } + return fragment; + } - function getRootContainer(node) { - var parent; - while ( (parent = node.parentNode) ) { - node = parent; - } - return node; + function inspectNode(node) { + if (!node) { + return "[No node]"; + } + if (isCharacterDataNode(node)) { + return '"' + node.data + '"'; + } else if (node.nodeType == 1) { + var idAttr = node.id ? ' id="' + node.id + '"' : ""; + return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]"; + } else { + return node.nodeName; } + } - 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"); - } + /** + * @constructor + */ + function NodeIterator(root) { + this.root = root; + this._next = root; + } - // 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); + NodeIterator.prototype = { + _current: null, - if (childA === childB) { - // This shouldn't be possible - throw module.createError("comparePoints got to case 4 and childA and childB are the same!"); + 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 { - n = root.firstChild; - while (n) { - if (n === childA) { - return -1; - } else if (n === childB) { - return 1; - } - n = n.nextSibling; + 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); + } - /*----------------------------------------------------------------------------------------------------------------*/ + /** + * @constructor + */ + function DomPosition(node, offset) { + this.node = node; + this.offset = offset; + } - // 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; + DomPosition.prototype = { + equals: function(pos) { + return this.node === pos.node & this.offset == pos.offset; + }, - function isBrokenNode(node) { - var n; - try { - n = node.parentNode; - return false; - } catch (e) { - return true; - } + inspect: function() { + return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; } + }; - (function() { - var el = document.createElement("b"); - el.innerHTML = "1"; - var textNode = el.firstChild; - el.innerHTML = "
"; - crashyTextNodes = isBrokenNode(textNode); + /** + * @constructor + */ + function DOMException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "DOMException: " + this.codeName; + } - api.features.crashyTextNodes = crashyTextNodes; - })(); + DOMException.prototype = { + INDEX_SIZE_ERR: 1, + HIERARCHY_REQUEST_ERR: 3, + WRONG_DOCUMENT_ERR: 4, + NO_MODIFICATION_ALLOWED_ERR: 7, + NOT_FOUND_ERR: 8, + NOT_SUPPORTED_ERR: 9, + INVALID_STATE_ERR: 11 + }; - /*----------------------------------------------------------------------------------------------------------------*/ + DOMException.prototype.toString = function() { + return this.message; + }; - 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; - } + api.dom = { + arrayContains: arrayContains, + isHtmlNamespace: isHtmlNamespace, + parentElement: parentElement, + getNodeIndex: getNodeIndex, + getNodeLength: getNodeLength, + getCommonAncestor: getCommonAncestor, + isAncestorOf: isAncestorOf, + getClosestAncestorIn: getClosestAncestorIn, + isCharacterDataNode: isCharacterDataNode, + insertAfter: insertAfter, + splitDataNode: splitDataNode, + getDocument: getDocument, + getWindow: getWindow, + getIframeWindow: getIframeWindow, + getIframeDocument: getIframeDocument, + getBody: getBody, + getRootContainer: getRootContainer, + comparePoints: comparePoints, + inspectNode: inspectNode, + fragmentFromNodeChildren: fragmentFromNodeChildren, + createIterator: createIterator, + DomPosition: DomPosition + }; - function fragmentFromNodeChildren(node) { - var fragment = getDocument(node).createDocumentFragment(), child; - while ( (child = node.firstChild) ) { - fragment.appendChild(child); - } - return fragment; - } + api.DOMException = DOMException; +});rangy.createModule("DomRange", function(api, module) { + api.requireModules( ["DomUtil"] ); - 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; - } + var dom = api.dom; + var DomPosition = dom.DomPosition; + var DOMException = api.DOMException; + + /*----------------------------------------------------------------------------------------------------------------*/ - NodeIterator.prototype = { - _current: null, + // Utility functions - hasNext: function() { - return !!this._next; - }, + function isNonTextPartiallySelected(node, range) { + return (node.nodeType != 3) && + (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true)); + } - 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; - }, + function getRangeDocument(range) { + return dom.getDocument(range.startContainer); + } - detach: function() { - this._current = this._next = this.root = null; + function dispatchEvent(range, type, args) { + var listeners = range._listeners[type]; + if (listeners) { + for (var i = 0, len = listeners.length; i < len; ++i) { + listeners[i].call(range, {target: range, args: args}); } - }; - - 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; - }, + function getBoundaryBeforeNode(node) { + return new DomPosition(node.parentNode, dom.getNodeIndex(node)); + } - inspect: function() { - return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; - }, + function getBoundaryAfterNode(node) { + return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1); + } - toString: function() { - return this.inspect(); + function insertNodeAtPosition(node, n, o) { + var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; + if (dom.isCharacterDataNode(n)) { + if (o == n.length) { + dom.insertAfter(node, n); + } else { + n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o)); } - }; - - 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)); + } else if (o >= n.childNodes.length) { + n.appendChild(node); + } else { + n.insertBefore(node, n.childNodes[o]); } + return firstNodeInserted; + } - function getRangeDocument(range) { - return range.document || getDocument(range.startContainer); - } + function cloneSubtree(iterator) { + var partiallySelected; + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + partiallySelected = iterator.isPartiallySelectedSubtree(); - function getBoundaryBeforeNode(node) { - return new DomPosition(node.parentNode, getNodeIndex(node)); - } + node = node.cloneNode(!partiallySelected); + if (partiallySelected) { + subIterator = iterator.getSubtreeIterator(); + node.appendChild(cloneSubtree(subIterator)); + subIterator.detach(true); + } - function getBoundaryAfterNode(node) { - return new DomPosition(node.parentNode, getNodeIndex(node) + 1); + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); } + return frag; + } - function insertNodeAtPosition(node, n, o) { - var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; - if (isCharacterDataNode(n)) { - if (o == n.length) { - dom.insertAfter(node, n); + function iterateSubtree(rangeIterator, func, iteratorState) { + var it, n; + iteratorState = iteratorState || { stop: false }; + for (var node, subRangeIterator; node = rangeIterator.next(); ) { + //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node)); + if (rangeIterator.isPartiallySelectedSubtree()) { + // 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. + if (func(node) === false) { + iteratorState.stop = true; + return; } else { - n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o)); + subRangeIterator = rangeIterator.getSubtreeIterator(); + iterateSubtree(subRangeIterator, func, iteratorState); + subRangeIterator.detach(true); + if (iteratorState.stop) { + return; + } } - } else if (o >= n.childNodes.length) { - n.appendChild(node); } else { - n.insertBefore(node, n.childNodes[o]); + // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its + // descendant + it = dom.createIterator(node); + while ( (n = it.next()) ) { + if (func(n) === false) { + iteratorState.stop = true; + return; + } + } } - return firstNodeInserted; } + } - function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) { - assertRangeValid(rangeA); - assertRangeValid(rangeB); - - if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) { - throw new DOMException("WRONG_DOCUMENT_ERR"); + function deleteSubtree(iterator) { + var subIterator; + while (iterator.next()) { + if (iterator.isPartiallySelectedSubtree()) { + subIterator = iterator.getSubtreeIterator(); + deleteSubtree(subIterator); + subIterator.detach(true); + } else { + iterator.remove(); } + } + } - var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset), - endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset); + function extractSubtree(iterator) { - return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; - } + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { - 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); + if (iterator.isPartiallySelectedSubtree()) { + node = node.cloneNode(false); + subIterator = iterator.getSubtreeIterator(); + node.appendChild(extractSubtree(subIterator)); + subIterator.detach(true); + } else { + iterator.remove(); } - 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; - } - } - } + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); } + frag.appendChild(node); } + return frag; + } - function deleteSubtree(iterator) { - var subIterator; - while (iterator.next()) { - if (iterator.isPartiallySelectedSubtree()) { - subIterator = iterator.getSubtreeIterator(); - deleteSubtree(subIterator); - subIterator.detach(); - } else { - iterator.remove(); - } - } + function getNodesInRange(range, nodeTypes, filter) { + //log.info("getNodesInRange, " + nodeTypes.join(",")); + var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; + var filterExists = !!filter; + if (filterNodeTypes) { + regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); } - 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); + var nodes = []; + iterateSubtree(new RangeIterator(range, false), function(node) { + if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) { + nodes.push(node); } - return frag; - } + }); + return nodes; + } - function getNodesInRange(range, nodeTypes, filter) { - var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; - var filterExists = !!filter; - if (filterNodeTypes) { - regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); - } + 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 + ")]"; + } - 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; - } + // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) - nodes.push(node); - }); - return nodes; - } + /** + * @constructor + */ + function RangeIterator(range, clonePartiallySelectedTextNodes) { + this.range = range; + this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; - 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) + if (!range.collapsed) { + this.sc = range.startContainer; + this.so = range.startOffset; + this.ec = range.endContainer; + this.eo = range.endOffset; + var root = range.commonAncestorContainer; - function RangeIterator(range, clonePartiallySelectedTextNodes) { - this.range = range; - this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; + if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) { + this.isSingleCharacterDataNode = true; + this._first = this._last = this._next = this.sc; + } else { + this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ? + this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true); + this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ? + this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true); + } + } + } - if (!range.collapsed) { - this.sc = range.startContainer; - this.so = range.startOffset; - this.ec = range.endContainer; - this.eo = range.endOffset; - var root = range.commonAncestorContainer; + RangeIterator.prototype = { + _current: null, + _next: null, + _first: null, + _last: null, + isSingleCharacterDataNode: false, - 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); - } - } - } + reset: function() { + this._current = null; + this._next = this._first; + }, - RangeIterator.prototype = { - _current: null, - _next: null, - _first: null, - _last: null, - isSingleCharacterDataNode: false, + hasNext: function() { + return !!this._next; + }, - reset: function() { - this._current = null; - this._next = this._first; - }, + next: function() { + // Move to next node + var current = this._current = this._next; + if (current) { + this._next = (current !== this._last) ? current.nextSibling : null; - hasNext: function() { - return !!this._next; - }, + // Check for partially selected text nodes + if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { + if (current === this.ec) { - next: function() { - // Move to next node - var current = this._current = this._next; - if (current) { - this._next = (current !== this._last) ? current.nextSibling : null; + (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); + } + if (this._current === this.sc) { - // 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); - } + (current = current.cloneNode(true)).deleteData(0, this.so); } } + } - return current; - }, + return current; + }, - remove: function() { - var current = this._current, start, end; + 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 { - } + if (dom.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); } - }, - - // 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 { + if (current.parentNode) { + current.parentNode.removeChild(current); } 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; } - }; + }, - /*----------------------------------------------------------------------------------------------------------------*/ + // Checks if the current node is partially selected + isPartiallySelectedSubtree: function() { + var current = this._current; + return isNonTextPartiallySelected(current, this.range); + }, - 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]; + getSubtreeIterator: function() { + var subRange; + if (this.isSingleCharacterDataNode) { + subRange = this.range.cloneRange(); + subRange.collapse(); + } else { + subRange = new Range(getRangeDocument(this.range)); + var current = this._current; + var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current); - 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; + if (dom.isAncestorOf(current, this.sc, true)) { + startContainer = this.sc; + startOffset = this.so; + } + if (dom.isAncestorOf(current, this.ec, true)) { + endContainer = this.ec; + endOffset = this.eo; } - 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"); + updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); } - } + return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); + }, - function assertValidNodeType(node, invalidTypes) { - if (!arrayContains(invalidTypes, node.nodeType)) { - throw new DOMException("INVALID_NODE_TYPE_ERR"); + detach: function(detachRange) { + if (detachRange) { + this.range.detach(); } + this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; } + }; - 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"); - } - } + // Exceptions - function assertNodeNotReadOnly(node) { - if (getReadonlyAncestor(node, true)) { - throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); - } + /** + * @constructor + */ + function RangeException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "RangeException: " + this.codeName; + } + + RangeException.prototype = { + BAD_BOUNDARYPOINTS_ERR: 1, + INVALID_NODE_TYPE_ERR: 2 + }; + + RangeException.prototype.toString = function() { + return this.message; + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + /** + * Currently iterates through all nodes in the range on creation until I think of a decent way to do it + * TODO: Look into making this a proper iterator, not requiring preloading everything first + * @constructor + */ + function RangeNodeIterator(range, nodeTypes, filter) { + this.nodes = getNodesInRange(range, nodeTypes, filter); + this._next = this.nodes[0]; + this._position = 0; + } + + RangeNodeIterator.prototype = { + _current: null, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + this._current = this._next; + this._next = this.nodes[ ++this._position ]; + return this._current; + }, + + detach: function() { + this._current = this._next = this.nodes = null; } + }; - function assertNode(node, codeName) { - if (!node) { - throw new DOMException(codeName); + 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 (dom.arrayContains(nodeTypes, t)) { + return n; + } + n = n.parentNode; } + return null; + }; + } + + var getRootContainer = dom.getRootContainer; + var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); + var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); + var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); + + function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { + if (getDocTypeNotationEntityAncestor(node, allowSelf)) { + throw new RangeException("INVALID_NODE_TYPE_ERR"); } + } - function isOrphan(node) { - return (crashyTextNodes && dom.isBrokenNode(node)) || - !arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true); + function assertNotDetached(range) { + if (!range.startContainer) { + throw new DOMException("INVALID_STATE_ERR"); } + } - function isValidOffset(node, offset) { - return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length); + function assertValidNodeType(node, invalidTypes) { + if (!dom.arrayContains(invalidTypes, node.nodeType)) { + throw new RangeException("INVALID_NODE_TYPE_ERR"); } + } - 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 assertValidOffset(node, offset) { + if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) { + throw new DOMException("INDEX_SIZE_ERR"); } + } - function assertRangeValid(range) { - if (!isRangeValid(range)) { - throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")"); - } + 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"); + } + } - // 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 + function assertNode(node, codeName) { + if (!node) { + throw new DOMException(codeName); } + } - api.features.htmlParsingConforms = htmlParsingConforms; + function isOrphan(node) { + return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true); + } - var createContextualFragment = htmlParsingConforms ? + function isValidOffset(node, offset) { + return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length); + } - // 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); + function isRangeValid(range) { + return (!!range.startContainer && !!range.endContainer + && !isOrphan(range.startContainer) + && !isOrphan(range.endContainer) + && isValidOffset(range.startContainer, range.startOffset) + && isValidOffset(range.endContainer, range.endOffset)); + } - // "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"); - } + function assertRangeValid(range) { + assertNotDetached(range); + if (!isRangeValid(range)) { + throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")"); + } + } - // "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; + // 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 + } - // "Text, Comment: node's parentElement" - } else if (isCharacterDataNode(node)) { - el = dom.parentElement(node); - } + api.features.htmlParsingConforms = htmlParsingConforms; - // "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); - } + var createContextualFragment = htmlParsingConforms ? - // "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); - }; + // 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 = dom.getDocument(node); - function splitRangeBoundaries(range, positionsToPreserve) { - assertRangeValid(range); + // "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"); + } - var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset; - var startEndSame = (sc === ec); + // "Let element be as follows, depending on node's interface:" + // Document, Document Fragment: null + var el = null; - if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { - splitDataNode(ec, eo, positionsToPreserve); - } + // "Element: node" + if (node.nodeType == 1) { + el = node; - 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; + // "Text, Comment: node's parentElement" + } else if (dom.isCharacterDataNode(node)) { + el = dom.parentElement(node); } - 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; - } - /*----------------------------------------------------------------------------------------------------------------*/ + // "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(dom.getDocument(el).documentElement) + && dom.isHtmlNamespace(el) + )) { - 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; + // "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); + } - 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); - }, + // "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; - insertNode: function(node) { - assertRangeValid(this); - assertValidNodeType(node, insertableNodeTypes); - assertNodeNotReadOnly(this.startContainer); + // "If this raises an exception, then abort these steps. Otherwise, let new + // children be the nodes returned." - if (isOrIsAncestorOf(node, this.startContainer)) { - throw new DOMException("HIERARCHY_REQUEST_ERR"); - } + // "Let fragment be a new DocumentFragment." + // "Append all new children to fragment." + // "Return fragment." + return dom.fragmentFromNodeChildren(el); + } : - // 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 + // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that + // previous versions of Rangy used (with the exception of using a body element rather than a div) + function(fragmentStr) { + assertNotDetached(this); + var doc = getRangeDocument(this); + var el = doc.createElement("body"); + el.innerHTML = fragmentStr; - var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); - this.setStartBefore(firstNodeInserted); - }, + return dom.fragmentFromNodeChildren(el); + }; - 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; - } - }, + var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer"]; - canSurroundContents: function() { - assertRangeValid(this); - assertNodeNotReadOnly(this.startContainer); - assertNodeNotReadOnly(this.endContainer); + var s2s = 0, s2e = 1, e2e = 2, e2s = 3; + var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; - // 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; - }, + function RangePrototype() {} - surroundContents: function(node) { - assertValidNodeType(node, surroundNodeTypes); + RangePrototype.prototype = { + attachListener: function(type, listener) { + this._listeners[type].push(listener); + }, - if (!this.canSurroundContents()) { - throw new DOMException("INVALID_STATE_ERR"); - } + 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 dom.comparePoints(nodeA, offsetA, nodeB, offsetB); + }, - // Extract the contents - var content = this.extractContents(); + insertNode: function(node) { + assertRangeValid(this); + assertValidNodeType(node, insertableNodeTypes); + assertNodeNotReadOnly(this.startContainer); - // Clear the children of the node - if (node.hasChildNodes()) { - while (node.lastChild) { - node.removeChild(node.lastChild); - } - } + if (dom.isAncestorOf(node, this.startContainer, true)) { + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } - // Insert the new node and add the extracted contents - insertNodeAtPosition(node, this.startContainer, this.startOffset); - node.appendChild(content); + // 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 - this.selectNode(node); - }, + var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); + this.setStartBefore(firstNodeInserted); + }, - 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; - }, + cloneContents: function() { + assertRangeValid(this); - 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) : ""; + var clone, frag; + if (this.collapsed) { + return getRangeDocument(this).createDocumentFragment(); + } else { + if (this.startContainer === this.endContainer && dom.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 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); - } - }); + var iterator = new RangeIterator(this, true); + clone = cloneSubtree(iterator); iterator.detach(); - return textParts.join(""); } - }, + return clone; + } + }, - // The methods below are all non-standard. The following batch were introduced by Mozilla but have since - // been removed from Mozilla. + 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; + }, - compareNode: function(node) { - assertRangeValid(this); + surroundContents: function(node) { + assertValidNodeType(node, surroundNodeTypes); - var parent = node.parentNode; - var nodeIndex = getNodeIndex(node); + if (!this.canSurroundContents()) { + throw new RangeException("BAD_BOUNDARYPOINTS_ERR"); + } - if (!parent) { - throw new DOMException("NOT_FOUND_ERR"); - } + // Extract the contents + var content = this.extractContents(); - 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; + // Clear the children of the node + if (node.hasChildNodes()) { + while (node.lastChild) { + node.removeChild(node.lastChild); } - return 0; - }, + } - createContextualFragment: createContextualFragment, + // Insert the new node and add the extracted contents + insertNodeAtPosition(node, this.startContainer, this.startOffset); + node.appendChild(content); - toHtml: function() { - return rangeToHtml(this); - }, + this.selectNode(node); + }, - // 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; - } + 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; + }, - var parent = node.parentNode, offset = getNodeIndex(node); - assertNode(parent, "NOT_FOUND_ERR"); + toString: function() { + assertRangeValid(this); + var sc = this.startContainer; + if (sc === this.endContainer && dom.isCharacterDataNode(sc)) { + return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; + } else { + var textBits = [], iterator = new RangeIterator(this, true); - var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset), - endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset); + iterateSubtree(iterator, function(node) { + // Accept only text or CDATA nodes, not comments - return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; - }, + if (node.nodeType == 3 || node.nodeType == 4) { + textBits.push(node.data); + } + }); + iterator.detach(); + return textBits.join(""); + } + }, - isPointInRange: function(node, offset) { - assertRangeValid(this); - assertNode(node, "HIERARCHY_REQUEST_ERR"); - assertSameDocumentOrFragment(node, this.startContainer); + // The methods below are all non-standard. The following batch were introduced by Mozilla but have since + // been removed from Mozilla. - return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && - (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); - }, + compareNode: function(node) { + assertRangeValid(this); - // The methods below are non-standard and invented by me. + var parent = node.parentNode; + var nodeIndex = dom.getNodeIndex(node); - // Sharing a boundary start-to-end or end-to-start does not count as intersection. - intersectsRange: function(range) { - return rangesIntersect(this, range, false); - }, + if (!parent) { + throw new DOMException("NOT_FOUND_ERR"); + } - // Sharing a boundary start-to-end or end-to-start does count as intersection. - intersectsOrTouchesRange: function(range) { - return rangesIntersect(this, range, true); - }, + var startComparison = this.comparePoint(parent, nodeIndex), + endComparison = this.comparePoint(parent, nodeIndex + 1); - 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); + if (startComparison < 0) { // Node starts before + return (endComparison > 0) ? n_b_a : n_b; + } else { + return (endComparison > 0) ? n_a : n_i; + } + }, - 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; - }, + comparePoint: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); - 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"); - } - }, + if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { + return -1; + } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { + return 1; + } + return 0; + }, - containsNode: function(node, allowPartial) { - if (allowPartial) { - return this.intersectsNode(node, false); - } else { - return this.compareNode(node) == n_i; - } - }, + createContextualFragment: createContextualFragment, - containsNodeContents: function(node) { - return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0; - }, + toHtml: function() { + assertRangeValid(this); + var container = getRangeDocument(this).createElement("div"); + container.appendChild(this.cloneContents()); + return container.innerHTML; + }, - containsRange: function(range) { - var intersection = this.intersection(range); - return intersection !== null && range.equals(intersection); - }, + // 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 (dom.getDocument(node) !== getRangeDocument(this)) { + return false; + } - 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); - } - }, + var parent = node.parentNode, offset = dom.getNodeIndex(node); + assertNode(parent, "NOT_FOUND_ERR"); - getNodes: function(nodeTypes, filter) { - assertRangeValid(this); - return getNodesInRange(this, nodeTypes, filter); - }, + var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset), + endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset); - getDocument: function() { - return getRangeDocument(this); - }, + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, - 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; - } + isPointInRange: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); - 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]); - } - } - } - }, + return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && + (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); + }, - getName: function() { - return "DomRange"; - }, + // The methods below are non-standard and invented by me. - equals: function(range) { - return Range.rangesEqual(this, range); - }, + // Sharing a boundary start-to-end or end-to-start does not count as intersection. + intersectsRange: function(range, touchingIsIntersecting) { + assertRangeValid(this); - isValid: function() { - return isRangeValid(this); - }, - - inspect: function() { - return inspect(this); - }, - - detach: function() { - // In DOM4, detach() is now a no-op. + if (getRangeDocument(range) != getRangeDocument(this)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); } - }); - - 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); - } + var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset), + endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset); - function createRangeContentRemover(remover, boundaryUpdater) { - return function() { - assertRangeValid(this); + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, - var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; + intersection: function(range) { + if (this.intersectsRange(range)) { + var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), + endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); - var iterator = new RangeIterator(this, true); + var intersectionRange = this.cloneRange(); - // 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; + if (startComparison == -1) { + intersectionRange.setStart(range.startContainer, range.startOffset); } + if (endComparison == 1) { + intersectionRange.setEnd(range.endContainer, range.endOffset); + } + return intersectionRange; + } + return null; + }, - // 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); + union: function(range) { + if (this.intersectsRange(range, true)) { + var unionRange = this.cloneRange(); + if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { + unionRange.setStart(range.startContainer, range.startOffset); + } + if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { + unionRange.setEnd(range.endContainer, range.endOffset); + } + return unionRange; + } else { + throw new RangeException("Ranges do not intersect"); + } + }, - return returnValue; - }; - } + containsNode: function(node, allowPartial) { + if (allowPartial) { + return this.intersectsNode(node, false); + } else { + return this.compareNode(node) == n_i; + } + }, - function createPrototypeRange(constructor, boundaryUpdater) { - function createBeforeAfterNodeSetter(isBefore, isStart) { - return function(node) { - assertValidNodeType(node, beforeAfterNodeTypes); - assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); + containsNodeContents: function(node) { + return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0; + }, - var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); - (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); - }; - } + containsRange: function(range) { + return this.intersection(range).equals(range); + }, - 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); - } + containsNodeText: function(node) { + var nodeRange = this.cloneRange(); + nodeRange.selectNode(node); + var textNodes = nodeRange.getNodes([3]); + if (textNodes.length > 0) { + nodeRange.setStart(textNodes[0], 0); + var lastTextNode = textNodes.pop(); + nodeRange.setEnd(lastTextNode, lastTextNode.length); + var contains = this.containsRange(nodeRange); + nodeRange.detach(); + return contains; + } else { + return this.containsNodeContents(node); } + }, - 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); - } - } + createNodeIterator: function(nodeTypes, filter) { + assertRangeValid(this); + return new RangeNodeIterator(this, nodeTypes, filter); + }, - // 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; - } + getNodes: function(nodeTypes, filter) { + assertRangeValid(this); + return getNodesInRange(this, nodeTypes, filter); + }, - 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); - } - }, + getDocument: function() { + return getRangeDocument(this); + }, - selectNodeContents: function(node) { - assertNoDocTypeNotationEntityAncestor(node, true); + collapseBefore: function(node) { + assertNotDetached(this); - boundaryUpdater(this, node, 0, node, getNodeLength(node)); - }, + this.setEndBefore(node); + this.collapse(false); + }, - selectNode: function(node) { - assertNoDocTypeNotationEntityAncestor(node, false); - assertValidNodeType(node, beforeAfterNodeTypes); + collapseAfter: function(node) { + assertNotDetached(this); - var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); - boundaryUpdater(this, start.node, start.offset, end.node, end.offset); - }, + this.setStartAfter(node); + this.collapse(true); + }, - extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), + getName: function() { + return "DomRange"; + }, - deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), + equals: function(range) { + return Range.rangesEqual(this, range); + }, - canSurroundContents: function() { - assertRangeValid(this); - assertNodeNotReadOnly(this.startContainer); - assertNodeNotReadOnly(this.endContainer); + isValid: function() { + return isRangeValid(this); + }, - // 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; - }, + inspect: function() { + return inspect(this); + } + }; - splitBoundaries: function() { - splitRangeBoundaries(this); - }, + function copyComparisonConstantsToObject(obj) { + obj.START_TO_START = s2s; + obj.START_TO_END = s2e; + obj.END_TO_END = e2e; + obj.END_TO_START = e2s; - splitBoundariesPreservingPositions: function(positionsToPreserve) { - splitRangeBoundaries(this, positionsToPreserve); - }, + obj.NODE_BEFORE = n_b; + obj.NODE_AFTER = n_a; + obj.NODE_BEFORE_AND_AFTER = n_b_a; + obj.NODE_INSIDE = n_i; + } - normalizeBoundaries: function() { - assertRangeValid(this); + function copyComparisonConstants(constructor) { + copyComparisonConstantsToObject(constructor); + copyComparisonConstantsToObject(constructor.prototype); + } - var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; + function createRangeContentRemover(remover, boundaryUpdater) { + return function() { + assertRangeValid(this); - 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 sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; - 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 iterator = new RangeIterator(this, true); - var normalizeStart = true; + // Work out where to position the range after content removal + var node, boundary; + if (sc !== root) { + node = dom.getClosestAncestorIn(sc, root, true); + boundary = getBoundaryAfterNode(node); + sc = boundary.node; + so = boundary.offset; + } - 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; - } + // Check none of the range is read-only + iterateSubtree(iterator, assertNodeNotReadOnly); - 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; - } + iterator.reset(); - boundaryUpdater(this, sc, so, ec, eo); - }, + // Remove the content + var returnValue = remover(iterator); + iterator.detach(); - collapseToPoint: function(node, offset) { - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); - this.setStartAndEnd(node, offset); - } - }); + // Move to the new position + boundaryUpdater(this, sc, so, sc, so); - copyComparisonConstants(constructor); - } + return returnValue; + }; + } - /*----------------------------------------------------------------------------------------------------------------*/ + function createPrototypeRange(constructor, boundaryUpdater, detacher) { + function createBeforeAfterNodeSetter(isBefore, isStart) { + return function(node) { + assertNotDetached(this); + assertValidNodeType(node, beforeAfterNodeTypes); + assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); - // 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); + var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); + (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); + }; } - function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { - range.startContainer = startContainer; - range.startOffset = startOffset; - range.endContainer = endContainer; - range.endOffset = endOffset; - range.document = dom.getDocument(startContainer); + 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) || dom.comparePoints(node, offset, ec, eo) == 1) { + ec = node; + eo = offset; + } + boundaryUpdater(range, node, offset, ec, eo); + } + } - updateCollapsedAndCommonAncestor(range); + 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) || dom.comparePoints(node, offset, sc, so) == -1) { + sc = node; + so = offset; + } + boundaryUpdater(range, sc, so, node, offset); + } } - function Range(doc) { - this.startContainer = doc; - this.startOffset = 0; - this.endContainer = doc; - this.endOffset = 0; - this.document = doc; - updateCollapsedAndCommonAncestor(this); + function setRangeStartAndEnd(range, node, offset) { + if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) { + boundaryUpdater(range, node, offset, node, offset); + } } - createPrototypeRange(Range, updateBoundaries); + constructor.prototype = new RangePrototype(); - 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.util.extend(constructor.prototype, { + setStart: function(node, offset) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); - api.DomRange = Range; - }); + setRangeStart(this, node, offset); + }, - /*----------------------------------------------------------------------------------------------------------------*/ + setEnd: function(node, offset) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); - // 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); - } + setRangeEnd(this, node, offset); + }, - 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); + setStartBefore: createBeforeAfterNodeSetter(true, true), + setStartAfter: createBeforeAfterNodeSetter(false, true), + setEndBefore: createBeforeAfterNodeSetter(true, false), + setEndAfter: createBeforeAfterNodeSetter(false, false), - // Always set both boundaries for the benefit of IE9 (see issue 35) - if (startMoved || endMoved || nativeRangeDifferent) { - range.setEnd(endContainer, endOffset); - range.setStart(startContainer, startOffset); - } + 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); } + }, - var createBeforeAfterNodeSetter; + selectNodeContents: function(node) { + // This doesn't seem well specified: the spec talks only about selecting the node's contents, which + // could be taken to mean only its children. However, browsers implement this the same as selectNode for + // text nodes, so I shall do likewise + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); - WrappedRange = function(range) { - if (!range) { - throw module.createError("WrappedRange: Range must be specified"); - } - this.nativeRange = range; - updateRangeProperties(this); - }; + boundaryUpdater(this, node, 0, node, dom.getNodeLength(node)); + }, - DomRange.createPrototypeRange(WrappedRange, updateNativeRange); + selectNode: function(node) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, false); + assertValidNodeType(node, beforeAfterNodeTypes); - rangeProto = WrappedRange.prototype; + var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); + boundaryUpdater(this, start.node, start.offset, end.node, end.offset); + }, - rangeProto.selectNode = function(node) { - this.nativeRange.selectNode(node); - updateRangeProperties(this); - }; + extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), - rangeProto.cloneContents = function() { - return this.nativeRange.cloneContents(); - }; + deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), - // 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. + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); - rangeProto.surroundContents = function(node) { - this.nativeRange.surroundContents(node); - updateRangeProperties(this); - }; + // 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; + }, - rangeProto.collapse = function(isStart) { - this.nativeRange.collapse(isStart); - updateRangeProperties(this); - }; + detach: function() { + detacher(this); + }, - rangeProto.cloneRange = function() { - return new WrappedRange(this.nativeRange.cloneRange()); - }; + splitBoundaries: function() { + assertRangeValid(this); - rangeProto.refresh = function() { - updateRangeProperties(this); - }; - rangeProto.toString = function() { - return this.nativeRange.toString(); - }; + var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; + var startEndSame = (sc === ec); - // Create test range and node for feature detection + if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { + dom.splitDataNode(ec, eo); - var testTextNode = document.createTextNode("test"); - getBody(document).appendChild(testTextNode); - var range = document.createRange(); + } - /*--------------------------------------------------------------------------------------------------------*/ + if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) { - // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and - // correct for it + sc = dom.splitDataNode(sc, so); + if (startEndSame) { + eo -= so; + ec = sc; + } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) { + eo++; + } + so = 0; - range.setStart(testTextNode, 0); - range.setEnd(testTextNode, 0); + } + boundaryUpdater(this, sc, so, ec, eo); + }, - try { - range.setStart(testTextNode, 1); + normalizeBoundaries: function() { + assertRangeValid(this); - rangeProto.setStart = function(node, offset) { - this.nativeRange.setStart(node, offset); - updateRangeProperties(this); - }; + var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; - rangeProto.setEnd = function(node, offset) { - this.nativeRange.setEnd(node, offset); - updateRangeProperties(this); - }; + 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); + } + }; - createBeforeAfterNodeSetter = function(name) { - return function(node) { - this.nativeRange[name](node); - updateRangeProperties(this); - }; - }; + 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 = dom.getNodeIndex(node); + if (eo == nodeIndex) { + ec = node; + eo = nodeLength; + } else if (eo > nodeIndex) { + eo--; + } + } + } + }; - } catch(ex) { + var normalizeStart = true; - rangeProto.setStart = function(node, offset) { - try { - this.nativeRange.setStart(node, offset); - } catch (ex) { - this.nativeRange.setEnd(node, offset); - this.nativeRange.setStart(node, offset); + if (dom.isCharacterDataNode(ec)) { + if (ec.length == eo) { + mergeForward(ec); + } + } else { + if (eo > 0) { + var endNode = ec.childNodes[eo - 1]; + if (endNode && dom.isCharacterDataNode(endNode)) { + mergeForward(endNode); } - updateRangeProperties(this); - }; + } + normalizeStart = !this.collapsed; + } - rangeProto.setEnd = function(node, offset) { - try { - this.nativeRange.setEnd(node, offset); - } catch (ex) { - this.nativeRange.setStart(node, offset); - this.nativeRange.setEnd(node, offset); + if (normalizeStart) { + if (dom.isCharacterDataNode(sc)) { + if (so == 0) { + mergeBackward(sc); } - updateRangeProperties(this); - }; - - createBeforeAfterNodeSetter = function(name, oppositeName) { - return function(node) { - try { - this.nativeRange[name](node); - } catch (ex) { - this.nativeRange[oppositeName](node); - this.nativeRange[name](node); + } else { + if (so < sc.childNodes.length) { + var startNode = sc.childNodes[so]; + if (startNode && dom.isCharacterDataNode(startNode)) { + mergeBackward(startNode); } - updateRangeProperties(this); - }; - }; + } + } + } else { + sc = ec; + so = eo; } - rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); - rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); - rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); - rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); + boundaryUpdater(this, sc, so, ec, eo); + }, - /*--------------------------------------------------------------------------------------------------------*/ + collapseToPoint: function(node, offset) { + assertNotDetached(this); - // 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)); - }; + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); - /*--------------------------------------------------------------------------------------------------------*/ + setRangeStartAndEnd(this, node, offset); + } + }); - // 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 + copyComparisonConstants(constructor); + } - 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 + // 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); + } - 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); - }; - } + function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { + var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset); + var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset); - /*--------------------------------------------------------------------------------------------------------*/ + range.startContainer = startContainer; + range.startOffset = startOffset; + range.endContainer = endContainer; + range.endOffset = endOffset; - // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107. + updateCollapsedAndCommonAncestor(range); + dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved}); + } - var el = document.createElement("div"); - el.innerHTML = "123"; - var textNode = el.firstChild; - var body = getBody(document); - body.appendChild(el); + function detach(range) { + assertNotDetached(range); + range.startContainer = range.startOffset = range.endContainer = range.endOffset = null; + range.collapsed = range.commonAncestorContainer = null; + dispatchEvent(range, "detach", null); + range._listeners = null; + } - range.setStart(textNode, 1); - range.setEnd(textNode, 2); - range.deleteContents(); + /** + * @constructor + */ + function Range(doc) { + this.startContainer = doc; + this.startOffset = 0; + this.endContainer = doc; + this.endOffset = 0; + this._listeners = { + boundarychange: [], + detach: [] + }; + updateCollapsedAndCommonAncestor(this); + } - 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); - }; + createPrototypeRange(Range, updateBoundaries, detach); + + api.rangePrototype = RangePrototype.prototype; + + Range.rangeProperties = rangeProperties; + Range.RangeIterator = RangeIterator; + Range.copyComparisonConstants = copyComparisonConstants; + Range.createPrototypeRange = createPrototypeRange; + Range.inspect = inspect; + Range.getRangeDocument = getRangeDocument; + Range.rangesEqual = function(r1, r2) { + return r1.startContainer === r2.startContainer && + r1.startOffset === r2.startOffset && + r1.endContainer === r2.endContainer && + r1.endOffset === r2.endOffset; + }; - rangeProto.extractContents = function() { - var frag = this.nativeRange.extractContents(); - updateRangeProperties(this); - return frag; - }; - } else { - } + api.DomRange = Range; + api.RangeException = RangeException; +});rangy.createModule("WrappedRange", function(api, module) { + api.requireModules( ["DomUtil", "DomRange"] ); - body.removeChild(el); - body = null; + /** + * @constructor + */ + var WrappedRange; + var dom = api.dom; + var DomPosition = dom.DomPosition; + var DomRange = api.DomRange; - /*--------------------------------------------------------------------------------------------------------*/ - // 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); + /* + 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): - rangeProto.getName = function() { - return "WrappedRange"; - }; + - api.WrappedRange = WrappedRange; + var range = document.selection.createRange(); + alert(range.parentElement().id); // Should alert "ul" but alerts "b" - 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); + 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) + */ + function getTextRangeContainerElement(textRange) { + var parentEl = textRange.parentElement(); - return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); - }; + 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); - var textRangeIsCollapsed = function(textRange) { - return textRange.compareEndPoints("StartToEnd", textRange) == 0; - }; + return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); + } - // 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; - } + function textRangeIsCollapsed(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. + function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) { + var workingRange = textRange.duplicate(); - // 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 - } - }; - } + workingRange.collapse(isStart); + var containerElement = workingRange.parentElement(); - var workingNode = dom.getDocument(containerElement).createElement("span"); + // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so + // check for that + // TODO: Find out when. Workaround for wholeRangeContainerElement may break this + if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) { + containerElement = wholeRangeContainerElement; - // 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); - } + // 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) { + return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); + } + var workingNode = dom.getDocument(containerElement).createElement("span"); + var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; + var previousNode, nextNode, boundaryPosition, boundaryNode; - // 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 {
+        // Move the working range through the container's children, starting at the end and working backwards, until the
+        // working range reaches or goes past the boundary we're interested in
+        do {
+            containerElement.insertBefore(workingNode, workingNode.previousSibling);
+            workingRange.moveToElementText(workingNode);
+        } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&
+                workingNode.previousSibling);
 
-                    // 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));
-                    }
-                }
+        // 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;
 
-                // Clean up
-                workingNode.parentNode.removeChild(workingNode);
+        if (comparison == -1 && boundaryNode && dom.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);
 
-                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;
-                }
+            var offset;
 
-                // Position the range immediately before the node containing the boundary
-                workingNode = doc.createElement("span");
+            if (/[\r\n]/.test(boundaryNode.data)) {
+                /*
+                For the particular case of a boundary within a text node containing line breaks (within a 
 element,
+                for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:
 
-                // 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;";
+                - 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
 
-                // 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);
-                }
+                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.
 
-                workingRange.moveToElementText(workingNode);
-                workingRange.collapse(!isStart);
+                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).
 
-                // Clean up
-                boundaryParent.removeChild(workingNode);
+                The method below is a hybrid of the two methods above. It uses the fact that a string containing the
+                TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the
+                text of the TextRange, so the start of the range is moved that length initially and then a character at
+                a time to make up for any trailing line breaks not contained in the 'text' property. This has good
+                performance in most situations compared to the previous two methods.
+                */
+                var tempRange = workingRange.duplicate();
+                var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
 
-                // Move the working range to the text offset, if required
-                if (nodeIsDataNode) {
-                    workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
+                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 {
 
-                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;
+            // 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;
 
-                // 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);
-            };
+            if (nextNode && dom.isCharacterDataNode(nextNode)) {
+                boundaryPosition = new DomPosition(nextNode, 0);
+            } else if (previousNode && dom.isCharacterDataNode(previousNode)) {
+                boundaryPosition = new DomPosition(previousNode, previousNode.length);
+            } else {
+                boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
+            }
+        }
 
-            WrappedTextRange.prototype.getName = function() {
-                return "WrappedTextRange";
-            };
+        // Clean up
+        workingNode.parentNode.removeChild(workingNode);
 
-            DomRange.copyComparisonConstants(WrappedTextRange);
+        return boundaryPosition;
+    }
 
-            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;
-                }
-            };
+    // 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/)
+    function createBoundaryTextRange(boundaryPosition, isStart) {
+        var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
+        var doc = dom.getDocument(boundaryPosition.node);
+        var workingNode, childNodes, workingRange = doc.body.createTextRange();
+        var nodeIsDataNode = dom.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;
+        }
 
-            WrappedTextRange.rangeToTextRange = rangeToTextRange;
+        // Position the range immediately before the node containing the boundary
+        workingNode = doc.createElement("span");
 
-            WrappedTextRange.prototype.toTextRange = function() {
-                return rangeToTextRange(this);
-            };
+        // 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, which is what we want
+        workingNode.innerHTML = "&#feff;";
 
-            api.WrappedTextRange = WrappedTextRange;
+        // 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);
+        }
 
-            // 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(f) { return f("return this;")(); })(Function);
-                if (typeof globalObj.Range == "undefined") {
-                    globalObj.Range = WrappedTextRange;
-                }
+        workingRange.moveToElementText(workingNode);
+        workingRange.collapse(!isStart);
 
-                api.createNativeRange = function(doc) {
-                    doc = getContentDocument(doc, module, "createNativeRange");
-                    return getBody(doc).createTextRange();
-                };
+        // Clean up
+        boundaryParent.removeChild(workingNode);
 
-                api.WrappedRange = WrappedTextRange;
-            }
+        // Move the working range to the text offset, if required
+        if (nodeIsDataNode) {
+            workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
         }
 
-        api.createRange = function(doc) {
-            doc = getContentDocument(doc, module, "createRange");
-            return new api.WrappedRange(api.createNativeRange(doc));
-        };
+        return workingRange;
+    }
 
-        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);
-        };
+    if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {
+        // 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
 
-        api.createIframeRangyRange = function(iframeEl) {
-            module.deprecationNotice("createIframeRangyRange()", "createRangyRange(iframeEl)");
-            return api.createRangyRange(iframeEl);
-        };
+        (function() {
+            var rangeProto;
+            var rangeProperties = DomRange.rangeProperties;
+            var canSetRangeStartAfterEnd;
 
-        api.addShimListener(function(win) {
-            var doc = win.document;
-            if (typeof doc.createRange == "undefined") {
-                doc.createRange = function() {
-                    return api.createRange(doc);
-                };
+            function updateRangeProperties(range) {
+                var i = rangeProperties.length, prop;
+                while (i--) {
+                    prop = rangeProperties[i];
+                    range[prop] = range.nativeRange[prop];
+                }
             }
-            doc = win = null;
-        });
-    });
 
-    /*----------------------------------------------------------------------------------------------------------------*/
+            function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {
+                var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
+                var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
 
-    // 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);
+                // Always set both boundaries for the benefit of IE9 (see issue 35)
+                if (startMoved || endMoved) {
+                    range.setEnd(endContainer, endOffset);
+                    range.setStart(startContainer, startOffset);
+                }
             }
-        }
-
-        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);
+            function detach(range) {
+                range.nativeRange.detach();
+                range.detached = true;
+                var i = rangeProperties.length, prop;
+                while (i--) {
+                    prop = rangeProperties[i];
+                    range[prop] = null;
+                }
             }
-            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");
+            var createBeforeAfterNodeSetter;
 
-        features.implementsWinGetSelection = implementsWinGetSelection;
-        features.implementsDocSelection = implementsDocSelection;
+            WrappedRange = function(range) {
+                if (!range) {
+                    throw new Error("Range must be specified");
+                }
+                this.nativeRange = range;
+                updateRangeProperties(this);
+            };
 
-        var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
+            DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);
 
-        if (useDocumentSelection) {
-            getNativeSelection = getDocSelection;
-            api.isSelectionValid = function(winParam) {
-                var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
+            rangeProto = WrappedRange.prototype;
 
-                // Check whether the selection TextRange is actually contained within the correct document
-                return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
+            rangeProto.selectNode = function(node) {
+                this.nativeRange.selectNode(node);
+                updateRangeProperties(this);
             };
-        } else if (implementsWinGetSelection) {
-            getNativeSelection = getWinSelection;
-            api.isSelectionValid = function() {
-                return true;
+
+            rangeProto.deleteContents = function() {
+                this.nativeRange.deleteContents();
+                updateRangeProperties(this);
             };
-        } 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]);
-                        }
-                    }
-                }
-            })();
-        }
+            rangeProto.extractContents = function() {
+                var frag = this.nativeRange.extractContents();
+                updateRangeProperties(this);
+                return frag;
+            };
 
-        features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
-        features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
+            rangeProto.cloneContents = function() {
+                return this.nativeRange.cloneContents();
+            };
 
-        // ControlRanges
-        var implementsControlRange = false, testControlRange;
+            // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
+            // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
+            // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
+            // insertNode, which works but is almost certainly slower than the native implementation.
+/*
+            rangeProto.insertNode = function(node) {
+                this.nativeRange.insertNode(node);
+                updateRangeProperties(this);
+            };
+*/
 
-        if (body && isHostMethod(body, "createControlRange")) {
-            testControlRange = body.createControlRange();
-            if (util.areHostProperties(testControlRange, ["item", "add"])) {
-                implementsControlRange = true;
-            }
-        }
-        features.implementsControlRange = implementsControlRange;
+            rangeProto.surroundContents = function(node) {
+                this.nativeRange.surroundContents(node);
+                updateRangeProperties(this);
+            };
 
-        // Selection collapsedness
-        if (selectionHasAnchorAndFocus) {
-            selectionIsCollapsed = function(sel) {
-                return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
+            rangeProto.collapse = function(isStart) {
+                this.nativeRange.collapse(isStart);
+                updateRangeProperties(this);
             };
-        } else {
-            selectionIsCollapsed = function(sel) {
-                return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
+
+            rangeProto.cloneRange = function() {
+                return new WrappedRange(this.nativeRange.cloneRange());
             };
-        }
 
-        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"];
-        }
+            rangeProto.refresh = function() {
+                updateRangeProperties(this);
+            };
 
-        function updateAnchorAndFocusFromNativeSelection(sel) {
-            var nativeSel = sel.nativeSelection;
-            sel.anchorNode = nativeSel.anchorNode;
-            sel.anchorOffset = nativeSel.anchorOffset;
-            sel.focusNode = nativeSel.focusNode;
-            sel.focusOffset = nativeSel.focusOffset;
-        }
+            rangeProto.toString = function() {
+                return this.nativeRange.toString();
+            };
 
-        function updateEmptySelection(sel) {
-            sel.anchorNode = sel.focusNode = null;
-            sel.anchorOffset = sel.focusOffset = 0;
-            sel.rangeCount = 0;
-            sel.isCollapsed = true;
-            sel._ranges.length = 0;
-        }
+            // Create test range and node for feature detection
 
-        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;
-        }
+            var testTextNode = document.createTextNode("test");
+            dom.getBody(document).appendChild(testTextNode);
+            var range = document.createRange();
 
-        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];
-        }
+            // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
+            // correct for it
 
-        // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
-        function isTextRange(range) {
-            return !!range && typeof range.text != "undefined";
-        }
+            range.setStart(testTextNode, 0);
+            range.setEnd(testTextNode, 0);
 
-        function updateFromTextRange(sel, range) {
-            // Create a Range from the selected TextRange
-            var wrappedRange = new WrappedRange(range);
-            sel._ranges = [wrappedRange];
+            try {
+                range.setStart(testTextNode, 1);
+                canSetRangeStartAfterEnd = true;
 
-            updateAnchorAndFocusFromRange(sel, wrappedRange, false);
-            sel.rangeCount = 1;
-            sel.isCollapsed = wrappedRange.collapsed;
-        }
+                rangeProto.setStart = function(node, offset) {
+                    this.nativeRange.setStart(node, offset);
+                    updateRangeProperties(this);
+                };
 
-        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);
+                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) {
+
+
+                canSetRangeStartAfterEnd = false;
+
+                rangeProto.setStart = function(node, offset) {
+                    try {
+                        this.nativeRange.setStart(node, offset);
+                    } catch (ex) {
+                        this.nativeRange.setEnd(node, offset);
+                        this.nativeRange.setStart(node, offset);
                     }
-                    sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
-                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
-                }
-            }
-        }
+                    updateRangeProperties(this);
+                };
 
-        function addRangeToControlSelection(sel, range) {
-            var controlRange = sel.docSelection.createRange();
-            var rangeElement = getSingleElementFromRange(range);
+                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);
+                };
 
-            // 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?)");
+                createBeforeAfterNodeSetter = function(name, oppositeName) {
+                    return function(node) {
+                        try {
+                            this.nativeRange[name](node);
+                        } catch (ex) {
+                            this.nativeRange[oppositeName](node);
+                            this.nativeRange[name](node);
+                        }
+                        updateRangeProperties(this);
+                    };
+                };
             }
-            newControlRange.select();
 
-            // Update the wrapped selection based on what's now in the native selection
-            updateControlSelection(sel);
-        }
+            rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
+            rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
+            rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
+            rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
 
-        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);
-                }
+            // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
+            // the 0th character of the text node
+            range.selectNodeContents(testTextNode);
+            if (range.startContainer == testTextNode && range.endContainer == testTextNode &&
+                    range.startOffset == 0 && range.endOffset == testTextNode.length) {
+                rangeProto.selectNodeContents = function(node) {
+                    this.nativeRange.selectNodeContents(node);
+                    updateRangeProperties(this);
+                };
+            } else {
+                rangeProto.selectNodeContents = function(node) {
+                    this.setStart(node, 0);
+                    this.setEnd(node, DomRange.getEndOffset(node));
+                };
+            }
 
-                return range;
-            };
-        }
+            /*--------------------------------------------------------------------------------------------------------*/
 
-        function WrappedSelection(selection, docSelection, win) {
-            this.nativeSelection = selection;
-            this.docSelection = docSelection;
-            this._ranges = [];
-            this.win = win;
-            this.refresh();
-        }
+            // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants
+            // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
 
-        WrappedSelection.prototype = api.selectionPrototype;
+            range.selectNodeContents(testTextNode);
+            range.setEnd(testTextNode, 3);
 
-        function deleteProperties(sel) {
-            sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
-            sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
-            sel.detached = true;
-        }
+            var range2 = document.createRange();
+            range2.selectNodeContents(testTextNode);
+            range2.setEnd(testTextNode, 4);
+            range2.setStart(testTextNode, 2);
 
-        var cachedRangySelections = [];
+            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
 
-        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;
+
+                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;
                     }
-                }
-            }
-            if (action == "deleteAll") {
-                cachedRangySelections.length = 0;
+                    return this.nativeRange.compareBoundaryPoints(type, range);
+                };
+            } else {
+                rangeProto.compareBoundaryPoints = function(type, range) {
+                    return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
+                };
             }
-            return null;
-        }
 
-        var getSelection = function(win) {
-            // Check if the parameter is a Rangy Selection object
-            if (win && win instanceof WrappedSelection) {
-                win.refresh();
-                return win;
+            /*--------------------------------------------------------------------------------------------------------*/
+
+            // Test for existence of createContextualFragment and delegate to it if it exists
+            if (api.util.isHostMethod(range, "createContextualFragment")) {
+                rangeProto.createContextualFragment = function(fragmentStr) {
+                    return this.nativeRange.createContextualFragment(fragmentStr);
+                };
             }
 
-            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;
-        };
+            // Clean up
+            dom.getBody(document).removeChild(testTextNode);
+            range.detach();
+            range2.detach();
+        })();
 
-        api.getSelection = getSelection;
+        api.createNativeRange = function(doc) {
+            doc = doc || document;
+            return doc.createRange();
+        };
+    } else if (api.features.implementsTextRange) {
+        // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
+        // prototype
 
-        api.getIframeSelection = function(iframeEl) {
-            module.deprecationNotice("getIframeSelection()", "getSelection(iframeEl)");
-            return api.getSelection(dom.getIframeWindow(iframeEl));
+        WrappedRange = function(textRange) {
+            this.textRange = textRange;
+            this.refresh();
         };
 
-        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?)");
-                }
+        WrappedRange.prototype = new DomRange(document);
+
+        WrappedRange.prototype.refresh = function() {
+            var start, end;
+
+            // 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);
+            } else {
+
+                start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
+                end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
             }
-            controlRange.select();
 
-            // Update the wrapped selection based on what's now in the native selection
-            updateControlSelection(sel);
+            this.setStart(start.node, start.offset);
+            this.setEnd(end.node, end.offset);
+        };
+
+        DomRange.copyComparisonConstants(WrappedRange);
+
+        // Add WrappedRange 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 = WrappedRange;
         }
 
-        // Selecting a range
-        if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
-            selProto.removeAllRanges = function() {
-                this.nativeSelection.removeAllRanges();
-                updateEmptySelection(this);
-            };
+        api.createNativeRange = function(doc) {
+            doc = doc || document;
+            return doc.body.createTextRange();
+        };
+    }
 
-            var addRangeBackward = function(sel, range) {
-                addRangeBackwardToNative(sel.nativeSelection, range);
-                sel.refresh();
-            };
+    if (api.features.implementsTextRange) {
+        WrappedRange.rangeToTextRange = function(range) {
+            if (range.collapsed) {
+                var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
 
-            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.
-                            var clonedNativeRange = getNativeRange(range).cloneRange();
-                            try {
-                                this.nativeSelection.addRange(clonedNativeRange);
-                            } catch (ex) {
-                            }
 
-                            // Check whether adding the range was successful
-                            this.rangeCount = this.nativeSelection.rangeCount;
 
-                            if (this.rangeCount == previousRangeCount + 1) {
-                                // The range was added successfully
+                return tr;
 
-                                // 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();
-                            }
-                        }
-                    }
-                };
+                //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
             } else {
-                selProto.addRange = function(range, direction) {
-                    if (isDirectionBackward(direction) && selectionHasExtend) {
-                        addRangeBackward(this, range);
-                    } else {
-                        this.nativeSelection.addRange(getNativeRange(range));
-                        this.refresh();
-                    }
-                };
+                var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
+                var textRange = dom.getDocument(range.startContainer).body.createTextRange();
+                textRange.setEndPoint("StartToStart", startRange);
+                textRange.setEndPoint("EndToEnd", endRange);
+                return textRange;
             }
+        };
+    }
 
-            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);
-            };
+    WrappedRange.prototype.getName = function() {
+        return "WrappedRange";
+    };
 
-            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);
-                }
-            };
+    api.WrappedRange = WrappedRange;
 
-            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;
-        }
+    api.createRange = function(doc) {
+        doc = doc || document;
+        return new WrappedRange(api.createNativeRange(doc));
+    };
 
-        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();
-            }
-        };
+    api.createRangyRange = function(doc) {
+        doc = doc || document;
+        return new DomRange(doc);
+    };
 
-        var refreshSelection;
+    api.createIframeRange = function(iframeEl) {
+        return api.createRange(dom.getIframeDocument(iframeEl));
+    };
 
-        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);
-                }
+    api.createIframeRangyRange = function(iframeEl) {
+        return api.createRangyRange(dom.getIframeDocument(iframeEl));
+    };
 
-                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);
-                }
+    api.addCreateMissingNativeApiListener(function(win) {
+        var doc = win.document;
+        if (typeof doc.createRange == "undefined") {
+            doc.createRange = function() {
+                return api.createRange(this);
             };
-        } else {
-            module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
-            return false;
         }
+        doc = win = null;
+    });
+});rangy.createModule("WrappedSelection", function(api, module) {
+    // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
+    // spec (http://html5.org/specs/dom-range.html)
 
-        selProto.refresh = function(checkForChanges) {
-            var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
-            var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
+    api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
 
-            refreshSelection(this);
-            if (checkForChanges) {
-                // Check the range count first
-                var i = oldRanges.length;
-                if (i != this._ranges.length) {
-                    return true;
-                }
+    api.config.checkSelectionRanges = 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;
-                }
+    var BOOLEAN = "boolean",
+        windowPropertyName = "_rangySelection",
+        dom = api.dom,
+        util = api.util,
+        DomRange = api.DomRange,
+        WrappedRange = api.WrappedRange,
+        DOMException = api.DOMException,
+        DomPosition = dom.DomPosition,
+        getSelection,
+        selectionIsCollapsed,
+        CONTROL = "Control";
 
-                // Finally, compare each range in turn
-                while (i--) {
-                    if (!rangesEqual(oldRanges[i], this._ranges[i])) {
-                        return true;
-                    }
-                }
-                return false;
-            }
+
+
+    function getWinSelection(winParam) {
+        return (winParam || window).getSelection();
+    }
+
+    function getDocSelection(winParam) {
+        return (winParam || window).document.selection;
+    }
+
+    // Test for the Range/TextRange and Selection features required
+    // Test for ability to retrieve selection
+    var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),
+        implementsDocSelection = api.util.isHostObject(document, "selection");
+
+    var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
+
+    if (useDocumentSelection) {
+        getSelection = getDocSelection;
+        api.isSelectionValid = function(winParam) {
+            var doc = (winParam || window).document, nativeSel = doc.selection;
+
+            // Check whether the selection TextRange is actually contained within the correct document
+            return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
+        };
+    } else if (implementsWinGetSelection) {
+        getSelection = getWinSelection;
+        api.isSelectionValid = function() {
+            return true;
         };
+    } else {
+        module.fail("Neither document.selection or window.getSelection() detected.");
+    }
 
-        // Removal of a single range
-        var removeRangeManually = function(sel, range) {
-            var ranges = sel.getAllRanges();
+    api.getNativeSelection = getSelection;
+
+    var testSelection = getSelection();
+    var testRange = api.createNativeRange(document);
+    var body = dom.getBody(document);
+
+    // Obtaining a range from a selection
+    var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&
+                                     util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));
+    api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
+
+    // Test for existence of native selection extend() method
+    var selectionHasExtend = util.isHostMethod(testSelection, "extend");
+    api.features.selectionHasExtend = selectionHasExtend;
+
+    // Test if rangeCount exists
+    var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
+    api.features.selectionHasRangeCount = selectionHasRangeCount;
+
+    var selectionSupportsMultipleRanges = false;
+    var collapsedNonEditableSelectionsSupported = true;
+
+    if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
+            typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {
+
+        (function() {
+            var iframe = document.createElement("iframe");
+            iframe.frameBorder = 0;
+            iframe.style.position = "absolute";
+            iframe.style.left = "-10000px";
+            body.appendChild(iframe);
+
+            var iframeDoc = dom.getIframeDocument(iframe);
+            iframeDoc.open();
+            iframeDoc.write("12");
+            iframeDoc.close();
+
+            var sel = dom.getIframeWindow(iframe).getSelection();
+            var docEl = iframeDoc.documentElement;
+            var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;
+
+            // Test whether the native selection will allow a collapsed selection within a non-editable element
+            var r1 = iframeDoc.createRange();
+            r1.setStart(textNode, 1);
+            r1.collapse(true);
+            sel.addRange(r1);
+            collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
             sel.removeAllRanges();
-            for (var i = 0, len = ranges.length; i < len; ++i) {
-                if (!rangesEqual(range, ranges[i])) {
-                    sel.addRange(ranges[i]);
-                }
+
+            // Test whether the native selection is capable of supporting multiple ranges
+            var r2 = r1.cloneRange();
+            r1.setStart(textNode, 0);
+            r2.setEnd(textNode, 2);
+            sel.addRange(r1);
+            sel.addRange(r2);
+
+            selectionSupportsMultipleRanges = (sel.rangeCount == 2);
+
+            // Clean up
+            r1.detach();
+            r2.detach();
+
+            body.removeChild(iframe);
+        })();
+    }
+
+    api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
+    api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
+
+    // ControlRanges
+    var implementsControlRange = false, testControlRange;
+
+    if (body && util.isHostMethod(body, "createControlRange")) {
+        testControlRange = body.createControlRange();
+        if (util.areHostProperties(testControlRange, ["item", "add"])) {
+            implementsControlRange = true;
+        }
+    }
+    api.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, backwards) {
+        var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "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 = range._selectionNativeRange;
+            if (!nativeRange) {
+                nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));
+                nativeRange.setEnd(range.endContainer, range.endOffset);
+                nativeRange.setStart(range.startContainer, range.startOffset);
+                range._selectionNativeRange = nativeRange;
+                range.attachListener("detach", function() {
+
+                    this._selectionNativeRange = null;
+                });
             }
-            if (!sel.rangeCount) {
-                updateEmptySelection(sel);
+        } else if (range instanceof WrappedRange) {
+            nativeRange = range.nativeRange;
+        } else if (api.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;
+    }
 
-        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();
+    function getSingleElementFromRange(range) {
+        var nodes = range.getNodes();
+        if (!rangeContainsSingleElement(nodes)) {
+            throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
+        }
+        return nodes[0];
+    }
 
-                    // Update the wrapped selection based on what's now in the native selection
-                    updateControlSelection(this);
-                } else {
-                    removeRangeManually(this, range);
-                }
-            };
+    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 {
-            selProto.removeRange = function(range) {
-                removeRangeManually(this, range);
-            };
+            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 = dom.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);
+            }
         }
+    }
 
-        // Detecting if a selection is backward
-        var selectionIsBackward;
-        if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
-            selectionIsBackward = winSelectionIsBackward;
+    function addRangeToControlSelection(sel, range) {
+        var controlRange = sel.docSelection.createRange();
+        var rangeElement = getSingleElementFromRange(range);
 
-            selProto.isBackward = function() {
-                return selectionIsBackward(this);
-            };
-        } else {
-            selectionIsBackward = selProto.isBackward = function() {
-                return false;
-            };
+        // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
+        // contained by the supplied range
+        var doc = dom.getDocument(controlRange.item(0));
+        var newControlRange = dom.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 new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
+        }
+        newControlRange.select();
 
-        // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
-        selProto.isBackwards = selProto.isBackward;
+        // Update the wrapped selection based on what's now in the native selection
+        updateControlSelection(sel);
+    }
 
-        // 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];
+    var getSelectionRangeAt;
+
+    if (util.isHostMethod(testSelection,  "getRangeAt")) {
+        getSelectionRangeAt = function(sel, index) {
+            try {
+                return sel.getRangeAt(index);
+            } catch(ex) {
+                return null;
             }
-            return rangeTexts.join("");
         };
+    } else if (selectionHasAnchorAndFocus) {
+        getSelectionRangeAt = function(sel) {
+            var doc = dom.getDocument(sel.anchorNode);
+            var range = api.createRange(doc);
+            range.setStart(sel.anchorNode, sel.anchorOffset);
+            range.setEnd(sel.focusNode, sel.focusOffset);
 
-        function assertNodeInSameDocument(sel, node) {
-            if (sel.win.document != getDocument(node)) {
-                throw new DOMException("WRONG_DOCUMENT_ERR");
+            // Handle the case when the selection was selected backwards (from the end to the start in the
+            // document)
+            if (range.collapsed !== this.isCollapsed) {
+                range.setStart(sel.focusNode, sel.focusOffset);
+                range.setEnd(sel.anchorNode, sel.anchorOffset);
             }
-        }
 
-        // 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;
+            return range;
         };
+    }
 
-        selProto.collapseToStart = function() {
-            if (this.rangeCount) {
-                var range = this._ranges[0];
-                this.collapse(range.startContainer, range.startOffset);
-            } else {
-                throw new DOMException("INVALID_STATE_ERR");
-            }
-        };
+    /**
+     * @constructor
+     */
+    function WrappedSelection(selection, docSelection, win) {
+        this.nativeSelection = selection;
+        this.docSelection = docSelection;
+        this._ranges = [];
+        this.win = win;
+        this.refresh();
+    }
 
-        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");
+    api.getSelection = function(win) {
+        win = win || window;
+        var sel = win[windowPropertyName];
+        var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
+        if (sel) {
+            sel.nativeSelection = nativeSel;
+            sel.docSelection = docSel;
+            sel.refresh(win);
+        } else {
+            sel = new WrappedSelection(nativeSel, docSel, win);
+            win[windowPropertyName] = sel;
+        }
+        return sel;
+    };
+
+    api.getIframeSelection = function(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 = dom.getDocument(ranges[0].startContainer);
+        var controlRange = dom.getBody(doc).createControlRange();
+        for (var i = 0, el; i < rangeCount; ++i) {
+            el = getSingleElementFromRange(ranges[i]);
+            try {
+                controlRange.add(el);
+            } catch (ex) {
+                throw new Error("setRanges(): Element within the 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);
         };
 
-        // 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);
+        var addRangeBackwards = function(sel, range) {
+            var doc = DomRange.getRangeDocument(range);
+            var endRange = api.createRange(doc);
+            endRange.collapseToPoint(range.endContainer, range.endOffset);
+            sel.nativeSelection.addRange(getNativeRange(endRange));
+            sel.nativeSelection.extend(range.startContainer, range.startOffset);
+            sel.refresh();
         };
 
-        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();
+        if (selectionHasRangeCount) {
+            selProto.addRange = function(range, backwards) {
+                if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+                    addRangeToControlSelection(this, range);
+                } else {
+                    if (backwards && selectionHasExtend) {
+                        addRangeBackwards(this, range);
+                    } else {
+                        var previousRangeCount;
+                        if (selectionSupportsMultipleRanges) {
+                            previousRangeCount = this.rangeCount;
+                        } else {
+                            this.removeAllRanges();
+                            previousRangeCount = 0;
+                        }
+                        this.nativeSelection.addRange(getNativeRange(range));
+
+                        // 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 && !DomRange.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, selectionIsBackwards(this.nativeSelection));
+                            this.isCollapsed = selectionIsCollapsed(this);
+                        } else {
+                            // The range was not added successfully. The simplest thing is to refresh
+                            this.refresh();
+                        }
                     }
-                    // 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]);
+                }
+            };
+        } else {
+            selProto.addRange = function(range, backwards) {
+                if (backwards && selectionHasExtend) {
+                    addRangeBackwards(this, range);
+                } else {
+                    this.nativeSelection.addRange(getNativeRange(range));
+                    this.refresh();
+                }
+            };
+        }
+
+        selProto.setRanges = function(ranges) {
+            if (implementsControlRange && ranges.length > 1) {
+                createControlSelection(this, ranges);
+            } else {
+                this.removeAllRanges();
+                for (var i = 0, len = ranges.length; i < len; ++i) {
+                    this.addRange(ranges[i]);
                 }
             }
         };
+    } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&
+               implementsControlRange && useDocumentSelection) {
 
-        // 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.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 = dom.getDocument(this.anchorNode);
+                    } else if (this.docSelection.type == CONTROL) {
+                        var controlRange = this.docSelection.createRange();
+                        if (controlRange.length) {
+                            doc = dom.getDocument(controlRange.item(0)).body.createTextRange();
+                        }
+                    }
+                    if (doc) {
+                        var textRange = doc.body.createTextRange();
+                        textRange.select();
+                        this.docSelection.empty();
+                    }
                 }
-            }
+            } catch(ex) {}
+            updateEmptySelection(this);
         };
 
-        selProto.getAllRanges = function() {
-            var ranges = [];
-            this.eachRange(function(range) {
-                ranges.push(range);
-            });
-            return ranges;
+        selProto.addRange = function(range) {
+            if (this.docSelection.type == CONTROL) {
+                addRangeToControlSelection(this, range);
+            } else {
+                WrappedRange.rangeToTextRange(range).select();
+                this._ranges[0] = range;
+                this.rangeCount = 1;
+                this.isCollapsed = this._ranges[0].collapsed;
+                updateAnchorAndFocusFromRange(this, range, false);
+            }
         };
 
-        selProto.setSingleRange = function(range, direction) {
+        selProto.setRanges = function(ranges) {
             this.removeAllRanges();
-            this.addRange(range, direction);
+            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.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.getRangeAt = function(index) {
+        if (index < 0 || index >= this.rangeCount) {
+            throw new DOMException("INDEX_SIZE_ERR");
+        } else {
+            return this._ranges[index];
         }
+    };
 
-        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);
-        };
+    var refreshSelection;
 
-        selProto.changeEachRange = function(func) {
-            var ranges = [];
-            var backward = this.isBackward();
+    if (useDocumentSelection) {
+        refreshSelection = function(sel) {
+            var range;
+            if (api.isSelectionValid(sel.win)) {
+                range = sel.docSelection.createRange();
+            } else {
+                range = dom.getBody(sel.win.document).createTextRange();
+                range.collapse(true);
+            }
 
-            this.eachRange(function(range) {
-                func(range);
-                ranges.push(range);
-            });
 
-            this.removeAllRanges();
-            if (backward && ranges.length == 1) {
-                this.addRange(ranges[0], "backward");
+            if (sel.docSelection.type == CONTROL) {
+                updateControlSelection(sel);
+            } else if (isTextRange(range)) {
+                updateFromTextRange(sel, range);
             } else {
-                this.setRanges(ranges);
+                updateEmptySelection(sel);
             }
         };
-
-        selProto.containsNode = function(node, allowPartial) {
-            return this.eachRange( function(range) {
-                return range.containsNode(node, allowPartial);
-            }, true ) || false;
+    } else if (util.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], selectionIsBackwards(sel.nativeSelection));
+                    sel.isCollapsed = selectionIsCollapsed(sel);
+                } else {
+                    updateEmptySelection(sel);
+                }
+            }
         };
-
-        selProto.getBookmark = function(containerNode) {
-            return {
-                backward: this.isBackward(),
-                rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
-            };
+    } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.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.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);
+    selProto.refresh = function(checkForChanges) {
+        var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
+        refreshSelection(this);
+        if (checkForChanges) {
+            var i = oldRanges.length;
+            if (i != this._ranges.length) {
+                return false;
             }
-            if (bookmark.backward) {
-                this.setSingleRange(selRanges[0], "backward");
-            } else {
-                this.setRanges(selRanges);
+            while (i--) {
+                if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
+                    return false;
+                }
             }
-        };
+            return true;
+        }
+    };
 
-        selProto.toHtml = function() {
-            var rangeHtmls = [];
-            this.eachRange(function(range) {
-                rangeHtmls.push( DomRange.toHtml(range) );
-            });
-            return rangeHtmls.join("");
-        };
+    // Removal of a single range
+    var removeRangeManually = function(sel, range) {
+        var ranges = sel.getAllRanges(), removed = false;
+        sel.removeAllRanges();
+        for (var i = 0, len = ranges.length; i < len; ++i) {
+            if (removed || range !== ranges[i]) {
+                sel.addRange(ranges[i]);
+            } else {
+                // According to the draft WHATWG Range spec, the same range may be added to the selection multiple
+                // times. removeRange should only remove the first instance, so the following ensures only the first
+                // instance is removed
+                removed = true;
+            }
+        }
+        if (!sel.rangeCount) {
+            updateEmptySelection(sel);
+        }
+    };
 
-        if (features.implementsTextRange) {
-            selProto.getNativeTextRange = function() {
-                var sel, textRange;
-                if ( (sel = this.docSelection) ) {
-                    var range = sel.createRange();
-                    if (isTextRange(range)) {
-                        return range;
+    if (implementsControlRange) {
+        selProto.removeRange = function(range) {
+            if (this.docSelection.type == CONTROL) {
+                var controlRange = this.docSelection.createRange();
+                var rangeElement = getSingleElementFromRange(range);
+
+                // Create a new ControlRange containing all the elements in the selected ControlRange minus the
+                // element contained by the supplied range
+                var doc = dom.getDocument(controlRange.item(0));
+                var newControlRange = dom.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 {
-                        throw module.createError("getNativeTextRange: selection is a control selection"); 
+                        removed = true;
                     }
-                } 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";
+                newControlRange.select();
 
-            if (typeof sel.rangeCount != "undefined") {
-                for (var i = 0, len = sel.rangeCount; i < len; ++i) {
-                    rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
-                }
+                // Update the wrapped selection based on what's now in the native selection
+                updateControlSelection(this);
+            } else {
+                removeRangeManually(this, range);
             }
-            return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
-                    ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
-        }
-
-        selProto.getName = function() {
-            return "WrappedSelection";
         };
-
-        selProto.inspect = function() {
-            return inspect(this);
+    } else {
+        selProto.removeRange = function(range) {
+            removeRangeManually(this, range);
         };
+    }
 
-        selProto.detach = function() {
-            actOnCachedSelection(this.win, "delete");
-            deleteProperties(this);
+    // Detecting if a selection is backwards
+    var selectionIsBackwards;
+    if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {
+        selectionIsBackwards = function(sel) {
+            var backwards = false;
+            if (sel.anchorNode) {
+                backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
+            }
+            return backwards;
         };
 
-        WrappedSelection.detachAll = function() {
-            actOnCachedSelection(null, "deleteAll");
+        selProto.isBackwards = function() {
+            return selectionIsBackwards(this);
         };
+    } else {
+        selectionIsBackwards = selProto.isBackwards = function() {
+            return false;
+        };
+    }
 
-        WrappedSelection.inspect = inspect;
-        WrappedSelection.isDirectionBackward = isDirectionBackward;
+    // Selection text
+    // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation
+    selProto.toString = function() {
 
-        api.Selection = WrappedSelection;
+        var rangeTexts = [];
+        for (var i = 0, len = this.rangeCount; i < len; ++i) {
+            rangeTexts[i] = "" + this._ranges[i];
+        }
+        return rangeTexts.join("");
+    };
 
-        api.selectionPrototype = selProto;
+    function assertNodeInSameDocument(sel, node) {
+        if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
+            throw new DOMException("WRONG_DOCUMENT_ERR");
+        }
+    }
 
-        api.addShimListener(function(win) {
-            if (typeof win.getSelection == "undefined") {
-                win.getSelection = function() {
-                    return getSelection(win);
-                };
+    // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
+    selProto.collapse = function(node, offset) {
+        assertNodeInSameDocument(this, node);
+        var range = api.createRange(dom.getDocument(node));
+        range.collapseToPoint(node, offset);
+        this.removeAllRanges();
+        this.addRange(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 HTML 5 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(dom.getDocument(node));
+        range.selectNodeContents(node);
+        this.removeAllRanges();
+        this.addRange(range);
+    };
+
+    selProto.deleteFromDocument = function() {
+        // Sepcial behaviour required for 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);
             }
-            win = null;
-        });
-    });
-    
+            this.refresh();
+        } else if (this.rangeCount) {
+            var ranges = this.getAllRanges();
+            this.removeAllRanges();
+            for (var i = 0, len = ranges.length; i < len; ++i) {
+                ranges[i].deleteContents();
+            }
+            // The HTML5 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.getAllRanges = function() {
+        return this._ranges.slice(0);
+    };
 
-    // Wait for document to load before initializing
-    var docReady = false;
+    selProto.setSingleRange = function(range) {
+        this.setRanges( [range] );
+    };
 
-    var loadHandler = function(e) {
-        if (!docReady) {
-            docReady = true;
-            if (!api.initialized && api.config.autoInitialize) {
-                init();
+    selProto.containsNode = function(node, allowPartial) {
+        for (var i = 0, len = this._ranges.length; i < len; ++i) {
+            if (this._ranges[i].containsNode(node, allowPartial)) {
+                return true;
             }
         }
+        return false;
     };
 
-    if (isBrowser) {
-        // Test whether the document has already been loaded and initialize immediately if so
-        if (document.readyState == "complete") {
-            loadHandler();
-        } else {
-            if (isHostMethod(document, "addEventListener")) {
-                document.addEventListener("DOMContentLoaded", loadHandler, false);
+    selProto.toHtml = function() {
+        var html = "";
+        if (this.rangeCount) {
+            var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");
+            for (var i = 0, len = this._ranges.length; i < len; ++i) {
+                container.appendChild(this._ranges[i].cloneContents());
             }
+            html = container.innerHTML;
+        }
+        return html;
+    };
+
+    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";
 
-            // Add a fallback in case the DOMContentLoaded event isn't supported
-            addListener(window, "load", loadHandler);
+        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() + "]";
+
     }
 
-    return api;
-}, this);
\ No newline at end of file
+    selProto.getName = function() {
+        return "WrappedSelection";
+    };
+
+    selProto.inspect = function() {
+        return inspect(this);
+    };
+
+    selProto.detach = function() {
+        this.win[windowPropertyName] = null;
+        this.win = this.anchorNode = this.focusNode = null;
+    };
+
+    WrappedSelection.inspect = inspect;
+
+    api.Selection = WrappedSelection;
+
+    api.selectionPrototype = selProto;
+
+    api.addCreateMissingNativeApiListener(function(win) {
+        if (typeof win.getSelection == "undefined") {
+            win.getSelection = function() {
+                return api.getSelection(this);
+            };
+        }
+        win = null;
+    });
+});
diff --git a/rangy-core.min.js b/rangy-core.min.js
index 8f91381..8549d10 100644
--- a/rangy-core.min.js
+++ b/rangy-core.min.js
@@ -1,11 +1,94 @@
-/**
- * Rangy, a cross-browser JavaScript range and selection library
- * https://github.com/timdown/rangy
- *
- * Copyright 2015, Tim Down
- * Licensed under the MIT license.
- * Version: 1.3.0-alpha.20150122
- * Build date: 22 January 2015
- */
-!function(e,t){"function"==typeof define&&define.amd?define(e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e():t.rangy=e()}(function(){function e(e,t){var n=typeof e[t];return n==v||!(n!=R||!e[t])||"unknown"==n}function t(e,t){return!(typeof e[t]!=R||!e[t])}function n(e,t){return typeof e[t]!=C}function r(e){return function(t,n){for(var r=n.length;r--;)if(!e(t,n[r]))return!1;return!0}}function o(e){return e&&w(e,y)&&T(e,S)}function i(e){return t(e,"body")?e.body:e.getElementsByTagName("body")[0]}function a(t){typeof console!=C&&e(console,"log")&&console.log(t)}function s(e,t){D&&t?alert(e):a(e)}function c(e){A.initialized=!0,A.supported=!1,s("Rangy is not supported in this environment. Reason: "+e,A.config.alertOnFail)}function d(e){s("Rangy warning: "+e,A.config.alertOnWarn)}function f(e){return e.message||e.description||String(e)}function u(){if(D&&!A.initialized){var t,n=!1,r=!1;e(document,"createRange")&&(t=document.createRange(),w(t,E)&&T(t,N)&&(n=!0));var s=i(document);if(!s||"body"!=s.nodeName.toLowerCase())return void c("No body element found");if(s&&e(s,"createTextRange")&&(t=s.createTextRange(),o(t)&&(r=!0)),!n&&!r)return void c("Neither Range nor TextRange are available");A.initialized=!0,A.features={implementsDomRange:n,implementsTextRange:r};var d,u;for(var l in _)(d=_[l])instanceof h&&d.init(d,A);for(var g=0,p=I.length;p>g;++g)try{I[g](A)}catch(m){u="Rangy init listener threw an exception. Continuing. Detail: "+f(m),a(u)}}}function l(e){e=e||window,u();for(var t=0,n=B.length;n>t;++t)B[t](e)}function h(e,t,n){this.name=e,this.dependencies=t,this.initialized=!1,this.supported=!1,this.initializer=n}function g(e,t,n){var r=new h(e,t,function(t){if(!t.initialized){t.initialized=!0;try{n(A,t),t.supported=!0}catch(r){var o="Module '"+e+"' failed to load: "+f(r);a(o),r.stack&&a(r.stack)}}});return _[e]=r,r}function p(){}function m(){}var R="object",v="function",C="undefined",N=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],E=["setStart","setStartBefore","setStartAfter","setEnd","setEndBefore","setEndAfter","collapse","selectNode","selectNodeContents","compareBoundaryPoints","deleteContents","extractContents","cloneContents","insertNode","surroundContents","cloneRange","toString","detach"],S=["boundingHeight","boundingLeft","boundingTop","boundingWidth","htmlText","text"],y=["collapse","compareEndPoints","duplicate","moveToElementText","parentElement","select","setEndPoint","getBoundingClientRect"],w=r(e),O=r(t),T=r(n),_={},D=typeof window!=C&&typeof document!=C,x={isHostMethod:e,isHostObject:t,isHostProperty:n,areHostMethods:w,areHostObjects:O,areHostProperties:T,isTextRange:o,getBody:i},A={version:"1.3.0-alpha.20150122",initialized:!1,isBrowser:D,supported:!0,util:x,features:{},modules:_,config:{alertOnFail:!0,alertOnWarn:!1,preferTextRange:!1,autoInitialize:typeof rangyAutoInitialize==C?!0:rangyAutoInitialize}};A.fail=c,A.warn=d;var b;({}).hasOwnProperty?(x.extend=b=function(e,t,n){var r,o;for(var i in t)t.hasOwnProperty(i)&&(r=e[i],o=t[i],n&&null!==r&&"object"==typeof r&&null!==o&&"object"==typeof o&&b(r,o,!0),e[i]=o);return t.hasOwnProperty("toString")&&(e.toString=t.toString),e},x.createOptions=function(e,t){var n={};return b(n,t),e&&b(n,e),n}):c("hasOwnProperty not supported"),D||c("Rangy can only run in a browser"),function(){var e;if(D){var t=document.createElement("div");t.appendChild(document.createElement("span"));var n=[].slice;try{1==n.call(t.childNodes,0)[0].nodeType&&(e=function(e){return n.call(e,0)})}catch(r){}}e||(e=function(e){for(var t=[],n=0,r=e.length;r>n;++n)t[n]=e[n];return t}),x.toArray=e}();var P;D&&(e(document,"addEventListener")?P=function(e,t,n){e.addEventListener(t,n,!1)}:e(document,"attachEvent")?P=function(e,t,n){e.attachEvent("on"+t,n)}:c("Document does not have required addEventListener or attachEvent method"),x.addListener=P);var I=[];A.init=u,A.addInitListener=function(e){A.initialized?e(A):I.push(e)};var B=[];A.addShimListener=function(e){B.push(e)},D&&(A.shim=A.createMissingNativeApi=l),h.prototype={init:function(){for(var e,t,n=this.dependencies||[],r=0,o=n.length;o>r;++r){if(t=n[r],e=_[t],!(e&&e instanceof h))throw new Error("required module '"+t+"' not found");if(e.init(),!e.supported)throw new Error("required module '"+t+"' not supported")}this.initializer(this)},fail:function(e){throw this.initialized=!0,this.supported=!1,new Error("Module '"+this.name+"' failed to load: "+e)},warn:function(e){A.warn("Module "+this.name+": "+e)},deprecationNotice:function(e,t){A.warn("DEPRECATED: "+e+" in module "+this.name+"is deprecated. Please use "+t+" instead")},createError:function(e){return new Error("Error in Rangy "+this.name+" module: "+e)}},A.createModule=function(e){var t,n;2==arguments.length?(t=arguments[1],n=[]):(t=arguments[2],n=arguments[1]);var r=g(e,n,t);A.initialized&&A.supported&&r.init()},A.createCoreModule=function(e,t,n){g(e,t,n)},A.RangePrototype=p,A.rangePrototype=new p,A.selectionPrototype=new m,A.createCoreModule("DomUtil",[],function(e,t){function n(e){var t;return typeof e.namespaceURI==x||null===(t=e.namespaceURI)||"http://www.w3.org/1999/xhtml"==t}function r(e){var t=e.parentNode;return 1==t.nodeType?t:null}function o(e){for(var t=0;e=e.previousSibling;)++t;return t}function i(e){switch(e.nodeType){case 7:case 10:return 0;case 3:case 8:return e.length;default:return e.childNodes.length}}function a(e,t){var n,r=[];for(n=e;n;n=n.parentNode)r.push(n);for(n=t;n;n=n.parentNode)if(I(r,n))return n;return null}function s(e,t,n){for(var r=n?t:t.parentNode;r;){if(r===e)return!0;r=r.parentNode}return!1}function c(e,t){return s(e,t,!0)}function d(e,t,n){for(var r,o=n?e:e.parentNode;o;){if(r=o.parentNode,r===t)return o;o=r}return null}function f(e){var t=e.nodeType;return 3==t||4==t||8==t}function u(e){if(!e)return!1;var t=e.nodeType;return 3==t||8==t}function l(e,t){var n=t.nextSibling,r=t.parentNode;return n?r.insertBefore(e,n):r.appendChild(e),e}function h(e,t,n){var r=e.cloneNode(!1);if(r.deleteData(0,t),e.deleteData(t,e.length-t),l(r,e),n)for(var i,a=0;i=n[a++];)i.node==e&&i.offset>t?(i.node=r,i.offset-=t):i.node==e.parentNode&&i.offset>o(e)&&++i.offset;return r}function g(e){if(9==e.nodeType)return e;if(typeof e.ownerDocument!=x)return e.ownerDocument;if(typeof e.document!=x)return e.document;if(e.parentNode)return g(e.parentNode);throw t.createError("getDocument: no document found for node")}function p(e){var n=g(e);if(typeof n.defaultView!=x)return n.defaultView;if(typeof n.parentWindow!=x)return n.parentWindow;throw t.createError("Cannot get a window object for node")}function m(e){if(typeof e.contentDocument!=x)return e.contentDocument;if(typeof e.contentWindow!=x)return e.contentWindow.document;throw t.createError("getIframeDocument: No Document object found for iframe element")}function R(e){if(typeof e.contentWindow!=x)return e.contentWindow;if(typeof e.contentDocument!=x)return e.contentDocument.defaultView;throw t.createError("getIframeWindow: No Window object found for iframe element")}function v(e){return e&&A.isHostMethod(e,"setTimeout")&&A.isHostObject(e,"document")}function C(e,t,n){var r;if(e?A.isHostProperty(e,"nodeType")?r=1==e.nodeType&&"iframe"==e.tagName.toLowerCase()?m(e):g(e):v(e)&&(r=e.document):r=document,!r)throw t.createError(n+"(): Parameter must be a Window object or DOM node");return r}function N(e){for(var t;t=e.parentNode;)e=t;return e}function E(e,n,r,i){var s,c,f,u,l;if(e==r)return n===i?0:i>n?-1:1;if(s=d(r,e,!0))return n<=o(s)?-1:1;if(s=d(e,r,!0))return o(s)[index:"+o(e)+",length:"+e.childNodes.length+"]["+(e.innerHTML||"[innerHTML not supported]").slice(0,25)+"]"}return e.nodeName}function w(e){for(var t,n=g(e).createDocumentFragment();t=e.firstChild;)n.appendChild(t);return n}function O(e){this.root=e,this._next=e}function T(e){return new O(e)}function _(e,t){this.node=e,this.offset=t}function D(e){this.code=this[e],this.codeName=e,this.message="DOMException: "+this.codeName}var x="undefined",A=e.util;A.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||t.fail("document missing a Node creation method"),A.isHostMethod(document,"getElementsByTagName")||t.fail("document missing getElementsByTagName method");var b=document.createElement("div");A.areHostMethods(b,["insertBefore","appendChild","cloneNode"]||!A.areHostObjects(b,["previousSibling","nextSibling","childNodes","parentNode"]))||t.fail("Incomplete Element implementation"),A.isHostProperty(b,"innerHTML")||t.fail("Element is missing innerHTML property");var P=document.createTextNode("test");A.areHostMethods(P,["splitText","deleteData","insertData","appendData","cloneNode"]||!A.areHostObjects(b,["previousSibling","nextSibling","childNodes","parentNode"])||!A.areHostProperties(P,["data"]))||t.fail("Incomplete Text Node implementation");var I=function(e,t){for(var n=e.length;n--;)if(e[n]===t)return!0;return!1},B=!1;!function(){var t=document.createElement("b");t.innerHTML="1";var n=t.firstChild;t.innerHTML="
",B=S(n),e.features.crashyTextNodes=B}();var H;typeof window.getComputedStyle!=x?H=function(e,t){return p(e).getComputedStyle(e,null)[t]}:typeof document.documentElement.currentStyle!=x?H=function(e,t){return e.currentStyle[t]}:t.fail("No means of obtaining computed style properties found"),O.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var e,t,n=this._current=this._next;if(this._current)if(e=n.firstChild)this._next=e;else{for(t=null;n!==this.root&&!(t=n.nextSibling);)n=n.parentNode;this._next=t}return this._current},detach:function(){this._current=this._next=this.root=null}},_.prototype={equals:function(e){return!!e&&this.node===e.node&&this.offset==e.offset},inspect:function(){return"[DomPosition("+y(this.node)+":"+this.offset+")]"},toString:function(){return this.inspect()}},D.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},D.prototype.toString=function(){return this.message},e.dom={arrayContains:I,isHtmlNamespace:n,parentElement:r,getNodeIndex:o,getNodeLength:i,getCommonAncestor:a,isAncestorOf:s,isOrIsAncestorOf:c,getClosestAncestorIn:d,isCharacterDataNode:f,isTextOrCommentNode:u,insertAfter:l,splitDataNode:h,getDocument:g,getWindow:p,getIframeWindow:R,getIframeDocument:m,getBody:A.getBody,isWindow:v,getContentDocument:C,getRootContainer:N,comparePoints:E,isBrokenNode:S,inspectNode:y,getComputedStyleProperty:H,fragmentFromNodeChildren:w,createIterator:T,DomPosition:_},e.DOMException=D}),A.createCoreModule("DomRange",["DomUtil"],function(e){function t(e,t){return 3!=e.nodeType&&(F(e,t.startContainer)||F(e,t.endContainer))}function n(e){return e.document||j(e.startContainer)}function r(e){return new M(e.parentNode,k(e))}function o(e){return new M(e.parentNode,k(e)+1)}function i(e,t,n){var r=11==e.nodeType?e.firstChild:e;return W(t)?n==t.length?B.insertAfter(e,t):t.parentNode.insertBefore(e,0==n?t:U(t,n)):n>=t.childNodes.length?t.appendChild(e):t.insertBefore(e,t.childNodes[n]),r}function a(e,t,r){if(w(e),w(t),n(t)!=n(e))throw new L("WRONG_DOCUMENT_ERR");var o=z(e.startContainer,e.startOffset,t.endContainer,t.endOffset),i=z(e.endContainer,e.endOffset,t.startContainer,t.startOffset);return r?0>=o&&i>=0:0>o&&i>0}function s(e){for(var t,r,o,i=n(e.range).createDocumentFragment();r=e.next();){if(t=e.isPartiallySelectedSubtree(),r=r.cloneNode(!t),t&&(o=e.getSubtreeIterator(),r.appendChild(s(o)),o.detach()),10==r.nodeType)throw new L("HIERARCHY_REQUEST_ERR");i.appendChild(r)}return i}function c(e,t,n){var r,o;n=n||{stop:!1};for(var i,a;i=e.next();)if(e.isPartiallySelectedSubtree()){if(t(i)===!1)return void(n.stop=!0);if(a=e.getSubtreeIterator(),c(a,t,n),a.detach(),n.stop)return}else for(r=B.createIterator(i);o=r.next();)if(t(o)===!1)return void(n.stop=!0)}function d(e){for(var t;e.next();)e.isPartiallySelectedSubtree()?(t=e.getSubtreeIterator(),d(t),t.detach()):e.remove()}function f(e){for(var t,r,o=n(e.range).createDocumentFragment();t=e.next();){if(e.isPartiallySelectedSubtree()?(t=t.cloneNode(!1),r=e.getSubtreeIterator(),t.appendChild(f(r)),r.detach()):e.remove(),10==t.nodeType)throw new L("HIERARCHY_REQUEST_ERR");o.appendChild(t)}return o}function u(e,t,n){var r,o=!(!t||!t.length),i=!!n;o&&(r=new RegExp("^("+t.join("|")+")$"));var a=[];return c(new h(e,!1),function(t){if(!(o&&!r.test(t.nodeType)||i&&!n(t))){var s=e.startContainer;if(t!=s||!W(s)||e.startOffset!=s.length){var c=e.endContainer;t==c&&W(c)&&0==e.endOffset||a.push(t)}}}),a}function l(e){var t="undefined"==typeof e.getName?"Range":e.getName();return"["+t+"("+B.inspectNode(e.startContainer)+":"+e.startOffset+", "+B.inspectNode(e.endContainer)+":"+e.endOffset+")]"}function h(e,t){if(this.range=e,this.clonePartiallySelectedTextNodes=t,!e.collapsed){this.sc=e.startContainer,this.so=e.startOffset,this.ec=e.endContainer,this.eo=e.endOffset;var n=e.commonAncestorContainer;this.sc===this.ec&&W(this.sc)?(this.isSingleCharacterDataNode=!0,this._first=this._last=this._next=this.sc):(this._first=this._next=this.sc!==n||W(this.sc)?V(this.sc,n,!0):this.sc.childNodes[this.so],this._last=this.ec!==n||W(this.ec)?V(this.ec,n,!0):this.ec.childNodes[this.eo-1])}}function g(e){return function(t,n){for(var r,o=n?t:t.parentNode;o;){if(r=o.nodeType,Y(e,r))return o;o=o.parentNode}return null}}function p(e,t){if(nt(e,t))throw new L("INVALID_NODE_TYPE_ERR")}function m(e,t){if(!Y(t,e.nodeType))throw new L("INVALID_NODE_TYPE_ERR")}function R(e,t){if(0>t||t>(W(e)?e.length:e.childNodes.length))throw new L("INDEX_SIZE_ERR")}function v(e,t){if(et(e,!0)!==et(t,!0))throw new L("WRONG_DOCUMENT_ERR")}function C(e){if(tt(e,!0))throw new L("NO_MODIFICATION_ALLOWED_ERR")}function N(e,t){if(!e)throw new L(t)}function E(e){return G&&B.isBrokenNode(e)||!Y(Z,e.nodeType)&&!et(e,!0)}function S(e,t){return t<=(W(e)?e.length:e.childNodes.length)}function y(e){return!!e.startContainer&&!!e.endContainer&&!E(e.startContainer)&&!E(e.endContainer)&&S(e.startContainer,e.startOffset)&&S(e.endContainer,e.endOffset)}function w(e){if(!y(e))throw new Error("Range error: Range is no longer valid after DOM mutation ("+e.inspect()+")")}function O(e,t){w(e);var n=e.startContainer,r=e.startOffset,o=e.endContainer,i=e.endOffset,a=n===o;W(o)&&i>0&&i0&&r=k(n)&&i++,r=0),e.setStartAndEnd(n,r,o,i)}function T(e){w(e);var t=e.commonAncestorContainer.parentNode.cloneNode(!1);return t.appendChild(e.cloneContents()),t.innerHTML}function _(e){e.START_TO_START=ct,e.START_TO_END=dt,e.END_TO_END=ft,e.END_TO_START=ut,e.NODE_BEFORE=lt,e.NODE_AFTER=ht,e.NODE_BEFORE_AND_AFTER=gt,e.NODE_INSIDE=pt}function D(e){_(e),_(e.prototype)}function x(e,t){return function(){w(this);var n,r,i=this.startContainer,a=this.startOffset,s=this.commonAncestorContainer,d=new h(this,!0);i!==s&&(n=V(i,s,!0),r=o(n),i=r.node,a=r.offset),c(d,C),d.reset();var f=e(d);return d.detach(),t(this,i,a,i,a),f}}function A(n,i){function a(e,t){return function(n){m(n,X),m(Q(n),Z);var i=(e?r:o)(n);(t?s:c)(this,i.node,i.offset)}}function s(e,t,n){var r=e.endContainer,o=e.endOffset;(t!==e.startContainer||n!==e.startOffset)&&((Q(t)!=Q(r)||1==z(t,n,r,o))&&(r=t,o=n),i(e,t,n,r,o))}function c(e,t,n){var r=e.startContainer,o=e.startOffset;(t!==e.endContainer||n!==e.endOffset)&&((Q(t)!=Q(r)||-1==z(t,n,r,o))&&(r=t,o=n),i(e,r,o,t,n))}var u=function(){};u.prototype=e.rangePrototype,n.prototype=new u,H.extend(n.prototype,{setStart:function(e,t){p(e,!0),R(e,t),s(this,e,t)},setEnd:function(e,t){p(e,!0),R(e,t),c(this,e,t)},setStartAndEnd:function(){var e=arguments,t=e[0],n=e[1],r=t,o=n;switch(e.length){case 3:o=e[2];break;case 4:r=e[2],o=e[3]}i(this,t,n,r,o)},setBoundary:function(e,t,n){this["set"+(n?"Start":"End")](e,t)},setStartBefore:a(!0,!0),setStartAfter:a(!1,!0),setEndBefore:a(!0,!1),setEndAfter:a(!1,!1),collapse:function(e){w(this),e?i(this,this.startContainer,this.startOffset,this.startContainer,this.startOffset):i(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(e){p(e,!0),i(this,e,0,e,q(e))},selectNode:function(e){p(e,!1),m(e,X);var t=r(e),n=o(e);i(this,t.node,t.offset,n.node,n.offset)},extractContents:x(f,i),deleteContents:x(d,i),canSurroundContents:function(){w(this),C(this.startContainer),C(this.endContainer);var e=new h(this,!0),n=e._first&&t(e._first,this)||e._last&&t(e._last,this);return e.detach(),!n},splitBoundaries:function(){O(this)},splitBoundariesPreservingPositions:function(e){O(this,e)},normalizeBoundaries:function(){w(this);var e=this.startContainer,t=this.startOffset,n=this.endContainer,r=this.endOffset,o=function(e){var t=e.nextSibling;t&&t.nodeType==e.nodeType&&(n=e,r=e.length,e.appendData(t.data),t.parentNode.removeChild(t))},a=function(o){var i=o.previousSibling;if(i&&i.nodeType==o.nodeType){e=o;var a=o.length;if(t=i.length,o.insertData(0,i.data),i.parentNode.removeChild(i),e==n)r+=t,n=e;else if(n==o.parentNode){var s=k(o);r==s?(n=o,r=a):r>s&&r--}}},s=!0;if(W(n))n.length==r&&o(n);else{if(r>0){var c=n.childNodes[r-1];c&&W(c)&&o(c)}s=!this.collapsed}if(s){if(W(e))0==t&&a(e);else if(tx",ot=3==rt.firstChild.nodeType}catch(it){}e.features.htmlParsingConforms=ot;var at=ot?function(e){var t=this.startContainer,n=j(t);if(!t)throw new L("INVALID_STATE_ERR");var r=null;return 1==t.nodeType?r=t:W(t)&&(r=B.parentElement(t)),r=null===r||"HTML"==r.nodeName&&B.isHtmlNamespace(j(r).documentElement)&&B.isHtmlNamespace(r)?n.createElement("body"):r.cloneNode(!1),r.innerHTML=e,B.fragmentFromNodeChildren(r)}:function(e){var t=n(this),r=t.createElement("body");return r.innerHTML=e,B.fragmentFromNodeChildren(r)},st=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],ct=0,dt=1,ft=2,ut=3,lt=0,ht=1,gt=2,pt=3;H.extend(e.rangePrototype,{compareBoundaryPoints:function(e,t){w(this),v(this.startContainer,t.startContainer);var n,r,o,i,a=e==ut||e==ct?"start":"end",s=e==dt||e==ct?"start":"end";return n=this[a+"Container"],r=this[a+"Offset"],o=t[s+"Container"],i=t[s+"Offset"],z(n,r,o,i)},insertNode:function(e){if(w(this),m(e,J),C(this.startContainer),F(e,this.startContainer))throw new L("HIERARCHY_REQUEST_ERR");var t=i(e,this.startContainer,this.startOffset);this.setStartBefore(t)},cloneContents:function(){w(this);var e,t;if(this.collapsed)return n(this).createDocumentFragment();if(this.startContainer===this.endContainer&&W(this.startContainer))return e=this.startContainer.cloneNode(!0),e.data=e.data.slice(this.startOffset,this.endOffset),t=n(this).createDocumentFragment(),t.appendChild(e),t;var r=new h(this,!0);return e=s(r),r.detach(),e},canSurroundContents:function(){w(this),C(this.startContainer),C(this.endContainer);var e=new h(this,!0),n=e._first&&t(e._first,this)||e._last&&t(e._last,this);return e.detach(),!n},surroundContents:function(e){if(m(e,K),!this.canSurroundContents())throw new L("INVALID_STATE_ERR");var t=this.extractContents();if(e.hasChildNodes())for(;e.lastChild;)e.removeChild(e.lastChild);i(e,this.startContainer,this.startOffset),e.appendChild(t),this.selectNode(e)},cloneRange:function(){w(this);for(var e,t=new I(n(this)),r=st.length;r--;)e=st[r],t[e]=this[e];return t},toString:function(){w(this);var e=this.startContainer;if(e===this.endContainer&&W(e))return 3==e.nodeType||4==e.nodeType?e.data.slice(this.startOffset,this.endOffset):"";var t=[],n=new h(this,!0);return c(n,function(e){(3==e.nodeType||4==e.nodeType)&&t.push(e.data)}),n.detach(),t.join("")},compareNode:function(e){w(this);var t=e.parentNode,n=k(e);if(!t)throw new L("NOT_FOUND_ERR");var r=this.comparePoint(t,n),o=this.comparePoint(t,n+1);return 0>r?o>0?gt:lt:o>0?ht:pt},comparePoint:function(e,t){return w(this),N(e,"HIERARCHY_REQUEST_ERR"),v(e,this.startContainer),z(e,t,this.startContainer,this.startOffset)<0?-1:z(e,t,this.endContainer,this.endOffset)>0?1:0},createContextualFragment:at,toHtml:function(){return T(this)},intersectsNode:function(e,t){if(w(this),N(e,"NOT_FOUND_ERR"),j(e)!==n(this))return!1;var r=e.parentNode,o=k(e);N(r,"NOT_FOUND_ERR");var i=z(r,o,this.endContainer,this.endOffset),a=z(r,o+1,this.startContainer,this.startOffset);return t?0>=i&&a>=0:0>i&&a>0},isPointInRange:function(e,t){return w(this),N(e,"HIERARCHY_REQUEST_ERR"),v(e,this.startContainer),z(e,t,this.startContainer,this.startOffset)>=0&&z(e,t,this.endContainer,this.endOffset)<=0},intersectsRange:function(e){return a(this,e,!1)},intersectsOrTouchesRange:function(e){return a(this,e,!0)},intersection:function(e){if(this.intersectsRange(e)){var t=z(this.startContainer,this.startOffset,e.startContainer,e.startOffset),n=z(this.endContainer,this.endOffset,e.endContainer,e.endOffset),r=this.cloneRange();return-1==t&&r.setStart(e.startContainer,e.startOffset),1==n&&r.setEnd(e.endContainer,e.endOffset),r}return null},union:function(e){if(this.intersectsOrTouchesRange(e)){var t=this.cloneRange();return-1==z(e.startContainer,e.startOffset,this.startContainer,this.startOffset)&&t.setStart(e.startContainer,e.startOffset),1==z(e.endContainer,e.endOffset,this.endContainer,this.endOffset)&&t.setEnd(e.endContainer,e.endOffset),t}throw new L("Ranges do not intersect")},containsNode:function(e,t){return t?this.intersectsNode(e,!1):this.compareNode(e)==pt},containsNodeContents:function(e){return this.comparePoint(e,0)>=0&&this.comparePoint(e,q(e))<=0},containsRange:function(e){var t=this.intersection(e);return null!==t&&e.equals(t)},containsNodeText:function(e){var t=this.cloneRange();t.selectNode(e);var n=t.getNodes([3]);if(n.length>0){t.setStart(n[0],0);var r=n.pop();return t.setEnd(r,r.length),this.containsRange(t)}return this.containsNodeContents(e)},getNodes:function(e,t){return w(this),u(this,e,t)},getDocument:function(){return n(this)},collapseBefore:function(e){this.setEndBefore(e),this.collapse(!1)},collapseAfter:function(e){this.setStartAfter(e),this.collapse(!0)},getBookmark:function(t){var r=n(this),o=e.createRange(r);t=t||B.getBody(r),o.selectNodeContents(t);var i=this.intersection(o),a=0,s=0;return i&&(o.setEnd(i.startContainer,i.startOffset),a=o.toString().length,s=a+i.toString().length),{start:a,end:s,containerNode:t}},moveToBookmark:function(e){var t=e.containerNode,n=0;this.setStart(t,0),this.collapse(!0);for(var r,o,i,a,s=[t],c=!1,d=!1;!d&&(r=s.pop());)if(3==r.nodeType)o=n+r.length,!c&&e.start>=n&&e.start<=o&&(this.setStart(r,e.start-n),c=!0),c&&e.end>=n&&e.end<=o&&(this.setEnd(r,e.end-n),d=!0),n=o;else for(a=r.childNodes,i=a.length;i--;)s.push(a[i])},getName:function(){return"DomRange"},equals:function(e){return I.rangesEqual(this,e)},isValid:function(){return y(this)},inspect:function(){return l(this)},detach:function(){}}),A(I,P),H.extend(I,{rangeProperties:st,RangeIterator:h,copyComparisonConstants:D,createPrototypeRange:A,inspect:l,toHtml:T,getRangeDocument:n,rangesEqual:function(e,t){return e.startContainer===t.startContainer&&e.startOffset===t.startOffset&&e.endContainer===t.endContainer&&e.endOffset===t.endOffset}}),e.DomRange=I}),A.createCoreModule("WrappedRange",["DomRange"],function(e,t){var n,r,o=e.dom,i=e.util,a=o.DomPosition,s=e.DomRange,c=o.getBody,d=o.getContentDocument,f=o.isCharacterDataNode;if(e.features.implementsDomRange&&!function(){function r(e){for(var t,n=l.length;n--;)t=l[n],e[t]=e.nativeRange[t];e.collapsed=e.startContainer===e.endContainer&&e.startOffset===e.endOffset}function a(e,t,n,r,o){var i=e.startContainer!==t||e.startOffset!=n,a=e.endContainer!==r||e.endOffset!=o,s=!e.equals(e.nativeRange);(i||a||s)&&(e.setEnd(r,o),e.setStart(t,n))}var f,u,l=s.rangeProperties;n=function(e){if(!e)throw t.createError("WrappedRange: Range must be specified");this.nativeRange=e,r(this)},s.createPrototypeRange(n,a),f=n.prototype,f.selectNode=function(e){this.nativeRange.selectNode(e),r(this)},f.cloneContents=function(){return this.nativeRange.cloneContents()},f.surroundContents=function(e){this.nativeRange.surroundContents(e),r(this)},f.collapse=function(e){this.nativeRange.collapse(e),r(this)},f.cloneRange=function(){return new n(this.nativeRange.cloneRange())},f.refresh=function(){r(this)},f.toString=function(){return this.nativeRange.toString()};var h=document.createTextNode("test");c(document).appendChild(h);var g=document.createRange();g.setStart(h,0),g.setEnd(h,0);try{g.setStart(h,1),f.setStart=function(e,t){this.nativeRange.setStart(e,t),r(this)},f.setEnd=function(e,t){this.nativeRange.setEnd(e,t),r(this)},u=function(e){return function(t){this.nativeRange[e](t),r(this)}}}catch(p){f.setStart=function(e,t){try{this.nativeRange.setStart(e,t)}catch(n){this.nativeRange.setEnd(e,t),this.nativeRange.setStart(e,t)}r(this)},f.setEnd=function(e,t){try{this.nativeRange.setEnd(e,t)}catch(n){this.nativeRange.setStart(e,t),this.nativeRange.setEnd(e,t)}r(this)},u=function(e,t){return function(n){try{this.nativeRange[e](n)}catch(o){this.nativeRange[t](n),this.nativeRange[e](n)}r(this)}}}f.setStartBefore=u("setStartBefore","setEndBefore"),f.setStartAfter=u("setStartAfter","setEndAfter"),f.setEndBefore=u("setEndBefore","setStartBefore"),f.setEndAfter=u("setEndAfter","setStartAfter"),f.selectNodeContents=function(e){this.setStartAndEnd(e,0,o.getNodeLength(e))},g.selectNodeContents(h),g.setEnd(h,3);var m=document.createRange();m.selectNodeContents(h),m.setEnd(h,4),m.setStart(h,2),f.compareBoundaryPoints=-1==g.compareBoundaryPoints(g.START_TO_END,m)&&1==g.compareBoundaryPoints(g.END_TO_START,m)?function(e,t){return t=t.nativeRange||t,e==t.START_TO_END?e=t.END_TO_START:e==t.END_TO_START&&(e=t.START_TO_END),this.nativeRange.compareBoundaryPoints(e,t)}:function(e,t){return this.nativeRange.compareBoundaryPoints(e,t.nativeRange||t)};var R=document.createElement("div");R.innerHTML="123";var v=R.firstChild,C=c(document);C.appendChild(R),g.setStart(v,1),g.setEnd(v,2),g.deleteContents(),"13"==v.data&&(f.deleteContents=function(){this.nativeRange.deleteContents(),r(this)},f.extractContents=function(){var e=this.nativeRange.extractContents();return r(this),e}),C.removeChild(R),C=null,i.isHostMethod(g,"createContextualFragment")&&(f.createContextualFragment=function(e){return this.nativeRange.createContextualFragment(e)}),c(document).removeChild(h),f.getName=function(){return"WrappedRange"},e.WrappedRange=n,e.createNativeRange=function(e){return e=d(e,t,"createNativeRange"),e.createRange()}}(),e.features.implementsTextRange){var u=function(e){var t=e.parentElement(),n=e.duplicate();n.collapse(!0);var r=n.parentElement();n=e.duplicate(),n.collapse(!1);var i=n.parentElement(),a=r==i?r:o.getCommonAncestor(r,i);return a==t?a:o.getCommonAncestor(t,a)},l=function(e){return 0==e.compareEndPoints("StartToEnd",e)},h=function(e,t,n,r,i){var s=e.duplicate();s.collapse(n);var c=s.parentElement();if(o.isOrIsAncestorOf(t,c)||(c=t),!c.canHaveHTML){var d=new a(c.parentNode,o.getNodeIndex(c));return{boundaryPosition:d,nodeInfo:{nodeIndex:d.offset,containerElement:d.node}}}var u=o.getDocument(c).createElement("span");u.parentNode&&u.parentNode.removeChild(u);for(var l,h,g,p,m,R=n?"StartToStart":"StartToEnd",v=i&&i.containerElement==c?i.nodeIndex:0,C=c.childNodes.length,N=C,E=N;;){if(E==C?c.appendChild(u):c.insertBefore(u,c.childNodes[E]),s.moveToElementText(u),l=s.compareEndPoints(R,e),0==l||v==N)break;if(-1==l){if(N==v+1)break;v=E}else N=N==v+1?v:E;E=Math.floor((v+N)/2),c.removeChild(u)}if(m=u.nextSibling,-1==l&&m&&f(m)){s.setEndPoint(n?"EndToStart":"EndToEnd",e);var S;if(/[\r\n]/.test(m.data)){var y=s.duplicate(),w=y.text.replace(/\r\n/g,"\r").length;for(S=y.moveStart("character",w);-1==(l=y.compareEndPoints("StartToEnd",y));)S++,y.moveStart("character",1)}else S=s.text.length;p=new a(m,S)}else h=(r||!n)&&u.previousSibling,g=(r||n)&&u.nextSibling,p=g&&f(g)?new a(g,0):h&&f(h)?new a(h,h.data.length):new a(c,o.getNodeIndex(u));return u.parentNode.removeChild(u),{boundaryPosition:p,nodeInfo:{nodeIndex:E,containerElement:c}}},g=function(e,t){var n,r,i,a,s=e.offset,d=o.getDocument(e.node),u=c(d).createTextRange(),l=f(e.node);return l?(n=e.node,r=n.parentNode):(a=e.node.childNodes,n=st;++t)if(!D.isAncestorOf(e[0],e[t]))return!1;return!0}function l(e){var n=e.getNodes();if(!u(n))throw t.createError("getSingleElementFromRange: range "+e.inspect()+" did not consist of a single element");return n[0]}function h(e){return!!e&&"undefined"!=typeof e.text}function g(e,t){var n=new P(t);e._ranges=[n],s(e,n,!1),e.rangeCount=1,e.isCollapsed=n.collapsed}function p(t){if(t._ranges.length=0,"None"==t.docSelection.type)d(t);else{var n=t.docSelection.createRange();if(h(n))g(t,n);else{t.rangeCount=n.length;for(var r,o=L(n.item(0)),i=0;is;++s)a.add(r.item(s));try{a.add(o)}catch(d){throw t.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)")}a.select(),p(e)}function R(e,t,n){this.nativeSelection=e,this.docSelection=t,this._ranges=[],this.win=n,this.refresh()}function v(e){e.win=e.anchorNode=e.focusNode=e._ranges=null,e.rangeCount=e.anchorOffset=e.focusOffset=0,e.detached=!0}function C(e,t){for(var n,r,o=tt.length;o--;)if(n=tt[o],r=n.selection,"deleteAll"==t)v(r);else if(n.win==e)return"delete"==t?(tt.splice(o,1),!0):r;return"deleteAll"==t&&(tt.length=0),null}function N(e,n){for(var r,o=L(n[0].startContainer),i=W(o).createControlRange(),a=0,s=n.length;s>a;++a){r=l(n[a]);try{i.add(r)}catch(c){throw t.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)")}}i.select(),p(e)}function E(e,t){if(e.win.document!=L(t))throw new I("WRONG_DOCUMENT_ERR")}function S(t){return function(n,r){var o;this.rangeCount?(o=this.getRangeAt(0),o["set"+(t?"Start":"End")](n,r)):(o=e.createRange(this.win.document),o.setStartAndEnd(n,r)),this.setSingleRange(o,this.isBackward())}}function y(e){var t=[],n=new B(e.anchorNode,e.anchorOffset),r=new B(e.focusNode,e.focusOffset),o="function"==typeof e.getName?e.getName():"Selection";if("undefined"!=typeof e.rangeCount)for(var i=0,a=e.rangeCount;a>i;++i)t[i]=b.inspect(e.getRangeAt(i));return"["+o+"(Ranges: "+t.join(", ")+")(anchor: "+n.inspect()+", focus: "+r.inspect()+"]"}e.config.checkSelectionRanges=!0;var w,O,T="boolean",_="number",D=e.dom,x=e.util,A=x.isHostMethod,b=e.DomRange,P=e.WrappedRange,I=e.DOMException,B=D.DomPosition,H=e.features,M="Control",L=D.getDocument,W=D.getBody,k=b.rangesEqual,F=A(window,"getSelection"),j=x.isHostObject(document,"selection");H.implementsWinGetSelection=F,H.implementsDocSelection=j;var z=j&&(!F||e.config.preferTextRange);z?(w=i,e.isSelectionValid=function(e){var t=r(e,"isSelectionValid").document,n=t.selection;return"None"!=n.type||L(n.createRange().parentElement())==t}):F?(w=o,e.isSelectionValid=function(){return!0}):t.fail("Neither document.selection or window.getSelection() detected."),e.getNativeSelection=w;var U=w(),V=e.createNativeRange(document),q=W(document),Y=x.areHostProperties(U,["anchorNode","focusNode","anchorOffset","focusOffset"]);H.selectionHasAnchorAndFocus=Y;var Q=A(U,"extend");H.selectionHasExtend=Q;var G=typeof U.rangeCount==_;H.selectionHasRangeCount=G;var X=!1,Z=!0,$=Q?function(t,n){var r=b.getRangeDocument(n),o=e.createRange(r);o.collapseToPoint(n.endContainer,n.endOffset),t.addRange(f(o)),t.extend(n.startContainer,n.startOffset)}:null;x.areHostMethods(U,["addRange","getRangeAt","removeAllRanges"])&&typeof U.rangeCount==_&&H.implementsDomRange&&!function(){var t=window.getSelection();if(t){for(var n=t.rangeCount,r=n>1,o=[],i=a(t),s=0;n>s;++s)o[s]=t.getRangeAt(s);var c=W(document),d=c.appendChild(document.createElement("div"));d.contentEditable="false";var f=d.appendChild(document.createTextNode("   ")),u=document.createRange();if(u.setStart(f,1),u.collapse(!0),t.addRange(u),Z=1==t.rangeCount,t.removeAllRanges(),!r){var l=window.navigator.appVersion.match(/Chrome\/(.*?) /);if(l&&parseInt(l[1])>=36)X=!1;else{var h=u.cloneRange();u.setStart(f,0),h.setEnd(f,3),h.setStart(f,2),t.addRange(u),t.addRange(h),X=2==t.rangeCount}}for(c.removeChild(d),t.removeAllRanges(),s=0;n>s;++s)0==s&&i?$?$(t,o[s]):(e.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend"),t.addRange(o[s])):t.addRange(o[s])}}(),H.selectionSupportsMultipleRanges=X,H.collapsedNonEditableSelectionsSupported=Z;var J,K=!1;q&&A(q,"createControlRange")&&(J=q.createControlRange(),x.areHostProperties(J,["item","add"])&&(K=!0)),H.implementsControlRange=K,O=Y?function(e){return e.anchorNode===e.focusNode&&e.anchorOffset===e.focusOffset}:function(e){return e.rangeCount?e.getRangeAt(e.rangeCount-1).collapsed:!1};var et;A(U,"getRangeAt")?et=function(e,t){try{return e.getRangeAt(t)}catch(n){return null}}:Y&&(et=function(t){var n=L(t.anchorNode),r=e.createRange(n);return r.setStartAndEnd(t.anchorNode,t.anchorOffset,t.focusNode,t.focusOffset),r.collapsed!==this.isCollapsed&&r.setStartAndEnd(t.focusNode,t.focusOffset,t.anchorNode,t.anchorOffset),r}),R.prototype=e.selectionPrototype;var tt=[],nt=function(e){if(e&&e instanceof R)return e.refresh(),e;e=r(e,"getNativeSelection");var t=C(e),n=w(e),o=j?i(e):null;return t?(t.nativeSelection=n,t.docSelection=o,t.refresh()):(t=new R(n,o,e),tt.push({win:e,selection:t})),t};e.getSelection=nt,e.getIframeSelection=function(n){return t.deprecationNotice("getIframeSelection()","getSelection(iframeEl)"),e.getSelection(D.getIframeWindow(n))};var rt=R.prototype;if(!z&&Y&&x.areHostMethods(U,["removeAllRanges","addRange"])){rt.removeAllRanges=function(){this.nativeSelection.removeAllRanges(),d(this)};var ot=function(e,t){$(e.nativeSelection,t),e.refresh()};rt.addRange=G?function(t,r){if(K&&j&&this.docSelection.type==M)m(this,t);else if(n(r)&&Q)ot(this,t);else{var o;X?o=this.rangeCount:(this.removeAllRanges(),o=0);var i=f(t).cloneRange();try{this.nativeSelection.addRange(i)}catch(a){}if(this.rangeCount=this.nativeSelection.rangeCount,this.rangeCount==o+1){if(e.config.checkSelectionRanges){var c=et(this.nativeSelection,this.rangeCount-1);c&&!k(c,t)&&(t=new P(c))}this._ranges[this.rangeCount-1]=t,s(this,t,st(this.nativeSelection)),this.isCollapsed=O(this)}else this.refresh()}}:function(e,t){n(t)&&Q?ot(this,e):(this.nativeSelection.addRange(f(e)),this.refresh())},rt.setRanges=function(e){if(K&&j&&e.length>1)N(this,e);else{this.removeAllRanges();for(var t=0,n=e.length;n>t;++t)this.addRange(e[t])}}}else{if(!(A(U,"empty")&&A(V,"select")&&K&&z))return t.fail("No means of selecting a Range or TextRange was found"),!1;rt.removeAllRanges=function(){try{if(this.docSelection.empty(),"None"!=this.docSelection.type){var e;if(this.anchorNode)e=L(this.anchorNode);else if(this.docSelection.type==M){var t=this.docSelection.createRange();t.length&&(e=L(t.item(0)))}if(e){var n=W(e).createTextRange();n.select(),this.docSelection.empty()}}}catch(r){}d(this)},rt.addRange=function(t){this.docSelection.type==M?m(this,t):(e.WrappedTextRange.rangeToTextRange(t).select(),this._ranges[0]=t,this.rangeCount=1,this.isCollapsed=this._ranges[0].collapsed,s(this,t,!1))},rt.setRanges=function(e){this.removeAllRanges();var t=e.length;t>1?N(this,e):t&&this.addRange(e[0])}}rt.getRangeAt=function(e){if(0>e||e>=this.rangeCount)throw new I("INDEX_SIZE_ERR");return this._ranges[e].cloneRange()};var it;if(z)it=function(t){var n;e.isSelectionValid(t.win)?n=t.docSelection.createRange():(n=W(t.win.document).createTextRange(),n.collapse(!0)),t.docSelection.type==M?p(t):h(n)?g(t,n):d(t)};else if(A(U,"getRangeAt")&&typeof U.rangeCount==_)it=function(t){if(K&&j&&t.docSelection.type==M)p(t);else if(t._ranges.length=t.rangeCount=t.nativeSelection.rangeCount,t.rangeCount){for(var n=0,r=t.rangeCount;r>n;++n)t._ranges[n]=new e.WrappedRange(t.nativeSelection.getRangeAt(n));s(t,t._ranges[t.rangeCount-1],st(t.nativeSelection)),t.isCollapsed=O(t)}else d(t)};else{if(!Y||typeof U.isCollapsed!=T||typeof V.collapsed!=T||!H.implementsDomRange)return t.fail("No means of obtaining a Range or TextRange from the user's selection was found"),!1;it=function(e){var t,n=e.nativeSelection;n.anchorNode?(t=et(n,0),e._ranges=[t],e.rangeCount=1,c(e),e.isCollapsed=O(e)):d(e)}}rt.refresh=function(e){var t=e?this._ranges.slice(0):null,n=this.anchorNode,r=this.anchorOffset;if(it(this),e){var o=t.length;if(o!=this._ranges.length)return!0;if(this.anchorNode!=n||this.anchorOffset!=r)return!0;for(;o--;)if(!k(t[o],this._ranges[o]))return!0;return!1}};var at=function(e,t){var n=e.getAllRanges();e.removeAllRanges();for(var r=0,o=n.length;o>r;++r)k(t,n[r])||e.addRange(n[r]);e.rangeCount||d(e)};rt.removeRange=K&&j?function(e){if(this.docSelection.type==M){for(var t,n=this.docSelection.createRange(),r=l(e),o=L(n.item(0)),i=W(o).createControlRange(),a=!1,s=0,c=n.length;c>s;++s)t=n.item(s),t!==r||a?i.add(n.item(s)):a=!0;i.select(),p(this)}else at(this,e)}:function(e){at(this,e)};var st;!z&&Y&&H.implementsDomRange?(st=a,rt.isBackward=function(){return st(this)}):st=rt.isBackward=function(){return!1},rt.isBackwards=rt.isBackward,rt.toString=function(){for(var e=[],t=0,n=this.rangeCount;n>t;++t)e[t]=""+this._ranges[t];return e.join("")},rt.collapse=function(t,n){E(this,t);var r=e.createRange(t);r.collapseToPoint(t,n),this.setSingleRange(r),this.isCollapsed=!0},rt.collapseToStart=function(){if(!this.rangeCount)throw new I("INVALID_STATE_ERR");var e=this._ranges[0];this.collapse(e.startContainer,e.startOffset)},rt.collapseToEnd=function(){if(!this.rangeCount)throw new I("INVALID_STATE_ERR");var e=this._ranges[this.rangeCount-1];this.collapse(e.endContainer,e.endOffset)},rt.selectAllChildren=function(t){E(this,t);var n=e.createRange(t);n.selectNodeContents(t),this.setSingleRange(n)},rt.deleteFromDocument=function(){if(K&&j&&this.docSelection.type==M){for(var e,t=this.docSelection.createRange();t.length;)e=t.item(0),t.remove(e),e.parentNode.removeChild(e);this.refresh()}else if(this.rangeCount){var n=this.getAllRanges();if(n.length){this.removeAllRanges();for(var r=0,o=n.length;o>r;++r)n[r].deleteContents();this.addRange(n[o-1])}}},rt.eachRange=function(e,t){for(var n=0,r=this._ranges.length;r>n;++n)if(e(this.getRangeAt(n)))return t},rt.getAllRanges=function(){var e=[];return this.eachRange(function(t){e.push(t)}),e},rt.setSingleRange=function(e,t){this.removeAllRanges(),this.addRange(e,t)},rt.callMethodOnEachRange=function(e,t){var n=[];return this.eachRange(function(r){n.push(r[e].apply(r,t))}),n},rt.setStart=S(!0),rt.setEnd=S(!1),e.rangePrototype.select=function(e){nt(this.getDocument()).setSingleRange(this,e)},rt.changeEachRange=function(e){var t=[],n=this.isBackward();this.eachRange(function(n){e(n),t.push(n)}),this.removeAllRanges(),n&&1==t.length?this.addRange(t[0],"backward"):this.setRanges(t)},rt.containsNode=function(e,t){return this.eachRange(function(n){return n.containsNode(e,t)},!0)||!1},rt.getBookmark=function(e){return{backward:this.isBackward(),rangeBookmarks:this.callMethodOnEachRange("getBookmark",[e])}},rt.moveToBookmark=function(t){for(var n,r,o=[],i=0;n=t.rangeBookmarks[i++];)r=e.createRange(this.win),r.moveToBookmark(n),o.push(r);t.backward?this.setSingleRange(o[0],"backward"):this.setRanges(o)},rt.toHtml=function(){var e=[];return this.eachRange(function(t){e.push(b.toHtml(t))}),e.join("")},H.implementsTextRange&&(rt.getNativeTextRange=function(){var n;if(n=this.docSelection){var r=n.createRange();if(h(r))return r;throw t.createError("getNativeTextRange: selection is a control selection")}if(this.rangeCount>0)return e.WrappedTextRange.rangeToTextRange(this.getRangeAt(0));throw t.createError("getNativeTextRange: selection contains no range")}),rt.getName=function(){return"WrappedSelection"},rt.inspect=function(){return y(this)},rt.detach=function(){C(this.win,"delete"),v(this)},R.detachAll=function(){C(null,"deleteAll")},R.inspect=y,R.isDirectionBackward=n,e.Selection=R,e.selectionPrototype=rt,e.addShimListener(function(e){"undefined"==typeof e.getSelection&&(e.getSelection=function(){return nt(e)}),e=null})});var H=!1,M=function(){H||(H=!0,!A.initialized&&A.config.autoInitialize&&u())};return D&&("complete"==document.readyState?M():(e(document,"addEventListener")&&document.addEventListener("DOMContentLoaded",M,!1),P(window,"load",M))),A},this); \ No newline at end of file +/* + Rangy, a cross-browser JavaScript range and selection library + http://code.google.com/p/rangy/ + + Copyright 2012, Tim Down + Licensed under the MIT license. + Version: 1.2.3 + Build date: 26 February 2012 +*/ +window.rangy=function(){function l(p,u){var w=typeof p[u];return w=="function"||!!(w=="object"&&p[u])||w=="unknown"}function K(p,u){return!!(typeof p[u]=="object"&&p[u])}function H(p,u){return typeof p[u]!="undefined"}function I(p){return function(u,w){for(var B=w.length;B--;)if(!p(u,w[B]))return false;return true}}function z(p){return p&&A(p,x)&&v(p,t)}function C(p){window.alert("Rangy not supported in your browser. Reason: "+p);c.initialized=true;c.supported=false}function N(){if(!c.initialized){var p, +u=false,w=false;if(l(document,"createRange")){p=document.createRange();if(A(p,n)&&v(p,i))u=true;p.detach()}if((p=K(document,"body")?document.body:document.getElementsByTagName("body")[0])&&l(p,"createTextRange")){p=p.createTextRange();if(z(p))w=true}!u&&!w&&C("Neither Range nor TextRange are implemented");c.initialized=true;c.features={implementsDomRange:u,implementsTextRange:w};u=k.concat(f);w=0;for(p=u.length;w["+c.childNodes.length+"]":c.nodeName}function n(c){this._next=this.root=c}function t(c,f){this.node=c;this.offset=f}function x(c){this.code=this[c]; +this.codeName=c;this.message="DOMException: "+this.codeName}var A=l.util;A.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||K.fail("document missing a Node creation method");A.isHostMethod(document,"getElementsByTagName")||K.fail("document missing getElementsByTagName method");var q=document.createElement("div");A.areHostMethods(q,["insertBefore","appendChild","cloneNode"])||K.fail("Incomplete Element implementation");A.isHostProperty(q,"innerHTML")||K.fail("Element is missing innerHTML property"); +q=document.createTextNode("test");A.areHostMethods(q,["splitText","deleteData","insertData","appendData","cloneNode"])||K.fail("Incomplete Text Node implementation");var v=function(c,f){for(var k=c.length;k--;)if(c[k]===f)return true;return false};n.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var c=this._current=this._next,f;if(this._current)if(f=c.firstChild)this._next=f;else{for(f=null;c!==this.root&&!(f=c.nextSibling);)c=c.parentNode;this._next=f}return this._current}, +detach:function(){this._current=this._next=this.root=null}};t.prototype={equals:function(c){return this.node===c.node&this.offset==c.offset},inspect:function(){return"[DomPosition("+i(this.node)+":"+this.offset+")]"}};x.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};x.prototype.toString=function(){return this.message};l.dom={arrayContains:v,isHtmlNamespace:function(c){var f;return typeof c.namespaceURI== +"undefined"||(f=c.namespaceURI)===null||f=="http://www.w3.org/1999/xhtml"},parentElement:function(c){c=c.parentNode;return c.nodeType==1?c:null},getNodeIndex:H,getNodeLength:function(c){var f;return C(c)?c.length:(f=c.childNodes)?f.length:0},getCommonAncestor:I,isAncestorOf:function(c,f,k){for(f=k?f:f.parentNode;f;)if(f===c)return true;else f=f.parentNode;return false},getClosestAncestorIn:z,isCharacterDataNode:C,insertAfter:N,splitDataNode:function(c,f){var k=c.cloneNode(false);k.deleteData(0,f); +c.deleteData(f,c.length-f);N(k,c);return k},getDocument:O,getWindow:function(c){c=O(c);if(typeof c.defaultView!="undefined")return c.defaultView;else if(typeof c.parentWindow!="undefined")return c.parentWindow;else throw Error("Cannot get a window object for node");},getIframeWindow:function(c){if(typeof c.contentWindow!="undefined")return c.contentWindow;else if(typeof c.contentDocument!="undefined")return c.contentDocument.defaultView;else throw Error("getIframeWindow: No Window object found for iframe element"); +},getIframeDocument:function(c){if(typeof c.contentDocument!="undefined")return c.contentDocument;else if(typeof c.contentWindow!="undefined")return c.contentWindow.document;else throw Error("getIframeWindow: No Document object found for iframe element");},getBody:function(c){return A.isHostObject(c,"body")?c.body:c.getElementsByTagName("body")[0]},getRootContainer:function(c){for(var f;f=c.parentNode;)c=f;return c},comparePoints:function(c,f,k,r){var L;if(c==k)return f===r?0:f=e.childNodes.length?e.appendChild(a):e.insertBefore(a,e.childNodes[j]);return o}function O(a){for(var e,j,o=H(a.range).createDocumentFragment();j=a.next();){e=a.isPartiallySelectedSubtree();j=j.cloneNode(!e);if(e){e=a.getSubtreeIterator();j.appendChild(O(e));e.detach(true)}if(j.nodeType==10)throw new S("HIERARCHY_REQUEST_ERR");o.appendChild(j)}return o}function i(a,e,j){var o,E;for(j=j||{stop:false};o=a.next();)if(a.isPartiallySelectedSubtree())if(e(o)=== +false){j.stop=true;return}else{o=a.getSubtreeIterator();i(o,e,j);o.detach(true);if(j.stop)return}else for(o=g.createIterator(o);E=o.next();)if(e(E)===false){j.stop=true;return}}function n(a){for(var e;a.next();)if(a.isPartiallySelectedSubtree()){e=a.getSubtreeIterator();n(e);e.detach(true)}else a.remove()}function t(a){for(var e,j=H(a.range).createDocumentFragment(),o;e=a.next();){if(a.isPartiallySelectedSubtree()){e=e.cloneNode(false);o=a.getSubtreeIterator();e.appendChild(t(o));o.detach(true)}else a.remove(); +if(e.nodeType==10)throw new S("HIERARCHY_REQUEST_ERR");j.appendChild(e)}return j}function x(a,e,j){var o=!!(e&&e.length),E,T=!!j;if(o)E=RegExp("^("+e.join("|")+")$");var m=[];i(new q(a,false),function(s){if((!o||E.test(s.nodeType))&&(!T||j(s)))m.push(s)});return m}function A(a){return"["+(typeof a.getName=="undefined"?"Range":a.getName())+"("+g.inspectNode(a.startContainer)+":"+a.startOffset+", "+g.inspectNode(a.endContainer)+":"+a.endOffset+")]"}function q(a,e){this.range=a;this.clonePartiallySelectedTextNodes= +e;if(!a.collapsed){this.sc=a.startContainer;this.so=a.startOffset;this.ec=a.endContainer;this.eo=a.endOffset;var j=a.commonAncestorContainer;if(this.sc===this.ec&&g.isCharacterDataNode(this.sc)){this.isSingleCharacterDataNode=true;this._first=this._last=this._next=this.sc}else{this._first=this._next=this.sc===j&&!g.isCharacterDataNode(this.sc)?this.sc.childNodes[this.so]:g.getClosestAncestorIn(this.sc,j,true);this._last=this.ec===j&&!g.isCharacterDataNode(this.ec)?this.ec.childNodes[this.eo-1]:g.getClosestAncestorIn(this.ec, +j,true)}}}function v(a){this.code=this[a];this.codeName=a;this.message="RangeException: "+this.codeName}function c(a,e,j){this.nodes=x(a,e,j);this._next=this.nodes[0];this._position=0}function f(a){return function(e,j){for(var o,E=j?e:e.parentNode;E;){o=E.nodeType;if(g.arrayContains(a,o))return E;E=E.parentNode}return null}}function k(a,e){if(G(a,e))throw new v("INVALID_NODE_TYPE_ERR");}function r(a){if(!a.startContainer)throw new S("INVALID_STATE_ERR");}function L(a,e){if(!g.arrayContains(e,a.nodeType))throw new v("INVALID_NODE_TYPE_ERR"); +}function p(a,e){if(e<0||e>(g.isCharacterDataNode(a)?a.length:a.childNodes.length))throw new S("INDEX_SIZE_ERR");}function u(a,e){if(h(a,true)!==h(e,true))throw new S("WRONG_DOCUMENT_ERR");}function w(a){if(D(a,true))throw new S("NO_MODIFICATION_ALLOWED_ERR");}function B(a,e){if(!a)throw new S(e);}function V(a){return!!a.startContainer&&!!a.endContainer&&!(!g.arrayContains(ba,a.startContainer.nodeType)&&!h(a.startContainer,true))&&!(!g.arrayContains(ba,a.endContainer.nodeType)&&!h(a.endContainer, +true))&&a.startOffset<=(g.isCharacterDataNode(a.startContainer)?a.startContainer.length:a.startContainer.childNodes.length)&&a.endOffset<=(g.isCharacterDataNode(a.endContainer)?a.endContainer.length:a.endContainer.childNodes.length)}function J(a){r(a);if(!V(a))throw Error("Range error: Range is no longer valid after DOM mutation ("+a.inspect()+")");}function ca(){}function Y(a){a.START_TO_START=ia;a.START_TO_END=la;a.END_TO_END=ra;a.END_TO_START=ma;a.NODE_BEFORE=na;a.NODE_AFTER=oa;a.NODE_BEFORE_AND_AFTER= +pa;a.NODE_INSIDE=ja}function W(a){Y(a);Y(a.prototype)}function da(a,e){return function(){J(this);var j=this.startContainer,o=this.startOffset,E=this.commonAncestorContainer,T=new q(this,true);if(j!==E){j=g.getClosestAncestorIn(j,E,true);o=C(j);j=o.node;o=o.offset}i(T,w);T.reset();E=a(T);T.detach();e(this,j,o,j,o);return E}}function fa(a,e,j){function o(m,s){return function(y){r(this);L(y,$);L(d(y),ba);y=(m?z:C)(y);(s?E:T)(this,y.node,y.offset)}}function E(m,s,y){var F=m.endContainer,Q=m.endOffset; +if(s!==m.startContainer||y!==m.startOffset){if(d(s)!=d(F)||g.comparePoints(s,y,F,Q)==1){F=s;Q=y}e(m,s,y,F,Q)}}function T(m,s,y){var F=m.startContainer,Q=m.startOffset;if(s!==m.endContainer||y!==m.endOffset){if(d(s)!=d(F)||g.comparePoints(s,y,F,Q)==-1){F=s;Q=y}e(m,F,Q,s,y)}}a.prototype=new ca;l.util.extend(a.prototype,{setStart:function(m,s){r(this);k(m,true);p(m,s);E(this,m,s)},setEnd:function(m,s){r(this);k(m,true);p(m,s);T(this,m,s)},setStartBefore:o(true,true),setStartAfter:o(false,true),setEndBefore:o(true, +false),setEndAfter:o(false,false),collapse:function(m){J(this);m?e(this,this.startContainer,this.startOffset,this.startContainer,this.startOffset):e(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(m){r(this);k(m,true);e(this,m,0,m,g.getNodeLength(m))},selectNode:function(m){r(this);k(m,false);L(m,$);var s=z(m);m=C(m);e(this,s.node,s.offset,m.node,m.offset)},extractContents:da(t,e),deleteContents:da(n,e),canSurroundContents:function(){J(this);w(this.startContainer); +w(this.endContainer);var m=new q(this,true),s=m._first&&K(m._first,this)||m._last&&K(m._last,this);m.detach();return!s},detach:function(){j(this)},splitBoundaries:function(){J(this);var m=this.startContainer,s=this.startOffset,y=this.endContainer,F=this.endOffset,Q=m===y;g.isCharacterDataNode(y)&&F>0&&F0&&s=g.getNodeIndex(m)&&F++;s=0}e(this,m,s,y,F)},normalizeBoundaries:function(){J(this); +var m=this.startContainer,s=this.startOffset,y=this.endContainer,F=this.endOffset,Q=function(U){var R=U.nextSibling;if(R&&R.nodeType==U.nodeType){y=U;F=U.length;U.appendData(R.data);R.parentNode.removeChild(R)}},qa=function(U){var R=U.previousSibling;if(R&&R.nodeType==U.nodeType){m=U;var sa=U.length;s=R.length;U.insertData(0,R.data);R.parentNode.removeChild(R);if(m==y){F+=s;y=m}else if(y==U.parentNode){R=g.getNodeIndex(U);if(F==R){y=U;F=sa}else F>R&&F--}}},ga=true;if(g.isCharacterDataNode(y))y.length== +F&&Q(y);else{if(F>0)(ga=y.childNodes[F-1])&&g.isCharacterDataNode(ga)&&Q(ga);ga=!this.collapsed}if(ga)if(g.isCharacterDataNode(m))s==0&&qa(m);else{if(sx";X=P.firstChild.nodeType==3}catch(ta){}l.features.htmlParsingConforms=X;var ka=["startContainer","startOffset","endContainer","endOffset", +"collapsed","commonAncestorContainer"],ia=0,la=1,ra=2,ma=3,na=0,oa=1,pa=2,ja=3;ca.prototype={attachListener:function(a,e){this._listeners[a].push(e)},compareBoundaryPoints:function(a,e){J(this);u(this.startContainer,e.startContainer);var j=a==ma||a==ia?"start":"end",o=a==la||a==ia?"start":"end";return g.comparePoints(this[j+"Container"],this[j+"Offset"],e[o+"Container"],e[o+"Offset"])},insertNode:function(a){J(this);L(a,aa);w(this.startContainer);if(g.isAncestorOf(a,this.startContainer,true))throw new S("HIERARCHY_REQUEST_ERR"); +this.setStartBefore(N(a,this.startContainer,this.startOffset))},cloneContents:function(){J(this);var a,e;if(this.collapsed)return H(this).createDocumentFragment();else{if(this.startContainer===this.endContainer&&g.isCharacterDataNode(this.startContainer)){a=this.startContainer.cloneNode(true);a.data=a.data.slice(this.startOffset,this.endOffset);e=H(this).createDocumentFragment();e.appendChild(a);return e}else{e=new q(this,true);a=O(e);e.detach()}return a}},canSurroundContents:function(){J(this);w(this.startContainer); +w(this.endContainer);var a=new q(this,true),e=a._first&&K(a._first,this)||a._last&&K(a._last,this);a.detach();return!e},surroundContents:function(a){L(a,b);if(!this.canSurroundContents())throw new v("BAD_BOUNDARYPOINTS_ERR");var e=this.extractContents();if(a.hasChildNodes())for(;a.lastChild;)a.removeChild(a.lastChild);N(a,this.startContainer,this.startOffset);a.appendChild(e);this.selectNode(a)},cloneRange:function(){J(this);for(var a=new M(H(this)),e=ka.length,j;e--;){j=ka[e];a[j]=this[j]}return a}, +toString:function(){J(this);var a=this.startContainer;if(a===this.endContainer&&g.isCharacterDataNode(a))return a.nodeType==3||a.nodeType==4?a.data.slice(this.startOffset,this.endOffset):"";else{var e=[];a=new q(this,true);i(a,function(j){if(j.nodeType==3||j.nodeType==4)e.push(j.data)});a.detach();return e.join("")}},compareNode:function(a){J(this);var e=a.parentNode,j=g.getNodeIndex(a);if(!e)throw new S("NOT_FOUND_ERR");a=this.comparePoint(e,j);e=this.comparePoint(e,j+1);return a<0?e>0?pa:na:e>0? +oa:ja},comparePoint:function(a,e){J(this);B(a,"HIERARCHY_REQUEST_ERR");u(a,this.startContainer);if(g.comparePoints(a,e,this.startContainer,this.startOffset)<0)return-1;else if(g.comparePoints(a,e,this.endContainer,this.endOffset)>0)return 1;return 0},createContextualFragment:X?function(a){var e=this.startContainer,j=g.getDocument(e);if(!e)throw new S("INVALID_STATE_ERR");var o=null;if(e.nodeType==1)o=e;else if(g.isCharacterDataNode(e))o=g.parentElement(e);o=o===null||o.nodeName=="HTML"&&g.isHtmlNamespace(g.getDocument(o).documentElement)&& +g.isHtmlNamespace(o)?j.createElement("body"):o.cloneNode(false);o.innerHTML=a;return g.fragmentFromNodeChildren(o)}:function(a){r(this);var e=H(this).createElement("body");e.innerHTML=a;return g.fragmentFromNodeChildren(e)},toHtml:function(){J(this);var a=H(this).createElement("div");a.appendChild(this.cloneContents());return a.innerHTML},intersectsNode:function(a,e){J(this);B(a,"NOT_FOUND_ERR");if(g.getDocument(a)!==H(this))return false;var j=a.parentNode,o=g.getNodeIndex(a);B(j,"NOT_FOUND_ERR"); +var E=g.comparePoints(j,o,this.endContainer,this.endOffset);j=g.comparePoints(j,o+1,this.startContainer,this.startOffset);return e?E<=0&&j>=0:E<0&&j>0},isPointInRange:function(a,e){J(this);B(a,"HIERARCHY_REQUEST_ERR");u(a,this.startContainer);return g.comparePoints(a,e,this.startContainer,this.startOffset)>=0&&g.comparePoints(a,e,this.endContainer,this.endOffset)<=0},intersectsRange:function(a,e){J(this);if(H(a)!=H(this))throw new S("WRONG_DOCUMENT_ERR");var j=g.comparePoints(this.startContainer, +this.startOffset,a.endContainer,a.endOffset),o=g.comparePoints(this.endContainer,this.endOffset,a.startContainer,a.startOffset);return e?j<=0&&o>=0:j<0&&o>0},intersection:function(a){if(this.intersectsRange(a)){var e=g.comparePoints(this.startContainer,this.startOffset,a.startContainer,a.startOffset),j=g.comparePoints(this.endContainer,this.endOffset,a.endContainer,a.endOffset),o=this.cloneRange();e==-1&&o.setStart(a.startContainer,a.startOffset);j==1&&o.setEnd(a.endContainer,a.endOffset);return o}return null}, +union:function(a){if(this.intersectsRange(a,true)){var e=this.cloneRange();g.comparePoints(a.startContainer,a.startOffset,this.startContainer,this.startOffset)==-1&&e.setStart(a.startContainer,a.startOffset);g.comparePoints(a.endContainer,a.endOffset,this.endContainer,this.endOffset)==1&&e.setEnd(a.endContainer,a.endOffset);return e}else throw new v("Ranges do not intersect");},containsNode:function(a,e){return e?this.intersectsNode(a,false):this.compareNode(a)==ja},containsNodeContents:function(a){return this.comparePoint(a, +0)>=0&&this.comparePoint(a,g.getNodeLength(a))<=0},containsRange:function(a){return this.intersection(a).equals(a)},containsNodeText:function(a){var e=this.cloneRange();e.selectNode(a);var j=e.getNodes([3]);if(j.length>0){e.setStart(j[0],0);a=j.pop();e.setEnd(a,a.length);a=this.containsRange(e);e.detach();return a}else return this.containsNodeContents(a)},createNodeIterator:function(a,e){J(this);return new c(this,a,e)},getNodes:function(a,e){J(this);return x(this,a,e)},getDocument:function(){return H(this)}, +collapseBefore:function(a){r(this);this.setEndBefore(a);this.collapse(false)},collapseAfter:function(a){r(this);this.setStartAfter(a);this.collapse(true)},getName:function(){return"DomRange"},equals:function(a){return M.rangesEqual(this,a)},isValid:function(){return V(this)},inspect:function(){return A(this)}};fa(M,ha,function(a){r(a);a.startContainer=a.startOffset=a.endContainer=a.endOffset=null;a.collapsed=a.commonAncestorContainer=null;I(a,"detach",null);a._listeners=null});l.rangePrototype=ca.prototype; +M.rangeProperties=ka;M.RangeIterator=q;M.copyComparisonConstants=W;M.createPrototypeRange=fa;M.inspect=A;M.getRangeDocument=H;M.rangesEqual=function(a,e){return a.startContainer===e.startContainer&&a.startOffset===e.startOffset&&a.endContainer===e.endContainer&&a.endOffset===e.endOffset};l.DomRange=M;l.RangeException=v}); +rangy.createModule("WrappedRange",function(l){function K(i,n,t,x){var A=i.duplicate();A.collapse(t);var q=A.parentElement();z.isAncestorOf(n,q,true)||(q=n);if(!q.canHaveHTML)return new C(q.parentNode,z.getNodeIndex(q));n=z.getDocument(q).createElement("span");var v,c=t?"StartToStart":"StartToEnd";do{q.insertBefore(n,n.previousSibling);A.moveToElementText(n)}while((v=A.compareEndPoints(c,i))>0&&n.previousSibling);c=n.nextSibling;if(v==-1&&c&&z.isCharacterDataNode(c)){A.setEndPoint(t?"EndToStart":"EndToEnd", +i);if(/[\r\n]/.test(c.data)){q=A.duplicate();t=q.text.replace(/\r\n/g,"\r").length;for(t=q.moveStart("character",t);q.compareEndPoints("StartToEnd",q)==-1;){t++;q.moveStart("character",1)}}else t=A.text.length;q=new C(c,t)}else{c=(x||!t)&&n.previousSibling;q=(t=(x||t)&&n.nextSibling)&&z.isCharacterDataNode(t)?new C(t,0):c&&z.isCharacterDataNode(c)?new C(c,c.length):new C(q,z.getNodeIndex(n))}n.parentNode.removeChild(n);return q}function H(i,n){var t,x,A=i.offset,q=z.getDocument(i.node),v=q.body.createTextRange(), +c=z.isCharacterDataNode(i.node);if(c){t=i.node;x=t.parentNode}else{t=i.node.childNodes;t=A12");d.close();var h=c.getIframeWindow(b).getSelection(),D=d.documentElement.lastChild.firstChild;d=d.createRange();d.setStart(D,1);d.collapse(true);h.addRange(d);ha=h.rangeCount==1;h.removeAllRanges();var G=d.cloneRange();d.setStart(D,0);G.setEnd(D,2);h.addRange(d);h.addRange(G);ea=h.rangeCount==2;d.detach();G.detach();Y.removeChild(b)}();l.features.selectionSupportsMultipleRanges=ea; +l.features.collapsedNonEditableSelectionsSupported=ha;var M=false,g;if(Y&&f.isHostMethod(Y,"createControlRange")){g=Y.createControlRange();if(f.areHostProperties(g,["item","add"]))M=true}l.features.implementsControlRange=M;w=W?function(b){return b.anchorNode===b.focusNode&&b.anchorOffset===b.focusOffset}:function(b){return b.rangeCount?b.getRangeAt(b.rangeCount-1).collapsed:false};var Z;if(f.isHostMethod(B,"getRangeAt"))Z=function(b,d){try{return b.getRangeAt(d)}catch(h){return null}};else if(W)Z= +function(b){var d=c.getDocument(b.anchorNode);d=l.createRange(d);d.setStart(b.anchorNode,b.anchorOffset);d.setEnd(b.focusNode,b.focusOffset);if(d.collapsed!==this.isCollapsed){d.setStart(b.focusNode,b.focusOffset);d.setEnd(b.anchorNode,b.anchorOffset)}return d};l.getSelection=function(b){b=b||window;var d=b._rangySelection,h=u(b),D=V?I(b):null;if(d){d.nativeSelection=h;d.docSelection=D;d.refresh(b)}else{d=new x(h,D,b);b._rangySelection=d}return d};l.getIframeSelection=function(b){return l.getSelection(c.getIframeWindow(b))}; +g=x.prototype;if(!J&&W&&f.areHostMethods(B,["removeAllRanges","addRange"])){g.removeAllRanges=function(){this.nativeSelection.removeAllRanges();C(this)};var S=function(b,d){var h=k.getRangeDocument(d);h=l.createRange(h);h.collapseToPoint(d.endContainer,d.endOffset);b.nativeSelection.addRange(N(h));b.nativeSelection.extend(d.startContainer,d.startOffset);b.refresh()};g.addRange=fa?function(b,d){if(M&&V&&this.docSelection.type=="Control")t(this,b);else if(d&&da)S(this,b);else{var h;if(ea)h=this.rangeCount; +else{this.removeAllRanges();h=0}this.nativeSelection.addRange(N(b));this.rangeCount=this.nativeSelection.rangeCount;if(this.rangeCount==h+1){if(l.config.checkSelectionRanges)if((h=Z(this.nativeSelection,this.rangeCount-1))&&!k.rangesEqual(h,b))b=new r(h);this._ranges[this.rangeCount-1]=b;z(this,b,aa(this.nativeSelection));this.isCollapsed=w(this)}else this.refresh()}}:function(b,d){if(d&&da)S(this,b);else{this.nativeSelection.addRange(N(b));this.refresh()}};g.setRanges=function(b){if(M&&b.length> +1)A(this,b);else{this.removeAllRanges();for(var d=0,h=b.length;d1)A(this,b);else d&&this.addRange(b[0])}}else{K.fail("No means of selecting a Range or TextRange was found");return false}g.getRangeAt=function(b){if(b<0||b>=this.rangeCount)throw new L("INDEX_SIZE_ERR");else return this._ranges[b]}; +var $;if(J)$=function(b){var d;if(l.isSelectionValid(b.win))d=b.docSelection.createRange();else{d=c.getBody(b.win.document).createTextRange();d.collapse(true)}if(b.docSelection.type=="Control")n(b);else d&&typeof d.text!="undefined"?i(b,d):C(b)};else if(f.isHostMethod(B,"getRangeAt")&&typeof B.rangeCount=="number")$=function(b){if(M&&V&&b.docSelection.type=="Control")n(b);else{b._ranges.length=b.rangeCount=b.nativeSelection.rangeCount;if(b.rangeCount){for(var d=0,h=b.rangeCount;d 0 && offset < node.childNodes.length; + } + + function splitNodeAt(node, descendantNode, descendantOffset, rangesToPreserve) { + var newNode; + var splitAtStart = (descendantOffset == 0); + + if (dom.isAncestorOf(descendantNode, node)) { + + return node; + } + + if (dom.isCharacterDataNode(descendantNode)) { + if (descendantOffset == 0) { + descendantOffset = dom.getNodeIndex(descendantNode); + descendantNode = descendantNode.parentNode; + } else if (descendantOffset == descendantNode.length) { + descendantOffset = dom.getNodeIndex(descendantNode) + 1; + descendantNode = descendantNode.parentNode; + } else { + throw module.createError("splitNodeAt should not be called with offset in the middle of a data node (" + + descendantOffset + " in " + descendantNode.data); + } + } + + if (isSplitPoint(descendantNode, descendantOffset)) { + if (!newNode) { + newNode = descendantNode.cloneNode(false); + if (newNode.id) { + newNode.removeAttribute("id"); + } + var child; + while ((child = descendantNode.childNodes[descendantOffset])) { + newNode.appendChild(child); + } + dom.insertAfter(newNode, descendantNode); + } + return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, dom.getNodeIndex(newNode), rangesToPreserve); + } else if (node != descendantNode) { + newNode = descendantNode.parentNode; + + // Work out a new split point in the parent node + var newNodeIndex = dom.getNodeIndex(descendantNode); + + if (!splitAtStart) { + newNodeIndex++; + } + return splitNodeAt(node, newNode, newNodeIndex, rangesToPreserve); + } + return node; + } + + function areElementsMergeable(el1, el2) { + return el1.tagName == el2.tagName && haveSameClasses(el1, el2) && elementsHaveSameNonClassAttributes(el1, el2); + } + + function createAdjacentMergeableTextNodeGetter(forward) { + var propName = forward ? "nextSibling" : "previousSibling"; + + return function(textNode, checkParentElement) { + var el = textNode.parentNode; + var adjacentNode = textNode[propName]; + if (adjacentNode) { + // Can merge if the node's previous/next sibling is a text node + if (adjacentNode && adjacentNode.nodeType == 3) { + return adjacentNode; + } + } else if (checkParentElement) { + // Compare text node parent element with its sibling + adjacentNode = el[propName]; + + if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) { + return adjacentNode[forward ? "firstChild" : "lastChild"]; + } + } + return null; + } + } + + var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false), + getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true); + + + function Merge(firstNode) { + this.isElementMerge = (firstNode.nodeType == 1); + this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; + this.textNodes = [this.firstTextNode]; + } + + Merge.prototype = { + doMerge: function() { + var textBits = [], textNode, parent, text; + for (var i = 0, len = this.textNodes.length; i < len; ++i) { + textNode = this.textNodes[i]; + parent = textNode.parentNode; + textBits[i] = textNode.data; + if (i) { + parent.removeChild(textNode); + if (!parent.hasChildNodes()) { + parent.parentNode.removeChild(parent); + } + } + } + this.firstTextNode.data = text = textBits.join(""); + return text; + }, + + getLength: function() { + var i = this.textNodes.length, len = 0; + while (i--) { + len += this.textNodes[i].length; + } + return len; + }, + + toString: function() { + var textBits = []; + for (var i = 0, len = this.textNodes.length; i < len; ++i) { + textBits[i] = "'" + this.textNodes[i].data + "'"; + } + return "[Merge(" + textBits.join(",") + ")]"; + } + }; + + var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly"]; + + // Allow "class" as a property name in object properties + var mappedPropertyNames = {"class" : "className"}; + + function CssClassApplier(cssClass, options, tagNames) { + this.cssClass = cssClass; + var normalize, i, len, propName; + + var elementPropertiesFromOptions = null; + + // Initialize from options object + if (typeof options == "object" && options !== null) { + tagNames = options.tagNames; + elementPropertiesFromOptions = options.elementProperties; + + for (i = 0; propName = optionProperties[i++]; ) { + if (options.hasOwnProperty(propName)) { + this[propName] = options[propName]; + } + } + normalize = options.normalize; + } else { + normalize = options; + } + + // Backwards compatibility: the second parameter can also be a Boolean indicating whether normalization + this.normalize = (typeof normalize == "undefined") ? true : normalize; + + // Initialize element properties and attribute exceptions + this.attrExceptions = []; + var el = document.createElement(this.elementTagName); + this.elementProperties = {}; + for (var p in elementPropertiesFromOptions) { + if (elementPropertiesFromOptions.hasOwnProperty(p)) { + // Map "class" to "className" + if (mappedPropertyNames.hasOwnProperty(p)) { + p = mappedPropertyNames[p]; + } + el[p] = elementPropertiesFromOptions[p]; + + // Copy the property back from the dummy element so that later comparisons to check whether elements + // may be removed are checking against the right value. For example, the href property of an element + // returns a fully qualified URL even if it was previously assigned a relative URL. + this.elementProperties[p] = el[p]; + this.attrExceptions.push(p); + } + } + + this.elementSortedClassName = this.elementProperties.hasOwnProperty("className") ? + sortClassName(this.elementProperties.className + " " + cssClass) : cssClass; + + // Initialize tag names + this.applyToAnyTagName = false; + var type = typeof tagNames; + if (type == "string") { + if (tagNames == "*") { + this.applyToAnyTagName = true; + } else { + this.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/); + } + } else if (type == "object" && typeof tagNames.length == "number") { + this.tagNames = []; + for (i = 0, len = tagNames.length; i < len; ++i) { + if (tagNames[i] == "*") { + this.applyToAnyTagName = true; + } else { + this.tagNames.push(tagNames[i].toLowerCase()); + } + } + } else { + this.tagNames = [this.elementTagName]; + } + } + + CssClassApplier.prototype = { + elementTagName: defaultTagName, + elementProperties: {}, + ignoreWhiteSpace: true, + applyToEditableOnly: false, + + hasClass: function(node) { + return node.nodeType == 1 && dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && hasClass(node, this.cssClass); + }, + + getSelfOrAncestorWithClass: function(node) { + while (node) { + if (this.hasClass(node, this.cssClass)) { + return node; + } + node = node.parentNode; + } + return null; + }, + + isModifiable: function(node) { + return !this.applyToEditableOnly || isEditable(node); + }, + + // White space adjacent to an unwrappable node can be ignored for wrapping + isIgnorableWhiteSpaceNode: function(node) { + return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node); + }, + + // Normalizes nodes after applying a CSS class to a Range. + postApply: function(textNodes, range, isUndo) { + + var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; + + var merges = [], currentMerge; + + var rangeStartNode = firstNode, rangeEndNode = lastNode; + var rangeStartOffset = 0, rangeEndOffset = lastNode.length; + + var textNode, precedingTextNode; + + for (var i = 0, len = textNodes.length; i < len; ++i) { + textNode = textNodes[i]; + precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo); + + if (precedingTextNode) { + if (!currentMerge) { + currentMerge = new Merge(precedingTextNode); + merges.push(currentMerge); + } + currentMerge.textNodes.push(textNode); + if (textNode === firstNode) { + rangeStartNode = currentMerge.firstTextNode; + rangeStartOffset = rangeStartNode.length; + } + if (textNode === lastNode) { + rangeEndNode = currentMerge.firstTextNode; + rangeEndOffset = currentMerge.getLength(); + } + } else { + currentMerge = null; + } + } + + // Test whether the first node after the range needs merging + var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo); + + if (nextTextNode) { + if (!currentMerge) { + currentMerge = new Merge(lastNode); + merges.push(currentMerge); + } + currentMerge.textNodes.push(nextTextNode); + } + + // Do the merges + if (merges.length) { + + for (i = 0, len = merges.length; i < len; ++i) { + merges[i].doMerge(); + } + + + // Set the range boundaries + range.setStart(rangeStartNode, rangeStartOffset); + range.setEnd(rangeEndNode, rangeEndOffset); + } + + }, + + createContainer: function(doc) { + var el = doc.createElement(this.elementTagName); + api.util.extend(el, this.elementProperties); + addClass(el, this.cssClass); + return el; + }, + + applyToTextNode: function(textNode) { + + + var parent = textNode.parentNode; + if (parent.childNodes.length == 1 && dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) { + addClass(parent, this.cssClass); + } else { + var el = this.createContainer(dom.getDocument(textNode)); + textNode.parentNode.insertBefore(el, textNode); + el.appendChild(textNode); + } + + }, + + isRemovable: function(el) { + return el.tagName.toLowerCase() == this.elementTagName + && getSortedClassName(el) == this.elementSortedClassName + && elementHasProps(el, this.elementProperties) + && !elementHasNonClassAttributes(el, this.attrExceptions) + && this.isModifiable(el); + }, + + undoToTextNode: function(textNode, range, ancestorWithClass) { + + if (!range.containsNode(ancestorWithClass)) { + // Split out the portion of the ancestor from which we can remove the CSS class + //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass); + var ancestorRange = range.cloneRange(); + ancestorRange.selectNode(ancestorWithClass); + + if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)/* && isSplitPoint(range.endContainer, range.endOffset)*/) { + splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, [range]); + range.setEndAfter(ancestorWithClass); + } + if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)/* && isSplitPoint(range.startContainer, range.startOffset)*/) { + ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, [range]); + } + } + + if (this.isRemovable(ancestorWithClass)) { + replaceWithOwnChildren(ancestorWithClass); + } else { + removeClass(ancestorWithClass, this.cssClass); + } + }, + + applyToRange: function(range) { + range.splitBoundaries(); + var textNodes = getEffectiveTextNodes(range); + + if (textNodes.length) { + var textNode; + + for (var i = 0, len = textNodes.length; i < len; ++i) { + textNode = textNodes[i]; + + if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode) + && this.isModifiable(textNode)) { + this.applyToTextNode(textNode); + } + } + range.setStart(textNodes[0], 0); + textNode = textNodes[textNodes.length - 1]; + range.setEnd(textNode, textNode.length); + if (this.normalize) { + this.postApply(textNodes, range, false); + } + } + }, + + applyToSelection: function(win) { + + win = win || window; + var sel = api.getSelection(win); + + var range, ranges = sel.getAllRanges(); + sel.removeAllRanges(); + var i = ranges.length; + while (i--) { + range = ranges[i]; + this.applyToRange(range); + sel.addRange(range); + } + + }, + + undoToRange: function(range) { + + range.splitBoundaries(); + var textNodes = getEffectiveTextNodes(range); + var textNode, ancestorWithClass; + var lastTextNode = textNodes[textNodes.length - 1]; + + if (textNodes.length) { + for (var i = 0, len = textNodes.length; i < len; ++i) { + textNode = textNodes[i]; + ancestorWithClass = this.getSelfOrAncestorWithClass(textNode); + if (ancestorWithClass && this.isModifiable(textNode)) { + this.undoToTextNode(textNode, range, ancestorWithClass); + } + + // Ensure the range is still valid + range.setStart(textNodes[0], 0); + range.setEnd(lastTextNode, lastTextNode.length); + } + + + + if (this.normalize) { + this.postApply(textNodes, range, true); + } + } + }, + + undoToSelection: function(win) { + win = win || window; + var sel = api.getSelection(win); + var ranges = sel.getAllRanges(), range; + sel.removeAllRanges(); + for (var i = 0, len = ranges.length; i < len; ++i) { + range = ranges[i]; + this.undoToRange(range); + sel.addRange(range); + } + }, + + getTextSelectedByRange: function(textNode, range) { + var textRange = range.cloneRange(); + textRange.selectNodeContents(textNode); + + var intersectionRange = textRange.intersection(range); + var text = intersectionRange ? intersectionRange.toString() : ""; + textRange.detach(); + + return text; + }, + + isAppliedToRange: function(range) { + if (range.collapsed) { + return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer); + } else { + var textNodes = range.getNodes( [3] ); + for (var i = 0, textNode; textNode = textNodes[i++]; ) { + if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) + && this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) { + return false; + } + } + return true; + } + }, + + isAppliedToSelection: function(win) { + win = win || window; + var sel = api.getSelection(win); + var ranges = sel.getAllRanges(); + var i = ranges.length; + while (i--) { + if (!this.isAppliedToRange(ranges[i])) { + return false; + } + } + + return true; + }, + + toggleRange: function(range) { + if (this.isAppliedToRange(range)) { + this.undoToRange(range); + } else { + this.applyToRange(range); + } + }, + + toggleSelection: function(win) { + if (this.isAppliedToSelection(win)) { + this.undoToSelection(win); + } else { + this.applyToSelection(win); + } + }, + + detach: function() {} + }; + + function createCssClassApplier(cssClass, options, tagNames) { + return new CssClassApplier(cssClass, options, tagNames); + } + + CssClassApplier.util = { + hasClass: hasClass, + addClass: addClass, + removeClass: removeClass, + hasSameClasses: haveSameClasses, + replaceWithOwnChildren: replaceWithOwnChildren, + elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes, + elementHasNonClassAttributes: elementHasNonClassAttributes, + splitNodeAt: splitNodeAt, + isEditableElement: isEditableElement, + isEditingHost: isEditingHost, + isEditable: isEditable + }; + + api.CssClassApplier = CssClassApplier; + api.createCssClassApplier = createCssClassApplier; +}); diff --git a/rangy-cssclassapplier.min.js b/rangy-cssclassapplier.min.js new file mode 100644 index 0000000..60064a3 --- /dev/null +++ b/rangy-cssclassapplier.min.js @@ -0,0 +1,32 @@ +/* + CSS Class Applier module for Rangy. + Adds, removes and toggles CSS classes on Ranges and Selections + + Part of Rangy, a cross-browser JavaScript range and selection library + http://code.google.com/p/rangy/ + + Depends on Rangy core. + + Copyright 2012, Tim Down + Licensed under the MIT license. + Version: 1.2.3 + Build date: 26 February 2012 +*/ +rangy.createModule("CssClassApplier",function(i,v){function r(a,b){return a.className&&RegExp("(?:^|\\s)"+b+"(?:\\s|$)").test(a.className)}function s(a,b){if(a.className)r(a,b)||(a.className+=" "+b);else a.className=b}function o(a){return a.split(/\s+/).sort().join(" ")}function w(a,b){return o(a.className)==o(b.className)}function x(a){for(var b=a.parentNode;a.hasChildNodes();)b.insertBefore(a.firstChild,a);b.removeChild(a)}function y(a,b){var c=a.cloneRange();c.selectNodeContents(b);var d=c.intersection(a); +d=d?d.toString():"";c.detach();return d!=""}function z(a){return a.getNodes([3],function(b){return y(a,b)})}function A(a,b){if(a.attributes.length!=b.attributes.length)return false;for(var c=0,d=a.attributes.length,e,f;c0&&j charRange.start; - }, - - isContiguousWith: function(charRange) { - return this.start == charRange.end || this.end == charRange.start; - }, - - union: function(charRange) { - return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end)); - }, - - intersection: function(charRange) { - return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end)); - }, - - getComplements: function(charRange) { - var ranges = []; - if (this.start >= charRange.start) { - if (this.end <= charRange.end) { - return []; - } - ranges.push(new CharacterRange(charRange.end, this.end)); - } else { - ranges.push(new CharacterRange(this.start, Math.min(this.end, charRange.start))); - if (this.end > charRange.end) { - ranges.push(new CharacterRange(charRange.end, this.end)); - } - } - return ranges; - }, - - toString: function() { - return "[CharacterRange(" + this.start + ", " + this.end + ")]"; - } - }; - - CharacterRange.fromCharacterRange = function(charRange) { - return new CharacterRange(charRange.start, charRange.end); - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - var textContentConverter = { - rangeToCharacterRange: function(range, containerNode) { - var bookmark = range.getBookmark(containerNode); - return new CharacterRange(bookmark.start, bookmark.end); - }, - - characterRangeToRange: function(doc, characterRange, containerNode) { - var range = api.createRange(doc); - range.moveToBookmark({ - start: characterRange.start, - end: characterRange.end, - containerNode: containerNode - }); - - return range; - }, - - serializeSelection: function(selection, containerNode) { - var ranges = selection.getAllRanges(), rangeCount = ranges.length; - var rangeInfos = []; - - var backward = rangeCount == 1 && selection.isBackward(); - - for (var i = 0, len = ranges.length; i < len; ++i) { - rangeInfos[i] = { - characterRange: this.rangeToCharacterRange(ranges[i], containerNode), - backward: backward - }; - } - - return rangeInfos; - }, - - restoreSelection: function(selection, savedSelection, containerNode) { - selection.removeAllRanges(); - var doc = selection.win.document; - for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) { - rangeInfo = savedSelection[i]; - characterRange = rangeInfo.characterRange; - range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode); - selection.addRange(range, rangeInfo.backward); - } - } - }; - - registerHighlighterType("textContent", function() { - return textContentConverter; - }); - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Lazily load the TextRange-based converter so that the dependency is only checked when required. - registerHighlighterType("TextRange", (function() { - var converter; - - return function() { - if (!converter) { - // Test that textRangeModule exists and is supported - var textRangeModule = api.modules.TextRange; - if (!textRangeModule) { - throw new Error("TextRange module is missing."); - } else if (!textRangeModule.supported) { - throw new Error("TextRange module is present but not supported."); - } - - converter = { - rangeToCharacterRange: function(range, containerNode) { - return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) ); - }, - - characterRangeToRange: function(doc, characterRange, containerNode) { - var range = api.createRange(doc); - range.selectCharacters(containerNode, characterRange.start, characterRange.end); - return range; - }, - - serializeSelection: function(selection, containerNode) { - return selection.saveCharacterRanges(containerNode); - }, - - restoreSelection: function(selection, savedSelection, containerNode) { - selection.restoreCharacterRanges(containerNode, savedSelection); - } - }; - } - - return converter; - }; - })()); - - /*----------------------------------------------------------------------------------------------------------------*/ - - function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) { - if (id) { - this.id = id; - nextHighlightId = Math.max(nextHighlightId, id + 1); - } else { - this.id = nextHighlightId++; - } - this.characterRange = characterRange; - this.doc = doc; - this.classApplier = classApplier; - this.converter = converter; - this.containerElementId = containerElementId || null; - this.applied = false; - } - - Highlight.prototype = { - getContainerElement: function() { - return this.containerElementId ? this.doc.getElementById(this.containerElementId) : getBody(this.doc); - }, - - getRange: function() { - return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement()); - }, - - fromRange: function(range) { - this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement()); - }, - - getText: function() { - return this.getRange().toString(); - }, - - containsElement: function(el) { - return this.getRange().containsNodeContents(el.firstChild); - }, - - unapply: function() { - this.classApplier.undoToRange(this.getRange()); - this.applied = false; - }, - - apply: function() { - this.classApplier.applyToRange(this.getRange()); - this.applied = true; - }, - - getHighlightElements: function() { - return this.classApplier.getElementsWithClassIntersectingRange(this.getRange()); - }, - - toString: function() { - return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.className + ", character range: " + - this.characterRange.start + " - " + this.characterRange.end + ")]"; - } - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - function Highlighter(doc, type) { - type = type || "textContent"; - this.doc = doc || document; - this.classAppliers = {}; - this.highlights = []; - this.converter = getConverter(type); - } - - Highlighter.prototype = { - addClassApplier: function(classApplier) { - this.classAppliers[classApplier.className] = classApplier; - }, - - getHighlightForElement: function(el) { - var highlights = this.highlights; - for (var i = 0, len = highlights.length; i < len; ++i) { - if (highlights[i].containsElement(el)) { - return highlights[i]; - } - } - return null; - }, - - removeHighlights: function(highlights) { - for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) { - highlight = this.highlights[i]; - if (contains(highlights, highlight)) { - highlight.unapply(); - this.highlights.splice(i--, 1); - } - } - }, - - removeAllHighlights: function() { - this.removeHighlights(this.highlights); - }, - - getIntersectingHighlights: function(ranges) { - // Test each range against each of the highlighted ranges to see whether they overlap - var intersectingHighlights = [], highlights = this.highlights, converter = this.converter; - forEach(ranges, function(range) { - //var selCharRange = converter.rangeToCharacterRange(range); - forEach(highlights, function(highlight) { - if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) { - intersectingHighlights.push(highlight); - } - }); - }); - - return intersectingHighlights; - }, - - highlightCharacterRanges: function(className, charRanges, options) { - var i, len, j; - var highlights = this.highlights; - var converter = this.converter; - var doc = this.doc; - var highlightsToRemove = []; - var classApplier = className ? this.classAppliers[className] : null; - - options = createOptions(options, { - containerElementId: null, - exclusive: true - }); - - var containerElementId = options.containerElementId; - var exclusive = options.exclusive; - - var containerElement, containerElementRange, containerElementCharRange; - if (containerElementId) { - containerElement = this.doc.getElementById(containerElementId); - if (containerElement) { - containerElementRange = api.createRange(this.doc); - containerElementRange.selectNodeContents(containerElement); - containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length); - } - } - - var charRange, highlightCharRange, removeHighlight, isSameClassApplier, highlightsToKeep, splitHighlight; - - for (i = 0, len = charRanges.length; i < len; ++i) { - charRange = charRanges[i]; - highlightsToKeep = []; - - // Restrict character range to container element, if it exists - if (containerElementCharRange) { - charRange = charRange.intersection(containerElementCharRange); - } - - // Ignore empty ranges - if (charRange.start == charRange.end) { - continue; - } - - // Check for intersection with existing highlights. For each intersection, create a new highlight - // which is the union of the highlight range and the selected range - for (j = 0; j < highlights.length; ++j) { - removeHighlight = false; - - if (containerElementId == highlights[j].containerElementId) { - highlightCharRange = highlights[j].characterRange; - isSameClassApplier = (classApplier == highlights[j].classApplier); - splitHighlight = !isSameClassApplier && exclusive; - - // Replace the existing highlight if it needs to be: - // 1. merged (isSameClassApplier) - // 2. partially or entirely erased (className === null) - // 3. partially or entirely replaced (isSameClassApplier == false && exclusive == true) - if ( (highlightCharRange.intersects(charRange) || highlightCharRange.isContiguousWith(charRange)) && - (isSameClassApplier || splitHighlight) ) { - - // Remove existing highlights, keeping the unselected parts - if (splitHighlight) { - forEach(highlightCharRange.getComplements(charRange), function(rangeToAdd) { - highlightsToKeep.push( new Highlight(doc, rangeToAdd, highlights[j].classApplier, converter, null, containerElementId) ); - }); - } - - removeHighlight = true; - if (isSameClassApplier) { - charRange = highlightCharRange.union(charRange); - } - } - } - - if (removeHighlight) { - highlightsToRemove.push(highlights[j]); - highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId); - } else { - highlightsToKeep.push(highlights[j]); - } - } - - // Add new range (only if cssApplier is not false) - if (classApplier) { - highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId)); - } - this.highlights = highlights = highlightsToKeep; - } - - // Remove the old highlights - forEach(highlightsToRemove, function(highlightToRemove) { - highlightToRemove.unapply(); - }); - - // Apply new highlights - var newHighlights = []; - forEach(highlights, function(highlight) { - if (!highlight.applied) { - highlight.apply(); - newHighlights.push(highlight); - } - }); - - return newHighlights; - }, - - highlightRanges: function(className, ranges, options) { - var selCharRanges = []; - var converter = this.converter; - - options = createOptions(options, { - containerElement: null, - exclusive: true - }); - - var containerElement = options.containerElement; - var containerElementId = containerElement ? containerElement.id : null; - var containerElementRange; - if (containerElement) { - containerElementRange = api.createRange(containerElement); - containerElementRange.selectNodeContents(containerElement); - } - - forEach(ranges, function(range) { - var scopedRange = containerElement ? containerElementRange.intersection(range) : range; - selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) ); - }); - - return this.highlightCharacterRanges(className, selCharRanges, { - containerElementId: containerElementId, - exclusive: options.exclusive - }); - }, - - highlightSelection: function(className, options) { - var converter = this.converter; - var classApplier = className ? this.classAppliers[className] : false; - - options = createOptions(options, { - containerElementId: null, - selection: api.getSelection(), - exclusive: true - }); - - var containerElementId = options.containerElementId; - var exclusive = options.exclusive; - var selection = options.selection; - var doc = selection.win.document; - var containerElement = containerElementId ? doc.getElementById(containerElementId) : getBody(doc); - - if (!classApplier && className !== false) { - throw new Error("No class applier found for class '" + className + "'"); - } - - // Store the existing selection as character ranges - var serializedSelection = converter.serializeSelection(selection, containerElement); - - // Create an array of selected character ranges - var selCharRanges = []; - forEach(serializedSelection, function(rangeInfo) { - selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) ); - }); - - var newHighlights = this.highlightCharacterRanges(className, selCharRanges, { - containerElementId: containerElementId, - exclusive: exclusive - }); - - // Restore selection - converter.restoreSelection(selection, serializedSelection, containerElement); - - return newHighlights; - }, - - unhighlightSelection: function(selection) { - selection = selection || api.getSelection(); - var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() ); - this.removeHighlights(intersectingHighlights); - selection.removeAllRanges(); - return intersectingHighlights; - }, - - getHighlightsInSelection: function(selection) { - selection = selection || api.getSelection(); - return this.getIntersectingHighlights(selection.getAllRanges()); - }, - - selectionOverlapsHighlight: function(selection) { - return this.getHighlightsInSelection(selection).length > 0; - }, - - serialize: function(options) { - var highlights = this.highlights; - highlights.sort(compareHighlights); - var serializedHighlights = ["type:" + this.converter.type]; - options = createOptions(options, { - serializeHighlightText: false - }); - - forEach(highlights, function(highlight) { - var characterRange = highlight.characterRange; - var parts = [ - characterRange.start, - characterRange.end, - highlight.id, - highlight.classApplier.className, - highlight.containerElementId - ]; - if (options.serializeHighlightText) { - parts.push(highlight.getText()); - } - serializedHighlights.push( parts.join("$") ); - }); - - return serializedHighlights.join("|"); - }, - - deserialize: function(serialized) { - var serializedHighlights = serialized.split("|"); - var highlights = []; - - var firstHighlight = serializedHighlights[0]; - var regexResult; - var serializationType, serializationConverter, convertType = false; - if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) { - serializationType = regexResult[1]; - if (serializationType != this.converter.type) { - serializationConverter = getConverter(serializationType); - convertType = true; - } - serializedHighlights.shift(); - } else { - throw new Error("Serialized highlights are invalid."); - } - - var classApplier, highlight, characterRange, containerElementId, containerElement; - - for (var i = serializedHighlights.length, parts; i-- > 0; ) { - parts = serializedHighlights[i].split("$"); - characterRange = new CharacterRange(+parts[0], +parts[1]); - containerElementId = parts[4] || null; - containerElement = containerElementId ? this.doc.getElementById(containerElementId) : getBody(this.doc); - - // Convert to the current Highlighter's type, if different from the serialization type - if (convertType) { - characterRange = this.converter.rangeToCharacterRange( - serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement), - containerElement - ); - } - - classApplier = this.classAppliers[ parts[3] ]; - - if (!classApplier) { - throw new Error("No class applier found for class '" + parts[3] + "'"); - } - - highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId); - highlight.apply(); - highlights.push(highlight); - } - this.highlights = highlights; - } - }; - - api.Highlighter = Highlighter; - - api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) { - return new Highlighter(doc, rangeCharacterOffsetConverterType); - }; - }); - -}, this); diff --git a/rangy-highlighter.min.js b/rangy-highlighter.min.js deleted file mode 100644 index 9e097c7..0000000 --- a/rangy-highlighter.min.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Highlighter module for Rangy, a cross-browser JavaScript range and selection library - * https://github.com/timdown/rangy - * - * Depends on Rangy core, ClassApplier and optionally TextRange modules. - * - * Copyright 2015, Tim Down - * Licensed under the MIT license. - * Version: 1.3.0-alpha.20150122 - * Build date: 22 January 2015 - */ -!function(e,t){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(t.rangy)}(function(e){e.createModule("Highlighter",["ClassApplier"],function(e){function t(e,t){return e.characterRange.start-t.characterRange.start}function n(e,t){this.type=e,this.converterCreator=t}function r(e,t){f[e]=new n(e,t)}function i(e){var t=f[e];if(t instanceof n)return t.create();throw new Error("Highlighter type '"+e+"' is not valid")}function a(e,t){this.start=e,this.end=t}function s(e,t,n,r,i,a){i?(this.id=i,p=Math.max(p,i+1)):this.id=p++,this.characterRange=t,this.doc=e,this.classApplier=n,this.converter=r,this.containerElementId=a||null,this.applied=!1}function h(e,t){t=t||"textContent",this.doc=e||document,this.classAppliers={},this.highlights=[],this.converter=i(t)}var o=e.dom,c=o.arrayContains,l=o.getBody,g=e.util.createOptions,u=[].forEach?function(e,t){e.forEach(t)}:function(e,t){for(var n=0,r=e.length;r>n;++n)t(e[n])},p=1,f={};n.prototype.create=function(){var e=this.converterCreator();return e.type=this.type,e},e.registerHighlighterType=r,a.prototype={intersects:function(e){return this.starte.start},isContiguousWith:function(e){return this.start==e.end||this.end==e.start},union:function(e){return new a(Math.min(this.start,e.start),Math.max(this.end,e.end))},intersection:function(e){return new a(Math.max(this.start,e.start),Math.min(this.end,e.end))},getComplements:function(e){var t=[];if(this.start>=e.start){if(this.end<=e.end)return[];t.push(new a(e.end,this.end))}else t.push(new a(this.start,Math.min(this.end,e.start))),this.end>e.end&&t.push(new a(e.end,this.end));return t},toString:function(){return"[CharacterRange("+this.start+", "+this.end+")]"}},a.fromCharacterRange=function(e){return new a(e.start,e.end)};var d={rangeToCharacterRange:function(e,t){var n=e.getBookmark(t);return new a(n.start,n.end)},characterRangeToRange:function(t,n,r){var i=e.createRange(t);return i.moveToBookmark({start:n.start,end:n.end,containerNode:r}),i},serializeSelection:function(e,t){for(var n=e.getAllRanges(),r=n.length,i=[],a=1==r&&e.isBackward(),s=0,h=n.length;h>s;++s)i[s]={characterRange:this.rangeToCharacterRange(n[s],t),backward:a};return i},restoreSelection:function(e,t,n){e.removeAllRanges();for(var r,i,a,s=e.win.document,h=0,o=t.length;o>h;++h)i=t[h],a=i.characterRange,r=this.characterRangeToRange(s,i.characterRange,n),e.addRange(r,i.backward)}};r("textContent",function(){return d}),r("TextRange",function(){var t;return function(){if(!t){var n=e.modules.TextRange;if(!n)throw new Error("TextRange module is missing.");if(!n.supported)throw new Error("TextRange module is present but not supported.");t={rangeToCharacterRange:function(e,t){return a.fromCharacterRange(e.toCharacterRange(t))},characterRangeToRange:function(t,n,r){var i=e.createRange(t);return i.selectCharacters(r,n.start,n.end),i},serializeSelection:function(e,t){return e.saveCharacterRanges(t)},restoreSelection:function(e,t,n){e.restoreCharacterRanges(n,t)}}}return t}}()),s.prototype={getContainerElement:function(){return this.containerElementId?this.doc.getElementById(this.containerElementId):l(this.doc)},getRange:function(){return this.converter.characterRangeToRange(this.doc,this.characterRange,this.getContainerElement())},fromRange:function(e){this.characterRange=this.converter.rangeToCharacterRange(e,this.getContainerElement())},getText:function(){return this.getRange().toString()},containsElement:function(e){return this.getRange().containsNodeContents(e.firstChild)},unapply:function(){this.classApplier.undoToRange(this.getRange()),this.applied=!1},apply:function(){this.classApplier.applyToRange(this.getRange()),this.applied=!0},getHighlightElements:function(){return this.classApplier.getElementsWithClassIntersectingRange(this.getRange())},toString:function(){return"[Highlight(ID: "+this.id+", class: "+this.classApplier.className+", character range: "+this.characterRange.start+" - "+this.characterRange.end+")]"}},h.prototype={addClassApplier:function(e){this.classAppliers[e.className]=e},getHighlightForElement:function(e){for(var t=this.highlights,n=0,r=t.length;r>n;++n)if(t[n].containsElement(e))return t[n];return null},removeHighlights:function(e){for(var t,n=0,r=this.highlights.length;r>n;++n)t=this.highlights[n],c(e,t)&&(t.unapply(),this.highlights.splice(n--,1))},removeAllHighlights:function(){this.removeHighlights(this.highlights)},getIntersectingHighlights:function(e){{var t=[],n=this.highlights;this.converter}return u(e,function(e){u(n,function(n){e.intersectsRange(n.getRange())&&!c(t,n)&&t.push(n)})}),t},highlightCharacterRanges:function(t,n,r){var i,h,o,c=this.highlights,l=this.converter,p=this.doc,f=[],d=t?this.classAppliers[t]:null;r=g(r,{containerElementId:null,exclusive:!0});var v,R,m,C=r.containerElementId,w=r.exclusive;C&&(v=this.doc.getElementById(C),v&&(R=e.createRange(this.doc),R.selectNodeContents(v),m=new a(0,R.toString().length)));var E,y,x,I,T,A;for(i=0,h=n.length;h>i;++i)if(E=n[i],T=[],m&&(E=E.intersection(m)),E.start!=E.end){for(o=0;o0},serialize:function(e){var n=this.highlights;n.sort(t);var r=["type:"+this.converter.type];return e=g(e,{serializeHighlightText:!1}),u(n,function(t){var n=t.characterRange,i=[n.start,n.end,t.id,t.classApplier.className,t.containerElementId];e.serializeHighlightText&&i.push(t.getText()),r.push(i.join("$"))}),r.join("|")},deserialize:function(e){var t,n,r,h=e.split("|"),o=[],c=h[0],g=!1;if(!c||!(t=/^type:(\w+)$/.exec(c)))throw new Error("Serialized highlights are invalid.");n=t[1],n!=this.converter.type&&(r=i(n),g=!0),h.shift();for(var u,p,f,d,v,R,m=h.length;m-->0;){if(R=h[m].split("$"),f=new a(+R[0],+R[1]),d=R[4]||null,v=d?this.doc.getElementById(d):l(this.doc),g&&(f=this.converter.rangeToCharacterRange(r.characterRangeToRange(this.doc,f,v),v)),u=this.classAppliers[R[3]],!u)throw new Error("No class applier found for class '"+R[3]+"'");p=new s(this.doc,f,u,this.converter,parseInt(R[2]),d),p.apply(),o.push(p)}this.highlights=o}},e.Highlighter=h,e.createHighlighter=function(e,t){return new h(e,t)}})},this); \ No newline at end of file diff --git a/rangy-selectionsaverestore.js b/rangy-selectionsaverestore.js index 4616fa8..768ad83 100644 --- a/rangy-selectionsaverestore.js +++ b/rangy-selectionsaverestore.js @@ -1,248 +1,195 @@ /** - * Selection save and restore module for Rangy. + * @license 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 - * https://github.com/timdown/rangy + * http://code.google.com/p/rangy/ * * Depends on Rangy core. * - * Copyright 2015, Tim Down + * Copyright 2012, Tim Down * Licensed under the MIT license. - * Version: 1.3.0-alpha.20150122 - * Build date: 22 January 2015 + * Version: 1.2.3 + * Build date: 26 February 2012 */ -(function(factory, root) { - if (typeof define == "function" && define.amd) { - // AMD. Register as an anonymous module with a dependency on Rangy. - define(["./rangy-core"], factory); - } else if (typeof module != "undefined" && typeof exports == "object") { - // Node/CommonJS style - module.exports = factory( require("rangy") ); - } else { - // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) - factory(root.rangy); - } -})(function(rangy) { - rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) { - var dom = api.dom; +rangy.createModule("SaveRestore", function(api, module) { + api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] ); - var markerTextChar = "\ufeff"; + var dom = api.dom; - function gEBI(id, doc) { - return (doc || document).getElementById(id); - } + var markerTextChar = "\ufeff"; - 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 gEBI(id, doc) { + return (doc || document).getElementById(id); + } - 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 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); + boundaryRange.detach(); + 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 compareRanges(r1, r2) { + return r2.compareBoundaryPoints(r1.START_TO_START, r1); + } + + function saveSelection(win) { + win = win || window; + var doc = win.document; + 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; } + var sel = api.getSelection(win); + var ranges = sel.getAllRanges(); + var rangeInfos = [], startEl, endEl, range; - function saveRange(range, backward) { - var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString(); + // Order the ranges by position within the DOM, latest first + ranges.sort(compareRanges); + for (var i = 0, len = ranges.length; i < len; ++i) { + range = ranges[i]; if (range.collapsed) { endEl = insertRangeBoundaryMarker(range, false); - return { - document: doc, + rangeInfos.push({ markerId: endEl.id, collapsed: true - }; + }); } else { endEl = insertRangeBoundaryMarker(range, false); startEl = insertRangeBoundaryMarker(range, true); - return { - document: doc, + rangeInfos[i] = { startMarkerId: startEl.id, endMarkerId: endEl.id, collapsed: false, - backward: backward, - toString: function() { - return "original text: '" + text + "', new text: '" + range.toString() + "'"; - } + backwards: ranges.length == 1 && sel.isBackwards() }; } } - 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."); - } + // 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]; + if (range.collapsed) { + range.collapseBefore(gEBI(rangeInfos[i].markerId, doc)); } else { - setRangeBoundary(doc, range, rangeInfo.startMarkerId, true); - setRangeBoundary(doc, range, rangeInfo.endMarkerId, false); - } - - if (normalize) { - range.normalizeBoundaries(); + range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc)); + range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc)); } - - 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); + // Ensure current selection is unaffected + sel.setRanges(ranges); + return { + win: win, + doc: doc, + rangeInfos: rangeInfos, + restored: false + }; + } - for (var i = 0, len = ranges.length; i < len; ++i) { - rangeInfos[i] = saveRange(ranges[i], backward); - } + function restoreSelection(savedSelection, preserveDirection) { + if (!savedSelection.restored) { + var rangeInfos = savedSelection.rangeInfos; + var sel = api.getSelection(savedSelection.win); + var ranges = []; - // 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)); + // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid + // normalization affecting previously restored ranges. + for (var len = rangeInfos.length, i = len - 1, rangeInfo, range; i >= 0; --i) { + rangeInfo = rangeInfos[i]; + range = api.createRange(savedSelection.doc); + if (rangeInfo.collapsed) { + var markerEl = gEBI(rangeInfo.markerId, savedSelection.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 { - range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc)); - range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc)); + setRangeBoundary(savedSelection.doc, range, rangeInfo.startMarkerId, true); + setRangeBoundary(savedSelection.doc, range, rangeInfo.endMarkerId, false); } - } - - 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; + // Normalizing range boundaries is only viable if the selection contains only one range. For example, + // if the selection contained two ranges that were both contained within the same single text node, + // both would alter the same text node when restoring and break the other range. + if (len == 1) { + range.normalizeBoundaries(); + } + ranges[i] = range; } - 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"); + if (len == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backwards) { + sel.removeAllRanges(); + sel.addRange(ranges[0], true); } 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; - } + savedSelection.restored = true; } + } - function removeMarkerElement(doc, markerId) { - var markerEl = gEBI(markerId, doc); - if (markerEl) { - markerEl.parentNode.removeChild(markerEl); - } + 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); - } + 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); \ No newline at end of file + api.saveSelection = saveSelection; + api.restoreSelection = restoreSelection; + api.removeMarkerElement = removeMarkerElement; + api.removeMarkers = removeMarkers; +}); diff --git a/rangy-selectionsaverestore.min.js b/rangy-selectionsaverestore.min.js index f32ca1c..73e3070 100644 --- a/rangy-selectionsaverestore.min.js +++ b/rangy-selectionsaverestore.min.js @@ -1,15 +1,19 @@ -/** - * 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 - * https://github.com/timdown/rangy - * - * Depends on Rangy core. - * - * Copyright 2015, Tim Down - * Licensed under the MIT license. - * Version: 1.3.0-alpha.20150122 - * Build date: 22 January 2015 - */ -!function(e,n){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(n.rangy)}(function(e){e.createModule("SaveRestore",["WrappedRange"],function(e,n){function r(e,n){return(n||document).getElementById(e)}function t(e,n){var r,t="selectionBoundary_"+ +new Date+"_"+(""+Math.random()).slice(2),a=m.getDocument(e.startContainer),o=e.cloneRange();return o.collapse(n),r=a.createElement("span"),r.id=t,r.style.lineHeight="0",r.style.display="none",r.className="rangySelectionBoundary",r.appendChild(a.createTextNode(p)),o.insertNode(r),r}function a(e,t,a,o){var d=r(a,e);d?(t[o?"setStartBefore":"setEndBefore"](d),d.parentNode.removeChild(d)):n.warn("Marker element has been removed. Cannot restore selection.")}function o(e,n){return n.compareBoundaryPoints(e.START_TO_START,e)}function d(n,r){var a,o,d=e.DomRange.getRangeDocument(n),s=n.toString();return n.collapsed?(o=t(n,!1),{document:d,markerId:o.id,collapsed:!0}):(o=t(n,!1),a=t(n,!0),{document:d,startMarkerId:a.id,endMarkerId:o.id,collapsed:!1,backward:r,toString:function(){return"original text: '"+s+"', new text: '"+n.toString()+"'"}})}function s(t,o){var d=t.document;"undefined"==typeof o&&(o=!0);var s=e.createRange(d);if(t.collapsed){var l=r(t.markerId,d);if(l){l.style.display="inline";var i=l.previousSibling;i&&3==i.nodeType?(l.parentNode.removeChild(l),s.collapseToPoint(i,i.length)):(s.collapseBefore(l),l.parentNode.removeChild(l))}else n.warn("Marker element has been removed. Cannot restore selection.")}else a(d,s,t.startMarkerId,!0),a(d,s,t.endMarkerId,!1);return o&&s.normalizeBoundaries(),s}function l(n,t){var a,s,l=[];n=n.slice(0),n.sort(o);for(var i=0,c=n.length;c>i;++i)l[i]=d(n[i],t);for(i=c-1;i>=0;--i)a=n[i],s=e.DomRange.getRangeDocument(a),a.collapsed?a.collapseAfter(r(l[i].markerId,s)):(a.setEndBefore(r(l[i].endMarkerId,s)),a.setStartAfter(r(l[i].startMarkerId,s)));return l}function i(r){if(!e.isSelectionValid(r))return n.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus."),null;var t=e.getSelection(r),a=t.getAllRanges(),o=1==a.length&&t.isBackward(),d=l(a,o);return o?t.setSingleRange(a[0],"backward"):t.setRanges(a),{win:r,rangeInfos:d,restored:!1}}function c(e){for(var n=[],r=e.length,t=r-1;t>=0;t--)n[t]=s(e[t],!0);return n}function u(n,r){if(!n.restored){var t=n.rangeInfos,a=e.getSelection(n.win),o=c(t),d=t.length;1==d&&r&&e.features.selectionHasExtend&&t[0].backward?(a.removeAllRanges(),a.addRange(o[0],!0)):a.setRanges(o),n.restored=!0}}function f(e,n){var t=r(n,e);t&&t.parentNode.removeChild(t)}function g(e){for(var n,r=e.rangeInfos,t=0,a=r.length;a>t;++t)n=r[t],n.collapsed?f(e.doc,n.markerId):(f(e.doc,n.startMarkerId),f(e.doc,n.endMarkerId))}var m=e.dom,p="";e.util.extend(e,{saveRange:d,restoreRange:s,saveRanges:l,restoreRanges:c,saveSelection:i,restoreSelection:u,removeMarkerElement:f,removeMarkers:g})})},this); \ No newline at end of file +/* + 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 2012, Tim Down + Licensed under the MIT license. + Version: 1.2.3 + Build date: 26 February 2012 +*/ +rangy.createModule("SaveRestore",function(h,m){function n(a,g){var e="selectionBoundary_"+ +new Date+"_"+(""+Math.random()).slice(2),c,f=p.getDocument(a.startContainer),d=a.cloneRange();d.collapse(g);c=f.createElement("span");c.id=e;c.style.lineHeight="0";c.style.display="none";c.className="rangySelectionBoundary";c.appendChild(f.createTextNode(q));d.insertNode(c);d.detach();return c}function o(a,g,e,c){if(a=(a||document).getElementById(e)){g[c?"setStartBefore":"setEndBefore"](a);a.parentNode.removeChild(a)}else m.warn("Marker element has been removed. Cannot restore selection.")} +function r(a,g){return g.compareBoundaryPoints(a.START_TO_START,a)}function k(a,g){var e=(a||document).getElementById(g);e&&e.parentNode.removeChild(e)}h.requireModules(["DomUtil","DomRange","WrappedRange"]);var p=h.dom,q="\ufeff";h.saveSelection=function(a){a=a||window;var g=a.document;if(h.isSelectionValid(a)){var e=h.getSelection(a),c=e.getAllRanges(),f=[],d,j;c.sort(r);for(var b=0,i=c.length;b=0;--b){d=c[b];if(d.collapsed)d.collapseBefore((g||document).getElementById(f[b].markerId));else{d.setEndBefore((g||document).getElementById(f[b].endMarkerId));d.setStartAfter((g||document).getElementById(f[b].startMarkerId))}}e.setRanges(c);return{win:a,doc:g,rangeInfos:f,restored:false}}else m.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.")}; +h.restoreSelection=function(a,g){if(!a.restored){for(var e=a.rangeInfos,c=h.getSelection(a.win),f=[],d=e.length,j=d-1,b,i;j>=0;--j){b=e[j];i=h.createRange(a.doc);if(b.collapsed)if(b=(a.doc||document).getElementById(b.markerId)){b.style.display="inline";var l=b.previousSibling;if(l&&l.nodeType==3){b.parentNode.removeChild(b);i.collapseToPoint(l,l.length)}else{i.collapseBefore(b);b.parentNode.removeChild(b)}}else m.warn("Marker element has been removed. Cannot restore selection.");else{o(a.doc,i,b.startMarkerId, +true);o(a.doc,i,b.endMarkerId,false)}d==1&&i.normalizeBoundaries();f[j]=i}if(d==1&&g&&h.features.selectionHasExtend&&e[0].backwards){c.removeAllRanges();c.addRange(f[0],true)}else c.setRanges(f);a.restored=true}};h.removeMarkerElement=k;h.removeMarkers=function(a){for(var g=a.rangeInfos,e=0,c=g.length,f;e> 6) | 192, (c & 63) | 128); + } else { + utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128); + } + } + return utf8CharCodes; } - // Checksum for checking whether range can be serialized - var crc32 = (function() { - function utf8encode(str) { - var utf8CharCodes = []; - - for (var i = 0, len = str.length, c; i < len; ++i) { - c = str.charCodeAt(i); - if (c < 128) { - utf8CharCodes.push(c); - } else if (c < 2048) { - utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128); + var cachedCrcTable = null; + + function buildCRCTable() { + var table = []; + for (var i = 0, j, crc; i < 256; ++i) { + crc = i; + j = 8; + while (j--) { + if ((crc & 1) == 1) { + crc = (crc >>> 1) ^ 0xEDB88320; } else { - utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128); + crc >>>= 1; } } - return utf8CharCodes; + table[i] = crc >>> 0; } + return table; + } - var cachedCrcTable = null; - - function buildCRCTable() { - var table = []; - for (var i = 0, j, crc; i < 256; ++i) { - crc = i; - j = 8; - while (j--) { - if ((crc & 1) == 1) { - crc = (crc >>> 1) ^ 0xEDB88320; - } else { - crc >>>= 1; - } - } - table[i] = crc >>> 0; - } - return table; + function getCrcTable() { + if (!cachedCrcTable) { + cachedCrcTable = buildCRCTable(); } + return cachedCrcTable; + } - function getCrcTable() { - if (!cachedCrcTable) { - cachedCrcTable = buildCRCTable(); - } - return cachedCrcTable; + return function(str) { + var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable(); + for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) { + y = (crc ^ utf8CharCodes[i]) & 0xFF; + crc = (crc >>> 8) ^ crcTable[y]; } + return (crc ^ -1) >>> 0; + }; + })(); - return function(str) { - var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable(); - for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) { - y = (crc ^ utf8CharCodes[i]) & 0xFF; - crc = (crc >>> 8) ^ crcTable[y]; - } - return (crc ^ -1) >>> 0; - }; - })(); + var dom = api.dom; - var dom = api.dom; + function escapeTextForHtml(str) { + return str.replace(//g, ">"); + } - function escapeTextForHtml(str) { - return str.replace(//g, ">"); + function nodeToInfoString(node, infoParts) { + infoParts = infoParts || []; + var nodeType = node.nodeType, children = node.childNodes, childCount = children.length; + var nodeInfo = [nodeType, node.nodeName, childCount].join(":"); + var start = "", end = ""; + switch (nodeType) { + case 3: // Text node + start = escapeTextForHtml(node.nodeValue); + break; + case 8: // Comment + start = ""; + break; + default: + start = "<" + nodeInfo + ">"; + end = ""; + break; } + if (start) { + infoParts.push(start); + } + for (var i = 0; i < childCount; ++i) { + nodeToInfoString(children[i], infoParts); + } + if (end) { + infoParts.push(end); + } + return infoParts; + } - function nodeToInfoString(node, infoParts) { - infoParts = infoParts || []; - var nodeType = node.nodeType, children = node.childNodes, childCount = children.length; - var nodeInfo = [nodeType, node.nodeName, childCount].join(":"); - var start = "", end = ""; - switch (nodeType) { - case 3: // Text node - start = escapeTextForHtml(node.nodeValue); - break; - case 8: // Comment - start = ""; - break; - default: - start = "<" + nodeInfo + ">"; - end = ""; - break; - } - if (start) { - infoParts.push(start); - } - for (var i = 0; i < childCount; ++i) { - nodeToInfoString(children[i], infoParts); - } - if (end) { - infoParts.push(end); - } - return infoParts; + // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all + // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around + // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's + // innerHTML whenever the user changes an input within the element. + function getElementChecksum(el) { + var info = nodeToInfoString(el).join(""); + return crc32(info).toString(16); + } + + function serializePosition(node, offset, rootNode) { + var pathBits = [], n = node; + rootNode = rootNode || dom.getDocument(node).documentElement; + while (n && n != rootNode) { + pathBits.push(dom.getNodeIndex(n, true)); + n = n.parentNode; } + return pathBits.join("/") + ":" + offset; + } - // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all - // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around - // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's - // innerHTML whenever the user changes an input within the element. - function getElementChecksum(el) { - var info = nodeToInfoString(el).join(""); - return crc32(info).toString(16); + function deserializePosition(serialized, rootNode, doc) { + if (rootNode) { + doc = doc || dom.getDocument(rootNode); + } else { + doc = doc || document; + rootNode = doc.documentElement; } + var bits = serialized.split(":"); + var node = rootNode; + var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex; - function serializePosition(node, offset, rootNode) { - var pathParts = [], n = node; - rootNode = rootNode || dom.getDocument(node).documentElement; - while (n && n != rootNode) { - pathParts.push(dom.getNodeIndex(n, true)); - n = n.parentNode; + while (i--) { + nodeIndex = parseInt(nodeIndices[i], 10); + if (nodeIndex < node.childNodes.length) { + node = node.childNodes[parseInt(nodeIndices[i], 10)]; + } else { + throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) + + " has no child with index " + nodeIndex + ", " + i); } - return pathParts.join("/") + ":" + offset; } - function deserializePosition(serialized, rootNode, doc) { - if (!rootNode) { - rootNode = (doc || document).documentElement; - } - var parts = serialized.split(":"); - var node = rootNode; - var nodeIndices = parts[0] ? parts[0].split("/") : [], i = nodeIndices.length, nodeIndex; - - while (i--) { - nodeIndex = parseInt(nodeIndices[i], 10); - if (nodeIndex < node.childNodes.length) { - node = node.childNodes[nodeIndex]; - } else { - throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) + - " has no child with index " + nodeIndex + ", " + i); - } - } + return new dom.DomPosition(node, parseInt(bits[1], 10)); + } - return new dom.DomPosition(node, parseInt(parts[1], 10)); + function serializeRange(range, omitChecksum, rootNode) { + rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement; + if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) { + throw new Error("serializeRange: range is not wholly contained within specified root node"); } + var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," + + serializePosition(range.endContainer, range.endOffset, rootNode); + if (!omitChecksum) { + serialized += "{" + getElementChecksum(rootNode) + "}"; + } + return serialized; + } - function serializeRange(range, omitChecksum, rootNode) { - rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement; - if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) { - throw module.createError("serializeRange(): range " + range.inspect() + - " is not wholly contained within specified root node " + dom.inspectNode(rootNode)); - } - var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," + - serializePosition(range.endContainer, range.endOffset, rootNode); - if (!omitChecksum) { - serialized += "{" + getElementChecksum(rootNode) + "}"; - } - return serialized; + function deserializeRange(serialized, rootNode, doc) { + if (rootNode) { + doc = doc || dom.getDocument(rootNode); + } else { + doc = doc || document; + rootNode = doc.documentElement; } + var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized); + var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode); + if (checksum && checksum !== getElementChecksum(rootNode)) { + throw new Error("deserializeRange: checksums of serialized range root node (" + checksum + + ") and target root node (" + rootNodeChecksum + ") do not match"); + } + var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc); + var range = api.createRange(doc); + range.setStart(start.node, start.offset); + range.setEnd(end.node, end.offset); + return range; + } - var deserializeRegex = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/; - - function deserializeRange(serialized, rootNode, doc) { - if (rootNode) { - doc = doc || dom.getDocument(rootNode); - } else { - doc = doc || document; - rootNode = doc.documentElement; - } - var result = deserializeRegex.exec(serialized); - var checksum = result[4]; - if (checksum) { - var rootNodeChecksum = getElementChecksum(rootNode); - if (checksum !== rootNodeChecksum) { - throw module.createError("deserializeRange(): checksums of serialized range root node (" + checksum + - ") and target root node (" + rootNodeChecksum + ") do not match"); - } - } - var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc); - var range = api.createRange(doc); - range.setStartAndEnd(start.node, start.offset, end.node, end.offset); - return range; + function canDeserializeRange(serialized, rootNode, doc) { + if (rootNode) { + doc = doc || dom.getDocument(rootNode); + } else { + doc = doc || document; + rootNode = doc.documentElement; } + var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized); + var checksum = result[3]; + return !checksum || checksum === getElementChecksum(rootNode); + } - function canDeserializeRange(serialized, rootNode, doc) { - if (!rootNode) { - rootNode = (doc || document).documentElement; - } - var result = deserializeRegex.exec(serialized); - var checksum = result[3]; - return !checksum || checksum === getElementChecksum(rootNode); + function serializeSelection(selection, omitChecksum, rootNode) { + selection = selection || api.getSelection(); + var ranges = selection.getAllRanges(), serializedRanges = []; + for (var i = 0, len = ranges.length; i < len; ++i) { + serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode); } + return serializedRanges.join("|"); + } - function serializeSelection(selection, omitChecksum, rootNode) { - selection = api.getSelection(selection); - var ranges = selection.getAllRanges(), serializedRanges = []; - for (var i = 0, len = ranges.length; i < len; ++i) { - serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode); - } - return serializedRanges.join("|"); + function deserializeSelection(serialized, rootNode, win) { + if (rootNode) { + win = win || dom.getWindow(rootNode); + } else { + win = win || window; + rootNode = win.document.documentElement; } + var serializedRanges = serialized.split("|"); + var sel = api.getSelection(win); + var ranges = []; - function deserializeSelection(serialized, rootNode, win) { - if (rootNode) { - win = win || dom.getWindow(rootNode); - } else { - win = win || window; - rootNode = win.document.documentElement; - } - var serializedRanges = serialized.split("|"); - var sel = api.getSelection(win); - var ranges = []; + for (var i = 0, len = serializedRanges.length; i < len; ++i) { + ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document); + } + sel.setRanges(ranges); - for (var i = 0, len = serializedRanges.length; i < len; ++i) { - ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document); - } - sel.setRanges(ranges); + return sel; + } - return sel; + function canDeserializeSelection(serialized, rootNode, win) { + var doc; + if (rootNode) { + doc = win ? win.document : dom.getDocument(rootNode); + } else { + win = win || window; + rootNode = win.document.documentElement; } + var serializedRanges = serialized.split("|"); - function canDeserializeSelection(serialized, rootNode, win) { - var doc; - if (rootNode) { - doc = win ? win.document : dom.getDocument(rootNode); - } else { - win = win || window; - rootNode = win.document.documentElement; + for (var i = 0, len = serializedRanges.length; i < len; ++i) { + if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) { + return false; } - var serializedRanges = serialized.split("|"); + } - for (var i = 0, len = serializedRanges.length; i < len; ++i) { - if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) { - return false; - } - } + return true; + } - return true; - } - var cookieName = "rangySerializedSelection"; + var cookieName = "rangySerializedSelection"; - function getSerializedSelectionFromCookie(cookie) { - var parts = cookie.split(/[;,]/); - for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) { - nameVal = parts[i].split("="); - if (nameVal[0].replace(/^\s+/, "") == cookieName) { - val = nameVal[1]; - if (val) { - return decodeURIComponent(val.replace(/\s+$/, "")); - } + function getSerializedSelectionFromCookie(cookie) { + var parts = cookie.split(/[;,]/); + for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) { + nameVal = parts[i].split("="); + if (nameVal[0].replace(/^\s+/, "") == cookieName) { + val = nameVal[1]; + if (val) { + return decodeURIComponent(val.replace(/\s+$/, "")); } } - return null; } + return null; + } - function restoreSelectionFromCookie(win) { - win = win || window; - var serialized = getSerializedSelectionFromCookie(win.document.cookie); - if (serialized) { - deserializeSelection(serialized, win.doc); - } + function restoreSelectionFromCookie(win) { + win = win || window; + var serialized = getSerializedSelectionFromCookie(win.document.cookie); + if (serialized) { + deserializeSelection(serialized, win.doc) } + } - function saveSelectionCookie(win, props) { - win = win || window; - props = (typeof props == "object") ? props : {}; - var expires = props.expires ? ";expires=" + props.expires.toUTCString() : ""; - var path = props.path ? ";path=" + props.path : ""; - var domain = props.domain ? ";domain=" + props.domain : ""; - var secure = props.secure ? ";secure" : ""; - var serialized = serializeSelection(api.getSelection(win)); - win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure; - } + function saveSelectionCookie(win, props) { + win = win || window; + props = (typeof props == "object") ? props : {}; + var expires = props.expires ? ";expires=" + props.expires.toUTCString() : ""; + var path = props.path ? ";path=" + props.path : ""; + var domain = props.domain ? ";domain=" + props.domain : ""; + var secure = props.secure ? ";secure" : ""; + var serialized = serializeSelection(api.getSelection(win)); + win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure; + } + + api.serializePosition = serializePosition; + api.deserializePosition = deserializePosition; + + api.serializeRange = serializeRange; + api.deserializeRange = deserializeRange; + api.canDeserializeRange = canDeserializeRange; + + api.serializeSelection = serializeSelection; + api.deserializeSelection = deserializeSelection; + api.canDeserializeSelection = canDeserializeSelection; + + api.restoreSelectionFromCookie = restoreSelectionFromCookie; + api.saveSelectionCookie = saveSelectionCookie; - util.extend(api, { - serializePosition: serializePosition, - deserializePosition: deserializePosition, - serializeRange: serializeRange, - deserializeRange: deserializeRange, - canDeserializeRange: canDeserializeRange, - serializeSelection: serializeSelection, - deserializeSelection: deserializeSelection, - canDeserializeSelection: canDeserializeSelection, - restoreSelectionFromCookie: restoreSelectionFromCookie, - saveSelectionCookie: saveSelectionCookie, - getElementChecksum: getElementChecksum, - nodeToInfoString: nodeToInfoString - }); - - util.crc32 = crc32; - }); - -}, this); \ No newline at end of file + api.getElementChecksum = getElementChecksum; +}); diff --git a/rangy-serializer.min.js b/rangy-serializer.min.js index 5f1beb1..72f19f6 100644 --- a/rangy-serializer.min.js +++ b/rangy-serializer.min.js @@ -1,16 +1,23 @@ -/** - * Serializer module for Rangy. - * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a - * cookie or local storage and restore it on the user's next visit to the same page. - * - * Part of Rangy, a cross-browser JavaScript range and selection library - * https://github.com/timdown/rangy - * - * Depends on Rangy core. - * - * Copyright 2015, Tim Down - * Licensed under the MIT license. - * Version: 1.3.0-alpha.20150122 - * Build date: 22 January 2015 - */ -!function(e,n){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(n.rangy)}(function(e){e.createModule("Serializer",["WrappedSelection"],function(e,n){function t(e){return e.replace(//g,">")}function o(e,n){n=n||[];var r=e.nodeType,i=e.childNodes,c=i.length,a=[r,e.nodeName,c].join(":"),d="",u="";switch(r){case 3:d=t(e.nodeValue);break;case 8:d="";break;default:d="<"+a+">",u=""}d&&n.push(d);for(var s=0;c>s;++s)o(i[s],n);return u&&n.push(u),n}function r(e){var n=o(e).join("");return w(n).toString(16)}function i(e,n,t){var o=[],r=e;for(t=t||R.getDocument(e).documentElement;r&&r!=t;)o.push(R.getNodeIndex(r,!0)),r=r.parentNode;return o.join("/")+":"+n}function c(e,t,o){t||(t=(o||document).documentElement);for(var r,i=e.split(":"),c=t,a=i[0]?i[0].split("/"):[],d=a.length;d--;){if(r=parseInt(a[d],10),!(rc;++c)i[c]=a(r[c],t,o);return i.join("|")}function l(n,t,o){t?o=o||R.getWindow(t):(o=o||window,t=o.document.documentElement);for(var r=n.split("|"),i=e.getSelection(o),c=[],a=0,u=r.length;u>a;++a)c[a]=d(r[a],t,o.document);return i.setRanges(c),i}function f(e,n,t){var o;n?o=t?t.document:R.getDocument(n):(t=t||window,n=t.document.documentElement);for(var r=e.split("|"),i=0,c=r.length;c>i;++i)if(!u(r[i],n,o))return!1;return!0}function m(e){for(var n,t,o=e.split(/[;,]/),r=0,i=o.length;i>r;++r)if(n=o[r].split("="),n[0].replace(/^\s+/,"")==C&&(t=n[1]))return decodeURIComponent(t.replace(/\s+$/,""));return null}function p(e){e=e||window;var n=m(e.document.cookie);n&&l(n,e.doc)}function g(n,t){n=n||window,t="object"==typeof t?t:{};var o=t.expires?";expires="+t.expires.toUTCString():"",r=t.path?";path="+t.path:"",i=t.domain?";domain="+t.domain:"",c=t.secure?";secure":"",a=s(e.getSelection(n));n.document.cookie=encodeURIComponent(C)+"="+encodeURIComponent(a)+o+r+i+c}var h="undefined",v=e.util;(typeof encodeURIComponent==h||typeof decodeURIComponent==h)&&n.fail("encodeURIComponent and/or decodeURIComponent method is missing");var w=function(){function e(e){for(var n,t=[],o=0,r=e.length;r>o;++o)n=e.charCodeAt(o),128>n?t.push(n):2048>n?t.push(n>>6|192,63&n|128):t.push(n>>12|224,n>>6&63|128,63&n|128);return t}function n(){for(var e,n,t=[],o=0;256>o;++o){for(n=o,e=8;e--;)1==(1&n)?n=n>>>1^3988292384:n>>>=1;t[o]=n>>>0}return t}function t(){return o||(o=n()),o}var o=null;return function(n){for(var o,r=e(n),i=-1,c=t(),a=0,d=r.length;d>a;++a)o=255&(i^r[a]),i=i>>>8^c[o];return(-1^i)>>>0}}(),R=e.dom,S=/^([^,]+),([^,\{]+)(\{([^}]+)\})?$/,C="rangySerializedSelection";v.extend(e,{serializePosition:i,deserializePosition:c,serializeRange:a,deserializeRange:d,canDeserializeRange:u,serializeSelection:s,deserializeSelection:l,canDeserializeSelection:f,restoreSelectionFromCookie:p,saveSelectionCookie:g,getElementChecksum:r,nodeToInfoString:o}),v.crc32=w})},this); \ No newline at end of file +/* + Serializer module for Rangy. + Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a + cookie or local storage and restore it on the user's next visit to the same page. + + Part of Rangy, a cross-browser JavaScript range and selection library + http://code.google.com/p/rangy/ + + Depends on Rangy core. + + Copyright 2012, Tim Down + Licensed under the MIT license. + Version: 1.2.3 + Build date: 26 February 2012 +*/ +rangy.createModule("Serializer",function(g,n){function o(c,a){a=a||[];var b=c.nodeType,e=c.childNodes,d=e.length,f=[b,c.nodeName,d].join(":"),h="",k="";switch(b){case 3:h=c.nodeValue.replace(//g,">");break;case 8:h="