2   DOM Ranges for Internet Explorer (m2)
 
   4   Copyright (c) 2009 Tim Cameron Ryan
 
   5   Released under the MIT/X License
 
  10     http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
 
  11     http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsRange.cpp
 
  12     https://developer.mozilla.org/En/DOM:Range
 
  14     http://trac.webkit.org/browser/trunk/WebCore/page/DOMSelection.cpp
 
  16     http://msdn.microsoft.com/en-us/library/ms535872.aspx
 
  18     http://jorgenhorstink.nl/test/javascript/range/range.js
 
  19     http://jorgenhorstink.nl/2006/07/05/dom-range-implementation-in-ecmascript-completed/
 
  20     http://dylanschiemann.com/articles/dom2Range/dom2RangeExamples.html
 
  23 //[TODO] better exception support
 
  25 (function () {  // sandbox
 
  32         findChildPosition: function (node) {
 
  33                 for (var i = 0; node = node.previousSibling; i++)
 
  37         isDataNode: function (node) {
 
  38                 return node && node.nodeValue !== null && node.data !== null;
 
  40         isAncestorOf: function (parent, node) {
 
  41                 return !DOMUtils.isDataNode(parent) &&
 
  42                     (parent.contains(DOMUtils.isDataNode(node) ? node.parentNode : node) ||                 
 
  43                     node.parentNode == parent);
 
  45         isAncestorOrSelf: function (root, node) {
 
  46                 return DOMUtils.isAncestorOf(root, node) || root == node;
 
  48         findClosestAncestor: function (root, node) {
 
  49                 if (DOMUtils.isAncestorOf(root, node))
 
  50                         while (node && node.parentNode != root)
 
  51                                 node = node.parentNode;
 
  54         getNodeLength: function (node) {
 
  55                 return DOMUtils.isDataNode(node) ? node.length : node.childNodes.length;
 
  57         splitDataNode: function (node, offset) {
 
  58                 if (!DOMUtils.isDataNode(node))
 
  60                 var newNode = node.cloneNode(false);
 
  61                 node.deleteData(offset, node.length);
 
  62                 newNode.deleteData(0, offset);
 
  63                 node.parentNode.insertBefore(newNode, node.nextSibling);
 
  69   functions to simplify text range manipulation in ie
 
  72 var TextRangeUtils = {
 
  73         convertToDOMRange: function (textRange, document) {
 
  74                 function adoptBoundary(domRange, textRange, bStart) {
 
  75                         // iterate backwards through parent element to find anchor location
 
  76                         var cursorNode = document.createElement('a'), cursor = textRange.duplicate();
 
  77                         cursor.collapse(bStart);
 
  78                         var parent = cursor.parentElement();
 
  80                                 parent.insertBefore(cursorNode, cursorNode.previousSibling);
 
  81                                 cursor.moveToElementText(cursorNode);
 
  82                         } while (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) > 0 && cursorNode.previousSibling);
 
  84                         // when we exceed or meet the cursor, we've found the node
 
  85                         if (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) == -1 && cursorNode.nextSibling) {
 
  87                                 cursor.setEndPoint(bStart ? 'EndToStart' : 'EndToEnd', textRange);
 
  88                                 domRange[bStart ? 'setStart' : 'setEnd'](cursorNode.nextSibling, cursor.text.length);
 
  91                                 domRange[bStart ? 'setStartBefore' : 'setEndBefore'](cursorNode);
 
  93                         cursorNode.parentNode.removeChild(cursorNode);
 
  97                 var domRange = new DOMRange(document);
 
  98                 adoptBoundary(domRange, textRange, true);
 
  99                 adoptBoundary(domRange, textRange, false);
 
 103         convertFromDOMRange: function (domRange) {
 
 104                 function adoptEndPoint(textRange, domRange, bStart) {
 
 105                         // find anchor node and offset
 
 106                         var container = domRange[bStart ? 'startContainer' : 'endContainer'];
 
 107                         var offset = domRange[bStart ? 'startOffset' : 'endOffset'], textOffset = 0;
 
 108                         var anchorNode = DOMUtils.isDataNode(container) ? container : container.childNodes[offset];
 
 109                         var anchorParent = DOMUtils.isDataNode(container) ? container.parentNode : container;
 
 110                         // visible data nodes need a text offset
 
 111                         if (container.nodeType == 3 || container.nodeType == 4)
 
 114                         // create a cursor element node to position range (since we can't select text nodes)
 
 115                         var cursorNode = domRange._document.createElement('a');
 
 116                         anchorParent.insertBefore(cursorNode, anchorNode);
 
 117                         var cursor = domRange._document.body.createTextRange();
 
 118                         cursor.moveToElementText(cursorNode);
 
 119                         cursorNode.parentNode.removeChild(cursorNode);
 
 121                         textRange.setEndPoint(bStart ? 'StartToStart' : 'EndToStart', cursor);
 
 122                         textRange[bStart ? 'moveStart' : 'moveEnd']('character', textOffset);
 
 125                 // return an IE text range
 
 126                 var textRange = domRange._document.body.createTextRange();
 
 127                 adoptEndPoint(textRange, domRange, true);
 
 128                 adoptEndPoint(textRange, domRange, false);
 
 137 function DOMRange(document) {
 
 138         // save document parameter
 
 139         this._document = document;
 
 142 //[TODO] this should be located at document[0], document[0]
 
 143         this.startContainer = this.endContainer = document.body;
 
 144         this.endOffset = DOMUtils.getNodeLength(document.body);
 
 146 DOMRange.START_TO_START = 0;
 
 147 DOMRange.START_TO_END = 1;
 
 148 DOMRange.END_TO_END = 2;
 
 149 DOMRange.END_TO_START = 3;
 
 151 DOMRange.prototype = {
 
 153         startContainer: null,
 
 157         commonAncestorContainer: null,
 
 159         // private properties
 
 163         _refreshProperties: function () {
 
 164                 // collapsed attribute
 
 165                 this.collapsed = (this.startContainer == this.endContainer && this.startOffset == this.endOffset);
 
 166                 // find common ancestor
 
 167                 var node = this.startContainer;
 
 168                 while (node && node != this.endContainer && !DOMUtils.isAncestorOf(node, this.endContainer))
 
 169                         node = node.parentNode;
 
 170                 this.commonAncestorContainer = node;
 
 174 //[TODO] collapse if start is after end, end is before start
 
 175         setStart: function(container, offset) {
 
 176                 this.startContainer = container;
 
 177                 this.startOffset = offset;
 
 178                 this._refreshProperties();
 
 180         setEnd: function(container, offset) {
 
 181                 this.endContainer = container;
 
 182                 this.endOffset = offset;
 
 183                 this._refreshProperties();
 
 185         setStartBefore: function (refNode) {
 
 186                 // set start to beore this node
 
 187                 this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode));
 
 189         setStartAfter: function (refNode) {
 
 190                 // select next sibling
 
 191                 this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1);
 
 193         setEndBefore: function (refNode) {
 
 194                 // set end to beore this node
 
 195                 this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode));
 
 197         setEndAfter: function (refNode) {
 
 198                 // select next sibling
 
 199                 this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1);
 
 201         selectNode: function (refNode) {
 
 202                 this.setStartBefore(refNode);
 
 203                 this.setEndAfter(refNode);
 
 205         selectNodeContents: function (refNode) {
 
 206                 this.setStart(refNode, 0);
 
 207                 this.setEnd(refNode, DOMUtils.getNodeLength(refNode));
 
 209         collapse: function (toStart) {
 
 211                         this.setEnd(this.startContainer, this.startOffset);
 
 213                         this.setStart(this.endContainer, this.endOffset);
 
 217         cloneContents: function () {
 
 219                 return (function cloneSubtree(iterator) {
 
 220                         for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) {
 
 221                                 node = node.cloneNode(!iterator.hasPartialSubtree());
 
 222                                 if (iterator.hasPartialSubtree())
 
 223                                         node.appendChild(cloneSubtree(iterator.getSubtreeIterator()));
 
 224                                 frag.appendChild(node);
 
 227                 })(new RangeIterator(this));
 
 229         extractContents: function () {
 
 230                 // cache range and move anchor points
 
 231                 var range = this.cloneRange();
 
 232                 if (this.startContainer != this.commonAncestorContainer)
 
 233                         this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer));
 
 236                 return (function extractSubtree(iterator) {
 
 237                         for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) {
 
 238                                 iterator.hasPartialSubtree() ? node = node.cloneNode(false) : iterator.remove();
 
 239                                 if (iterator.hasPartialSubtree())
 
 240                                         node.appendChild(extractSubtree(iterator.getSubtreeIterator()));
 
 241                                 frag.appendChild(node);
 
 244                 })(new RangeIterator(range));
 
 246         deleteContents: function () {
 
 247                 // cache range and move anchor points
 
 248                 var range = this.cloneRange();
 
 249                 if (this.startContainer != this.commonAncestorContainer)
 
 250                         this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer));
 
 253                 (function deleteSubtree(iterator) {
 
 254                         while (iterator.next())
 
 255                                 iterator.hasPartialSubtree() ? deleteSubtree(iterator.getSubtreeIterator()) : iterator.remove();
 
 256                 })(new RangeIterator(range));
 
 258         insertNode: function (newNode) {
 
 259                 // set original anchor and insert node
 
 260                 if (DOMUtils.isDataNode(this.startContainer)) {
 
 261                         DOMUtils.splitDataNode(this.startContainer, this.startOffset);
 
 262                         this.startContainer.parentNode.insertBefore(newNode, this.startContainer.nextSibling);
 
 264                         this.startContainer.insertBefore(newNode, this.startContainer.childNodes[this.startOffset]);
 
 266                 // resync start anchor
 
 267                 this.setStart(this.startContainer, this.startOffset);
 
 269         surroundContents: function (newNode) {
 
 270                 // extract and surround contents
 
 271                 var content = this.extractContents();
 
 272                 this.insertNode(newNode);
 
 273                 newNode.appendChild(content);
 
 274                 this.selectNode(newNode);
 
 278         compareBoundaryPoints: function (how, sourceRange) {
 
 280                 var containerA, offsetA, containerB, offsetB;
 
 282                     case DOMRange.START_TO_START:
 
 283                     case DOMRange.START_TO_END:
 
 284                         containerA = this.startContainer;
 
 285                         offsetA = this.startOffset;
 
 287                     case DOMRange.END_TO_END:
 
 288                     case DOMRange.END_TO_START:
 
 289                         containerA = this.endContainer;
 
 290                         offsetA = this.endOffset;
 
 294                     case DOMRange.START_TO_START:
 
 295                     case DOMRange.END_TO_START:
 
 296                         containerB = sourceRange.startContainer;
 
 297                         offsetB = sourceRange.startOffset;
 
 299                     case DOMRange.START_TO_END:
 
 300                     case DOMRange.END_TO_END:
 
 301                         containerB = sourceRange.endContainer;
 
 302                         offsetB = sourceRange.endOffset;
 
 307                 return containerA.sourceIndex < containerB.sourceIndex ? -1 :
 
 308                     containerA.sourceIndex == containerB.sourceIndex ?
 
 309                         offsetA < offsetB ? -1 : offsetA == offsetB ? 0 : 1
 
 312         cloneRange: function () {
 
 313                 // return cloned range
 
 314                 var range = new DOMRange(this._document);
 
 315                 range.setStart(this.startContainer, this.startOffset);
 
 316                 range.setEnd(this.endContainer, this.endOffset);
 
 319         detach: function () {
 
 320 //[TODO] Releases Range from use to improve performance. 
 
 322         toString: function () {
 
 323                 return TextRangeUtils.convertFromDOMRange(this).text;
 
 325         createContextualFragment: function (tagString) {
 
 326                 // parse the tag string in a context node
 
 327                 var content = (DOMUtils.isDataNode(this.startContainer) ? this.startContainer.parentNode : this.startContainer).cloneNode(false);
 
 328                 content.innerHTML = tagString;
 
 329                 // return a document fragment from the created node
 
 330                 for (var fragment = this._document.createDocumentFragment(); content.firstChild; )
 
 331                         fragment.appendChild(content.firstChild);
 
 340 function RangeIterator(range) {
 
 345 //[TODO] ensure this works
 
 347         var root = range.commonAncestorContainer;
 
 348         this._next = range.startContainer == root && !DOMUtils.isDataNode(range.startContainer) ?
 
 349             range.startContainer.childNodes[range.startOffset] :
 
 350             DOMUtils.findClosestAncestor(root, range.startContainer);
 
 351         this._end = range.endContainer == root && !DOMUtils.isDataNode(range.endContainer) ?
 
 352             range.endContainer.childNodes[range.endOffset] :
 
 353             DOMUtils.findClosestAncestor(root, range.endContainer).nextSibling;
 
 356 RangeIterator.prototype = {
 
 359         // private properties
 
 365         hasNext: function () {
 
 370                 var current = this._current = this._next;
 
 371                 this._next = this._current && this._current.nextSibling != this._end ?
 
 372                     this._current.nextSibling : null;
 
 374                 // check for partial text nodes
 
 375                 if (DOMUtils.isDataNode(this._current)) {
 
 376                         if (this.range.endContainer == this._current)
 
 377                                 (current = current.cloneNode(true)).deleteData(this.range.endOffset, current.length - this.range.endOffset);
 
 378                         if (this.range.startContainer == this._current)
 
 379                                 (current = current.cloneNode(true)).deleteData(0, this.range.startOffset);
 
 383         remove: function () {
 
 384                 // check for partial text nodes
 
 385                 if (DOMUtils.isDataNode(this._current) &&
 
 386                     (this.range.startContainer == this._current || this.range.endContainer == this._current)) {
 
 387                         var start = this.range.startContainer == this._current ? this.range.startOffset : 0;
 
 388                         var end = this.range.endContainer == this._current ? this.range.endOffset : this._current.length;
 
 389                         this._current.deleteData(start, end - start);
 
 391                         this._current.parentNode.removeChild(this._current);
 
 393         hasPartialSubtree: function () {
 
 394                 // check if this node be partially selected
 
 395                 return !DOMUtils.isDataNode(this._current) &&
 
 396                     (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer) ||
 
 397                         DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer));
 
 399         getSubtreeIterator: function () {
 
 400                 // create a new range
 
 401                 var subRange = new DOMRange(this.range._document);
 
 402                 subRange.selectNodeContents(this._current);
 
 403                 // handle anchor points
 
 404                 if (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer))
 
 405                         subRange.setStart(this.range.startContainer, this.range.startOffset);
 
 406                 if (DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer))
 
 407                         subRange.setEnd(this.range.endContainer, this.range.endOffset);
 
 409                 return new RangeIterator(subRange);
 
 417 //[NOTE] This is a very shallow implementation of the Selection object, based on Webkit's
 
 418 // implementation and without redundant features. Complete selection manipulation is still
 
 419 // possible with just removeAllRanges/addRange/getRangeAt.
 
 421 function DOMSelection(document) {
 
 422         // save document parameter
 
 423         this._document = document;
 
 425         // add DOM selection handler
 
 426         var selection = this;
 
 427         document.attachEvent('onselectionchange', function () { selection._selectionChangeHandler(); });
 
 430 DOMSelection.prototype = {
 
 433         // private properties
 
 437         _selectionChangeHandler: function () {
 
 438                 // check if there exists a range
 
 439                 this.rangeCount = this._selectionExists(this._document.selection.createRange()) ? 1 : 0;
 
 441         _selectionExists: function (textRange) {
 
 442                 // checks if a created text range exists or is an editable cursor
 
 443                 return textRange.compareEndPoints('StartToEnd', textRange) != 0 ||
 
 444                     textRange.parentElement().isContentEditable;
 
 448         addRange: function (range) {
 
 449                 // add range or combine with existing range
 
 450                 var selection = this._document.selection.createRange(), textRange = TextRangeUtils.convertFromDOMRange(range);
 
 451                 if (!this._selectionExists(selection))
 
 458                         // only modify range if it intersects with current range
 
 459                         if (textRange.compareEndPoints('StartToStart', selection) == -1)
 
 460                                 if (textRange.compareEndPoints('StartToEnd', selection) > -1 &&
 
 461                                     textRange.compareEndPoints('EndToEnd', selection) == -1)
 
 462                                         selection.setEndPoint('StartToStart', textRange);
 
 464                                 if (textRange.compareEndPoints('EndToStart', selection) < 1 &&
 
 465                                     textRange.compareEndPoints('EndToEnd', selection) > -1)
 
 466                                         selection.setEndPoint('EndToEnd', textRange);
 
 470         removeAllRanges: function () {
 
 472                 this._document.selection.empty();
 
 474         getRangeAt: function (index) {
 
 475                 // return any existing selection, or a cursor position in content editable mode
 
 476                 var textRange = this._document.selection.createRange();
 
 477                 if (this._selectionExists(textRange))
 
 478                         return TextRangeUtils.convertToDOMRange(textRange, this._document);
 
 481         toString: function () {
 
 482                 // get selection text
 
 483                 return this._document.selection.createRange().text;
 
 491 document.createRange = function () {
 
 492         return new DOMRange(document);
 
 495 var selection = new DOMSelection(document);
 
 496 window.getSelection = function () {
 
 500 //[TODO] expose DOMRange/DOMSelection to window.?