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.?