edit.js

Summary

Tools for basic text editing operations.

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

Author: James A. Overton


Class Summary
mozile.edit.Command  
mozile.edit.CommandGroup  
mozile.edit.Navigate  
mozile.edit.State  

/* ***** 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 basic text editing operations.
 * @link http://mozile.mozdev.org 
 * @author James A. Overton <james@overton.ca>
 * @version 0.8
 * $Id: overview-summary-edit.js.html,v 1.12 2008/02/20 18:47:09 jameso Exp $
 */

mozile.require("mozile.dom");
mozile.require("mozile.xml");
mozile.provide("mozile.edit.*");
mozile.provide("mozile.execCommand");

/**
 * Editing tools.
 * @type Object
 */
mozile.edit = new Object();
// JSDoc hack
mozile.edit.prototype = new mozile.Module;

/**
 * Translations strings for the commands.
 * @type Object
 */
mozile.edit.strings = mozile.getLocalization("mozile.edit", "commands");

/**
 * Indicates whether Mozile is currently allowed to edit the document.
 * A value of "true" means the document can be edited.
 * A value of "false" means that editing is disabled.
 * @type Boolean
 */
mozile.edit.editable = true;

/**
 * Indicates Mozile's current editing status.
 * A value of "true" means the document is being edited.
 * A value of "false" means that editing is currently disabled.
 * @type Boolean
 */
mozile.edit.status = false;

/**
 * Flag for editing direction.
 * @type Integer
 */
mozile.edit.NEXT = 1;

/**
 * Flag for editing direction.
 * @type Integer
 */
mozile.edit.PREVIOUS = -1;

/**
 * An array of all elements marked with setMark().
 * @type Array
 */
mozile.edit.marked = new Array();

/**
 * An associative array of all the Command objects (including subclasses) in this document. Keys are command names, and values are the command objects.
 */
mozile.edit.allCommands = new Object();

/**
 * Return a string listing all the registered commands.
 * @type String
 */
mozile.edit.allCommands.toString = function() {
	var keys = new Array();
	for(var key in this) {
		switch(key) {
			case "toString":
			case "undefined":
				break;
			default: keys.push(key);
		}
	}
	keys.sort();
	return keys.join(", ");
}

/**
 * An associative array of keyCodes and their standard names. Used to convert keyCode to strings in accelerators.
 * See: http://www.xulplanet.com/references/objref/KeyboardEvent.html
 * TODO: Make sure this is not Mozilla-centric.
 * @type Object
 */
mozile.edit.keyCodes = {
	8:  "Backspace",
	9:  "Tab",
	12: "Clear",
	13: "Return",
	14: "Enter",
	19: "Pause",
	27: "Escape",
	32: "Space",
	33: "Page-Up",
	34: "Page-Down",
	35: "End",
	36: "Home",
	37: "Left",
	38: "Up",
	39: "Right",
	40: "Down",
	45: "Insert",
	46: "Delete",
	112: "F1",
	113: "F2",
	114: "F3",
	115: "F4",
	116: "F5",
	117: "F6",
	118: "F7",
	119: "F8",
	121: "F9",
	122: "F10",
	123: "F11",
	123: "F12"
}


/**
 * Searches for an ancestor which mas been marked by Mozile as editable.
 * Containers themselves should not be edited, but the nodes they contain can be.
 * Elements with the "contentEditable" attribute set to true are marked as editable when found.
 * @param {Element} element The element to check.
 * @type Element
 */
mozile.edit.getContainer = function(element) {
	if(!element || !element.nodeType) return null;
	if(element.nodeType != mozile.dom.ELEMENT_NODE) element = element.parentNode;
	if(!element || !element.nodeType) return null;

	var doc = element.ownerDocument;
	while(element && element.nodeType &&
		element.nodeType == mozile.dom.ELEMENT_NODE) {
		if(mozile.edit.isEditableElement(element)) return element;

		switch(element.getAttribute("contentEditable")) {
			case "true":
				mozile.editElement(element);
				return element;
			case "false":
				// TODO: Protect element?
				return null;
		}
		element = element.parentNode;
	}

	return null;
}

/**
 * Detect whether this node is inside an editable container element.
 * @param {Node} node The node to check.
 * @type Boolean
 */
mozile.edit.isEditable = function(node) {
	if(!node) return false;
	var container = mozile.edit.getContainer(node)
	if(container && container != node) return true;
	else return false;
}

/**
 * Determines whether this element has been marked as editable by Mozile.
 * @param {Element} element The element to check.
 * @type Boolean
 */
mozile.edit.isEditableElement = function(element) {
	if(element && mozile.edit.getMark(element, "editable")) return true;
	return false;
}

/**
 * Sets the editing status for the document.
 * In browsers that do not support contentEditable, the document.designMode is turned on or off.
 * If the status is already set to the desired value, nothing is changed.
 * @param {Boolean} status The desired editing status.
 * @type Boolean
 */
mozile.edit.setStatus = function(status) {
	status = Boolean(status);
	if(mozile.edit.status != status) {
		mozile.edit.status = status;
		if(mozile.useDesignMode == true && 
			typeof(mozile.document.documentElement.contentEditable) == "undefined") {
			mozile.document.designMode = (status) ? "on" : "off";
			//alert("Design Mode set to "+ document.designMode);
		}
		//alert("Editing status changed to "+ status);
	}
	return mozile.edit.status;
}

/**
 * Enables editing of the current document.
 * All elements marked as "editingDisabled" will have their contentEditable attributes set to "true".
 * @type Boolean
 */
mozile.edit.start = function() {
	mozile.edit.editable = true;
	mozile.edit.setStatus(true);
	if(mozile.gui) mozile.gui.show();
	var list = mozile.edit.getMarked("editingDisabled", true);
	for(var i=0; i < list.length; i++) {
		mozile.edit.setMark(list[i], "editingDisabled", undefined);
		list[i].setAttribute("contentEditable", "true");
	}
	return mozile.edit.editable;
}

/**
 * Disables editing of the current document.
 * All elements in the document that have their contentEditable attribute set to "true" will be marked as "editingDisabled" and contentEditable will be set to "false. This prevents further editing in IE.
 * @type Boolean
 */
mozile.edit.stop = function() {
	mozile.edit.editable = false;
	mozile.edit.setStatus(false);
	if(mozile.gui) mozile.gui.hide();
	var list = mozile.dom.getElements("contentEditable", "true");
	for(var i=0; i < list.length; i++) {
		mozile.edit.setMark(list[i], "editingDisabled", true);
		list[i].setAttribute("contentEditable", "false");
	}
	return mozile.edit.editable;
}

/**
 * Sets a property of a special "mozile" object for an element, which stores data for Mozile to use.
 * Note: this is restricted to elements because of limitations with IE6.
 * Warning: Won't work for XML element in IE6.
 * @param {Element} element The element to mark.
 * @param {String} key The name of the property to set. Must be a valid JavaScript property name.
 * @param value The value for the new property.
 * @return The value, if set. Otherwise null.
 */
mozile.edit.setMark = function(element, key, value) {
	if(!element || element.nodeType == undefined) return null;
	if(element.nodeType != mozile.dom.ELEMENT_NODE) return null;
	if(!key || typeof(key) != "string") return null;
	
	// Catch errors thrown by IE6 in the case of XML elements.
	try {
		if(element.mozile == undefined || typeof(element.mozile) != "object") {
			element.mozile = new Object();
			mozile.edit.marked.push(element);
		}
		element.mozile[key] = value;
		return value;
	} catch(e) {
		return null;
	}
}

/**
 * Gets a property of a special "mozile" object belonging to an element. 
 * @param {Element} element The element to get check.
 * @param {String} key The name of the property to set. Must be a valid JavaScript property name.
 * @return The value, if set. Otherwise undefined.
 */
mozile.edit.getMark = function(element, key) {
	if(!element || element.nodeType == undefined) return undefined;
	if(element.nodeType != mozile.dom.ELEMENT_NODE) return undefined;
	if(!key || typeof(key) != "string") return undefined;
	if(element.mozile == undefined || !element.mozile) return undefined;
	if(element.mozile[key] == undefined) return undefined;
	return element.mozile[key];
}

/**
 * Gets an array of all elements marked with matching key and value.
 * @param {String} key The name of the property to set. Must be a valid JavaScript property name.
 * @param {String} value Optional. The value to check for. If non is given, any value is accepted.
 * @type Array
 */
mozile.edit.getMarked = function(key, value) {
	var list = new Array();
	if(!key || typeof(key) != "string") return list;

	var element;
	for(var i=0; i < mozile.edit.marked.length; i++) {
		element = mozile.edit.marked[i];
		if(!element.mozile || element.mozile[key] == undefined) continue;
		if(value === undefined) list.push(element);
		else if(element.mozile[key] == value) list.push(element);
	}

	return list;
}

/**
 * Gets the mozile.rng.Element object which corresponds to the given element.
 * Currently returns the first RNG object of type "element" with a name matching the given element's nodeName (changed to lower case).
 * TODO: Should be smarter.
 * @param {Node} node The node to find the RNG rule for.
 * @type mozile.rng.Element
 */
mozile.edit.lookupRNG = function(node) {
	if(!node) return null;
	var element = node;
	if(node.nodeType != mozile.dom.ELEMENT_NODE) element = node.parentNode;

	if(!mozile.schema) return null;
	var name = mozile.dom.getLocalName(element);
	if(name && mozile.dom.isHTML(node)) name = name.toLowerCase();
	var matches = mozile.schema.getNodes("element", name);
	if(matches.length > 0) return matches[0];
	else return null;
}


/**
 * Search through the children of the given node (following any references) for MES definitions.
 * @param {mozile.edit.Command} container The Command object to attach new commands to.
 * @parse {Node} node A node in the RNG schema to search for new commands.
 * @param {Object} localization Optional. The localization to use.
 * @type Void
 */
mozile.edit.parseMES = function(container, node, localization) {
	if(node.nodeType != mozile.dom.ELEMENT_NODE) return;
	var command, define;

	for(var i=0; i < node.childNodes.length; i++) {
		var child = node.childNodes[i];
		switch(mozile.dom.getNamespaceURI(child)) {
			// Mozile namespace case
			case mozile.xml.ns.mes:
				switch(mozile.dom.getLocalName(child)) {
					case "ref":  // Follow references
						define = mozile.edit.followMESRef(child);
						if(define) mozile.edit.parseMES(container, define, localization);
						break;
					case "command":
						command = mozile.edit.generateCommand(child, localization);
						if(command) container.addCommand(command);
						break;
					case "group":				
						command = mozile.edit.generateCommand(child, localization);
						if(command) {
							container.addCommand(command);
							// Get more commands.
							if(command._commands.length == 0) 
								mozile.edit.parseMES(command, child, localization);
						}
						break;			
				}
				break;

			// RNG namespace case
			case mozile.xml.ns.rng:
				// Follow RNG references.
				if(child.nodeName == "ref") {
					var name = child.getAttribute("name");
					if(!name) continue;
					define = mozile.schema._root.getDefinition(name);
					mozile.edit.parseMES(container, define._element, localization);
				}
				break;
		}
	}
}

/**
 * Follow an MES reference and return an MES define element.
 * Currently only works for define elements immediately under the root of the document that the ref element belongs to.
 * @param {Element} element The "ref" element to follow.
 * @type Element
 */
mozile.edit.followMESRef = function(element) {
	var define = mozile.edit.getMark(element, "define");
	if(define && define.nodeType && define.nodeType == mozile.dom.ELEMENT_NODE)
		return define;

	var name = element.getAttribute("name");
	if(!name) return null;

	// Climb the tree looking for a "grammar" node.
    var node = element;
    while(node) {
        if(mozile.dom.getNamespaceURI(node) == mozile.xml.ns.rng && 
            mozile.dom.getLocalName(node) == "grammar") break;
        else node = node.parentNode;
    }
    if(!node) return null;
    
    // Search the grammar node for a "define" child with the right @name.
    define = null;
    var child;
    for(var i=0; i < node.childNodes.length; i++) {
        child = node.childNodes[i];
        if(mozile.dom.getNamespaceURI(child) == mozile.xml.ns.mes &&
            mozile.dom.getLocalName(child) == "define" &&
            child.getAttribute("name") == name) {
            define = child;
            break;
        }
    }
	
	if(define) {
		mozile.edit.setMark(element, "define", define);
		return define;
	}
	else return null;
}

/**
 * For each RNG Element in the given schema, add all of the appropriate commands.
 * @param {mozile.rng.Schema} schema The schema to generate commands for.
 * @param {Object} localization Optional. The localization to use.
 * @type Void
 */
mozile.edit.generateCommands = function(schema, localization) {
	var elements = schema.getNodes("element");
	var name, uniqueName;
	var j=0;

	for(var i=0; i < elements.length; i++) {
		// Create a new command group with a unique name.
		if(elements[i].commands == undefined) {
			name = elements[i].getName() + "_commands";
			uniqueName = name;
			if(mozile.edit.getCommand(uniqueName)) { 
				j=0;
				while(true) {
					uniqueName = name +"_"+ j;
					if(!mozile.edit.getCommand(uniqueName)) break;
					j++;
				}
			}
			elements[i].commands = new mozile.edit.CommandGroup(uniqueName);
		}

		// General commands.
		elements[i].commands.addCommand(mozile.edit.navigateLeftRight);

		// Common text editing commands.
		if(elements[i].mayContain("text")) {
			elements[i].commands.addCommand(mozile.edit.insertText);
			elements[i].commands.addCommand(mozile.edit.removeText);
		}
		
		// Common rich editing commands.
		if(mozile.edit.remove) elements[i].commands.addCommand(mozile.edit.remove);

		// Add other commands, using the extended RNG schema information.
		mozile.edit.parseMES(elements[i].commands, elements[i]._element, localization);
	}
}

/**
 * Generate a Mozile command from an RNG Element.
 * @param {mozile.rng.Element} rng The RNG element to generate commands for.
 * @param {Object} localization Optional. The localization to use.
 * @type mozile.rng.Command
 */
mozile.edit.generateCommand = function(node, localization) {
	var name = node.getAttribute("name");
	if(!name) return null;
	
	// Check for a command with this name.
	if(mozile.edit.allCommands[name]) return mozile.edit.allCommands[name];

	// Create Command or CommandGroup object
	var command;
	if(mozile.dom.getLocalName(node) == "command") {
		var className = node.getAttribute("class");
		if(className && mozile.edit[className]) {
			//alert("Class found: "+ className);
			eval("command = new mozile.edit."+ className +"(name)");
		}
		else command = new mozile.edit.Command(name);

		// Parse child elements.
		var child = node.firstChild;
		while(child) {
			if(child.nodeType == mozile.dom.ELEMENT_NODE) {
				switch(mozile.dom.getLocalName(child)) {
					case "element":
						var element = mozile.dom.getFirstChildElement(child);
						if(child.getAttribute("import") == "true")
							element = mozile.dom.importNode(element, true);
						if(element) command.element = element;
						break;
					case "script":
						command.script = child;
						break;
				}
			}
			child = child.nextSibling;
		}
	}

	else if(mozile.dom.getLocalName(node) == "group") {
		command = new mozile.edit.CommandGroup(name);
	}
	else return null;

	// Assign properties.
	command.node = node;
	var properties = ["priority", "label", "image", "tooltip", "accel", "makesChanges", "watchesChanges", "element", "text", "remove", "nested", "direction", "target", "collapse", "copyAttributes", "className", "styleName", "styleValue"];
	for(var i=0; i < properties.length; i++) {
		var property = properties[i];
		if(node.getAttribute(property)) {
			var value = node.getAttribute(property);
			// TODO: Convert numeric values?
			if(value.toLowerCase() == "true") value = true;
			else if(value.toLowerCase() == "false") value = false;
			command[property] = value;
		}
	}
	
	// Localize the command.
	command.localize(localization);
	
	// Prepare any accelerators.
	if(command.accel) {
		command.accels = mozile.edit.splitAccelerators(command.accel);
	}
	
	// Add a direction property.
	if(command.target && !command.direction) {
		command.direction = null;
	}

	// Evaluate any scripts.
	if(command.script) {		
		child = command.script.firstChild;
		while(child) {
			if(child.nodeType == mozile.dom.TEXT_NODE ||
				child.nodeType == mozile.dom.CDATA_SECTION_NODE) {
				command.evaluate(child.data);
			}
			child = child.nextSibling;
		}
	}
	

	return command;
}



/**** Editing Commands ****/




/**
 * Check an event against an array of "accelerator" strings (i.e. a specified key combination) or array of strings.
 * @param {Event} event The event to check.
 * @param {Array} accelerator An array of strings denoting the key combination(s).
 * @type Boolean
 */
mozile.edit.checkAccelerators = function(event, accelerators) {
	if(!event) return false;
	if(typeof(accelerators) != "object" || !accelerators.length) return false;
	for(var i=0; i < accelerators.length; i++) {
		if(mozile.edit.checkAccelerator(event, accelerators[i])) return true;
	}
	return false;
}

/**
 * Check an event against an "accelerator" string (i.e. a specified key combination) or array of strings.
 * Examples of accelerators: "Command-D", "Command-Shift-D". "Command" is translated as "Control" under Windows and Linux, and "Meta" under Mac OS.
 * <p>Note: The sequence is important. The order must be "Command-Meta-Control-Alt-Shift-UpperCaseCharacter". (Comparison is done using lower case. Not all combinations will work on all platforms.)
 * @param {Event} event The event to check.
 * @param {String} accelerator A string denoting the key combination(s).
 * @type Boolean
 */
mozile.edit.checkAccelerator = function(event, accelerator) {
	if(!event) return false;
	if(typeof(accelerator) != "string") return false;
	//if(event.type.indexOf("key")==0) mozile.debug.debug("", [event.type, event.keyCode, mozile.edit.keyCodes[event.keyCode]].join("\n"));

	if(mozile.browser.isIE) {
		if(event.type != "keydown") {
			if(event.type != "keypress") return false;
			if(event.keyCode && !mozile.edit.keyCodes[event.keyCode]) return false;
		}
	}
	else if(event.type != "keypress") return false;

	if(event.accel == undefined) event.accel = mozile.edit.generateAccelerator(event);
	if(event.accel.toLowerCase() == accelerator.toLowerCase()) return true;
	else return false;
}


/**
 * Takes an event and returns a representation of the event as an accelerator string.
 * @param {Event} event The event to generate the accelerator from.
 * @type String
 */
mozile.edit.generateAccelerator = function(event) {
	if(!event) return "";
	var accel = "";

	if(event.metaKey)  accel = accel + "Meta-";
	if(event.ctrlKey)  accel = accel + "Control-";
	if(event.altKey)   accel = accel + "Alt-";
	if(event.shiftKey) accel = accel + "Shift-";

	if(event.keyCode && mozile.edit.convertKeyCode(event.keyCode)) {
		accel = accel + mozile.edit.convertKeyCode(event.keyCode);
	}
	// Special case for "Space"
	else if(event.charCode == 32) accel = accel + "Space";
	else accel = accel + String.fromCharCode(event.charCode).toUpperCase();
	
	var command = "Control";
	if(mozile.os.isMac) command = "Meta";
	accel = accel.replace(command, "Command");

	return accel;
}

/**
 * Splits a space-separated list of accelerator strings, and cleans them.
 * @param {String} accelerators A space-separated list of accelerators.
 * @type Array
 */
mozile.edit.splitAccelerators = function(accelerators) {
	var accels = new Array();
	var split = accelerators.split(/\s/);
	var accel;
	for(var i=0; i < split.length; i++) {
		accel = split[i];
		accel = accel.replace(/\s+/g, "");
		if(accel) accels.push(accel);
	}
	return accels;
}

/**
 * Takes an accelerator string and returns an object with easy-to-use properties.
 * @param {String} accelerator The accelerator string to check. See @see #mozile.edit.checkAccelerator
 * @type Object
 * @return The returned object has Boolean properties for: command, meta, ctrl, alt, and shift, and integer "charCode", and a string "character" and a string "abbr".
 */
mozile.edit.parseAccelerator = function(accelerator) {
	// Remove everything after any white-space character.
	accelerator = accelerator.replace(/\s.*/, "");

	var accel = {
		command: false,
		meta: false,
		ctrl: false,
		alt: false,
		shift: false,
		charCode: 0,
		character: "",
		abbr: ""
	}
	if(accelerator.indexOf("Command")  > -1) accel.command = true;
	if(accelerator.indexOf("Meta")     > -1) accel.meta    = true;
	if(accelerator.indexOf("Control")  > -1) accel.ctrl    = true;
	if(accelerator.indexOf("Alt")      > -1) accel.alt     = true;
	if(accelerator.indexOf("Shift")    > -1) accel.shift   = true;
	accel.character = accelerator.substring(accelerator.lastIndexOf("-")+1);
	
	// TODO: Use images instead?
	if(mozile.os.isMac) {
		if(accel.ctrl) accel.abbr += "\u2303";
		if(accel.alt) accel.abbr += "\u2325";
		if(accel.shift) accel.abbr += "\u21E7";
		if(accel.command) accel.abbr += "\u2318";
		accel.abbr += accel.character;
	}
	else {
		if(accel.command) accel.abbr += "Ctrl+";
		if(accel.alt) accel.abbr += "Alt+";
		if(accel.shift) accel.abbr += "Shift+";
		accel.abbr += accel.character;
	}
	
	return accel;
}

/**
 * Converts a key code to a key name. E.g. 14 becomes "Enter".
 * @param {Integer} keyCode The key code to convert.
 * @type String
 */
mozile.edit.convertKeyCode = function(keyCode) {
	if(mozile.edit.keyCodes[keyCode]) return mozile.edit.keyCodes[keyCode];
	else return null;
}



/**** Support Methods for Commands ****/

/**
 * Gets a command from the list of all commands.
 * @param {String} name The name of the command.
 * @type mozile.edit.Command
 */
mozile.edit.getCommand = function(name) {
	//return "Get command: "+ name +"\n"+ mozile.edit.allCommands[name];
	if(mozile.edit.allCommands[name]) return mozile.edit.allCommands[name];
	else return null;
}

/**
 * Look-up table for comparing change types.
 * @type Object
 */
mozile.edit._isHigherThan = {
	none:      {none: 0, selection: 0, state: 0, text: 0, node: 0},
	selection: {none: 1, selection: 0, state: 0, text: 0, node: 0},
	state:     {none: 1, selection: 1, state: 0, text: 0, node: 0},
	text:      {none: 1, selection: 1, state: 1, text: 0, node: 0},
	node:      {none: 1, selection: 1, state: 1, text: 1, node: 0}
}

/**
 * Compares two change types to determine if one is higher priority than the other.
 * @param {String} higher The first change type to test.
 * @param {String} lower The second change type to test.
 * @type Boolean
 */
mozile.edit.isHigherPriority = function(higher, lower) {
	if(this._isHigherThan[higher] == undefined) return false;
	if(this._isHigherThan[higher][lower] == undefined) return true;
	return Boolean(this._isHigherThan[higher][lower]);
}

/**
 * Executes the named command with the given arguments.
 * @param {String} name The name of the command to be executed
 * @param a* Other optional arguments, which will be sent to the prepare() method.
 * @type mozile.edit.State
 */
mozile.execCommand = function(name, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) {
	if(!name) return null;

	var command = mozile.edit.getCommand(name);
	if(!command) return null;
	
	// Execute the command.
	//var selection = mozile.dom.selection.get();
	//selection.restore();

	var state = command.request(null, true, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
	if(!state) return state;
	
	// Store for undo and update the GUI.
	mozile.edit.done(state);
	if(state.changesMade) {
		var event = new Object();
		mozile.edit._getNode(event);
		if(mozile.gui) mozile.gui.update(event, state.changesMade);
	}

	return state;
}



/**** RelaxNG Element Extensions ****/

/**
 * Add command functionality to the RNG system.
 * @type Void
 */
mozile.edit.extendRNG = function() {

/**
 * Creates a new element using the information from this RNG Element object.
 * @param {Element} parent Optional. If a parent element is given the new node will be appended as a child and the parent will be returned.
 * @type Element
 */
mozile.rng.Element.prototype.create = function(parent) {
	var node = mozile.dom.createElement(this.getName());
	// TODO: Create children.
	if(parent) {
		parent.appendChild(node);
		return parent;
	}
	else return node;
}

} // End of mozile.edit.extendRNG() method.


// If the RNG module has been loaded, extend its functionality.
if(mozile.rng) mozile.edit.extendRNG();





/**** Command State Object ****/

/**
 * The State object is a container for information used to execute and unexecute a command. It is associated with a particular command execution.
 * @param {mozile.edit.Command} command A reference to the command which this state belongs to.
 * @param {Selection} selection Optional. The current selection, or a range to be stored as the current selection. If the value is "false" then no selection is stored.
 * @constructor
 */
mozile.edit.State = function(command, selection) {
	/**
	 * A reference to the command that created this state.
	 * @type mozile.edit.Command
	 */
	this.command = command;
	
	/**
	 * A reference to the current selection object.
	 * @type mozile.dom.Selection
	 */
	this.selection = null;

	if(selection !== false) {
		if(!selection) selection = mozile.dom.selection.get();
		this.selection = { before: selection.store() };
	}

	/**
	 * Indicates that the command can be undone.
	 * @type Boolean
	 */
	this.reversible = true;

	/**
	 * Indicates that the command should be cancelled by mozile.event.handle() once complete.
	 * @type Boolean
	 */
	this.cancel = true;

	/**
	 * Specified the kind of change that this state has made.
	 * See mozile.edit.Command.respond for possible values.
	 * @type String
	 */
	this.changesMade = command.makesChanges;

	/**
	 * Indicates that the command is waiting for later execution.
	 * @type Boolean
	 */
	this.delayExecution = false;

	/**
	 * Indicates that the command has been executed.
	 * @type Boolean
	 */
	this.executed = false;
}

/**
 * Returns a description of this object.
 * @type String
 */
mozile.edit.State.prototype.toString = function() {
	return "[object mozile.edit.State]";
}

/**
 * Used to sanity check command arguments. If given a node, it returns an XPath. If given a string, it tries to make sure it's an XPath. Given anything else, it will return null;
 * @param input A node or XPath string.
 * @type String
 */
mozile.edit.State.prototype.storeNode = function(input) {
	if(!input) return null;

	if(typeof(input) == "string") {
		if(input.indexOf("/") != 0) return null;
		return input;
	}

	else {
		var xpath = mozile.xpath.getXPath(input);
		if(xpath) return xpath;
		else return null;
	}

	return null;
}


/**** Command Object ****/

/**
 * Commands are objects capable of making undoable changes to the document, and aware of the context in which those changes can be made.
 * @param {String} name The command's name.
 * @param {Object} localization Optional. The localization object to use.
 * @constructor
 */
mozile.edit.Command = function(name, localization) { 
	/**
	 * The name for this command.
	 * @type String
	 */
	 this.name = name;
	 
	 /**
	  * Indicates that this command does not contain more commands.
	  * @type Boolean
	  */
	 this.group = false;
	 
	 /**
	  * Specifies what kind of change this command makes to the document.
	  * Can be "none", "state", "text", or "node". Each includes all the previous types.
	  * @type String
	  */
	 this.makesChanges = "node";
	 
	 /**
	  * Specifies what kind of change will cause this command to change its isActive or isAvailable states.
	  * Can be any of the values of makesChange.
	  * @type String
	  */
	 this.watchesChanges = "node";
	 
	 /**
	  * A localization object for this command.
	  * @type Object
	  */
	 this.strings = {};
	 
	 // Perform localization.
	 this.localize(localization);
	 
	 // Register this command on the list of all commands.
	 mozile.edit.allCommands[this.name] = this;
}


/**
 * Returns a description of this object.
 * @param {Object} localization Optional. The localization object to use. Defaults to mozile.edit.strings.
 * @type Boolean
 */
mozile.edit.Command.prototype.localize = function(localization) {
	var properties = ["accel", "accels", "label", "tooltip", "image"];
	if(!localization) localization = mozile.edit.strings;
	if(!localization) return false;
	if(localization[this.name]) {
		this.strings = localization[this.name];
		for(var i=0; i < properties.length; i++) {
			if(localization[this.name][properties[i]])
				this[properties[i]] = localization[this.name][properties[i]]
		}
	}
	return true;
}


/**
 * Returns a description of this object.
 * @type String
 */
mozile.edit.Command.prototype.toString = function() {
	return "[object mozile.edit.Command '"+ this.name +"']";
}

/**
 * Evaluates the given JavaScript code string in the context of this object instance. The method can be used to customize an instance of the Command class.
 * @param {String} code The JavaScript code to be evaluated.
 * @type Void
 */
mozile.edit.Command.prototype.evaluate = function(code) {
	eval(code);
}

/**
 * Determines whether this command should respond to a change of a given type.
 * Compares the given change type (usually the value of the last command's makesChanges property), and ompares it with this command's watchesChanges property.
 * @param {String} change The type of change. Can be "selection", "state", "text", or "node". Each includes all the previous types. Can also be "none", which means all changes are ignored.
 * @type Boolean
 */
mozile.edit.Command.prototype.respond = function(change) {
	if(this.watchesChanges == change) return true;
	return mozile.edit.isHigherPriority(change, this.watchesChanges);
}

/**
 * Indicates that the command is available to be used.
 * @param {Event} event Optional. The current event object.
 * @type Boolean
 */
mozile.edit.Command.prototype.isAvailable = function(event) {
	return true;
}

/**
 * Indicates that the command is currently active.
 * In the case of a "strong" command it would me that the cursor is inside a "strong" element, and the command's button should indicate that fact.
 * @param {Event} event Optional. The current event object.
 * @type Boolean
 */
mozile.edit.Command.prototype.isActive = function(event) {
	return false;
}

/**
 * Tests to see if the command should be executed. Returns true if the event matches the accelerator.
 * @param {Event} event Optional. The event object which caused this command to be tested.
 * @type Boolean
 */
mozile.edit.Command.prototype.test = function(event) {
	if(event) {
		if(this.accels) return mozile.edit.checkAccelerators(event, this.accels);
		else if(this.accel) {
			this.accels = mozile.edit.splitAccelerators(this.accel);
			return mozile.edit.checkAccelerator(event, this.accel);
		}
		else return false;
	}
	
	return true;
}

/**
 * Creates a "state" object with all of the information needed to execute the command.
 * @param {Event} event The event object to be converted into a state.
 * @type mozile.edit.State
 */
mozile.edit.Command.prototype.prepare = function(event) {
	var state = new mozile.edit.State(this);

	// Some commands may need additional information from the user.
	// If so, define a method named "prompt", which takes the event and the state and returns a Boolean true when it succeeds.
	if(this.prompt) {
		if(!this.prompt(event, state)) return null;
	}

	return state;
}

/**
 * Executes the command, but only if the test is successful.
 * @param {Event} event The event object which may trigger the command.
 * @type mozile.edit.State
 */
mozile.edit.Command.prototype.trigger = function(event) {
	if(window.trigger) window.trigger.push(this.name);
	if(this.test(event)) {
		var state = this.prepare(event);
		if(state.delayExecution) return "delayed";
		else return this.execute(state, true);
	}
	return null;
}

/**
 * Used by commands to call other commands. Executes the command if the test is successful. If the command is executed, its new state is added to the original state's actions array.
 * @param {mozile.edit.State} state The state object of the calling command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @param a* Other optional arguments, which will be sent to the perpare() method.
 * @type mozile.edit.State
 */
mozile.edit.Command.prototype.request = function(state, fresh, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) {
	var test = this.test(null, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
	if(!test) return null;
	
	var newState = this.prepare(null, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
	if(!newState) return null;
	if(newState.delayExecution) return "delayed";

	newState = this.execute(newState, fresh);
	if(!newState || !newState.executed) return null;
	
	if(state && typeof(state) == "object") {
		if(!state.actions) state.actions = new Array();
		state.actions.push(newState);
	}
	return newState;
}

/**
 * Executes the command and returns a "state" object which stores the information necessary to unexecute the command.
 * <p>This method is meant to be overridden by instances and subclasses.
 * @param {mozile.edit.State} state A state object with the information necessary for executing this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type mozile.edit.State
 */
mozile.edit.Command.prototype.execute = function(state, fresh) {
	mozile.debug.inform("mozile.edit.Command.execute", "Command '"+ this.name +"' executed with state "+ state);
	state.executed = true;
	return state;
}

/**
 * Reverses the operation of the execute command.
 * This is an "undo" operation which should leave the document exactly as it was before the command was originally executed.
 * <p>This method is meant to be overridden by instances and subclasses.
 * @param {mozile.edit.State} state The state object returned by the execute() method. It will contain enough information to unexecute the command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type mozile.edit.State
 */
mozile.edit.Command.prototype.unexecute = function(state, fresh) {
	var selection = mozile.dom.selection.get();
	if(!fresh && state.selection && state.selection.after)
		selection.restore(state.selection.after);

	// Unexecute actions in reverse order.
	if(state.actions) {
		for(var i = state.actions.length - 1; i >= 0; i--) {
			state.actions[i] = state.actions[i].command.unexecute(state.actions[i], fresh);
			if(state.actions[i].executed) mozile.debug.inform(this.name +".unexecute", "Child command "+ i +" failed to unexecute.");
		}
	}

	if(state.selection && state.selection.before)
		selection.restore(state.selection.before);
	state.executed = false;
	return state;
}


/**** CommandGroup Object ****/

/**
 * CommandGroups contain other commands.
 * @param {String} name The group's name.
 * @param {Object} localization Optional. The localization object to use.
 * @constructor
 */
mozile.edit.CommandGroup = function(name, localization) { 
	/**
	 * The name for this command group.
	 * @type String
	 */
	 this.name = name;
	 
	 /**
	  * Indicates that this command contains more commands.
	  * @type Boolean
	  */
	 this.group = true;
	 
	 /**
	  * Specifies what kind of change this command makes to the document.
	  * See mozile.edit.Command.respond for possible values.
	  * @type String
	  */
	 this.makesChanges = "none";
	 
	 /**
	  * Specifies what kind of change will cause this command to change its isActive or isAvailable states.
	  * See mozile.edit.Command.respond for possible values.
	  * @type String
	  */
	 this.watchesChanges = "none";
	 
	 /**
	  * An array of commands belonging to this group.
	  * @private
	  * @type Array
	  */
	 this._commands = new Array();
	 
	 /**
	  * An array of commands sorted by their "priority" attribute.
	  * @private
	  * @type Array
	  */
	 this._priority = new Array();
	 
	 // Perform localization.
	 this.localize(localization);

	 // Register this command group on the list of all commands.
	 mozile.edit.allCommands[this.name] = this;
}
mozile.edit.CommandGroup.prototype = new mozile.edit.Command;
mozile.edit.CommandGroup.prototype.constructor = mozile.edit.CommandGroup;

/**
 * Returns a description of this object.
 * @type String
 */
mozile.edit.CommandGroup.prototype.toString = function() {
	return "[object mozile.edit.CommandGroup '"+ this.name +"']";
}

/**
 * Adds a command to the list of commands for this CommandGroup.
 * @param {mozile.edit.Command} command The command to be added.
 * @param {mozile.edit.Command} previousCommand Optional. The new command will be added after this command.
 * @type mozile.edit.Command
 */
mozile.edit.CommandGroup.prototype.addCommand = function(command, previousCommand) {
	if(!command) return null;
	if(!this._commands) this._commands = new Array();
	if(!this._priority) this._priority = new Array();
	// Don't allow duplicates.
	for(var i=0; i < this._commands.length; i++) {
		if(this._commands[i] == command) return null;
	}

	var added = false;
	if(previousCommand) {
		for(i=0; i < this._commands.length - 1; i++) {
			if(this._commands[i] == previousCommand) {
				this._commands.splice(i+1, 0, command);
				added = true;
				break;
			}
		}
		if(!added) {
			this._commands.unshift(command);
			added = true;
		}
	}
	if(!added) this._commands.push(command);

	this._priority.push(command);
	this._priority.sort(this.compareCommands);
	return command;
}

/**
 * Removes all commands from this command group. The command objects themselves are not deleted.
 * @type Void
 */
mozile.edit.CommandGroup.prototype.removeCommands = function() {
	this._commands = new Array();
	this._priority = new Array();
}

/**
 * Compares a pair commands by their "priority" attribute. Higher values come first.
 * Designed to be used by JavaScript's array.sort() method.
 * @param {mozile.edit.Command} command1
 * @param {mozile.edit.Command} command2
 * @type Number
 */
mozile.edit.CommandGroup.prototype.compareCommands = function(command1, command2) {
	if(command1.priority == undefined || Number(command1.priority) == NaN) 
		command1.priority = 0;
	if(command2.priority == undefined || Number(command2.priority) == NaN) 
		command2.priority = 0;
	return command2.priority - command1.priority;
}

/**
 * Takes an event object and uses it to try and trigger all of the commands associated with this CommandGroup.
 * @param {Event} event The event to handle.
 * @type mozile.edit.State
 */
mozile.edit.CommandGroup.prototype.trigger = function(event) {
	//alert(this.getName() +" is handling event "+ event +" with commands "+ this._commands);
	if(!this._priority) return null;
	
	var state;
	for(var i=0; i < this._priority.length; i++) {
		state = this._priority[i].trigger(event);
		if(state) return state;
	}
	return null;
}


/**
 * A CommandGroup which will contain the global commands.
 * @type mozile.edit.CommandGroup
 */
mozile.edit.commands = new mozile.edit.CommandGroup("commands");

/**
 * A CommandGroup which will contain default commands.
 * @type mozile.edit.CommandGroup
 */
mozile.edit.defaults = new mozile.edit.CommandGroup("defaults");





/**** Undo / Redo System ****/

/**
 * An array of undo states.
 * @private
 * @type Array
 */
mozile.document._undoStack = new Array();

/**
 * The index of the last executed state in the undo stack.
 * @private
 * @type Integer
 */
mozile.document._undoIndex = -1;

/**
 * The current state.
 * @type mozile.edit.State
 */
mozile.edit.currentState = null;

/**
 * Displays the contents of the undo stack.
 * @type Void
 */
mozile.edit.dumpUndoStack = function() {
	if(!mozile.document._undoStack) {
		mozile.document._undoStack = new Array();
		mozile.document._undoIndex = -1;
	}
	var entries = new Array("Undo Stack [ "+ mozile.document._undoIndex +" / "+ mozile.document._undoStack.length +" ]");
	for(var i=0; i < mozile.document._undoStack.length; i++) {
		var picked = "  ";
		if(i == mozile.document._undoIndex) picked = "> "
		entries.push(picked + i +". "+ mozile.document._undoStack[i].command.name);
	}
	return entries.join("\n");
}

/**
 * Records the result of a command in such a way that it can be undone.
 * @param {mozile.edit.State} state The state to record. Expected to be the result of a Command's execute method.
 * @type Void
 */
mozile.edit.done = function(state) {
	if(!state || !state.reversible) return;
	if(!mozile.document._undoStack) {
		mozile.document._undoStack = new Array();
		mozile.document._undoIndex = -1;
	}
	mozile.document._undoStack = mozile.document._undoStack.slice(0, mozile.document._undoIndex + 1);
	mozile.document._undoStack.push(state);
	mozile.document._undoIndex = mozile.document._undoStack.length - 1;
}

/**
 * Sets the current state. Uses the undoIndex to find the state in the undoStack.
 * @param {Document} doc Optional. The target document.
 * @type State
 */
mozile.edit.getCurrentState = function(doc) {
	if(!doc) doc = mozile.document;
	if(!doc) return null;
	if(!doc._undoStack) return null;
	if(doc._undoIndex === undefined) return null;
	return doc._undoStack[doc._undoIndex];
}





/**** Global Commands ****/


/**
 * Shows document source.
 * @type mozile.edit.Command
 */
mozile.edit.save = new mozile.edit.Command("save");
mozile.edit.save.makesChanges = "none";
mozile.edit.save.watchesChanges = "state";
mozile.edit.commands.addCommand(mozile.edit.save);

/**
 * Indicates that the command is available to be used.
 * @param {Event} event Optional. The current event object.
 * @type Boolean
 */
mozile.edit.save.isAvailable = function(event) {
	if(!mozile.save) return false;
	if(mozile.save.isSaved()) return false;
	else return true;
}

/**
 * Dumps the page source to a new window.
 * @type Object
 */
mozile.edit.save.execute = function(state, fresh) {
	mozile.save.save();
	
	state.reversible = false;
	state.executed = true;
	return state;
}

/**
 * Shows document source.
 * @type mozile.edit.Command
 */
mozile.edit.source = new mozile.edit.Command("source");
mozile.edit.source.makesChanges = "none";
mozile.edit.source.watchesChanges = "none";
mozile.edit.commands.addCommand(mozile.edit.source);

/**
 * Dumps the page source to a new window.
 * @type Object
 */
mozile.edit.source.execute = function(state, fresh) {
	if(mozile.save && mozile.gui){
		var content = mozile.save.getContent(mozile.document);
		content = mozile.save.cleanMarkup(content);
		mozile.gui.display("<h3>Page Source</h3>\n<pre>"+ content +"</pre>");
	}
	
	state.reversible = false;
	state.executed = true;
	return state;
}


/**
 * Shows debugging information.
 * @type mozile.edit.Command
 */
mozile.edit.debug = new mozile.edit.Command("debug");
mozile.edit.debug.makesChanges = "none";
mozile.edit.debug.watchesChanges = "none";
mozile.edit.commands.addCommand(mozile.edit.debug);

/**
 * Displays information used for testing and development.
 * @type Object
 */
mozile.edit.debug.execute = function(state, fresh) {
	mozile.debug.show();
	state.reversible = false;
	state.executed = true;
	return state;
}


/**** Undo ****/

/**
 * Reverses the action of the last command in the global undo stack.
 * @type mozile.edit.Command
 */
mozile.edit.undo = new mozile.edit.Command("undo");
mozile.edit.undo.makesChanges = "node";
mozile.edit.undo.watchesChanges = "state";
mozile.edit.commands.addCommand(mozile.edit.undo);

/**
 * True if there are states to undo and the event matches the accelerator.
 * @param {Event} event Optional. The event object to be tested.
 * @type Boolean
 */
mozile.edit.undo.test = function(event) {
	if(mozile.document._undoIndex == undefined ||
		mozile.document._undoIndex < 0) return false;
	if(event) {
		return mozile.edit.checkAccelerator(event, this.accel);
	}
	return true;
}

/**
 * Indicates that the command is available to be used.
 * @param {Event} event Optional. The current event object.
 * @type Boolean
 */
mozile.edit.undo.isAvailable = function(event) {
	if(mozile.document._undoIndex == undefined ||
		mozile.document._undoIndex < 0) return false;
	else return true;
}

/**
 * Prepares a state object for the undo command.
 * @param {Event} event The event object to be converted into a state.
 * @param {Boolean} repeated Optional. Indicates that the undo operation is being repeated. Planned to be used to set the "freshness" of commands.
 * @type mozile.edit.State
 */
mozile.edit.undo.prepare = function(event, repeated) {
	var state = new mozile.edit.State(this, false); // don't store the selection
	
	state.repeated = false;
	if(repeated) state.repeated = repeated;
	if(event) state.repeated = event.repeat;

	state.reversible = false;
	return state;
}

/**
 * Undo the previous action on the global undo stack.
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.undo.execute = function(state, fresh) {
	var undoState = mozile.document._undoStack[mozile.document._undoIndex];
	if(undoState) {
		// TODO: use "state.repeated" for freshness
		undoState.command.unexecute(undoState, false); 
		mozile.document._undoIndex--;
		state.changesMade = undoState.changesMade;
	}
	state.executed = true;
	return state;
}


/**** Redo ****/

/**
 * Executes the current command in the global undo stack.
 * @type mozile.edit.Command
 */
mozile.edit.redo = new mozile.edit.Command("redo");
mozile.edit.redo.makesChanges = "node";
mozile.edit.redo.watchesChanges = "state";
mozile.edit.commands.addCommand(mozile.edit.redo);

/**
 * True if there are states to redo and the event matches the accelerator.
 * @param {Event} event The event object to be tested.
 * @type Boolean
 */
mozile.edit.redo.test = function(event) {
	if(mozile.document._undoIndex == undefined ||
		mozile.document._undoIndex + 1 >= mozile.document._undoStack.length) return false;
	if(event) {
		return mozile.edit.checkAccelerator(event, this.accel);
	}
	return true;
}

/**
 * Indicates that the command is available to be used.
 * @param {Event} event Optional. The current event object.
 * @type Boolean
 */
mozile.edit.redo.isAvailable = function(event) {
	if(mozile.document._undoIndex == undefined ||
		mozile.document._undoIndex + 1 >= mozile.document._undoStack.length) return false;
	else return true;
}

/**
 * Prepares a state object for the redo command.
 * @param {Event} event The event object to be converted into a state.
 * @param {Boolean} repeated Optional. Indicates that the undo operation is being repeated. Planned to be used to set the "freshness" of commands.
 * @type mozile.edit.State
 */
mozile.edit.redo.prepare = function(event, repeated) {
	var state = new mozile.edit.State(this, false); // don't store the selection

	state.repeated = false;
	if(repeated) state.repeated = repeated;
	if(event) state.repeated = event.repeat;

	state.reversible = false;
	return state;
}

/**
 * Redo the previous action on the global undo stack.
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.redo.execute = function(state, fresh) {
	var redoState = mozile.document._undoStack[mozile.document._undoIndex + 1];
	if(redoState) {
		mozile.document._undoIndex++;
		// TODO: use "state.repeated" for freshness
		redoState.command.execute(redoState, false); 
		state.changesMade = redoState.changesMade;
	}
	state.executed = true;
	return state;
}



/**
 * Used to execute a set of commands, with no action of its own.
 * @type mozile.edit.Command
 */
mozile.edit.executionGroup = new mozile.edit.Command("ExecutionGroup");

/**
 * True if there are states to redo and the event matches the accelerator.
 * @param {Event} event The event object to be tested.
 * @type Boolean
 */
mozile.edit.executionGroup.test = function(event) {
	if(event) return false;
	return true;
}

/**
 * Redo the previous action on the global undo stack.
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.executionGroup.execute = function(state, fresh) {
	var selection = mozile.dom.selection.get();
	if(!fresh) selection.restore(state.selection.before);

	if(state.actions) {
		for(var i=0; i < state.actions.length; i++) {		
			state.actions[i] = state.actions[i].command.execute(state.actions[i], fresh);
			if(!state.actions[i].executed) mozile.debug.inform(this.name +".unexecute", "Child command "+ i +" failed to execute.");
		}
	}

	state.selection.after = selection.store();
	state.executed = true;
	return state;
}


/**** Clipboard System ****/

/**
 * The local clipboard. Contains document fragments created using the copy and cut commands.
 * @type DocumentFragment
 */
mozile.edit.clipboard = null;

/**
 * Updates the local clipboard with data from the system clipboard, when possible.
 * @type Void
 */
mozile.edit.updateClipboard = function() {
	// TODO: Implement.
}


/**** Copy ****/

/**
 * Copies the current selection to the clipboard.
 * @type mozile.edit.Command
 */
mozile.edit.copy = new mozile.edit.Command("copy");
mozile.edit.copy.makesChanges = "node";
mozile.edit.copy.watchesChanges = "selection";
mozile.edit.commands.addCommand(mozile.edit.copy);

/**
 * Indicates that the command is available to be used.
 * @param {Event} event Optional. The current event object.
 * @type Boolean
 */
mozile.edit.copy.isAvailable = function(event) {
	var selection;
	if(event && event.selection) selection = event.selection;
	if(!selection) selection = mozile.dom.selection.get();
	if(selection.isCollapsed) return false;
	return true;
}

/**
 * True if the selection is not collapsed.
 * @param {Event} event Optional. The current event object.
 * @type Boolean
 */
mozile.edit.copy.test = function(event) {
	if(event) {
		if(this.accels) return mozile.edit.checkAccelerators(event, this.accels);
		else if(this.accel) {
			this.accels = mozile.edit.splitAccelerators(this.accel);
			return mozile.edit.checkAccelerator(event, this.accel);
		}
		else return false;
	}

	var selection;
	if(event && event.selection) selection = event.selection;
	if(!selection) selection = mozile.dom.selection.get();
	if(selection.isCollapsed) return false;
	return true;
}

/**
 * Prepares a state object for the command.
 * @param {Event} event The event object to be converted into a state.
 * @type mozile.edit.State
 */
mozile.edit.copy.prepare = function(event) {
	var state = new mozile.edit.State(this, false); // don't store the selection
	state.reversible = false;
	state.cancel = false;
	return state;
}

/**
 * Copies the selected content. If rich editing is enabled, use range.toString() for text and range.cloneContents() for nodes. If rich editing is not enabled, always use range.toString().
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.copy.execute = function(state, fresh) {
	var selection = mozile.dom.selection.get();
	var range = selection.getRangeAt(0);
	
	if(range.commonAncestorContainer.nodeType == mozile.dom.TEXT_NODE ||
		!mozile.edit.rich) {
		mozile.edit.clipboard = range.toString();
	}
	else mozile.edit.clipboard = range.cloneContents();

	state.executed = true;
	return state;
}


/**** Cut ****/

/**
 * Copies the current selection to the clipboard, then removes it.
 * @type mozile.edit.Command
 */
mozile.edit.cut = new mozile.edit.Command("cut");
mozile.edit.cut.makesChanges = "node";
mozile.edit.cut.watchesChanges = "selection";
mozile.edit.commands.addCommand(mozile.edit.cut);

/**
 * Indicates that the command is available to be used.
 * @param {Event} event Optional. The current event object.
 * @type Boolean
 */
mozile.edit.cut.isAvailable = function(event) {
	var selection;
	if(event && event.selection) selection = event.selection;
	if(!selection) selection = mozile.dom.selection.get();
	if(selection.isCollapsed) return false;
	return true;
}

/**
 * True if there is a non-collapsed selection and the event matches the accelerator.
 * @param {Event} event The event object to be tested.
 * @type Boolean
 */
mozile.edit.cut.test = function(event) {
	if(event) {
		if(!event.editable) return false;
		if(!mozile.edit.checkAccelerator(event, this.accel)) return false;
		if(!mozile.edit.rich) {
			if(event.node) return false;
			if(event.node.nodeType != mozile.dom.TEXT_NODE) return false;
		}
	}

	var selection;
	if(event && event.selection) selection = event.selection;
	if(!selection) selection = mozile.dom.selection.get();
	if(selection.isCollapsed) return false;
	return true;
}

/**
 * Copies the selected content. If rich editing is enabled, use range.toString() for text and range.cloneContents() for nodes. If rich editing is not enabled, always use range.toString().
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.cut.execute = function(state, fresh) {
	var selection = mozile.dom.selection.get();
	if(!fresh) selection.restore(state.selection.before);
	var range = selection.getRangeAt(0);
	state.actions = new Array();
	
	if(range.commonAncestorContainer.nodeType == mozile.dom.TEXT_NODE ||
		!mozile.edit.rich) {
		mozile.edit.clipboard = range.toString();
		mozile.edit.removeText.request(state, fresh);
	}
	else {
		mozile.edit.clipboard = range.cloneContents();
		mozile.edit.remove.request(state, fresh);
	}
	
	state.selection.after = selection.store();
	state.executed = true;
	return state;
}



/**** Paste ****/

/**
 * Copies the current selection to the clipboard.
 * @type mozile.edit.Command
 */
mozile.edit.paste = new mozile.edit.Command("paste");
mozile.edit.commands.addCommand(mozile.edit.paste);

/**
 * Indicates that the command is available to be used.
 * @param {Event} event Optional. The current event object.
 * @type Boolean
 */
mozile.edit.paste.isAvailable = function(event) {
	if(!event || !event.editable) return false;
	if(mozile.edit.clipboard) return true;
	return false;
}

/**
 * True if there is a non-collapsed selection and the event matches the accelerator.
 * @param {Event} event The event object to be tested.
 * @type Boolean
 */
mozile.edit.paste.test = function(event) {
	// IE has its own methods now, see _preExecute and _postExecute.
	if(event && mozile.browser.isIE) return false;
	
	if(!mozile.edit.clipboard) return false;

	if(event) {
		if(!event.editable) return false;
		if(!mozile.edit.checkAccelerator(event, this.accel)) return false;
		if(!mozile.edit.rich) {
			if(typeof(mozile.edit.clipboard) != "string") return false;
			if(!event.node) return false;
			if(event.node.nodeType != mozile.dom.TEXT_NODE) return false;
		}
	}

	return true;
}

/**
 * Prepares a state object for the command.
 * @param {Event} event The event object to be converted into a state.
 * @type mozile.edit.State
 */
mozile.edit.paste.prepare = function(event) {
	var state = new mozile.edit.State(this);
	
	if(typeof(mozile.edit.clipboard) == "string") {
		state.content = mozile.edit.clipboard;
	}
	else state.content = mozile.edit.clipboard.cloneNode(true);
	
	state.reversible = true;
	return state;
}

/**
 * Pastes the content of the clipboard.
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.paste.execute = function(state, fresh) {
	var selection = mozile.dom.selection.get();
	if(!fresh) selection.restore(state.selection.before);
	var range = selection.getRangeAt(0);
	state.actions = new Array();
	
	if(!selection.isCollapsed) {
		if(mozile.edit.remove) mozile.edit.remove.request(state, fresh);
		else mozile.edit.removeText.request(state, fresh);
	}

	// Text case	
	if(typeof(state.content) == "string") {
		mozile.edit.insertText.request(state, fresh, mozile.edit.NEXT, state.content);
	}

	// Rich case
	else {
		var previousNode = null;
		var nextNode = null;
		// Split text node
		if(selection.focusNode.nodeType == mozile.dom.TEXT_NODE) {
			var newState = mozile.edit.splitNode.request(state, fresh, 
				selection.focusNode, selection.focusOffset);
			previousNode = newState.oldContainer;
			nextNode = newState.newContainer;
		}
		else {
			previousNode = selection.focusNode.childNodes[selection.focusOffset - 1];
			nextNode = selection.focusNode.childNodes[selection.focusOffset];
		}

		// Clone nodes and insert them.
		var moveNode, firstNode, lastNode;
		for(var i=state.content.childNodes.length-1; i >= 0; i--) {
			moveNode = state.content.childNodes[i].cloneNode(true);
			mozile.edit.insertNode.request(state, fresh, null, previousNode, moveNode);
			if(i == state.content.childNodes.length-1) lastNode = moveNode;
			if(i == 0) firstNode = moveNode;
		}

		// Restore the selection.
		var IP = mozile.edit.getInsertionPoint(firstNode, mozile.edit.NEXT);
		if(IP) {
		  IP.select();
		  IP = mozile.edit.getInsertionPoint(lastNode, mozile.edit.PREVIOUS);
		  if(IP) IP.extend();
		}

		// Normalize		
		mozile.edit._normalize(state, fresh, lastNode, nextNode);
		mozile.edit._normalize(state, fresh, previousNode, firstNode);
	}

	state.selection.after = selection.store();
	state.executed = true;
	return state;
}

/**
 * Prepare for pasting from the system clipboard with Internet Explorer.
 * This means creating a container to catch the contents, selecting it, and setting a timer to catch the contents.
 * @type Void
 */
mozile.edit.paste._preExecute = function() {	
	var container = mozile.dom.createElement("div");
	container.setAttribute("contentEditable", true);
	container.style.height = "0px";
	container.appendChild(mozile.document.createTextNode(""));
	mozile.dom.getBody().appendChild(container);
	mozile.edit.paste._pasteContainer = container;
	
	var selection = mozile.dom.selection.get();
	mozile.edit.paste._storedSelection = selection.store();
	selection.collapse(container.firstChild, 0);

	mozile.window.setTimeout(mozile.edit.paste._postExecute, 50);
}

/**
 * Moves content pasted with Internet Explorer into the Mozile paste system. The container is cleaned and removed, and its content is set as the clipboard content. Then a normal paste operation is executed.
 * @type Void
 */
mozile.edit.paste._postExecute = function() {
	var container = mozile.edit.paste._pasteContainer;
	if(!container) return null;
	
	container.parentNode.removeChild(container);

	mozile.edit.clipboard = mozile.edit.paste._cleanHTML(container);

	var selection = mozile.dom.selection.get();
	selection.restore(mozile.edit.paste._storedSelection);
	mozile.execCommand("paste");
	
	delete mozile.edit.paste._pasteContainer;
	delete mozile.edit.paste._storedSelection;
}


/**
 * Used to remove useless HTML tags and styles from pasted code.
 * @param {Element} element The element to clean.
 * @type Element
 */
mozile.edit.paste._cleanHTML = function(element) {
	for(var i=0; i < element.all.length; i++) {
		// Remove "className" attributes.
		element.all[i].removeAttribute("className", "", 0);
		
		// Remove "mso-*" styles.
		css = element.all[i].style.cssText;
		css = css.replace(/mso-.*?(;|$)/mg, "");
		element.all[i].style.cssText = css;
	}

	return element;
}


/**
 * A command used for testing purposes only.
 * @type mozile.edit.Command
 */
mozile.edit.test = new mozile.edit.Command("test");
mozile.edit.commands.addCommand(mozile.edit.test);

/**
 * Displays information used for testing and development.
 * @type Object
 */
mozile.edit.test.execute = function(state, fresh) {
	mozile.require("mozile.util");
	var output = new Array();
	output.push("Debugging Information:");

	output.push("Undo: "+ mozile.document._undoIndex +" / "+ mozile.document._undoStack.length);
	
	var selection = mozile.dom.selection.get();
	output.push("Selection:\n"+ mozile.util.dumpValues(selection.store()));

	var element = selection.focusNode;
	if(element.nodeType != mozile.dom.ELEMENT_NODE) element = element.parentNode;
	var rng = mozile.edit.lookupRNG(element);
	if(rng) {
		if(rng.getName()) output.push("RNG: "+ rng +" "+ rng.getName());
		else output.push("RNG: "+ rng);
		output.push("Text? "+ rng.mayContain("text"));
	}
	else output.push("No matching RNG object.");

	alert(output.join("\n"));
	state.reversible = false;
	state.executed = true;
	return state;
}


/**
 * A tweaking command. For testing only
 * @type mozile.edit.Command
 */
mozile.edit.tweak = new mozile.edit.Command("tweak");
mozile.edit.commands.addCommand(mozile.edit.tweak);

/**
 * Displays information used for testing and development.
 * @type Object
 */
mozile.edit.tweak.execute = function(state, fresh) {
	if(mozile.browser.isIE) {
		var selection = mozile.dom.selection.get();
		var range = selection.getRangeAt(0);
		range._range.move("character", 1);
		selection.removeAllRanges();
		selection.addRange(range);
	}
	
	state.reversible = false;
	state.executed = true;
	return state;
}





/**** General Commands ****/
mozile.require("mozile.edit.InsertionPoint");

/**
 * Inserts text into a text node.
 * @type mozile.edit.Command
 */
mozile.edit.navigateLeftRight = new mozile.edit.Command("navigateLeftRight");
mozile.edit.navigateLeftRight.priority = 15;
mozile.edit.navigateLeftRight.accel = "Left Right";
mozile.edit.navigateLeftRight.accels = 
	mozile.edit.splitAccelerators(mozile.edit.navigateLeftRight.accel);
mozile.edit.navigateLeftRight.makesChanges = "none";
mozile.edit.navigateLeftRight.watchesChanges = "none";

/**
 * Prepares a state object for the insert text command.
 * @param {Event} event Optional. The event object to be converted into a state.
 * @param {Boolean} direction Optional. The direction to move in. Defautls to next.
 * @param {Boolean} extend Optional. When true the selection is extended. Otherwise it si collapsed.
 * @type mozile.edit.State
 */
mozile.edit.navigateLeftRight.prepare = function(event, direction, extend) {
	var state = new mozile.edit.State(this, false); // Don't store selection	
	
	state.direction = mozile.edit.NEXT;
	if(direction) state.direction = direction;
	else if(event && event.keyCode == 37) state.direction = mozile.edit.PREVIOUS;

	state.extend = false;
	if(extend) state.extend = extend;
	else if(event) state.extend = event.shiftKey;

	state.reversible = false; // This command is not undoable.
	return state;
}

/**
 * Moves the cursor to the next insertion point. If a range is selected, the range is collapsed.
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.navigateLeftRight.execute = function(state, fresh) {
	var selection = mozile.dom.selection.get();
	
	// Move the selection.
	if(selection.isCollapsed || state.extend) {
		var IP = selection.getInsertionPoint();
		IP.seek(state.direction, true, mozile.document.documentElement);
		if(state.extend) IP.extend();
		else IP.select();
		//alert(IP +"\n"+ mozile.util.dumpValues(selection.store()));
	}

	// Collapse the selection.
	else {
		if(state.direction == mozile.edit.NEXT) selection.collapseToEnd();
		else selection.collapseToStart();
	}

	state.executed = true;
	return state;
}




/**
 * Used to move thr cursor through the document.
 * @param {String} name The group's name.
 * @constructor
 */
mozile.edit.Navigate = function(name) { 
	/**
	 * The name for this command group.
	 * @type String
	 */
	 this.name = name;
	 
	 /**
	  * Indicates that this command does not contain more commands.
	  * @type Boolean
	  */
	 this.group = false;
	 
	 /**
	  * Indicates that any contents of the selection should be removed before inserting.
	  * @type Boolean
	  */
	 this.remove = true;
	 
	 /**
	  * Specifies what kind of change this command makes to the document.
	  * See mozile.edit.Command.respond for possible values.
	  * @type String
	  */
	 this.makesChanges = "none";
	 
	 /**
	  * Specifies what kind of change will cause this command to change its isActive or isAvailable states.
	  * See mozile.edit.Command.respond for possible values.
	  * @type String
	  */
	 this.watchesChanges = "none";
	 
	 /**
	  * Indicates the target of the navigation. @see #mozile.edit._getTarget
	  * @type String
	  */
	 this.target = "text";
	 
	 /**
	  * Indicates the direction for the navigation. @see #mozile.edit._getTarget
	  * @type String
	  */
	 this.direction = "next";
	 
	 /**
	  * Indicates how the selection should be collapsed. Can be null, "start" or "end".
	  * @type String
	  */
	 this.collapse = null;

	 // Register this command group on the list of all commands.
	 mozile.edit.allCommands[this.name] = this;
}
mozile.edit.Navigate.prototype = new mozile.edit.Command;
mozile.edit.Navigate.prototype.constructor = mozile.edit.Navigate;


/**
 * Prepares a state object for the Split command.
 * @param {Event} event The event object to be converted into a state.
 * @type mozile.edit.State
 */
mozile.edit.Navigate.prototype.prepare = function(event) {
	var state = new mozile.edit.State(this);

	var target = mozile.edit._getTarget(event, this.target, this.direction);
	state.target = state.storeNode(target);

	// Some commands may need additional information from the user.
	// If so, define a method named "prompt", which takes the event and the state and returns a Boolean true when it succeeds.
	if(this.prompt) {
		if(!this.prompt(event, state)) return null;
	}

	state.reversible = false; // This command is not undoable.
	return state;
}

/**
 * Inserts a new element at the selection.
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.Navigate.prototype.execute = function(state, fresh) {
	var selection = mozile.dom.selection.get();

	var target = mozile.xpath.getNode(state.target);
	//alert(mozile.xpath.getXPath(target) +"\nDisplay: "+ 
	//	mozile.dom.getStyle(target, "display"));
	var direction = mozile.edit.NEXT;
	if(this.direction == "previous") direction = mozile.edit.PREVIOUS;

	// Move the selection.
	var IP = mozile.edit.getInsertionPoint(target, direction);
	if(IP) {
		IP.select();
		IP = mozile.edit.getInsertionPoint(target, -1 * direction);
		if(IP) IP.extend();
		if(this.collapse == "start") selection.collapseToStart();
		else if(this.collapse == "end") selection.collapseToEnd();
		selection.scroll();
	}
	//else alert("Could not get IP in "+ state.target);

	state.executed = true;
	return state;
}


/**
 * Inserts text into a text node.
 * @type mozile.edit.Command
 */
mozile.edit.insertText = new mozile.edit.Command("insertText");
mozile.edit.insertText.priority = 10;
mozile.edit.insertText.makesChanges = "text";
mozile.edit.insertText.watchesChanges = "none";

/**
 * True if the event was a keypress of a non-control and non-arrow character.
 * @param {Event} event Optional. The event object to be tested.
 * @param {Integer} direction Optional. The direction of the insertion. Defaults to next.
 * @param {String} content Optional. The content string to be inserted.
 * @param {Text} node Optional. A text node which will have its data replace with "content".
 * @type Boolean
 */
mozile.edit.insertText.test = function(event, direction, content, node) {
	if(event) {
		if(mozile.browser.isIE) {
			// Spaces are treated differently inside iframes?
			if(event.charCode == 32) {
				if(event.type != "keydown") return false;
			}
			else if(event.type != "keypress") return false;
		}
		else if(event.type != "keypress") return false;

		if(event.ctrlKey || event.metaKey) return false;
		if(!mozile.os.isMac && event.altKey) return false;

		// Special case: spaces
		// Don't insert a second consecutive space unless there is an alternateSpace
		if(!node && event.charCode == 32 && !mozile.alternateSpace) {
			var range = event.range;
			if(!range) range = mozile.dom.selection.get().getRangeAt(0);
			if(range.startContainer.nodeType == mozile.dom.TEXT_NODE) {
				if(range.startContainer.data.charAt(range.startOffset-1) == " ") {
					return false;
				}
			}
		}

		// Accept non-control characters
		if(event.charCode && event.charCode >= 32) {
			if(mozile.browser.isSafari && 
				event.charCode >= 63232 && 
				event.charCode <= 63235) return false;
			return true;
		}
		return false;
	}
	else {
		if(typeof(content) != "string") return false;
		//if(content.length < 1) return false;
	}

	return true;
}

/**
 * Prepares a state object for the insert text command.
 * @param {Event} event The event Optional. object to be converted into a state.
 * @param {Integer} direction Optional. The direction of the insertion. Defaults to next.
 * @param {String} content Optional. The content string to be inserted.
 * @param {Text} node Optional. A text node which will have its data replace with "content".
 * @type mozile.edit.State
 */
mozile.edit.insertText.prepare = function(event, direction, content, node) {
	var state = new mozile.edit.State(this);

	state.direction = mozile.edit.NEXT;
	if(direction) state.direction = direction;

	state.content = " ";
	if(content) state.content = content;
	else if(event) state.content = String.fromCharCode(event.charCode);
	
	state.collapse = false;
	if(event) state.collapse = true;
	
	state.node = state.storeNode(node);

	state.remove = false;

	// Handle special cases with spaces.
	var selection = null;
	if(event && event.selection) selection = event.selection;
	else selection = mozile.dom.selection.get();
	if(mozile.alternateSpace && !state.node && state.content == " ") {
		var range = selection.getRangeAt(0);
		var alt = mozile.alternateSpace;
		var text = range.startContainer;
		var offset = range.startOffset;

		// Get the next and previous characters. Determine if the preceeding character is an alternateSpace.
		var nextChar = null;
		if(range.endContainer.nodeType == mozile.dom.TEXT_NODE)
			nextChar = range.endContainer.data.charAt(range.endOffset);
		var previousChar = null;
		var previousAlt = false;
		if(text.nodeType == mozile.dom.TEXT_NODE) {
			previousChar = text.data.charAt(offset-1);
			var data = text.data.substring(0, offset);
			if(offset && data.lastIndexOf(alt) + alt.length == offset) {
				previousAlt = true;
				previousChar = text.data.charAt(offset - alt.length - 1);
			}
		}

		// Set the content, based on the variables we've collected.
		//alert(previousAlt +" '"+ previousChar +"' '"+ nextChar +"'");
		if(previousAlt) {
			if(previousChar && previousChar != " ") {
				state.remove = true;
				state.content = " " + alt;
			}
			//else if(!nextChar || nextChar == " ") state.content = alt;
			else if(nextChar && nextChar == " ") state.content = alt;
			// else insert the space
		}
		//else if(!nextChar || nextChar == " ") state.content = alt;
		else if(nextChar && nextChar == " ") state.content = alt;
		else if(!previousChar || previousChar == " ") state.content = alt;
		//alert("'"+state.content+"'");
	}

	return state;
}

/**
 * Inserts text at the current selection.
 * If the selection is not collapsed, the range is removed first. 
 * Uses the DOM Text.insertData() method.
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.insertText.execute = function(state, fresh) {
	var selection = mozile.dom.selection.get();
	if(!fresh) selection.restore(state.selection.before);
	state.emptyToken = false;
	state.actions = new Array();
	
	// Remove a non-collapsed range.
	if(!state.node && !selection.isCollapsed) {
		if(mozile.edit.remove) mozile.edit.remove.request(state, fresh, state.direction, null, true);
		else mozile.edit.removeText.request(state, fresh, state.direction);
	}
	
	// If a node was given, insert the data there.
	if(state.node) {
		var node = mozile.xpath.getNode(state.node);
		state.changedNode = node;
		state.oldData = node.data;
		node.data = state.content;
		if(state.direction == mozile.edit.NEXT) {
			selection.collapse(node, 0);
			selection.extend(node, node.data.length);
		}
		else {
			selection.collapse(node, node.data.length);
			selection.extend(node, 0);
		}
		state.selection.after = selection.store();
	}
	
	// If this node is an empty token, replace the token with the data.
	else if(mozile.edit.isEmptyToken(selection.focusNode)) {
		state.emptyToken = true;
		selection.focusNode.data = state.content;
		if(!state.collapse && state.content.length > 0) {
			selection.collapse(selection.focusNode, 0);
			selection.extend(selection.focusNode, state.content.length);
		}
		else selection.collapse(selection.focusNode, state.content.length);
		state.selection.after = selection.store();
	}
	
	// If this is not a text node, insert one.
	else if(selection.focusNode.nodeType != mozile.dom.TEXT_NODE) {
		state.newNode = mozile.document.createTextNode(state.content);
		if(selection.focusOffset == 0) mozile.dom.prependChild(state.newNode, selection.focusNode);
		else selection.focusNode.insertBefore(state.newNode, selection.focusNode.childNodes[selection.focusOffset]);
		if(!state.collapse && state.newNode.data.length > 0) {
			selection.collapse(state.newNode, 0);
			selection.extend(state.newNode, state.newNode.data.length);
		}
		else selection.collapse(state.newNode, state.newNode.data.length);
		state.selection.after = selection.store();
	}

	// Otherwise insert the text into this text node.
	else {	
		var text = selection.focusNode;
		var offset = selection.focusOffset;
		if(state.remove) {
			mozile.edit.removeText.request(state, fresh, -1 * state.direction, mozile.alternateSpace);
			offset -= mozile.alternateSpace.length;
		}
		text.insertData(offset, state.content);
		var newOffset = offset + (state.direction * state.content.length);
		//if(!mozile.browser.isIE) selection.collapse(selection.focusNode, newOffset);
		if(!state.collapse && offset != newOffset) {
			selection.collapse(text, offset);
			selection.extend(text, newOffset);
		}
		else selection.collapse(text, newOffset);
		if(state.actions.length == 0) 
			state.selection.after = selection.store(state.selection.before, newOffset);
		else state.selection.after = selection.store();
	}

	state.executed = true;
	return state;
}

/**
 * Removes inserted text and restores any removed range.
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.insertText.unexecute = function(state, fresh) {
	var selection = mozile.dom.selection.get();
	if(!fresh) selection.restore(state.selection.after);

	// Remove empty token, new node, or text.
	if(state.changedNode) state.changedNode.data = state.oldData;
	else if(state.emptyToken) selection.focusNode.data = mozile.emptyToken;
	else if(state.newNode) state.newNode.parentNode.removeChild(state.newNode);
	else selection.focusNode.deleteData(selection.focusOffset - state.content.length, state.content.length);

	// Unexecute any other actions in reverse order.
	for(var i = state.actions.length - 1; i >= 0; i--) {
		state.actions[i] = state.actions[i].command.unexecute(state.actions[i], fresh);
		if(state.actions[i].executed) throw("Error: mozile.edit.inertText.unexecute Child command unexecute failed at action "+ i +".");
	}
	
	
	selection.restore(state.selection.before);
	state.executed = false;
	return state;
}




/**
 * Removes text from a node.
 * @type mozile.edit.Command
 */
mozile.edit.removeText = new mozile.edit.Command("removeText");
mozile.edit.removeText.priority = 10;
mozile.edit.removeText.makesChanges = "text";
mozile.edit.removeText.watchesChanges = "none";

/**
 * True if the selection is text, if the event was a keypress of the backspace key or delete key, and if the operation won't take the selection out of the current node.
 * @param {Event} event Optional. The event object to be tested.
 * @param {Integer} direction Optional. The direction for the removal to use. Defaults to previous.
 * @param {String} content Optional. The content removed.
 * @type Boolean
 */
mozile.edit.removeText.test = function(event, direction, content) {
	var dir;
	if(event) {
		if(mozile.edit.remove) return false;
		if(mozile.edit.checkAccelerator(event, "Backspace"))
			dir = mozile.edit.PREVIOUS;
		else if(mozile.edit.checkAccelerator(event, "Delete"))
			dir = mozile.edit.NEXT;
		if(!dir) return false;	
	}
	
	if(!dir) dir = mozile.edit.PREVIOUS;
	if(direction == mozile.edit.NEXT) dir = direction;

	if(!event) event = {type: "fake"};
	var node = mozile.edit._getNode(event);
	if(!node || node.nodeType != mozile.dom.TEXT_NODE) return false;

	// Make sure the removal will stay within the text node.
	if(event.type != "fake") {
		var selection = event.selection;
		if(selection.isCollapsed) {
			var IP = selection.getInsertionPoint(true);
			if(!IP) return false;
			if(!IP.seek(dir)) return false;
			if(!IP || IP.getNode() !== node) return false;
		}
		return true;
	}
	else return true;
}


/**
 * Prepares a state object for the remove text command.
 * @param {Event} event The event Optional. object to be converted into a state.
 * @param {Integer} direction Optional. The direction for the removal to use. Defaults to previous.
 * @param {String} content Optional. The content removed.
 * @type mozile.edit.State
 */
mozile.edit.removeText.prepare = function(event, direction, content) {
	var state = new mozile.edit.State(this);
	
	state.direction = mozile.edit.PREVIOUS;
	if(direction) state.direction = direction;
	else if(event && mozile.edit.convertKeyCode(event.keyCode) == "Delete")
		state.direction = mozile.edit.NEXT;

	state.content = null;
	if(content) state.content = content;

	return state;
}

/**
 * Removes text at the current selection. Stores the removed text so that the operation can be undone. 
 * If the selection is collapsed, the state.direction is used to deleted the next character in that direction.
 * If the selection is not collapsed, all text inside the range is removed.
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.removeText.execute = function(state, fresh) {
	//alert("Removing Text...");
	var selection = mozile.dom.selection.get();
	if(!fresh) selection.restore(state.selection.before);
	if(!state.direction) state.direction = mozile.edit.PREVIOUS;
	
	if(selection.isCollapsed) {
		var firstOffset = selection.focusOffset;
		var secondOffset = selection.focusOffset;
		if(!state.content) {
			var IP = selection.getInsertionPoint();
			IP.seek(state.direction);
			if(state.direction == mozile.edit.PREVIOUS) firstOffset = IP.getOffset();
			else secondOffset = IP.getOffset();
			state.content = selection.focusNode.data.substring(firstOffset, secondOffset);
		}
		else {
			if(state.direction == mozile.edit.PREVIOUS)
				firstOffset -= state.content.length;
			else secondOffset += state.content.length;
		}
		if(firstOffset < 0) firstOffset = 0;
		if(firstOffset + state.content.length <= selection.focusNode.data.length) {
			selection.focusNode.deleteData(firstOffset, state.content.length);
			selection.collapse(selection.focusNode, firstOffset);
			state.selection.after = selection.store(state.selection.before, firstOffset);
		}
		else mozile.debug.debug("mozile.edit.removeText.execute", "Content length too great. firstOffset="+ firstOffset +" content='"+ state.content +"' data='"+ selection.focusNode.data +"'");
	}

	else {
		var range = selection.getRangeAt(0);
		// Hack to compensate for a problem translating IE TextRanges to Ranges in the case of adjacent text nodes.
		if(mozile.browser.isIE && range.startContainer != range.endContainer) {
			if(range.endOffset == 0) range.setEnd(range.startContainer, range.startContainer.data.length);
			else range.setStart(range.endContainer, 0);
		}
		//alert(mozile.util.dumpValues(range.store()));
		state.content = range.startContainer.data.substring(range.startOffset, range.endOffset);
		range.startContainer.deleteData(range.startOffset, range.endOffset - range.startOffset);
		selection.collapse(range.startContainer, range.startOffset);
		state.selection.after = selection.store(state.selection.before, range.startOffset);
	}

	state.executed = true;
	//alert("Removed Text");
	return state;
}

/**
 * Restores removed text.
 * @param {mozile.edit.State} state The state information needed to unexecute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.removeText.unexecute = function(state, fresh) {
	var selection = mozile.dom.selection.get();
	if(!fresh) selection.restore(state.selection.after);

	if(!selection || !selection.focusNode) throw("Error: mozile.edit.removeText.unexecute no selection.focusNode");
	selection.focusNode.insertData(selection.focusOffset, state.content);

	//var newOffset = selection.focusOffset;
	//if(state.direction == mozile.edit.PREVIOUS) newOffset += state.content.length;
	//Optimize for IE? if(!mozile.browser.isIE || mozile.edit.NEXT) selection.collapse(selection.focusNode, newOffset);
	//selection.collapse(selection.focusNode, newOffset);
	selection.restore(state.selection.before);
	state.executed = false;
	return state;
}


/**
 * Searches for text in the editor.
 * @type mozile.edit.Command
 */
mozile.edit.findText = new mozile.edit.Command("findText");
mozile.edit.findText.makesChanges = "none";
mozile.edit.findText.watchesChanges = "none";


/**
 * True as long as a targetText string is provided.
 * @param {Event} event Optional. The event object to be tested.
 * @param {String} targetText The text to search for.
 * @param {Integer} direction Optional. The direction for the search to use. Defaults to next.
 * @param {Boolean} ignoreCase Optional. When true the case will be ignored when matching. Defaults to false.
 * @param {Boolean} wrapAround Optional. When true the search will wrap from the end of the container to the beginning. Defaults to true.
 * @param {Node} container Optional. The node to search. Defaults to the selection's editable container.
 * @type Boolean
 */
mozile.edit.findText.test = function(event, targetText, direction, ignoreCase, wrapAround, container) {
	if(!targetText) return false;
	return true;
}


/**
 * Prepares a state object for the find text command.
 * @param {Event} event Optional. The event object to be tested.
 * @param {String} targetText The text to search for.
 * @param {Integer} direction Optional. The direction for the search to use. Defaults to next.
 * @param {Boolean} ignoreCase Optional. When true the case will be ignored when matching. Defaults to false.
 * @param {Boolean} wrapAround Optional. When true the search will wrap from the end of the container to the beginning. Defaults to true.
 * @param {Node} container Optional. The node to search. Defaults to the selection's editable container.
 * @type Boolean
 */
mozile.edit.findText.prepare = function(event, targetText, direction, ignoreCase, wrapAround, container) {
	var state = new mozile.edit.State(this);
    state.reversible = false;

	state.targetText = targetText;
	
	state.direction = mozile.edit.NEXT;
	if(direction) state.direction = direction;
	state.ignoreCase = false;
	if(ignoreCase) state.ignoreCase = true;
	state.wrapAround = true;
	if(wrapAround === false || wrapAround === null) state.wrapAround = false;
	state.container = null;
	if(container) state.container = state.storeNode(container);	

	return state;
}



/**
 * Searches for the next occurrence of the target string.
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.findText.execute = function(state, fresh) {
	var selection = mozile.dom.selection.get();
	if(!fresh) selection.restore(state.selection.before);

	var direction = state.direction;
    var target = state.targetText;

    if(direction == mozile.edit.NEXT) selection.collapseToEnd();
    else selection.collapseToStart();

    var startIP = selection.getInsertionPoint();
    var IP = new mozile.edit.InsertionPoint(startIP.getNode(), startIP.getOffset());
    var container;
    if(state.container) container = mozile.xpath.getNode(state.container);
    else container = mozile.edit.getContainer(startIP.getNode());

    var wrapped = false;
    var offset = 0;
    if(direction == mozile.edit.PREVIOUS) offset = 1;
    var matched = "";
    var index = 0;
    var matchStart;
    while(true) {
        //console.info(IP.getNode(), IP.getOffset(), IP.charAt());
        var character = IP.charAt();
        if(direction == mozile.edit.NEXT) position = matched.length;
		else position = target.length - matched.length - 1;
		if(position >= 0) {
			var compare = false;
			if(state.ignoreCase) {
				if(character.toLowerCase() == target.charAt(position).toLowerCase())
					compare = true;
			}
			else if(character == target.charAt(position)) compare = true;
			if(compare) {
				if(matched.length == 0) {
					matchStart = new mozile.edit.InsertionPoint(
						IP.getNode(), IP.getOffset() + offset);
				}
				if(direction == mozile.edit.NEXT) matched = matched + character;
				else matched = character + matched;
				if(matched.length == target.length) {
					if(direction == mozile.edit.NEXT) IP.next();
					break;
				}
			}
			else if(character) {
				matched = "";
				matchStart = null;
			}
        }

        if(state.wrapAround) {
        	if(wrapped && startIP.getNode() == IP.getNode()) {
        		if(direction == mozile.edit.NEXT) {
        			if(IP.getOffset() >= startIP.getOffset()) break;
        		}
        		else {
        			if(IP.getOffset() <= startIP.getOffset()) break;
        		}
        	}
        	if(!IP.seek(direction, false, container)) {
        		if(wrapped) break;
        		wrapped = true;
				matched = "";
				matchStart = null;
        		IP = mozile.edit.getInsertionPoint(container, direction, true);
        	}
        }
        else if(!IP.seek(direction, false, container)) break;
    }

    if(matchStart) {
    	if(direction == mozile.edit.NEXT) {
			matchStart.select();
			IP.extend();
        }
        else {
			IP.select();
			matchStart.extend();
        }
    }

    state.executed = true;
    return state;
}




/**
 * Replaces all occurrences of the target text with a replacement.
 * @type mozile.edit.Command
 */
mozile.edit.replaceText = new mozile.edit.Command("replaceText");
mozile.edit.replaceText.makesChanges = "text";
mozile.edit.replaceText.watchesChanges = "none";


/**
 * True as long as a targetText and replacementText strings is provided.
 * @param {Event} event Optional. The event object to be tested.
 * @param {String} targetText The text to search for.
 * @param {String} replacementText The new text to replace the old text.
 * @param {Boolean} ignoreCase Optional. When true the case will be ignored when matching. Defaults to false.
 * @param {Boolean} wrapAround Optional. When true the search will wrap from the end of the container to the beginning. Defaults to true.
 * @param {Node} container Optional. The node to search. Defaults to the selection's editable container.
 * @type Boolean
 */
mozile.edit.replaceText.test = function(event, targetText, replacementText, ignoreCase, wrapAround, container) {
	if(!targetText) return false;
	if(!replacementText) return false;
	return true;
}


/**
 * Prepares a state object for the replace text command.
 * @param {Event} event Optional. The event object to be tested.
 * @param {String} targetText The text to search for.
 * @param {String} replacementText The new text to replace the old text.
 * @param {Boolean} ignoreCase Optional. When true the case will be ignored when matching. Defaults to false.
 * @param {Boolean} wrapAround Optional. When true the search will wrap from the end of the container to the beginning. Defaults to true.
 * @param {Node} container Optional. The node to search. Defaults to the selection's editable container.
 * @type Boolean
 */
mozile.edit.replaceText.prepare = function(event, targetText, replacementText, ignoreCase, wrapAround, container) {
	var state = new mozile.edit.State(this);

	state.targetText = targetText;
	state.replacementText = replacementText;
	
	state.ignoreCase = false;
	if(ignoreCase) state.ignoreCase = true;
	state.wrapAround = false;
	if(wrapAround === false || wrapAround === null) state.wrapAround = false;
	state.container = null;
	if(container) state.container = state.storeNode(container);	

	return state;
}



/**
 * Replace all occurrence of the targetText with the replacementText.
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.replaceText.execute = function(state, fresh) {
	var selection = mozile.dom.selection.get();
	if(!fresh) selection.restore(state.selection.before);
	state.actions = new Array();

	var direction = mozile.edit.NEXT;
    var target = state.targetText;
    var replacement = state.replacementText;

    var selection = mozile.dom.selection.get();
    selection.restore();
    selection.collapseToEnd();
    var startIP = selection.getInsertionPoint();
    var IP = new mozile.edit.InsertionPoint(startIP.getNode(), startIP.getOffset());
    var container;
    if(state.container) container = mozile.xpath.getNode(state.container);
    else container = mozile.edit.getContainer(startIP.getNode());

    var wrapped = false;
    var matched = "";
    var matchStart, compare;
    while(true) {
        //console.info(IP.getNode(), IP.getOffset(), IP.charAt());
        var character = IP.charAt();
        position = matched.length;
		compare = false;
		if(state.ignoreCase) {
			if(character.toLowerCase() == target.charAt(position).toLowerCase())
				compare = true;
		}
		else if(character == target.charAt(position)) compare = true;
		if(compare) {
			if(matched.length == 0) {
				matchStart = new mozile.edit.InsertionPoint(
					IP.getNode(), IP.getOffset());
			}
			matched = matched + character;
			if(matched.length == target.length) {
				IP.next();
				matchStart.select();
				IP.extend();
				// This is a bit of a hack:
				if(mozile.event) {
					mozile.event.storeSelection({selection: selection});
				}
				mozile.edit.insertText.request(state, false, null, replacement);
			}
		}
		else if(character) {
			matched = "";
			matchStart = null;
		}

        if(state.wrapAround) {
        	if(wrapped && startIP.getNode() == IP.getNode()) {
				if(IP.getOffset() >= startIP.getOffset()) break;
        	}
        	if(!IP.seek(direction, false, container)) {
        		if(wrapped) break;
        		wrapped = true;
        		IP = mozile.edit.getInsertionPoint(container, direction, true);
        	}
        }
        else if(!IP.seek(direction, false, container)) break;
    }
    
    state.selection.after = selection.store();    
    state.executed = true;
    return state;
}





/**
 * Display 
 * @type mozile.edit.Command
 */
mozile.edit.showHidden = new mozile.edit.Command("showHidden");
mozile.edit.showHidden.makesChanges = "node";
mozile.edit.showHidden.watchesChanges = "node";
mozile.edit.showHidden.linkElement = null;
mozile.edit.commands.addCommand(mozile.edit.showHidden);

/**
 * Prepares a state object for the remove command.
 * @param {Event} event The event object to be converted into a state.
 * @type mozile.edit.State
 */
mozile.edit.showHidden.isActive = function(event) {
	if(this.linkElement) return true;
	return false;
}


/**
 * True as long as a targetText and replacementText strings is provided.
 * @param {Event} event Optional. The event object to be tested.
 * @type Boolean
 */
mozile.edit.showHidden.test = function(event) {
	if(event) {
		if(!mozile.edit.checkAccelerators(event, this.accels)) return false;	
	}

	return true;
}

/**
 * Removes both text and ranges.
 * @param {mozile.edit.State} state The state information needed to execute this command.
 * @param {Boolean} fresh Optional. A value of "true" indicates that the window's selection is already in the correct place and does not need to be moved.
 * @type Object
 */
mozile.edit.showHidden.execute = function(state, fresh) {
	// Remove the stylesheet if it's already present.
	if(this.linkElement && this.linkElement.parentNode) {
		this.linkElement.parentNode.removeChild(this.linkElement);
		this.linkElement = null;
	}

	// Add the stylesheet if it's not already present.
	else {
		var path = mozile.joinPaths(mozile.root, "src", "gui", "hidden.css");
		this.linkElement = mozile.dom.addStyleSheet(path);
	}

	state.reversible = false;
	state.executed = true;
	return state;
}







/**** Detection Methods ****/


/**
 * A temporary hack to check whether a node is a block level element or not.
 * TODO: Replace with an RNG based method.
 * @param {Node} node
 * @type Boolean
 */
mozile.edit.isBlock = function(node) {
	if(!node) return false;
	if(node.nodeType != mozile.dom.ELEMENT_NODE) return false;
	var display = mozile.dom.getStyle(node, "display");
	switch(display) {
		// TODO: Include more cases
		case "block":
		case "list-item":
			return true;
	}
	return false;
}


/**
 * Returns the node if it is a block, or the first ancestor which is a block.
 * @param {Node} node
 * @type Element
 */
mozile.edit.getParentBlock = function(node) {
	while(node) {
		if(mozile.edit.isBlock(node)) return node;
		else node = node.parentNode;
	}
	return null;
}


/**
 * Checks a node to see if it may contain non-whitespace text.
 * If an element is given, it is checked. If a text node is given, the parent element is checked. For all other nodes the method retuns false.
 * If there is an RNG rule which allows the element to contain text, then the method returns true. If there is no RNG rule, but the element contains non-white-space text node children, then the method returns true. Otherwise it return false.
 * @param {Node} node The node to check.
 * @type Boolean
 */
mozile.edit.mayContainText = function(node) {
	if(node && node.nodeType == mozile.dom.TEXT_NODE) node = node.parentNode;
	if(node && node.nodeType == mozile.dom.ELEMENT_NODE) {
		var rng = mozile.edit.lookupRNG(node);
		if(rng) return rng.mayContain("text");
		else {
			if(mozile.edit.getMark(node, "mayContainText") == true) return true;
			mozile.debug.debug("mozile.edit.mayContainText", "No RNG Element for element named '"+ node.nodeName +"'.");
			// If any child text node is editable, then this node may contain text.
			for(var i=0; i < node.childNodes.length; i++) {
				if(mozile.edit.isTextEditable(node.childNodes[i]))
					return mozile.edit.setMark(node, "mayContainText", true);
			}
			return false;
		}
	}

	return false;
}

/**
 * Checks a text node to see if it is editable. Only non-white-space and empty token nodes are editable.
 * @param {Node} node The node to check.
 * @type Boolean
 */
mozile.edit.isTextEditable = function(node) {
	if(node.nodeType != mozile.dom.TEXT_NODE) return false;
	if(!node.data) return false;
	if(mozile.edit.isEmptyToken(node)) return true;
	if(mozile.dom.isWhitespace(node)) return false;
	return true;	
}

/**
 * A temporary hack to check whether a node can have child nodes.
 * TODO: Replace with an RNG based method.
 * @param {Node} node
 * @type Boolean
 */
mozile.edit.isChildless = function(node) {
	if(node.nodeType == mozile.dom.COMMENT_NODE) return true;
	if(node.nodeType != mozile.dom.ELEMENT_NODE) return false;
	
	var rng = mozile.edit.lookupRNG(node);
	if(rng) {
		if(rng.mayContain("element")) return false;
		else return true;
	}
	else return false;
}

/**
 * Creates an empty token node.
 * @type Text
 */
mozile.edit.createEmptyToken = function() {
	return mozile.document.createTextNode(mozile.emptyToken);
}

/**
 * Determines whether a node is an "empty token" instance. That is, is it a text node which contains only the mozile.emptyToken character(s)?
 * @param {Node} node The text node to check.
 * @type Boolean
 */
mozile.edit.isEmptyToken = function(node) {
	if(node && node.nodeType == mozile.dom.TEXT_NODE &&
		node.data == mozile.emptyToken) return true;
	else return false;
}

/**
 * Determines whether a text node ends with an empty token instance.
 * @param {Node} node The text node to check.
 * @param {Integer} offset Optional. An offset within the text node. The method will look for an empty token immediately after this offset. If none is given, the method searches for any empty token. 
 * @type Boolean
 */
mozile.edit.containsEmptyToken = function(node, offset) {
	if(!node || node.nodeType != mozile.dom.TEXT_NODE) return false;
	if(offset == undefined || Number(offset)) {
		if(node.data.indexOf(mozile.emptyToken) > -1) return true;
		else return false;
	}
	else {
		var data = node.data.substring(offset);
		if(data.indexOf(mozile.emptyToken) == 0) return true;
		else return false;
	}
}

/**
 * Determines whether a node is empty: it contains no non-white-space text and no empty tokens inside any of its children.
 * @param {Node} node The node to check.
 * @type Boolean
 */
mozile.edit.isEmpty = function(node) {
	switch(node.nodeType) {
		case mozile.dom.TEXT_NODE:
			if(node.data.match(/\S/)) return false;
			if(mozile.edit.isEmptyToken(node)) return false;
			return true;

		case mozile.dom.ELEMENT_NODE:
			var children = node.childNodes;
			var i=0;
			// Check text nodes.
			for(i=0; i < children.length; i++) {
				if(children[i].nodeType == mozile.dom.TEXT_NODE &&
					!mozile.edit.isEmpty(children[i]) ) 
					return false;
			}
			// Check element nodes.
			for(i=0; i < children.length; i++) {
				if(children[i].nodeType == mozile.dom.ELEMENT_NODE &&
					!mozile.edit.isEmpty(children[i]) ) 
					return false;
			}
			return true;

		default: 
			return true;
	}
}



/**** Support Methods ****/

/**
 * Finds the name of the command's element.
 * @private
 * @param {mozile.edit.Command} command The calling command.
 * @type Element
 */
mozile.edit._getElementName = function(command) {
	var elementName;
	if(typeof(command.element) == "string") elementName = command.element;
	else if(command.element && command.element.cloneNode)
		elementName = mozile.dom.getLocalName(command.element);
	elementName = elementName.toLowerCase();
	return elementName;
}

/**
 * A function shared by several commands gets the current selection, range, and commonAncestorContainer.
 * @private
 * @param {Event} event Optional. The current event.
 * @type Element
 */
mozile.edit._getNode = function(event) {
	var node;
	if(event && event.node) node = event.node;
	if(!node) {
		var selection;
		if(event && event.selection) selection = event.selection;
		if(!selection) selection = mozile.dom.selection.get();
		if(!selection) return false;
		var range;
		if(event && event.range) range = event.range;
		if(!selection.rangeCount) return false;
		if(!range) range = selection.getRangeAt(0);
		if(!range) return false;
		node = range.commonAncestorContainer;
		if(event) {
			event.selection = selection;
			event.range = range;
			event.node = node;
		}
	}

	if(!node) return null;
	else return node;
}


/**
 * A function shared by several commands which finds a target element.
 * The technique is to generate test function from the given target string, and then to iterate through nodes in the given direction, testing each node with the new test function until a positive result is found.
 * @private
 * @param {Event} event Optional. The current event.
 * @param target A string specifying the target, or a function which returns the target. Strings can be: "any", "text", "element", "block" or "localname tagName".
 * @param {String} direction Optional. A string specifying the direction to search in. Can be "ancestor" (the default), "descendant", "next", or "previous".
 * @param {Boolean} allowInvisible Optional. When true, non-visible nodes will be returned.
 * @type Element
 */
mozile.edit._getTarget = function(event, target, direction, allowInvisible) {
	// Set the direction.
	if(!direction) direction = "ancestor";
	if(direction != "ancestor" && direction != "descendant" &&
		direction != "next" && direction != "previous") {
		mozile.debug.debug("mozile.edit._getTarget", "Invalid direction '"+ direction +"'.");
		return null;
	}

	// Check the cache.	
	if(typeof(target) == "string") {
		if(event && !event.targetCache) event.targetCache = new Object();
		var cacheKey = target.replace(" ", "_") + "__" + direction;
		if(event && event.targetCache[cacheKey])
			return event.targetCache[cacheKey];
	}

	var node = mozile.edit._getNode(event);
	if(!node) return null;
	var test, result;

	// Check the target. Either get a result or generate a test function.
	if(typeof(target) == "function") {
		result = target(event, null);
	}
	else if(typeof(target) == "string") {
		// "Any" case
		if(target.toLowerCase() == "any") {
			test = function(node) {
				if(node) return true;
				else return false;
			}
		}

		// Text case
		else if(target.toLowerCase() == "text") {
			test = function(node) {
				if(node.nodeType == mozile.dom.TEXT_NODE) return true;
				else return false;
			}
		}

		// Element case
		else if(target.toLowerCase() == "element") {
			test = function(node) {
				if(node.nodeType == mozile.dom.ELEMENT_NODE) return true;
				else return false;
			}
		}
		
		// Block case
		else if(target.toLowerCase() == "block") {
			test = function(node) {
				if(mozile.edit.isBlock(node)) return true;
				else return false;
			}
		}
		
		// LocalName case
		else if(target.toLowerCase().indexOf("localname") == 0) {
			var name = target.substring(10);
			name = name.toLowerCase();
			test = function(node) {
				var localName = mozile.dom.getLocalName(node);
				if(localName && localName.toLowerCase() == name) return true;
				else return false;
			}
		}
		
		// Unknown case
		else return null;
	}
	else return null;
	
	// Run the test function. Iterate in the given direction.
	var treeWalker;
	if(test && !result) {
		if(direction != "ancestor" && !treeWalker) {
			var root = mozile.document.documentElement;
			if(direction == "descendant") {
				root = node;
				if(root.nodeType != mozile.dom.ELEMENT_NODE) root = root.parentNode;
				direction = "next";
			}
			treeWalker = document.createTreeWalker(root, mozile.dom.NodeFilter.SHOW_ALL, null, false);
			treeWalker.currentNode = node;
		}
		
		var startNode = node;
		while(node) {
			if(direction == "next") node = treeWalker.nextNode();
			else if(direction == "previous") {
				node = treeWalker.previousNode();
				if(mozile.dom.isAncestorOf(node, startNode)) continue;
			}
			//alert(mozile.xpath.getXPath(node));
			// TODO: We want to make sure that the node is editable,
			// but I'm not sure this is thr right way to do it.
			if(node && test(node) && mozile.edit.isEditable(node) &&
				(allowInvisible || mozile.dom.isVisible(node)) &&
				mozile.edit.getInsertionPoint(node, mozile.edit.NEXT)) {
				result = node;
				break;
			}
			if(direction == "ancestor") node = node.parentNode;
		}
	}

	if(result) {
		if(event) event.targetCache[cacheKey] = result;
		return result;
	}
	else return null;
}


/**** Final Configuration ****/
// Enable basic text editing.
mozile.enableEditing(false);



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