-/*
- 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.?
-
-})();