InsertionPoint.js

Summary

Tools for editing operations.

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

Author: James A. Overton


Class Summary
mozile.edit.InsertionPoint  

/* ***** 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 James A. Overton's code (james@overton.ca).
 *
 * The Initial Developer of the Original Code is James A. Overton.
 * 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 Tools for editing operations.
 * @link http://mozile.mozdev.org 
 * @author James A. Overton <james@overton.ca>
 * @version 0.8
 * $Id: overview-summary-InsertionPoint.js.html,v 1.10 2008/02/20 18:47:09 jameso Exp $
 */

mozile.require("mozile.dom");
mozile.require("mozile.edit");
mozile.provide("mozile.edit.InsertionPoint");


/** 
 * An insertion point is the pair of either a) a text node and an offset within the text, or b) an element and an offset among its child nodes. This corresponds to the method used to denote points by the Selection and Range objects. 
 * @constructor
 * @param {Node} node
 * @param {Integer} offset The offset within the node.
 */
mozile.edit.InsertionPoint = function(node, offset) {
	/**
	 * Stores the current node.
	 * @private
	 */ 
	this._node = node;

	/**
	 * Stores the current offset.
	 * @private
	 */
	this._offset = offset;
}

// Define some regular expressions.
/**
 * Matches whitespace at the beginning of a string.
 * @private
 * @type RegExp
 */
mozile.edit.InsertionPoint.prototype._matchLeadingWS = /^(\s*)/;

/**
 * Matches whitespace at the end of a string.
 * @private
 * @type RegExp
 */
mozile.edit.InsertionPoint.prototype._matchTrailingWS = /(\s*)$/;

/**
 * Matches any non-whitespace character.
 * @private
 * @type RegExp
 */
mozile.edit.InsertionPoint.prototype._matchNonWS = /\S/;

/**
 * Gets the current node.
 * @type Node
 */
mozile.edit.InsertionPoint.prototype.getNode = function() { return this._node; }

/**
 * Gets the offset in the current node.
 * @type Integer
 */
mozile.edit.InsertionPoint.prototype.getOffset = function() { 
	if(this._offset < 0) this._offset = 0;
	// TODO: Handle too-long case.
	return this._offset; 
}

/**
 * Returns a string representation of the IP.
 * @type String
 */
mozile.edit.InsertionPoint.prototype.toString = function() { 
	return "Insertion Point: "+ mozile.xpath.getXPath(this._node) +" "+ this._offset;
}

/**
 * If the IP is inside a text node, get the character at the index.
 * @type String
 */
mozile.edit.InsertionPoint.prototype.charAt = function() { 
	var node = this.getNode();
	if(node && node.nodeType != mozile.dom.TEXT_NODE) return "";
	if(node.data.length <= this.getOffset()) return "";
	return node.data.charAt(this.getOffset());
}


/**
 * Collapses the selection to the current IP.
 * @type Void
 */
mozile.edit.InsertionPoint.prototype.select = function() {
	try {
		var selection = mozile.dom.selection.get();
		selection.collapse(this.getNode(), this.getOffset());
	} catch(e) {
		mozile.debug.debug("mozile.edit.InsertionPoint.prototype.select", "Bad collapse for IP "+ mozile.xpath.getXPath(this.getNode()) +" "+ this.getOffset() +"\n"+ mozile.dumpError(e));
	}
}

/**
 * Extends the selection to the IP.
 * @type Void
 */
mozile.edit.InsertionPoint.prototype.extend = function() {
	try {
		var selection = mozile.dom.selection.get();
		selection.extend(this.getNode(), this.getOffset());
	} catch(e) { 
		mozile.debug.debug("mozile.edit.InsertionPoint.prototype.extend", "Bad extend for IP "+ mozile.xpath.getXPath(this.getNode()) +" "+ this.getOffset() +"\n"+ mozile.dumpError(e));
	}
}

/**
 * Sets the node and offset to the next insertion point.
 * @param {Boolean} extraStep Optional. Defaults to "true". When "false" the method will not move an extra offset step.
 * @param {Element} container Optional. The insertion point will stay inside this container. By default it is the node's editable container.
 * @type Boolean
 */
mozile.edit.InsertionPoint.prototype.next = function(extraStep, container) {
	return this.seek(mozile.edit.NEXT, extraStep, container);
}

/**
 * Sets the node and offset to the previous insertion point.
 * @param {Boolean} extraStep Optional. Defaults to "true". When "false" the method will not move an extra offset step.
 * @param {Element} container Optional. The insertion point will stay inside this container. By default it is the node's editable container.
 * @type Boolean
 */
mozile.edit.InsertionPoint.prototype.previous = function(extraStep, container) {
	return this.seek(mozile.edit.PREVIOUS, extraStep, container);
}

/**
 * Sets the node and offset to the next insertion point.
 * <p>If the node is not a text node, or the offset and direction will mean that the IP leaves the node, then seekNode is returned instead.
 * <p>Otherwise we are inside a text node and have to worry about the XML white space rules. We want to treat adjacent whitespace as a single character. So we measure the length of the whitespace after the offset (if any). Then "moveBy" is set based on the length of the result and the CSS white-space mode. If the length takes the offset to the end of the node, seekNode is called.
 * @param {Integer} direction A coded integer. Can be NEXT (1) or PREVIOUS (-1).
 * @param {Boolean} extraStep Optional. Defaults to "true". When "false" the method will not move an extra offset step.
 * @param {Element} container Optional. The insertion point will stay inside this container. By default it is the node's editable container.
 * @type Boolean
 */
mozile.edit.InsertionPoint.prototype.seek = function(direction, extraStep, container) {
	var node = this.getNode();
	var offset = this.getOffset();
	if(!node || typeof(offset) == "undefined") return false;

	// Seek next node if: 
	// this node is an element OR
	// this node not supposed to contain text and is empty OR
	// we're at the beginning of the text node and we're moving left OR
	// we're at the end of the text node and we're moving right OR
	// the text node only contains an empty token and we're moving right.
	if(node.nodeType != mozile.dom.TEXT_NODE ||
		(!mozile.edit.mayContainText(node) && mozile.edit.isEmpty(node)) ||
		(direction == mozile.edit.PREVIOUS && offset == 0) ||
		(direction == mozile.edit.NEXT && offset == node.data.length) ||
		(direction == mozile.edit.NEXT && mozile.edit.isEmptyToken(node)) ) {
		return this.seekNode(direction, extraStep, container);
	}
	else offset = offset + direction;
	if(!node || typeof(offset) == "undefined") return false;

	// Move to the leftmost position in an empty token.
	if(mozile.edit.isEmptyToken(node)) {
		this._offset = 0;
		return true;
	}

	// Measure the length of the white-space and the distance to the first alternateSpace token. 
	var content = node.data;
	var substring, result, altSpaceIndex;
	if(direction == mozile.edit.NEXT) {
		substring = content.substring(this.getOffset());
		result = substring.match(this._matchLeadingWS);
		if(mozile.alternateSpace) 
			altSpaceIndex = substring.indexOf(mozile.alternateSpace);
	}
	else {
		substring = content.substring(0, this.getOffset());
		result = substring.match(this._matchTrailingWS);
		if(mozile.alternateSpace) {
			altSpaceIndex = substring.length;
			altSpaceIndex -= substring.lastIndexOf(mozile.alternateSpace) + 1;
		}
	}
	// Use the smallest length as wsLength.
	var wsLength = result[0].length;
	if(Number(altSpaceIndex) != NaN && altSpaceIndex > -1 &&
		altSpaceIndex < wsLength) {
		wsLength = altSpaceIndex;
	}

	// Skip over white space, if necessary.	
	var moveBy = 0;
	if(wsLength < 2) moveBy = direction;
	else if(mozile.dom.getStyle(node.parentNode, "white-space").toLowerCase() == "pre") moveBy = direction;
	else if(wsLength < substring.length) moveBy = wsLength * direction;
	else if(wsLength == substring.length) 
		return this.seekNode(direction, extraStep, container); 
	else throw Error("Unhandled case in InsertionPoint.seek()");

	this._node = node;
	this._offset = this.getOffset() + moveBy;
	return true;
}

/**
 * Seeks the next node which allows text to be inserted.
 * <p>The method involves a treeWalker, but unfortunately the setup section is complicated. The chief cause of the complexity is that, if the node is a first child, then we do not want the parentNode but the parentNode's previousNode.
 * <p>The method is to build a treeWalker, set the currentNode to this.getNode(), and move through the tree working as follows:
 * <ul>
 *   <li>Seeks the next text node, unless it finds two consecutive non edtiable elements (comments are optional), in which case the insertion point is inserted between them.
 * </ul>
 * @param {Integer} direction A coded integer. Can be NEXT (1) or PREVIOUS (-1).
 * @param {Boolean} extraStep Optional. Defaults to "true". When "false" the method will not move an extra offset step.
 * @param {Element} container Optional. The insertion point will stay inside this container. By default it is the node's editable container.
 * @type Void
 */
mozile.edit.InsertionPoint.prototype.seekNode = function(direction, extraStep, container) {
	if(extraStep !== false) extraStep = true;
	var node = this.getNode();
	if(!node) return false;

	// Setup element. Offset when the direction is NEXT.
	var offset = this.getOffset();
	if(direction == mozile.edit.NEXT && offset > 0) offset--;
	if(node.nodeType == mozile.dom.ELEMENT_NODE && 
		node.childNodes[offset]) 
		node = node.childNodes[offset];

	// Setup TreeWalker.
	if(!container) mozile.edit.getContainer(node);
	if(!container) container = mozile.document.documentElement;
	var treeWalker = document.createTreeWalker(container, mozile.dom.NodeFilter.SHOW_ALL, null, false);
	treeWalker.currentNode = node;

	// Seek the next eligbile node.
	var startNode = node;
	var IP, tempNode, lastNode, nextNode;
	while(node) {
		// Move to the next node.
		lastNode = node;
		if(direction == mozile.edit.NEXT) node = treeWalker.nextNode();
		else {
			tempNode = node;
			node = treeWalker.previousNode();
			while(node && node.firstChild == tempNode) {
				tempNode = node;
				node = treeWalker.previousNode();
			}
		}
		if(!node) break;
		//alert(mozile.xpath.getXPath(node));
		
		// Get an Insertion Point.
		IP = mozile.edit.getInsertionPoint(node, direction);
		if(IP) {
			this._node = IP.getNode();
			this._offset = IP.getOffset();
			if(mozile.edit.isEmptyToken(this._node)) this._offset = 0;
			// When entering inline elements move an extra step.
			else if(extraStep && mozile.edit.mayContainText(lastNode) && 
				mozile.edit.getParentBlock(node) == 
				mozile.edit.getParentBlock(startNode)) 
				this._offset = this._offset + direction;
			// When passing comments move an extra step.
			else if(extraStep && lastNode.nodeType == mozile.dom.COMMENT_NODE && 
				node == IP.getNode())
				this._offset = this._offset + direction;
			return true;
		}
		
		// If there's no IP, look ahead to the next node.
		nextNode = node;
		while(nextNode) {
			if(direction == mozile.edit.NEXT) nextNode = nextNode.nextSibling;
			else nextNode = nextNode.previousSibling;
			if(nextNode && nextNode.nodeType == mozile.dom.COMMENT_NODE) continue;
			else break;
		}
		if(nextNode) {
			IP = mozile.edit.getInsertionPoint(nextNode, direction);
			if(IP) continue; // continue to the next iteration of the loop.
		}
		
		// Settle for an IP between elements.
		if(node.nodeType == mozile.dom.ELEMENT_NODE && 
			mozile.edit.mayContainText(node.parentNode)) {
			this._node = node.parentNode;
			this._offset = mozile.dom.getIndex(node);
			// Offset by one. 
			if(direction == mozile.edit.NEXT && 
				this._offset < this._node.childNodes.length) 
				this._offset++;
			// Move the cursor to the left of any comments.
			while(this._node.childNodes[this._offset-1] && 
				this._node.childNodes[this._offset-1].nodeType == 
				mozile.dom.COMMENT_NODE) 
				this._offset--;
			return true;
		}
	}
	
	return false;
}








/**
 * Create a new insertion point using the selection's focus.
 * @param {Boolean} force Optional. When true the fact that the given node is not editable is ignored.
 * @type mozile.edit.InsertionPoint
 */
mozile.dom.Selection.prototype.getInsertionPoint = function(force) {
	if(!this.focusNode || this.focusOffset == null) return null;
	else return new mozile.edit.InsertionPoint(this.focusNode, this.focusOffset, force);
}

// Copy this method to the IE Selection object.
if(mozile.dom.InternetExplorerSelection) {
	/**
	 * Create a new insertion point using the selection's focus.
	 * @type mozile.edit.InsertionPoint
	 */
	mozile.dom.InternetExplorerSelection.prototype.getInsertionPoint = mozile.dom.Selection.prototype.getInsertionPoint;
}


/**
 * Get the first insertion point in the given node and the given direction.
 * If the direction is "next" then the first point is returned. If the direction is "previous" then the last point us returned.
 * @param {Node} node The node to search for an insertion point.
 * @param {Integer} direction A coded integer. Can be NEXT (1) or PREVIOUS (-1).
 * @param {Boolean} force Optional. When true the fact that the given node is not editable is ignored.
 * @type mozile.edit.InsertionPoint
 */
mozile.edit.getInsertionPoint = function(node, direction, force) {
	if(!node) return false;
	var offset, IP;
	
	if(!mozile.edit.isEmpty(node) || mozile.edit.mayContainText(node) || force) {
		if(node.nodeType == mozile.dom.TEXT_NODE) {
			if(direction == mozile.edit.NEXT) offset = 0;
			else if(mozile.edit.isEmptyToken(node)) offset = 0;
			else offset = node.data.length;
			return new mozile.edit.InsertionPoint(node, offset);
		}
		
		// Try to dig into this node.
		if(direction == mozile.edit.NEXT) IP = mozile.edit.getInsertionPoint(node.firstChild, direction, force);
		else IP = mozile.edit.getInsertionPoint(node.lastChild, direction, force);
		if(IP) return IP;

		// Set the IP at the beginning or the end.
		if(direction == mozile.edit.NEXT) offset = 0;
		else offset = node.childNodes.length;
		return new mozile.edit.InsertionPoint(node, offset);
	}

	return null;
}




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