/*
DOM Ranges for Internet Explorer (m1)
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;
};
window.Range = DOMRange; // exposed for make MaSha work!
})();