InsertionPoint.js

Summary

Tools for editing operations.

Version: 0.8 $Id: overview-summary-InsertionPoint.js.html,v 1.7 2006/08/23 23:30:17 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.7 2006/08/23 23:30:17 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 Integer
 */
mozile.edit.InsertionPoint.prototype.toString = function() { 
	return "Insertion Point: "+ mozile.xpath.getXPath(this._node) +" "+ this._offset;
}


/**
 * 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.
 * @type Void
 */
mozile.edit.InsertionPoint.prototype.next = function() {
	this.seek(mozile.edit.NEXT);
}

/**
 * Sets the node and offset to the previous insertion point.
 * @type Void
 */
mozile.edit.InsertionPoint.prototype.previous = function() {
	this.seek(mozile.edit.PREVIOUS);
}

/**
 * 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).
 * @type Void
 */
mozile.edit.InsertionPoint.prototype.seek = function(direction) {
	var node = this.getNode();
	var offset = this.getOffset();
	if(!node || typeof(offset) == "undefined") return false;

	if(node.nodeType != mozile.dom.TEXT_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);
	}
	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);
	}
	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.
 * @type Void
 */
mozile.edit.InsertionPoint.prototype.seekNode = function(direction, extraStep) {
	if(extraStep !== false) extraStep = true;
	var node = this.getNode();
	if(!node) return false;

	var startNode = node;

	// Setup
	var container = mozile.edit.getContainer(node);
	if(!container) container = document.documentElement;
	var treeWalker = document.createTreeWalker(container, mozile.dom.NodeFilter.SHOW_ALL, null, false);
	// Setup text node
	if(node.nodeType == mozile.dom.TEXT_NODE) {
		treeWalker.currentNode = node;
		if(direction == mozile.edit.NEXT) node = treeWalker.nextNode();
		// Make sure the previousNode isn't just the parent node.
		else {
			var tempNode = node;
			node = treeWalker.previousNode();
			while(node && node.firstChild == tempNode) {
				tempNode = node;
				node = treeWalker.previousNode();
			}
		}
	}
	// Setup element
	else {
		node = node.childNodes[this.getOffset()];
		if(node) {
			startNode = node;
			treeWalker.currentNode = node;
		}
		// Handle offset at the end of a non-empty element.
		else if(this.getOffset() > 0 &&
		  this.getOffset() == this.getNode().childNodes.length &&		
		  this.getNode().childNodes[this.getOffset() - 1]) {
		  node = this.getNode().childNodes[this.getOffset() - 1];
		  startNode = node;
		  treeWalker.currentNode = node;
		}
		// Handle empty node case.
		else {
			node = this.getNode(); 
			startNode = node;
			treeWalker.currentNode = node;
			if(direction == mozile.edit.NEXT) node = treeWalker.nextNode();
			// Make sure the previousNode isn't just the parent node.
			else {
				var tempNode = node;
				node = treeWalker.previousNode();
				while(node && node.firstChild == tempNode) {
					tempNode = node;
					node = treeWalker.previousNode();
				}
			}
		}
	}
	if(!node) {
		mozile.debug.debug("mozile.edit.InsertionPoint.prototype.seekNode", "Lost node.");
		return false;
	}

	// Seek the next eligbile node.
	var IP;	
	var offset = null;
	var lastNode = node;
	var skippedOne = false;
	while(node) {
		//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();
			// When entering inline elements or passing comments move an extra step.
			if( extraStep &&
				(mozile.edit.getParentBlock(node) == mozile.edit.getParentBlock(startNode) && mozile.edit.isNodeEditable(lastNode)) ||
				(lastNode.nodeType == mozile.dom.COMMENT_NODE && node == IP.getNode()) )
				this._offset = this._offset + direction;
			if(mozile.edit.isEmptyToken(IP.getNode())) this._offset = 0;
			return true;
		}

		// If this node allows text insertion, look ahead to the next sibling.
		else if(node.nodeType == mozile.dom.ELEMENT_NODE && 
			mozile.edit.isNodeEditable(node.parentNode) ) {
			var 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 it does not have an insertion point, then make an IP here.
			IP = mozile.edit.getInsertionPoint(nextNode, direction);
			if(!IP) {
				var newNode = node;
				if(direction == mozile.edit.PREVIOUS) {
					if(nextNode.parentNode == this._node && !skippedOne) {
						skippedOne = true; 
						lastNode = node;
						node = nextNode;
						treeWalker.currentNode = node;
						continue;
					}
					newNode = nextNode;
				}
				this._node = newNode.parentNode;
				this._offset = mozile.dom.getIndex(newNode) + 1;
				//if(direction == mozile.edit.NEXT) this._offset++;
				return true;
			}
		}

		lastNode = node;
		if(direction == mozile.edit.NEXT) node = treeWalker.nextNode();
		else node = treeWalker.previousNode();
	}
	
	return false;
}








/**
 * Create a new insertion point using the selection's focus.
 * @type mozile.edit.InsertionPoint
 */
mozile.dom.Selection.prototype.getInsertionPoint = function() {
	if(!this.focusNode || this.focusOffset == null) return null;
	else return new mozile.edit.InsertionPoint(this.focusNode, this.focusOffset);
}

// 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.isNodeEditable(node) || force) {
		if(node.nodeType == mozile.dom.TEXT_NODE) {
			if(direction == mozile.edit.NEXT) 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);
		else IP = mozile.edit.getInsertionPoint(node.lastChild, direction);
		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 Aug 23 18:45:51 2006