InternetExplorerRange.js

Summary

Provides a W3C Range implementation under Internet Explorer.

History: The original code was written by Jorgen Horstink (http://jorgenhorstink.nl/2006/03/11/w3c-range-in-internet-explorer/). It was extensively modified by David Kingma. This version has been adapted for use with Mozile by James A. Overton. Key changes include wrapping the objects in the Mozile namespace, so as to minimize the impact on other scripts in the same page.

Version: 0.8 $Id: overview-summary-InternetExplorerRange.js.html,v 1.12 2008/02/20 18:47:09 jameso Exp $

Author: James A. Overton


Class Summary
mozile.dom.InternetExplorerRange  

/* ***** BEGIN LICENSE BLOCK *****
 * Licensed under Version: MPL 1.1/GPL 2.0/LGPL 2.1
 * Full Terms at http://mozile.mozdev.org/0.8/LICENSE
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Jorgen Horstink and David Kingma's code.
 *
 * The Initial Developers of the Original Code are Jorgen Horstink and David Kingma.
 * Portions created by the Initial Developer are Copyright (C) 2005-2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *	James A. Overton <james@overton.ca>
 *
 * ***** END LICENSE BLOCK ***** */

/**
 * @fileoverview Provides a W3C Range implementation under Internet Explorer.
 * <p>History: The original code was written by Jorgen Horstink (http://jorgenhorstink.nl/2006/03/11/w3c-range-in-internet-explorer/).
 * It was extensively modified by David Kingma.
 * This version has been adapted for use with Mozile by James A. Overton.
 * Key changes include wrapping the objects in the Mozile namespace, so as to minimize the impact on other scripts in the same page.
 *
 * @link http://mozile.mozdev.org 
 * @author James A. Overton <james@overton.ca>
 * @version 0.8
 * $Id: overview-summary-InternetExplorerRange.js.html,v 1.12 2008/02/20 18:47:09 jameso Exp $
 */

mozile.require("mozile.dom");
mozile.require("mozile.xpath");
mozile.provide("mozile.dom.InternetExplorerRange");


/**
 * Range object, see http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
 * @constructor
 */
mozile.dom.InternetExplorerRange = function(range) {
	/**
	 * A reference to an IE native TextRange object.
	 * @private
	 * @type TextRange
	 */
	this._range = null;

	if(range)
		this._range = range;
	else {
		this._range = mozile.document.body.createTextRange();
	}

	/**
	 * A boolean indicating whether the range's start and end points are at the same position.
	 * @type Boolean
	 */
	this.collapsed = null;

	/**
	 * The deepest Node that contains the startContainer and endContainer Nodes.
	 * @type Node
	 */
	this.commonAncestorContainer = null;
	
	/**
	 * The Node within which the Range starts.
	 * @type Node
	 */
	this.startContainer = null;

	/**
	 * A number representing where in the endContainer the Range starts.
	 * @type Integer
	 */
	this.startOffset = null;

	/**
	 * The Node within which the Range ends.
	 * @type Node
	 */
	this.endContainer = null;

	/**
	 * A number representing where in the endContainer the Range ends.
	 * @type Integer
	 */
	this.endOffset = null;
}


/**
 * Initializes the properties of this range according to the internal
 * IE range (_range).
 * @private
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype._init = function () {
	
	//startPoint
	var beginRange = this._range.duplicate();
	beginRange.collapse(true);
	var position = this._getPosition(beginRange);
	this.startContainer = position.node;
	this.startOffset = position.offset;

	//endPoint
	var endRange = this._range.duplicate();
	endRange.collapse(false);
	position = this._getPosition(endRange);
	this.endContainer = position.node;
	this.endOffset = position.offset;
	
	this._commonAncestorContainer();
	this._collapsed();
	
}


/**
 * Takes an Internet Explorer TextRange object and returns a W3C node and offset pair.
 * <p>The basic method is as follows:
 * <ul><li>Create a new range with its start at the beginning of the element and its end at the target position. Set the rangeLength to the length of the range's text.
 * <li>Starting with the first child, for each child:
 * <ul><li>If the child is a text node, and its length is less than the rangeLength, then move the range's start by the text node's length.
 * <li>If the child is a text node and its length is less than the rangeLength then we've found the target. Return the node and use the remaining rangeLength as the offset.
 * <li>If the child is an element, move the range's start by the length of the element's innerText.
 * </ul></ul>
 * <p>This algorithm works fastest when the target is close to the beginning of the parent element.
 * The current implementation is smart enough pick the closest end point of the parent element (i.e. the start or the end), and work forward or backward from there.
 * @private
 * @param {TextRange} textRange A TextRange object. Its start position will be found.
 * @type Object
 * @return An object with "node" and "offset" properties.
 */
mozile.dom.InternetExplorerRange.prototype._getPosition = function(textRange) {
	var element = textRange.parentElement();
	var range = element.ownerDocument.body.createTextRange();
	range.moveToElementText(element);
	range.setEndPoint("EndToStart", textRange);
	var rangeLength = range.text.length;

	// Choose Direction
	if(rangeLength < element.innerText.length / 2) {
		var direction = 1;
		var node = element.firstChild;
	}
	else {
		direction = -1;
		node = element.lastChild;
		range.moveToElementText(element);
		range.setEndPoint("StartToStart", textRange);
		rangeLength = range.text.length;
	}

	// Loop through child nodes
	while(node) {
		switch(node.nodeType) {
			case mozile.dom.TEXT_NODE:
				nodeLength = node.data.length;
				if(nodeLength < rangeLength) {
					var difference = rangeLength - nodeLength;
					if(direction == 1) range.moveStart("character", difference);
					else range.moveEnd("character", -difference);
					rangeLength = difference;
				}
				else {
					if(direction == 1) return {node: node, offset: rangeLength};
					else return {node: node, offset: nodeLength - rangeLength};
				}
				break;

			case mozile.dom.ELEMENT_NODE:
				nodeLength = node.innerText.length;
				if(direction == 1) range.moveStart("character", nodeLength);
				else range.moveEnd("character", -nodeLength);
				rangeLength = rangeLength - nodeLength;
				break;
		}
	
		if(direction == 1) node = node.nextSibling;
		else node = node.previousSibling;
	}


	// TODO: This should throw a warning.
	//throw("Error in mozile.dom.InternetExplorerRange._getPosition: Ran out of child nodes before the range '"+ textRange.text +"' inside '"+ mozile.xpath.getXPath(element) +"' was found.");
	
	// The TextRange was not found. Return a reasonable value instead.
	return {node: element, offset: 0};
	
}


/**
 * Find the TextRange offset for a given text node and offset. Effectively the opposite of getPosition().
 * The method used is to count the innerText length for elements and the data length for text nodes.
 * @private
 * @param {Text} startNode The target text node.
 * @param {Integer} startOffset
 * @type Integer
 */
mozile.dom.InternetExplorerRange.prototype._getOffset = function (startNode, startOffset) {
	var node, moveCharacters;
	if(startNode.nodeType == mozile.dom.TEXT_NODE) {
		moveCharacters = startOffset;
		node = startNode.previousSibling;
	}
	else if(startNode.nodeType == mozile.dom.ELEMENT_NODE) {
		moveCharacters = 0;
		if(startOffset > 0) node = startNode.childNodes[startOffset - 1];
		else return 0;
	}
	else {
		mozile.debug.inform("mozile.dom.InternetExplorerRange.prototype._getOffset", "Bad node given: "+ mozile.xpath.getXPath(startNode));
		return 0;
	}

	while (node) {
		var nodeLength = 0;
		if(node.nodeType == mozile.dom.ELEMENT_NODE) {
			nodeLength = node.innerText.length;
			if(this._isChildless(node)) nodeLength = 1; // Tweak childless nodes.
			if(this._isBlock(node)) nodeLength++; // Tweak block level elements.
			//if(nodeLength == 0) nodeLength++; // minimum length is 1
		}
		else if(node.nodeType == mozile.dom.TEXT_NODE) {
			 nodeLength = node.data.length;
		}
		moveCharacters += nodeLength;
		node = node.previousSibling;
	}
	
	return moveCharacters;
}

/**
 * Internet Explorer pads certain elements with an extra space at the end. This method detects those elements.
 * TODO: This method should be smarter about detecting non-HTML or using CSS.
 * @param {Node} node The node to check.
 * @type Boolean
 */
mozile.dom.InternetExplorerRange.prototype._isBlock = function(node) {
	switch (node.nodeName.toLowerCase()) {
		case 'p':
		case 'div':
		case 'h1':
		case 'h2':
		case 'h3':
		case 'h4':
		case 'h5':
		case 'h6':
		case 'pre':
			return true;
	}
	return false;
}

/**
 * Internet Explorer sets the length of certain elements which cannot have child nodes to 1.
 * TODO: Complete this list.
 * @param {Node} node The node to check.
 * @type Boolean
 */
mozile.dom.InternetExplorerRange.prototype._isChildless = function(node) {
	switch (node.nodeName.toLowerCase()) {
		case 'img':
		case 'br':
		case 'hr':
			return true;
	}
	return false;
}


// == positioning ==
/**
 * Sets the start position of a Range
 * If the startNode is a Node of type Text, Comment, or CDATASection, then startOffset 
 * is the number of characters from the start of startNode. For other Node types, 
 * startOffset is the number of child nodes between the start of the startNode.
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.setStart = function (startNode, startOffset) {
	var container = startNode;
	if(startNode.nodeType == mozile.dom.TEXT_NODE || 
		startNode.nodeType == mozile.dom.COMMENT_NODE || 
		startNode.nodeType == mozile.dom.CDATA_SECTION_NODE) {
		container = container.parentNode;
	}

	var copyRange = this._range.duplicate();
	copyRange.moveToElementText(container);
	copyRange.collapse(true);
	copyRange.move('Character', this._getOffset(startNode, startOffset));
	this._range.setEndPoint('StartToStart', copyRange);

	//update object properties
	this.startContainer = startNode;
	this.startOffset    = startOffset;
	if (this.endContainer == null && this.endOffset == null) {
		this.endContainer = startNode;
		this.endOffset    = startOffset;
	}
	this._commonAncestorContainer();
	this._collapsed();
}


/**
 * Sets the end position of a Range.
 * Creates a clone of the current range, moves it to the desired spot
 * and the we move the endPoint of the current range to the clones endpoint
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.setEnd = function (endNode, endOffset) {
	// Store the start of the range
	var copyRange = this._range.duplicate();
	copyRange.collapse(true);
	
	var container = endNode;
	if(endNode.nodeType == mozile.dom.TEXT_NODE || 
		endNode.nodeType == mozile.dom.COMMENT_NODE || 
		endNode.nodeType == mozile.dom.CDATA_SECTION_NODE) {
		container = container.parentNode;
	}

	copyRange = this._range.duplicate();
	copyRange.moveToElementText(container);
	copyRange.collapse(true);
	copyRange.move('Character', this._getOffset(endNode, endOffset));
	this._range.setEndPoint('EndToEnd', copyRange);
	
	//update object properties
	this.endContainer = endNode;
	this.endOffset    = endOffset;
	if (this.startContainer == null && this.startOffset == null) {
		this.startContainer = endNode;
		this.startOffset    = endOffset;
	}
	this._commonAncestorContainer();
	this._collapsed();
}


/**
 * Sets the start position of a Range relative to another Node.
 * The parent Node of the start of the Range will be the same as 
 * that for the referenceNode.
 * @param {Node} referenceNode
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.setStartBefore = function (referenceNode) {
	this.setStart(referenceNode.parentNode, mozile.dom.getIndex(referenceNode));
};

/**
 * Sets the start position of a Range relative to another Node.
 * @param {Node} referenceNode
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.setStartAfter = function (referenceNode) {
	this.setStart(referenceNode.parentNode, mozile.dom.getIndex(referenceNode) + 1);
};

/**
 * Sets the end position of a Range relative to another Node.
 * @param {Node} referenceNode
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.setEndBefore = function (referenceNode) {
	this.setEnd(referenceNode.parentNode, mozile.dom.getIndex(referenceNode));
};

/**
 * Sets the end position of a Range relative to another Node.
 * @param {Node} referenceNode
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.setEndAfter = function (referenceNode) {
	this.setEnd(referenceNode.parentNode, mozile.dom.getIndex(referenceNode) + 1);
};

/**
 * Sets the Range to contain the node and its contents.
 * The parent Node of the start and end of the Range will be the same as
 * the parent of the referenceNode.
 * @param {Node} referenceNode
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.selectNode = function (referenceNode) {
	this.setStartBefore(referenceNode);
	this.setEndAfter(referenceNode);
};

/**
 * Sets the Range to contain the contents of a Node.
 * The parent Node of the start and end of the Range will be the referenceNode. The 
 * startOffset is 0, and the endOffset is the number of child Nodes or number of characters 
 * contained in the reference node.
 * @param {Node} referenceNode
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.selectNodeContents = function (referenceNode) {
	this.setStart(referenceNode, 0);
	if(referenceNode.nodeType == mozile.dom.TEXT_NODE)
		this.setEnd(referenceNode, referenceNode.data.length);
	else
		this.setEnd(referenceNode, referenceNode.childNodes.length);
};

/**
 * Collapses the Range to one of its boundary points.
 * @param {Boolean} toStart When true the Range is collapsed to the start position, when false to the end position.
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.collapse = function (toStart) {
	this._range.collapse(toStart);
	
	//update the properties
	if(toStart) {
		this.endContainer = this.startContainer;
		this.endOffset = this.startOffset;
	} else {
		this.startContainer = this.endContainer;
		this.startOffset = this.endOffset;
	}
	this._commonAncestorContainer();
	this._collapsed();
};

// == editing ==
/**
 * Returns a document fragment copying the nodes of a Range.
 * Partially selected nodes include the parent tags necessary to make the 
 * document fragment valid.
 * @type Range
 */
mozile.dom.InternetExplorerRange.prototype.cloneContents = function () {
	var df = mozile.document.createDocumentFragment();
	
	var container = this.commonAncestorContainer;
	if(container.nodeType == mozile.dom.TEXT_NODE) {
		df.appendChild(mozile.document.createTextNode(this._range.text));
		return df;
	}
	
	var startNode = this.startContainer;
	if(this.startContainer.nodeType != mozile.dom.TEXT_NODE)
		startNode = this.startContainer.childNodes[this.startOffset];
	var endNode = this.endContainer;
	if(this.endContainer.nodeType != mozile.dom.TEXT_NODE)
		endNode = this.endContainer.childNodes[this.endOffset - 1];

	if(startNode == endNode) {
		df.appendChild(startNode.cloneNode(true));
		return df;
	}

	// Walk the tree.
	var current = container.firstChild;
	var parent = null;
	var clone;
	while(current) {
		//alert(current.nodeName +"\n"+ df.innerHTML);
		// Watch for the start node, then start adding nodes.
		if(!parent) {
			if(mozile.dom.isAncestorOf(current, startNode, container)) {
				parent = df;
			}
			// Skip this node.
			else {
				current = current.nextSibling;
				continue;
			}
		}

		// Clone the node.
		if(current == startNode && this.startContainer.nodeType == mozile.dom.TEXT_NODE) {
			content = this.startContainer.data.substring(this.startOffset);
			parent.appendChild(mozile.document.createTextNode(content));
		}
		else if(current == endNode) {
			if(this.endContainer.nodeType == mozile.dom.TEXT_NODE) {
				content = this.endContainer.data.substring(0, this.endOffset);
				parent.appendChild(mozile.document.createTextNode(content));
			}
			else parent.appendChild(endNode.cloneNode(false));
			// We're done.
			break; 
		}
		else {
			clone = current.cloneNode(false);
			parent.appendChild(clone);
		}
		
		// Move
		if(current.firstChild) {
			parent = clone;
			current = current.firstChild;
		}
		else if(current.nextSibling) {
			current = current.nextSibling;
		}
		// Climb the tree
		else while(current) {
			if(current.parentNode) {
				parent = parent.parentNode;
				current = current.parentNode;
				if(current.nextSibling) {
					current = current.nextSibling;
					break;
				}
			}
			else current = null;
		}
	}
	
	return df;
};

/**
 * Removes the contents of a Range from the document.
 * Unlike extractContents, this method does not return a documentFragment 
 * containing the deleted content. 
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.deleteContents = function () {
	this._range.pasteHTML('');//This is incorrect, it might also delete the container

	//update properties
	this.endContainer = this.startContainer;
	this.endOffset = this.startOffset;
	this._commonAncestorContainer();
	this._collapsed();
};

/**
 * Moves contents of a Range from the document tree into a document fragment.
 * @type DocumentFragment
 */
mozile.dom.InternetExplorerRange.prototype.extractContents = function () {
	var fragment = this.cloneContents();
	this.deleteContents();
	return fragment;
};

/**
 * Insert a node at the start of a Range.
 * newNode is inserted at the start boundary point of the Range. If the newNodes 
 * is to be added to a text Node, that Node is split at the insertion point, and 
 * the insertion occurs between the two text Nodes
 * 
 * If newNode is a document fragment, the children of the document fragment are  
 * inserted instead.
 * @param {Node} newNode The node to insert.
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.insertNode = function (newNode) {
	//salert('mozile.dom.InternetExplorerRange.insertNode() is not implemented yet');
	if(this.startContainer.nodeType == mozile.dom.TEXT_NODE){
		this.startContainer.splitText(this.startOffset);
		this.startContainer.parentNode.insertBefore(newNode, this.startContainer.nextSibling);
		this.setStart(this.startContainer, this.startOffset);
		//Mozilla collapses, is this needed?
		//this.collapse(true);
		//this._range.select();
		return;
	} else { //Element node
		var parentNode = this.startContainer.parentNode;
		if(this.startContainer.childNodes.length == this.startOffset) {
			parentNode.appendChild(newNode);
		}else {
			this.startContainer.insertBefore(newNode, this.startContainer.childNodes.item(this.startOffset));
			this.setStart(this.startContainer, this.startOffset+1);
			//this._range.select();
			return;
		}
	}
};

/**
 * Moves content of a Range into a new node.
 * SurroundContents is equivalent to newNode.appendChild(range.extractContents());
 * range.insertNode(newNode). After surrounding, the boundary points of the Range 
 * include newNode.
 * @param {Node} newNode The node to select.
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.surroundContents = function (newNode) {
	newNode.appendChild(this.extractContents());
	this.insertNode(newNode);
};

// == Other ==
/**
 * NOT IMPLEMENTED. Compares the boundary points of two Ranges
 * Returns a number, 1, 0, or -1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange
 * Any of the following constants can be passed as the value of how parameter:
 * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range.
 * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range.
 * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range.
 * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range
 * @param {Integer} how A code for the comparison method.
 * @param {Range} sourceRange
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.compareBoundaryPoints = function (how, sourceRange) {
	alert('mozile.dom.InternetExplorerRange.compareBoundaryPoints() is not implemented yet');
};

/**
 * Returns a Range object with boundary points identical to the cloned Range.
 * @type Range
 */
mozile.dom.InternetExplorerRange.prototype.cloneRange = function () {
	var r = new mozile.dom.InternetExplorerRange(this._range.duplicate());
	var properties = ["startContainer", "startOffset", "endContainer", "endOffset", "commonAncestorContainer", "collapsed"];
	for(var i=0; i < properties.length; i++) {
		r[properties[i]] = this[properties[i]];
	}
	return r;
};

/**
 * Releases Range from use to improve performance.
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.detach = function () {};

/**
 * Returns the text of the Range.
 * @type String
 */
mozile.dom.InternetExplorerRange.prototype.toString = function () {
	return this._range.text;
};

/**
 * Finds the commonAncestorComtainer.
 * @private
 * @type Element
 */
mozile.dom.InternetExplorerRange.prototype._commonAncestorContainer = function () {
	if(this.startContainer == null || this.endContainer == null){
		this.commonAncestorContainer = null;
		return;
	}
	if(this.startContainer == this.endContainer) {
		this.commonAncestorContainer = this.startContainer;	
	}
	else {
		this.commonAncestorContainer = mozile.dom.getCommonAncestor(this.startContainer, this.endContainer);
	}
}

/**
 * Check to see if this selection is collapsed, and assign a value to this.collapsed.
 * @private
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype._collapsed = function () {
	this.collapsed = (this.startContainer == this.endContainer && this.startOffset == this.endOffset);
}



/**
 * Store the details about this range in an object which can be used to restore the range.
 * @type Object
 */
mozile.dom.InternetExplorerRange.prototype.store = mozile.dom.Range.prototype.store;

/**
 * Takes a stored range object and creates a new range with the same properties.
 * @param {Object} state A state object from Selection.store() or Range.store().
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.restore = mozile.dom.Range.prototype.restore;



Documentation generated by JSDoc on Wed Feb 20 13:25:28 2008