+/*
+ DOM Ranges for Internet Explorer (m2)
+
+ Copyright (c) 2009 Tim Cameron Ryan
+ Released under the MIT/X License
+ */
+
+/*
+ Range reference:
+ http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
+ http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsRange.cpp
+ https://developer.mozilla.org/En/DOM:Range
+ Selection reference:
+ http://trac.webkit.org/browser/trunk/WebCore/page/DOMSelection.cpp
+ TextRange reference:
+ http://msdn.microsoft.com/en-us/library/ms535872.aspx
+ Other links:
+ http://jorgenhorstink.nl/test/javascript/range/range.js
+ http://jorgenhorstink.nl/2006/07/05/dom-range-implementation-in-ecmascript-completed/
+ http://dylanschiemann.com/articles/dom2Range/dom2RangeExamples.html
+*/
+
+//[TODO] better exception support
+
+(function () { // sandbox
+
+/*
+ DOM functions
+ */
+
+var DOMUtils = {
+ findChildPosition: function (node) {
+ for (var i = 0; node = node.previousSibling; i++)
+ continue;
+ return i;
+ },
+ isDataNode: function (node) {
+ return node && node.nodeValue !== null && node.data !== null;
+ },
+ isAncestorOf: function (parent, node) {
+ return !DOMUtils.isDataNode(parent) &&
+ (parent.contains(DOMUtils.isDataNode(node) ? node.parentNode : node) ||
+ node.parentNode == parent);
+ },
+ isAncestorOrSelf: function (root, node) {
+ return DOMUtils.isAncestorOf(root, node) || root == node;
+ },
+ findClosestAncestor: function (root, node) {
+ if (DOMUtils.isAncestorOf(root, node))
+ while (node && node.parentNode != root)
+ node = node.parentNode;
+ return node;
+ },
+ getNodeLength: function (node) {
+ return DOMUtils.isDataNode(node) ? node.length : node.childNodes.length;
+ },
+ splitDataNode: function (node, offset) {
+ if (!DOMUtils.isDataNode(node))
+ return false;
+ var newNode = node.cloneNode(false);
+ node.deleteData(offset, node.length);
+ newNode.deleteData(0, offset);
+ node.parentNode.insertBefore(newNode, node.nextSibling);
+ }
+};
+
+/*
+ Text Range utilities
+ functions to simplify text range manipulation in ie
+ */
+
+var TextRangeUtils = {
+ convertToDOMRange: function (textRange, document) {
+ function adoptBoundary(domRange, textRange, bStart) {
+ // iterate backwards through parent element to find anchor location
+ var cursorNode = document.createElement('a'), cursor = textRange.duplicate();
+ cursor.collapse(bStart);
+ var parent = cursor.parentElement();
+ do {
+ parent.insertBefore(cursorNode, cursorNode.previousSibling);
+ cursor.moveToElementText(cursorNode);
+ } while (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) > 0 && cursorNode.previousSibling);
+
+ // when we exceed or meet the cursor, we've found the node
+ if (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) == -1 && cursorNode.nextSibling) {
+ // data node
+ cursor.setEndPoint(bStart ? 'EndToStart' : 'EndToEnd', textRange);
+ domRange[bStart ? 'setStart' : 'setEnd'](cursorNode.nextSibling, cursor.text.length);
+ } else {
+ // element
+ domRange[bStart ? 'setStartBefore' : 'setEndBefore'](cursorNode);
+ }
+ cursorNode.parentNode.removeChild(cursorNode);
+ }
+
+ // return a DOM range
+ var domRange = new DOMRange(document);
+ adoptBoundary(domRange, textRange, true);
+ adoptBoundary(domRange, textRange, false);
+ return domRange;
+ },
+
+ convertFromDOMRange: function (domRange) {
+ function adoptEndPoint(textRange, domRange, bStart) {
+ // find anchor node and offset
+ var container = domRange[bStart ? 'startContainer' : 'endContainer'];
+ var offset = domRange[bStart ? 'startOffset' : 'endOffset'], textOffset = 0;
+ var anchorNode = DOMUtils.isDataNode(container) ? container : container.childNodes[offset];
+ var anchorParent = DOMUtils.isDataNode(container) ? container.parentNode : container;
+ // visible data nodes need a text offset
+ if (container.nodeType == 3 || container.nodeType == 4)
+ textOffset = offset;
+
+ // create a cursor element node to position range (since we can't select text nodes)
+ var cursorNode = domRange._document.createElement('a');
+ anchorParent.insertBefore(cursorNode, anchorNode);
+ var cursor = domRange._document.body.createTextRange();
+ cursor.moveToElementText(cursorNode);
+ cursorNode.parentNode.removeChild(cursorNode);
+ // move range
+ textRange.setEndPoint(bStart ? 'StartToStart' : 'EndToStart', cursor);
+ textRange[bStart ? 'moveStart' : 'moveEnd']('character', textOffset);
+ }
+
+ // return an IE text range
+ var textRange = domRange._document.body.createTextRange();
+ adoptEndPoint(textRange, domRange, true);
+ adoptEndPoint(textRange, domRange, false);
+ return textRange;
+ }
+};
+
+/*
+ DOM Range
+ */
+
+function DOMRange(document) {
+ // save document parameter
+ this._document = document;
+
+ // initialize range
+//[TODO] this should be located at document[0], document[0]
+ this.startContainer = this.endContainer = document.body;
+ this.endOffset = DOMUtils.getNodeLength(document.body);
+}
+DOMRange.START_TO_START = 0;
+DOMRange.START_TO_END = 1;
+DOMRange.END_TO_END = 2;
+DOMRange.END_TO_START = 3;
+
+DOMRange.prototype = {
+ // public properties
+ startContainer: null,
+ startOffset: 0,
+ endContainer: null,
+ endOffset: 0,
+ commonAncestorContainer: null,
+ collapsed: false,
+ // private properties
+ _document: null,
+
+ // private methods
+ _refreshProperties: function () {
+ // collapsed attribute
+ this.collapsed = (this.startContainer == this.endContainer && this.startOffset == this.endOffset);
+ // find common ancestor
+ var node = this.startContainer;
+ while (node && node != this.endContainer && !DOMUtils.isAncestorOf(node, this.endContainer))
+ node = node.parentNode;
+ this.commonAncestorContainer = node;
+ },
+
+ // range methods
+//[TODO] collapse if start is after end, end is before start
+ setStart: function(container, offset) {
+ this.startContainer = container;
+ this.startOffset = offset;
+ this._refreshProperties();
+ },
+ setEnd: function(container, offset) {
+ this.endContainer = container;
+ this.endOffset = offset;
+ this._refreshProperties();
+ },
+ setStartBefore: function (refNode) {
+ // set start to beore this node
+ this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode));
+ },
+ setStartAfter: function (refNode) {
+ // select next sibling
+ this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1);
+ },
+ setEndBefore: function (refNode) {
+ // set end to beore this node
+ this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode));
+ },
+ setEndAfter: function (refNode) {
+ // select next sibling
+ this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1);
+ },
+ selectNode: function (refNode) {
+ this.setStartBefore(refNode);
+ this.setEndAfter(refNode);
+ },
+ selectNodeContents: function (refNode) {
+ this.setStart(refNode, 0);
+ this.setEnd(refNode, DOMUtils.getNodeLength(refNode));
+ },
+ collapse: function (toStart) {
+ if (toStart)
+ this.setEnd(this.startContainer, this.startOffset);
+ else
+ this.setStart(this.endContainer, this.endOffset);
+ },
+
+ // editing methods
+ cloneContents: function () {
+ // clone subtree
+ return (function cloneSubtree(iterator) {
+ for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) {
+ node = node.cloneNode(!iterator.hasPartialSubtree());
+ if (iterator.hasPartialSubtree())
+ node.appendChild(cloneSubtree(iterator.getSubtreeIterator()));
+ frag.appendChild(node);
+ }
+ return frag;
+ })(new RangeIterator(this));
+ },
+ extractContents: function () {
+ // cache range and move anchor points
+ var range = this.cloneRange();
+ if (this.startContainer != this.commonAncestorContainer)
+ this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer));
+ this.collapse(true);
+ // extract range
+ return (function extractSubtree(iterator) {
+ for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) {
+ iterator.hasPartialSubtree() ? node = node.cloneNode(false) : iterator.remove();
+ if (iterator.hasPartialSubtree())
+ node.appendChild(extractSubtree(iterator.getSubtreeIterator()));
+ frag.appendChild(node);
+ }
+ return frag;
+ })(new RangeIterator(range));
+ },
+ deleteContents: function () {
+ // cache range and move anchor points
+ var range = this.cloneRange();
+ if (this.startContainer != this.commonAncestorContainer)
+ this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer));
+ this.collapse(true);
+ // delete range
+ (function deleteSubtree(iterator) {
+ while (iterator.next())
+ iterator.hasPartialSubtree() ? deleteSubtree(iterator.getSubtreeIterator()) : iterator.remove();
+ })(new RangeIterator(range));
+ },
+ insertNode: function (newNode) {
+ // set original anchor and insert node
+ if (DOMUtils.isDataNode(this.startContainer)) {
+ DOMUtils.splitDataNode(this.startContainer, this.startOffset);
+ this.startContainer.parentNode.insertBefore(newNode, this.startContainer.nextSibling);
+ } else {
+ this.startContainer.insertBefore(newNode, this.startContainer.childNodes[this.startOffset]);
+ }
+ // resync start anchor
+ this.setStart(this.startContainer, this.startOffset);
+ },
+ surroundContents: function (newNode) {
+ // extract and surround contents
+ var content = this.extractContents();
+ this.insertNode(newNode);
+ newNode.appendChild(content);
+ this.selectNode(newNode);
+ },
+
+ // other methods
+ compareBoundaryPoints: function (how, sourceRange) {
+ // get anchors
+ var containerA, offsetA, containerB, offsetB;
+ switch (how) {
+ case DOMRange.START_TO_START:
+ case DOMRange.START_TO_END:
+ containerA = this.startContainer;
+ offsetA = this.startOffset;
+ break;
+ case DOMRange.END_TO_END:
+ case DOMRange.END_TO_START:
+ containerA = this.endContainer;
+ offsetA = this.endOffset;
+ break;
+ }
+ switch (how) {
+ case DOMRange.START_TO_START:
+ case DOMRange.END_TO_START:
+ containerB = sourceRange.startContainer;
+ offsetB = sourceRange.startOffset;
+ break;
+ case DOMRange.START_TO_END:
+ case DOMRange.END_TO_END:
+ containerB = sourceRange.endContainer;
+ offsetB = sourceRange.endOffset;
+ break;
+ }
+
+ // compare
+ return containerA.sourceIndex < containerB.sourceIndex ? -1 :
+ containerA.sourceIndex == containerB.sourceIndex ?
+ offsetA < offsetB ? -1 : offsetA == offsetB ? 0 : 1
+ : 1;
+ },
+ cloneRange: function () {
+ // return cloned range
+ var range = new DOMRange(this._document);
+ range.setStart(this.startContainer, this.startOffset);
+ range.setEnd(this.endContainer, this.endOffset);
+ return range;
+ },
+ detach: function () {
+//[TODO] Releases Range from use to improve performance.
+ },
+ toString: function () {
+ return TextRangeUtils.convertFromDOMRange(this).text;
+ },
+ createContextualFragment: function (tagString) {
+ // parse the tag string in a context node
+ var content = (DOMUtils.isDataNode(this.startContainer) ? this.startContainer.parentNode : this.startContainer).cloneNode(false);
+ content.innerHTML = tagString;
+ // return a document fragment from the created node
+ for (var fragment = this._document.createDocumentFragment(); content.firstChild; )
+ fragment.appendChild(content.firstChild);
+ return fragment;
+ }
+};
+
+/*
+ Range iterator
+ */
+
+function RangeIterator(range) {
+ this.range = range;
+ if (range.collapsed)
+ return;
+
+//[TODO] ensure this works
+ // get anchors
+ var root = range.commonAncestorContainer;
+ this._next = range.startContainer == root && !DOMUtils.isDataNode(range.startContainer) ?
+ range.startContainer.childNodes[range.startOffset] :
+ DOMUtils.findClosestAncestor(root, range.startContainer);
+ this._end = range.endContainer == root && !DOMUtils.isDataNode(range.endContainer) ?
+ range.endContainer.childNodes[range.endOffset] :
+ DOMUtils.findClosestAncestor(root, range.endContainer).nextSibling;
+}
+
+RangeIterator.prototype = {
+ // public properties
+ range: null,
+ // private properties
+ _current: null,
+ _next: null,
+ _end: null,
+
+ // public methods
+ hasNext: function () {
+ return !!this._next;
+ },
+ next: function () {
+ // move to next node
+ var current = this._current = this._next;
+ this._next = this._current && this._current.nextSibling != this._end ?
+ this._current.nextSibling : null;
+
+ // check for partial text nodes
+ if (DOMUtils.isDataNode(this._current)) {
+ if (this.range.endContainer == this._current)
+ (current = current.cloneNode(true)).deleteData(this.range.endOffset, current.length - this.range.endOffset);
+ if (this.range.startContainer == this._current)
+ (current = current.cloneNode(true)).deleteData(0, this.range.startOffset);
+ }
+ return current;
+ },
+ remove: function () {
+ // check for partial text nodes
+ if (DOMUtils.isDataNode(this._current) &&
+ (this.range.startContainer == this._current || this.range.endContainer == this._current)) {
+ var start = this.range.startContainer == this._current ? this.range.startOffset : 0;
+ var end = this.range.endContainer == this._current ? this.range.endOffset : this._current.length;
+ this._current.deleteData(start, end - start);
+ } else
+ this._current.parentNode.removeChild(this._current);
+ },
+ hasPartialSubtree: function () {
+ // check if this node be partially selected
+ return !DOMUtils.isDataNode(this._current) &&
+ (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer) ||
+ DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer));
+ },
+ getSubtreeIterator: function () {
+ // create a new range
+ var subRange = new DOMRange(this.range._document);
+ subRange.selectNodeContents(this._current);
+ // handle anchor points
+ if (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer))
+ subRange.setStart(this.range.startContainer, this.range.startOffset);
+ if (DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer))
+ subRange.setEnd(this.range.endContainer, this.range.endOffset);
+ // return iterator
+ return new RangeIterator(subRange);
+ }
+};
+
+/*
+ DOM Selection
+ */
+
+//[NOTE] This is a very shallow implementation of the Selection object, based on Webkit's
+// implementation and without redundant features. Complete selection manipulation is still
+// possible with just removeAllRanges/addRange/getRangeAt.
+
+function DOMSelection(document) {
+ // save document parameter
+ this._document = document;
+
+ // add DOM selection handler
+ var selection = this;
+ document.attachEvent('onselectionchange', function () { selection._selectionChangeHandler(); });
+}
+
+DOMSelection.prototype = {
+ // public properties
+ rangeCount: 0,
+ // private properties
+ _document: null,
+
+ // private methods
+ _selectionChangeHandler: function () {
+ // check if there exists a range
+ this.rangeCount = this._selectionExists(this._document.selection.createRange()) ? 1 : 0;
+ },
+ _selectionExists: function (textRange) {
+ // checks if a created text range exists or is an editable cursor
+ return textRange.compareEndPoints('StartToEnd', textRange) != 0 ||
+ textRange.parentElement().isContentEditable;
+ },
+
+ // public methods
+ addRange: function (range) {
+ // add range or combine with existing range
+ var selection = this._document.selection.createRange(), textRange = TextRangeUtils.convertFromDOMRange(range);
+ if (!this._selectionExists(selection))
+ {
+ // select range
+ textRange.select();
+ }
+ else
+ {
+ // only modify range if it intersects with current range
+ if (textRange.compareEndPoints('StartToStart', selection) == -1)
+ if (textRange.compareEndPoints('StartToEnd', selection) > -1 &&
+ textRange.compareEndPoints('EndToEnd', selection) == -1)
+ selection.setEndPoint('StartToStart', textRange);
+ else
+ if (textRange.compareEndPoints('EndToStart', selection) < 1 &&
+ textRange.compareEndPoints('EndToEnd', selection) > -1)
+ selection.setEndPoint('EndToEnd', textRange);
+ selection.select();
+ }
+ },
+ removeAllRanges: function () {
+ // remove all ranges
+ this._document.selection.empty();
+ },
+ getRangeAt: function (index) {
+ // return any existing selection, or a cursor position in content editable mode
+ var textRange = this._document.selection.createRange();
+ if (this._selectionExists(textRange))
+ return TextRangeUtils.convertToDOMRange(textRange, this._document);
+ return null;
+ },
+ toString: function () {
+ // get selection text
+ return this._document.selection.createRange().text;
+ }
+};
+
+/*
+ scripting hooks
+ */
+
+document.createRange = function () {
+ return new DOMRange(document);
+};
+
+var selection = new DOMSelection(document);
+window.getSelection = function () {
+ return selection;
+};
+
+//[TODO] expose DOMRange/DOMSelection to window.?
+
+})();