dom.js

Summary

A collection of objects and methods to extend the Document Object Model.

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

Author: James A. Overton


Class Summary
mozile.dom.Range  
mozile.dom.Selection  

/* ***** 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 A collection of objects and methods to extend the Document Object Model.
 * @link http://mozile.mozdev.org 
 * @author James A. Overton <james@overton.ca>
 * @version 0.8
 * $Id: overview-summary-dom.js.html,v 1.15 2008/02/20 18:47:09 jameso Exp $
 */

mozile.provide("mozile.dom.*");
mozile.provide("mozile.dom.Range");
mozile.provide("mozile.dom.Selection");

/**
 * A collection of DOM classes and methods.
 * @type Object
 */
mozile.dom = new Object();
// JSDoc hack
mozile.dom.prototype = new mozile.Module;

/**
 * Define some shared constants.
 */
mozile.dom.ELEMENT_NODE                = 1;
mozile.dom.ATTRIBUTE_NODE              = 2;
mozile.dom.TEXT_NODE                   = 3;
mozile.dom.CDATA_SECTION_NODE          = 4;
mozile.dom.ENTITY_REFERENCE_NODE       = 5;
mozile.dom.ENTITY_NODE                 = 6;
mozile.dom.PROCESSING_INSTRUCTION_NODE = 7;
mozile.dom.COMMENT_NODE                = 8;
mozile.dom.DOCUMENT_NODE               = 9;
mozile.dom.DOCUMENT_TYPE_NODE          = 10;
mozile.dom.DOCUMENT_FRAGMENT_NODE      = 11;
mozile.dom.NOTATION_NODE               = 12;


/**
 * An array of "link" elements which have been added to this document.
 * @type Array
 */
mozile.dom.links = new Array();


/**
 * Gets the document's body element, if it exists. If none exists the document element is returned.
 * @param {Node} node Optional. A node belonging to the document to be searched. If none is given, the current document is used.
 * @type Element
 */
mozile.dom.getBody = function(node) {
	var doc = mozile.document;
	if(node) doc = node.ownerDocument;
	var elements = doc.getElementsByTagName("body");
	if(elements && elements[0]) return elements[0];
	else return doc.documentElement;
}

/**
 * Gets the document's head element, if it exists. If none exists the document element is returned.
 * @param {Node} node Optional. A node belonging to the document to be searched. If none is given, the current document is used.
 * @type Element
 */
mozile.dom.getHead = function(node) {
	var doc = mozile.document;
	if(node) doc = node.ownerDocument;
	var elements = doc.getElementsByTagName("head");
	if(elements && elements[0]) return elements[0];
	else return doc.documentElement;
}

/**
 * Gets the given node's first child element, if one exists.
 * @param {Node} parent The parent node to check.
 * @type Element
 */
mozile.dom.getFirstChildElement = function(parent) {
	for(var i=0; i < parent.childNodes.length; i++) {
		if(parent.childNodes[i].nodeType == mozile.dom.ELEMENT_NODE) 
			return parent.childNodes[i];
	}
	return null;
}

/**
 * Returns an array of all given node's child elements.
 * @param {Node} parent The parent node to check.
 * @type Array
 */
mozile.dom.getChildElements = function(parent) {
	var children = new Array();
	for(var i=0; i < parent.childNodes.length; i++) {
		if(parent.childNodes[i].nodeType == mozile.dom.ELEMENT_NODE) 
			children.push(parent.childNodes[i]);
	}
	return children;
}

/**
 * Gets the given node's next sibling element, if one exists.
 * @param {Node} node The node to start with.
 * @type Element
 */
mozile.dom.getNextSiblingElement = function(node) {
	var sibling = node.nextSibling;
	while(sibling) {
		if(sibling.nodeType == mozile.dom.ELEMENT_NODE) return sibling;
		sibling = sibling.nextSibling;
	}
	return null;
}

/**
 * Gets the given node's previous sibling element, if one exists.
 * @param {Node} node The node to start with.
 * @type Element
 */
mozile.dom.getPreviousSiblingElement = function(node) {
	var sibling = node.previousSibling;
	while(sibling) {
		if(sibling.nodeType == mozile.dom.ELEMENT_NODE) return sibling;
		sibling = sibling.previousSibling;
	}
	return null;
}



/**
 * Returns a list of elements, given a bunch of arguments.
 * Examples:
 * - Get all elements with class "editor": mozile.dom.getElements(null, "editor");
 * - Get all elements with contentEditable "true": mozile.dom.getElements("contentEditable", "true");
 * @param {String} attr The name of an attribute to search for. Defaults to "class". If the value is "local name" then the local name of the element is used (lowercase).
 * @param {String} value Optional. The value of an attribute. If none is given any value is accepted.
 * @param {Element} root Optional. The root element for the search. Defaults to the documentElement.
 * @param {Boolean} single Optional. When true, only one item is added.
 * @type Array
 */
mozile.dom.getElements = function(attr, value, root, single) {
	var list = new Array();
	if(!attr) attr = "class";
	if(!root) root = mozile.document.documentElement;

	if(document.createTreeWalker) {
		var treeWalker = document.createTreeWalker(root, mozile.dom.NodeFilter.SHOW_ELEMENT, null, false);
		var node = treeWalker.currentNode;
		while(node) {
			if(attr == "class" && mozile.dom.hasClass(node, value))
				list.push(node);
			else if(attr != "local name" && node.getAttribute(attr)) {
				if(!value) list.push(node);
				else if(node.getAttribute(attr) == value) list.push(node);
			}
			else if(attr == "local name" && mozile.dom &&
				mozile.dom.getLocalName(node).toLowerCase() == value) {
				list.push(node);
			}
			if(single && list.length > 0) break;
			node = treeWalker.nextNode();
		}
	}
	
	return list;
}



/**
 * Finds all of the text nodes under a given node and returns a string with their concatenated contents.
 * @param {Node} node The node to search.
 * @type String
 */
mozile.dom.getText = function(node) {
	if(!node) return "";

	if(node.nodeType == mozile.dom.TEXT_NODE && !mozile.dom.isWhitespace(node)) {
		return node.data;
	}
	else if(node.nodeType == mozile.dom.ATTRIBUTE_NODE) {
		return node.nodeValue;
	}
	else if(node.nodeType == mozile.dom.ELEMENT_NODE) {
		var text = "";
		for(var i=0; i < node.childNodes.length; i++) {
			text += mozile.dom.getText(node.childNodes[i]);
		}
		return text;
	}
	
	return "";
}


/**
 * Inserts the newNode after the refNode in the refNode's parent node.
 * @param {Node} newNode The node to insert.
 * @param {Node} refNode Insert the new node after this node.
 * @type Node
 */
mozile.dom.insertAfter = function(newNode, refNode) {
	if(!newNode) throw("Error [mozile.dom.insertAfter]: No newNode. newNode: "+ newNode +" refNode: "+ refNode);
	if(!refNode) throw("Error [mozile.dom.insertAfter]: No refNode. newNode: "+ newNode +" refNode: "+ refNode);
	var parentNode = refNode.parentNode;
	if(!parentNode) return null;
	if(refNode.nextSibling) return parentNode.insertBefore(newNode, refNode.nextSibling);
	else return parentNode.appendChild(newNode);
}

/**
 * Inserts a new node as the first child of the given parent node.
 * @param {Node} newNode The node to insert.
 * @param {Node} parentNode The parent to attach this node to.
 * @type Node
 */
mozile.dom.prependChild = function(newNode, parentNode) {
	if(parentNode.firstChild) return parentNode.insertBefore(newNode, parentNode.firstChild);
	else return parentNode.appendChild(newNode);
}

/**
 * Determines whether the ancestorNode is an ancestor of the descendantNode. If the ancestorNode is the descendantNode, the method returns true.
 * @param {Node} ancestorNode 
 * @param {Node} descendantNode 
 * @param {Node} limitNode Optional. The search will stop at this node, no matter what happens. 
 * @type Boolean
 */
mozile.dom.isAncestorOf = function(ancestorNode, descendantNode, limitNode) {
	var checkNode = descendantNode;
	while(checkNode) {
		if(checkNode == ancestorNode) return true;
		else if(checkNode == limitNode) return false;
		else checkNode = checkNode.parentNode;
	}
	return false;
}

/**
 * Returns the first node which is an ancestor of both given nodes.
 * @param {Node} firstNode 
 * @param {Node} secondNode
 * @type Node
 */
mozile.dom.getCommonAncestor = function(firstNode, secondNode) {
	var ancestor = firstNode;
	while(ancestor) {
		if(mozile.dom.isAncestorOf(ancestor, secondNode)) return ancestor;
		else ancestor = ancestor.parentNode;
	}
	return null;
}

/**
 * A regular expression to check for non-whitespace characters.
 * @private
 * @type RegExp
 */
mozile.dom._matchNonWhitespace = /\S/m;

/**
 * Decides whether the given node contains non-whitespace text.
 * @param {Node} node The node to be checked.
 * @type Boolean
 */
mozile.dom.isWhitespace = function(node) {
	if(!node || !node.nodeValue) return false;
	if(node.nodeValue.match(mozile.dom._matchNonWhitespace)) return false;
	return true;
}

/**
 * Determines whether a given node is visible to the user. It will not be visible if it is not attached to the current document, or if it has CSS display:none, visibility:hidden, or visibility:collapse.
 * @param {Node} node The node to be checked.
 * @type Boolean
 */
mozile.dom.isVisible = function(node) {
	var visibility;
	while(node) {
		if(node == mozile.document) return true;
		if(node.nodeType == mozile.dom.ELEMENT_NODE) {
			if(mozile.dom.getStyle(node, "display") == "none") return false;
			visibility = mozile.dom.getStyle(node, "visibility");
			if(visibility == "hidden" || visibility == "collapse") return false;
		}
		node = node.parentNode;
	}
	return false;
}


/**
 * A test to see if the given node belongs to an X/HTML document. Fairly crude test at the moment.
 * @param {Node} node A node belonging to the document to be checked.
 * @type Boolean
 */
mozile.dom.isHTML = function(node) {
	if(!node) node = mozile.document;
	var doc = node;
	if(node.ownerDocument) doc = node.ownerDocument;
	if(!doc.documentElement) return false;
	var name = doc.documentElement.nodeName;
	if(doc.documentElement.nodeName) name = doc.documentElement.nodeName;
	if(name.toLowerCase() == "html") return true;
	else return false;
}


/**
 * A test to see if the given node should be ignored.
 * @param {Node} node The node to check.
 * @type Boolean
 */
mozile.dom.isIgnored = function(node) {
	if(node.nodeType == mozile.dom.ATTRIBUTE_NODE){
		if(node.nodeName.indexOf("xmlns") == 0) return true;
		if(mozile.browser.isOpera && node.nodeName.toLowerCase() == "shape") return true;
	}
	return false;
}

/**
 * Gets the local part of the qualified name for this node.
 * If the name is not qualified, the nodeName is returned.
 * @param {Node} node The node to check.
 * @type String
 */
mozile.dom.getLocalName = function(node) {
	if(!node) return null;
	if(node.localName) return node.localName;
	else if(node.nodeName && node.nodeName.indexOf(":") > -1) 
		return node.nodeName.substring(node.nodeName.indexOf(":") + 1);
	else return node.nodeName;
}

/**
 * Gets the prefix part of the qualified name for this node.
 * If the name is not qualified, a null value is returned.
 * @param {Node} node The node to check.
 * @type String
 */
mozile.dom.getPrefix = function(node) {
	if(!node) return null;
	if(node.prefix) return node.prefix;
	else if(node.nodeName.indexOf(":") > -1) 
		return node.nodeName.substring(0, node.nodeName.indexOf(":"));
	else return null;
}

/**
 * Gets the index of this node among all its parent's child nodes.
 * @param {Node} node The node to check.
 * @type Integer
 */
mozile.dom.getIndex = function(node) {
	if(!node.parentNode) return null;
	var index = 0;
	var previous = node.previousSibling;
	while(previous) {
		index++;
		previous = previous.previousSibling;
	}
	return index;
}

/**
 * Gets the index of this node among its parent's children of the same type and name. Used for XPath positions.
 * @param {Node} node The node to check.
 * @type Integer
 */
mozile.dom.getPosition = function(node) {
	var index = 1;
	var previous = node.previousSibling;
	while(previous) {
		if(node.nodeType == mozile.dom.ELEMENT_NODE) {
			if(previous.nodeName == node.nodeName) index++;
		}
		else if(previous.nodeType == node.nodeType) index++;
		previous = previous.previousSibling;
	}
	return index;
}

/**
 * Removes all the child nodes of the given node.
 * Does not remove attributes.
 * @param {Node} node The node to remove the child nodes from.
 * @type Void
 */
mozile.dom.removeChildNodes = function(node) {
	if(node.childNodes.length == 0) return;
	while(node.firstChild) {
		node.removeChild(node.firstChild);
	}
}

/**
 * Get the namespace of a node.
 * @param {Node} node The node to check.
 * @type String
 */
mozile.dom.getNamespaceURI = function(node) {
	if(!node) return null;
	if(node.namespaceURI) return node.namespaceURI;
	else if(node.nodeName.indexOf(":") > -1) {
		var prefix = node.nodeName.substring(0, node.nodeName.indexOf(":"));
		return mozile.dom.lookupNamespaceURI(node, prefix);
	}
	return mozile.dom.getDefaultNamespaceURI(node);
}

/**
 * Climb the tree looking for the first "xmlns" attribute.
 * @param {Node} node The node to check.
 * @type String
 */
mozile.dom.getDefaultNamespaceURI = function(node) {
	var namespaceURI = null;
	while(node) {
	  if(node.nodeType == mozile.dom.ELEMENT_NODE && node.getAttribute("xmlns"))
	  	return node.getAttribute("xmlns");
	  node = node.parentNode;
	}
	return namespaceURI;
}

/**
 * Tries to find and return the URI of the namespace of this node.
 * In browsers that support lookupNamespaceURI(prefix), when a prefix is given that method is used.
 * Otherwise, if the node has a qualified name with a prefix, the method looks for an "xmlns:prefix" attribute. 
 * If the node has no prefix, the method looks for an "xmlns" attribute.
 * Searches climb the DOM tree, starting with the node, and return the first match found.
 * @param {Node} node The node to check.
 * @param {String} prefix Optional. The prefix to check for. If none is given, the prefix of the node is used (if there is one).
 * @type String
 */
mozile.dom.lookupNamespaceURI = function(node, prefix) {
	if(!prefix) prefix = mozile.dom.getPrefix(node);
	var attr = "xmlns";

	// Proper method when a prefix is given.
	// Mozilla automatically climbs the DOM tree, but Opera does not.
	if(prefix && node.lookupNamespaceURI) {
		while(node) {
			var ns = node.lookupNamespaceURI(prefix);
			if(ns) return ns;
			else node = node.parentNode;
		}
		return null;
	}

	// Safari has to use getAttributeNS, while other browsers do not.
	if(prefix && mozile.browser.isSafari) {
		while(node && node.getAttributeNS) {
			//alert("Prefix: "+ node.nodeName +" "+ attr +":"+ prefix +" "+  node.getAttributeNS(attr, prefix));
			if(node.getAttributeNS(attr, prefix)) return node.getAttributeNS(attr, prefix);
			else node = node.parentNode;
		}
		return null;
	}

	// IE supported method for prefixes.
	if(prefix) attr = "xmlns:"+prefix;
	
	// General case.
	while(node) {
		//alert("No prefix: "+ node.nodeName +" "+ attr +" "+ node.getAttribute(attr));
		if(node.getAttribute(attr)) return node.getAttribute(attr);
		else node = node.parentNode;
	}
	return null;
}

/**
 * Tries to find and return the prefix for the qualified names of node with the given namespace URI.
 * Climbs the DOM tree, checking for any attributes starting with "xmlns:". It then checks those against the given namesace URI. If they match, the prefix part of the attribute name is returned.
 * @param {Node} node The node to check.
 * @param {String} namespaceURI Optional. The namespace URI to find the prefix for. If none is given, this node's namespaceURI is used.
 * @type String
 */
mozile.dom.lookupPrefix = function(node, namespaceURI) {
	if(!namespaceURI) namespaceURI = node.namespaceURI;
	if(!namespaceURI) return null;

	while(node && node.attributes) {
		for(var i=0; i < node.attributes.length; i++) {
			if(node.attributes[i].nodeName.indexOf("xmlns:") == 0 &&
				node.attributes[i].nodeValue == namespaceURI) {
				return node.attributes[i].nodeName.substring(6);
			}
		}
		node = node.parentNode;
	}
	return null;
}


/**
 * Converts a CSS style name (hyphenated) to a JavaScript style name (camelCase).
 * @param {String} styleName The name of a CSS rule.
 * @type String
 */
mozile.dom.convertStyleName = function(styleName) {
	if(!styleName || typeof(styleName) != "string") return null;
	return styleName.replace(/\-(\w)/g, function (strMatch, p1){
			return p1.toUpperCase();
	});
}

/**
 * Get the string value of a CSS style declaration.
 * Adapted from http://www.robertnyman.com/2006/04/24/get-the-rendered-style-of-an-element/
 * @param {Node} node The node which has the style.
 * @param {String} cssRule The name of a CSS rule.
 * @type String
 */
mozile.dom.getStyle = function(node, cssRule) {
	var value = "";
	if(!node) return value;
	if(node.nodeType != mozile.dom.ELEMENT_NODE) node = node.parentNode;
	if(!node || node.nodeType != mozile.dom.ELEMENT_NODE) return value;

	if(mozile.document.defaultView && mozile.document.defaultView.getComputedStyle){
		value = mozile.document.defaultView.getComputedStyle(node, "").getPropertyValue(cssRule);
	}
	else if(node.currentStyle){
		cssRule = mozile.dom.convertStyleName(cssRule);
		value = node.currentStyle[cssRule];
	}

	return value;
}

/**
 * Sets the CSS style of an element.
 * @param {Element} element The element which has the style.
 * @param style Either a string denoting the CSS style or an object with keys
 * @type String
 */
mozile.dom.setStyle = function(element, rule, value) {
	if(!element) return;
	if(!rule || typeof(rule) != "string") return;
	
	rule = mozile.dom.convertStyleName(rule);
	if(element.style) element.style[rule] = value;
	else mozile.debug.debug("mozile.dom.setStyle", "Element does not have a 'style' attribute.");
}

/**
 * Adds a "link" element with the given href and type to the head of the document.
 * @param {String} href The target of the link.
 * @param {String} type Optional. The type of the link. Defaults to "text/css".
 * @type Element
 */
mozile.dom.addStyleSheet = function(href, type) {
	var link;
	if(mozile.defaultNS != null) {
		mozile.require("mozile.xml");
		link = mozile.dom.createElementNS(mozile.xml.ns.xhtml, "link");
	}
	else link = mozile.dom.createElement("link");
	link.setAttribute("class", "mozileLink");
	link.setAttribute("href", href);
	link.setAttribute("rel", "stylesheet");
	if(!type) type = "text/css";
	link.setAttribute("type", type);
	mozile.dom.getHead().appendChild(link);
	mozile.dom.links.push(link);
	return link;
}

/**
 * Adds a "link" element with the given href and type to the head of the document.
 * @param {String} href The target of the link.
 * @param {String} type Optional. The type of the link. Defaults to "text/css".
 * @type Void
 */
mozile.dom.getStyleSheet = function(element) {
	if(!element) return null;
	
	if(element.styleSheet != undefined) return element.styleSheet;
	else if(element.sheet != undefined) return element.sheet;

	return null;
}

/**
 * Returns the class attribute for the given element. IE uses a non-standard className property.
 * @param {Element} element The element to check.
 * @type String
 */
mozile.dom.getClass = function(element) {
	if(!element || element.nodeType != mozile.dom.ELEMENT_NODE) return "";
	var value;
	if(element.className != undefined) value = element.className;
	else value = element.getAttribute("class");
	if(value) return value;
	else return "";
}

/**
 * Sets the class attribute for the given element. IE uses a non-standard className property.
 * @param {Element} element The element to change.
 * @param {String} value The value to set.
 * @type String
 */
mozile.dom.setClass = function(element, value) {
	if(!element || element.nodeType != mozile.dom.ELEMENT_NODE) return null;
	if(element.className != undefined) element.className = value;
	else element.setAttribute("class", value);
	return value;
}

/**
 * Determines whether an element's class attribute includes a given class name.
 * Breaks the attribute string up by white space and checks each part.
 * @param {Element} element The element to check.
 * @param {String} className Optional. The name of the class to check for. If none is given, any class name is accepted.
 * @type Boolean
 */
mozile.dom.hasClass = function(element, className) {
	if(!element) return false;
	
	var attr = mozile.dom.getClass(element);
	if(!attr) return false;
	if(!className) return true;
	attr = attr.split(/\s+/);
	
	for(var i=0; i < attr.length; i++) {
		if(attr[i] == className) return true;
	}
	
	return false;
}

/**
 * Adds a new class to an element, without changing its other classes.
 * @param {Element} element The element to check.
 * @param {String} className The name to add.
 * @type Boolean
 */
mozile.dom.addClass = function(element, className) {
	if(!element) return false;
	
	if(this.hasClass(element, className)) return true;
	var cls = this.getClass(element);
	this.setClass(element, cls +" "+ className);
	return true;
}

/**
 * Removes a class from an element, without changing its other classes.
 * @param {Element} element The element to check.
 * @param {String} className The name to remove.
 * @type Boolean
 */
mozile.dom.removeClass = function(element, className) {
	if(!element) return false;
	
	if(!this.hasClass(element, className)) return false;
	var classes = this.getClass(element).split(/\s+/);
	for(var i=0; i < classes.length; i++) {
		if(classes[i] == className) {
			classes.splice(i, 1);
			i--;
		}
	}
	this.setClass(element, classes.join(" "));
	return true;
}


/**
 * Gets the position of the element using offsetLeft relative to the container (if provided) or the document element.
 * Adapted from http://www.quirksmode.org/js/findpos.html
 * @param {Node} node The target node.
 * @param {Container} container Optional. The element relative to which the X position will be measured.
 * @type Number
 */
mozile.dom.getX = function(node, container) {
	if(!node) return 0;
	if(node.nodeType != mozile.dom.ELEMENT_NODE) node = node.parentNode;
	if(!node) return 0;
	var x = 0;
	if(node.offsetParent) {
		while(node.offsetParent) {
			x += node.offsetLeft;
			node = node.offsetParent;
			if(node == container) break;
		}
	}
	else if (node.x) x += node.x;
	return x;
}

/**
 * Gets the position of the element using offsetTop relative to the container (if provided) or the document element.
 * Adapted from http://www.quirksmode.org/js/findpos.html
 * @param {Node} node The target element.
 * @param {Container} container Optional. The element relative to which the X position will be measured.
 * @type Number
 */
mozile.dom.getY = function(node, container) {
	if(!node) return 0;
	if(node.nodeType != mozile.dom.ELEMENT_NODE) node = node.parentNode;
	if(!node) return 0;
	var y = 0;
	if(node.offsetParent) {
		while(node.offsetParent) {
			y += node.offsetTop;
			node = node.offsetParent;
			if(node == container) break;
		}
	}
	else if (node.y) y += node.y;
	return y;
}


/**
 * Get any attribute, even tricky ones like "class".
 * @param {Element} element
 * @param {String} name The name of the attribute to change.
 * @type String
 */
mozile.dom.getAttribute = function(element, name) {
	if(!element || !element.nodeType || 
		element.nodeType != mozile.dom.ELEMENT_NODE)
		return null;
	if(!name || typeof(name) != "string") return null;

	var lowercase = name.toLowerCase();
	if(lowercase == "class" || lowercase == "classname") {
		return mozile.dom.getClass(element);
	}
	else if(mozile.browser.isIE && lowercase == "style") {
		return element.style.cssText;
	}
	else if(lowercase == "value" && element.value != undefined) {
		return element.value;
	}
	else if(lowercase == "checked" && element.checked != undefined) {
		return element.checked;
	}

	return element.getAttribute(name);
}


/**
 * Set any attribute along with the element's properties.
 * @param {Element} element
 * @param {String} attributeName The name of the attribute to change.
 * @param {String} value The new value.
 * @type String
 */
mozile.dom.setAttribute = function(element, name, value) {
	if(!element || !element.nodeType || 
		element.nodeType != mozile.dom.ELEMENT_NODE)
		return null;
	if(!name || typeof(name) != "string") return null;

	var lowercase = name.toLowerCase();
	if(lowercase == "class" || lowercase == "classname" || lowercase == "cls") {
		return mozile.dom.setClass(element, value);
	}
	else if(mozile.browser.isIE && lowercase == "style") {
		element.style.cssText = value;
	}
	else if(lowercase == "value" && element.value != undefined) {
		element.value = value;
		element.setAttribute(name, value);
	}
	else if(lowercase == "checked" && element.checked != undefined) {
		element.checked = value;
		element.setAttribute(name, value);
	}
	else {
		if(value) element.setAttribute(name, value);
		// TODO: This might do bad things to IE.
		else element.removeAttribute(name);
	}
	return mozile.dom.getAttribute(element, name);
}


/**
 * Get a namespaced attribute's value.
 * @param {Element} element
 * @param {String} namespaceURI The URI of the namespace to check.
 * @param {String} name The local name of the attribute.
 * @type String
 */
mozile.dom.getAttributeNS = function(element, namespaceURI, name) {
	if(!element) return null;
	if(element.getAttributeNS) return element.getAttributeNS(namespaceURI, name);
	else {
		prefix = mozile.dom.lookupPrefix(element, namespaceURI);
		if(prefix) return element.getAttribute(prefix+":"+name);
	}
	return null;
}


/**
 * Creates an element with a qualified name using mozile.defaultNS if this variable is defined, otherwise an unqualified element.
 * @param {String} name The name of the element.
 * @type Element
 */
mozile.dom.createElement = function(name) {
	if(mozile.defaultNS) {
		return mozile.dom.createElementNS(mozile.defaultNS, name);
	} else {
		return mozile.document.createElement(name);
	}
}

/**
 * Tries to find and return the prefix for the qualified names of node with the given namespace URI.
 * Climbs the DOM tree, checking for any attributes starting with "xmlns:". It then checks those against the given namesace URI. If they match, the prefix part of the attribute name is returned.
 * @param {Node} node The node to check.
 * @param {String} namespaceURI The namespace URI to find the prefix for.
 * @type String
 */
mozile.dom.createElementNS = function(namespaceURI, name) {
	if(mozile.document.createElementNS && !mozile.browser.isSafari) return mozile.document.createElementNS(namespaceURI, name);
	else {
		// This is the only hack I could figure out that would work in IE6.
		mozile.require("mozile.xml");
		return mozile.xml.parseElement('<'+ name +' xmlns="'+ namespaceURI +'"/>');
	}
	return null;
}


/**
 * Clones an element into the current document and namespace.
 * TODO: Support cloning attribute nodes.
 * @param {Node} node The node to clone.
 * @param {Boolean} deep When false the node and any attributes are cloned. When true the children are cloned.
 * @type Node
 */
mozile.dom.importNode = function(node, deep) {
	if(!node || !node.nodeType) return null;
	var clone;
	var i=0;
	switch(node.nodeType) {
		case mozile.dom.ELEMENT_NODE:
			clone = mozile.dom.createElement(node.nodeName);
			for(i = node.attributes.length - 1; i >= 0; i--) {
				clone.setAttribute(node.attributes[i].nodeName, node.attributes[i].nodeValue);
			}
			break;
		//case mozile.dom.ATTRIBUTE_NODE:
		//	clone = document.createAttribute(node.nodeName);
		//	clone.nodeValue = node.nodeValue;
		//	break;
		case mozile.dom.TEXT_NODE:
			clone = mozile.document.createTextNode(node.data);
			break;
		case mozile.dom.COMMENT_NODE:
			clone = mozile.document.createComment(node.data);
			break;
		default: 
			return null;
	}
	
	if(deep) {
		var child;
		for(i=0; i < node.childNodes.length; i++) {
			child = mozile.dom.importNode(node.childNodes[i], true);
			if(child) clone.appendChild(child);
		}
	}
	
	return clone;
}



/**
 * Takes a template node or HTML file and attaches it as a template to the given object.
 * @param template Node or String. The template node or file path.
 * @param {Object} object The object to attach the template to.
 * @type Node
 */
mozile.dom.attachTemplate = function(template, object) {
	if(typeof(template) == "string") {
		template = mozile.xml.load(template);
		template = mozile.dom.importNode(template.documentElement, true);
	}
	if(!template) return null;

	var clone = template.cloneNode(true);
	var treeWalker = document.createTreeWalker(clone, mozile.dom.NodeFilter.SHOW_ALL, null, false);

	treeWalker.currentNode = clone;
try {
	var node, i, j, k, value, obj, events, event, source, target;
	while(true) {
		node = treeWalker.currentNode;
		if(!node) break;

		switch(node.nodeType) {
			case mozile.dom.TEXT_NODE:
				if(node.data && node.data.match(this._matchSubstitution))
					node.data = this._substituteCode(node.data, object);
				break;
		
			case mozile.dom.ELEMENT_NODE:
				for(i=0; i < node.attributes.length; i++) {
					value = node.attributes[i].nodeValue;
					if(value && typeof(value) == "string" &&
						value.match(this._matchSubstitution)) {
						node.attributes[i].nodeValue = 
							this._substituteCode(value, object);
					}
				}
				if(node.getAttribute("mozileAttachPoint")) {
					value = node.getAttribute("mozileAttachPoint").split(".");
					obj = object;
					for(j=0; j < value.length - 1; j++) {
						obj = obj[value[j]];
					}
					obj[value[j]] = node;
				}
				// Attach events using Dojo's event system.
				if(mozile.window.dojo && node.getAttribute("dojoAttachEvent")) {
					value = node.getAttribute("dojoAttachEvent");
					events = value.split(";");
					for(j=0; j < events.length; j++) {
						event = events[j].split(":");
						source = event[0];
						target = event[1].split(".");
						obj = object;
						for(k=0; k < target.length - 1; k++) {
							// Using eval() seems unnecessary, but it works.
							obj = eval("(obj."+ target[k] +")");
						}
						//console.info(events[j], source, obj, target[k]);
						dojo.event.connect(node, source, obj, target[k]);
					}
				}
				// Attach events using Dojo's event system.
				if(mozile.window.Ext && node.getAttribute("mozileAttachEvent")) {
					value = node.getAttribute("mozileAttachEvent");
					events = value.split(";");
					for(j=0; j < events.length; j++) {
						event = events[j].split(":");
						source = event[0];
						target = event[1].split(".");
						obj = object;
						for(k=0; k < target.length - 1; k++) {
							// Using eval() seems unnecessary, but it works.
							obj = eval("(obj."+ target[k] +")");
						}
						var fn = obj[target[k]];
						var el = Ext.get(node);
						el.addListener(source, fn, obj);
					}
				}
				break;
		}
	
		if(!treeWalker.nextNode()) break;
	}
} catch(e) { alert("AttachTemplate "+ mozile.dumpError(e)); }
	return clone;
}
	
// _matchSubstitution: RegExp
//	 Matches ${substitution blocks} for replacement.
mozile.dom._matchSubstitution = /\$\{(.*?)\}/gm;

/**
 *
 */
mozile.dom._substituteCode = function(__TEXT__, __OBJECT__) {
	if(!__TEXT__ || typeof(__TEXT__) != "string") return __TEXT__;
	if(!__OBJECT__) return __TEXT__;
	
	var subs = function(__RESULT__, __CODE__) {
		__CODE__ = __CODE__.replace(/\bthis\b/, "__OBJECT__");
		try {
			return String(eval("("+ __CODE__ +")"));
		} catch(e) {
			return "ERROR";
		}
	}
	
	var result = __TEXT__.replace(mozile.dom._matchSubstitution,  subs);
	
	return result;
}


/**
 * Focus any element in a cross-browser fashion. Necessary for IE.
 * @param {Element} element The element to focus.
 * @type Void
 */
mozile.dom.focus = function(element) {
	if(!element || !element.nodeType || 
		element.nodeType != mozile.dom.ELEMENT_NODE)
		return;

    var focus_exceptions={A:1,AREA:1,BUTTON:1,INPUT:1,OBJECT:1,SELECT:1,TEXTAREA:1};

    if(!focus_exceptions[element.nodeName] && 
        (element.getAttribute("tabIndex")==null || 
        !element.getAttributeNode("tabIndex").specified)) {
        element.setAttribute("tabIndex","-1");
    }

	element.focus();
	element.focus();
}

/**
 *
 */
mozile.dom.Range = function(selectRange) {
	var range;
  if (mozile.document.createRange) {
  	if(selectRange) range = selectRange.cloneRange();
  	else range = mozile.document.createRange();
  	for(var field in mozile.dom.Range.prototype) 
  		range[field] = mozile.dom.Range.prototype[field];
  	return range;
  } else {
  	if(selectRange && selectRange._range) {
  		range = new mozile.dom.InternetExplorerRange(selectRange._range.duplicate());
  		range._init();
  		return range;
  	}
  	else return new mozile.dom.InternetExplorerRange();
  }
}


/**
 * Store the details about this range in an object which can be used to restore the range.
 * @type Object
 */
mozile.dom.Range.prototype.store = function () {
	var state = new Object();
	// TODO: Speed optmization for IE. Not stable.
	if(false && this._range) {
		state.format = "IE";
		state.bookmark = this._range.getBookmark();
	}
	else {
		mozile.require("mozile.xpath");
		state.type = "Range";
		state.format = "W3C";
		var commonAncestorPath = mozile.xpath.getXPath(
			this.commonAncestorContainer);
		state.startContainer = commonAncestorPath + mozile.xpath.getXPath(
			this.startContainer, this.commonAncestorContainer);
		state.startOffset = this.startOffset;
		if(this.startContainer == this.endContainer)
			state.endContainer = state.startContainer;
		else state.endContainer = commonAncestorPath + mozile.xpath.getXPath(
			this.endContainer, this.commonAncestorContainer);
		state.endOffset = this.endOffset;
		state.window = mozile.window;
		state.document = mozile.document;
	}
	return state;
}

/**
 * Takes a stored range object and creates a new range with the same properties.
 * @param {Object} state A state object from 
 * @type Void
 */
mozile.dom.Range.prototype.restore = function(state) {
try {
	// TODO: Speed optmization for IE. Actually slower?
	if(false && this._range) {
		this._range.moveToBookmark(state.bookmark);
		this._init();
	}
	else {
		mozile.require("mozile.xpath");
		//alert(mozile.util.dumpValues(state));
		var startContainer, endContainer;
		startContainer = mozile.xpath.getNode(state.startContainer, state.document);
		//alert(["startContainer", state.startContainer, startContainer.nodeName].join("\n"));
		this.setStart(startContainer, state.startOffset);
		if(state.endContainer == state.startContainer) 
			endContainer = startContainer;
		else endContainer = mozile.xpath.getNode(state.endContainer, state.document);
		//endContainer = mozile.xpath.getNode(state.endContainer);
		//alert(["endContainer", state.endContainer, endContainer.nodeName].join("\n"));
		this.setEnd(endContainer, state.endOffset);
	}
} catch(e) { 
	alert("Error [mozile.dom.Range.restore]:\n"+ mozile.dumpError(e))
	// +"\nUsing state oject:\n"+ mozile.util.dumpValues(state)); 
}
}


/**
 * Selection operations.
 * See this for some ideas: http://www.teria.com/~koseki/memo/xbselection/Selection.js
 * @type Object
 */
mozile.dom.selection = new Object();
// JSDoc hack
mozile.dom.selection.prototype = new mozile.Module;

/**
 * An offset number of pixels used when scrolling to a selection.
 * @type Number
 */
mozile.dom.selection.xPadding = 50;

/**
 * An offset number of pixels used when scrolling to a selection.
 * @type Number
 */
mozile.dom.selection.yPadding = 100;

/**
 * Get the global selection object. Initiate it if necessary.
 * @type Selection
 */
mozile.dom.selection.get = function() {
try {
	//return new mozile.dom.Selection();
	if(!mozile.dom._selection || this.lastDocument !== mozile.document)
		mozile.dom._selection = new mozile.dom.Selection();
	if(mozile.browser.isIE) mozile.dom._selection._init();
	this.lastDocument = mozile.document;
	return mozile.dom._selection;
} catch(e) {
	return null;
}
}


/**
 *
 */
mozile.dom.Selection = function(win) {
	if(!win) win = mozile.window;
	if(win.getSelection) {
		var selection = win.getSelection();
		if(!selection) return null;
		for(var field in mozile.dom.Selection.prototype) 
			selection[field] = mozile.dom.Selection.prototype[field];
		return selection;
	} else {
		return new mozile.dom.InternetExplorerSelection();
	}
}

/**
 * Store the details about this range in an object which can be used to restore the range.
 * @type Object
 */
mozile.dom.Selection.prototype.store = function(oldState, newOffset) {
	var state = new Object();
	if(oldState) {
		for(var i in oldState) state[i] = oldState[i];
		//state.startOffset = newOffset;
		//state.endOffset = newOffset;
		state.anchorOffset = newOffset;
		state.focusOffset = newOffset;
		return state;
	}
	else if(this.rangeCount > 0) {
		//var range = this.getRangeAt(0);
		//if(range) {
		//	range.store = mozile.dom.Range.prototype.store;
		//	return range.store();
		//}
		//else return null;
		// New idea.
		mozile.require("mozile.xpath");
		state.type = "Selection";
		state.format = "W3C";
		state.anchorNode = mozile.xpath.getXPath(this.anchorNode);
		state.anchorOffset = this.anchorOffset;
		if(this.focusNode == this.anchorNode)
			state.focusNode = state.anchorNode;
		else state.focusNode = mozile.xpath.getXPath(this.focusNode);
		state.focusOffset = this.focusOffset;
		state.isCollapsed = this.isCollapsed;
		state.window = mozile.window;
		state.document = mozile.document;
		return state;
	}
	return null;
}

/**
 * Takes a stored range object and creates a new range with the same properties.
 * @param {Object} state Optional. A state object from a store() call. If none is given, the data from mozile.dom.selection.last is used.
 * @type Void
 */
mozile.dom.Selection.prototype.restore = function(state) {
	//mozile.debug.debug("mozile.dom.Selection.restore", "Restoring");
	if(state) {
		if(state.type == "Selection") {
			mozile.require("mozile.xpath");
			var anchorNode, focusNode;
			anchorNode = mozile.xpath.getNode(state.anchorNode, state.document);
			this.collapse(anchorNode, state.anchorOffset);
			//if(!state.isCollapsed) {
			if(state.focusNode != state.anchorNode ||
				state.focusOffset != state.anchorOffset ) {
				if(state.focusNode == state.anchorNode) focusNode = anchorNode;
				else focusNode = mozile.xpath.getNode(state.focusNode,  state.document);
				try {
					this.extend(focusNode, state.focusOffset);
				} catch(e) {
					mozile.debug.debug("mozile.dom.Selection.restore", "Error extending selection to '"+ state.focusNode +" "+ state.focusOffset +"'.\n"+ mozile.dumpError(e) );
				}
			}
		}
		else if(state.type == "Range") {
			var range = new mozile.dom.Range();
			range.restore(state);
			this.removeAllRanges();
			this.addRange(range);
		}
	}
	else if(mozile.dom.selection.last) {
		this.collapse(mozile.dom.selection.last.anchorNode, 
			mozile.dom.selection.last.anchorOffset);
		if(!mozile.dom.selection.last.isCollapsed) {
			this.extend(mozile.dom.selection.last.focusNode, 
				mozile.dom.selection.last.focusOffset);
		}
	}
	
	//this.scroll();
}

/**
 * Scroll the window to the current selection.
 * @type Void
 */
mozile.dom.Selection.prototype.scroll = function() {
	if(!this.focusNode);
	// Scroll to the new selection.
	var x = mozile.dom.getX(this.focusNode);
	var y = mozile.dom.getY(this.focusNode);
	var pX = mozile.window.pageXOffset;
	var pY = mozile.window.pageYOffset;
	//alert([mozile.xpath.getXPath(this.focusNode), 
	//	x, y, pX, pY, mozile.window.innerWidth, mozile.window.innerHeight,
	//	pX + mozile.window.innerWidth, pY + mozile.window.innerHeight].join("\n"));
	if(x < pX || x > (pX + mozile.window.innerWidth) ||
		y < pY || y > (pY + mozile.window.innerHeight) ) {
		mozile.window.scroll(x - mozile.dom.selection.xPadding, 
			y - mozile.dom.selection.yPadding);
	}
}

	
/**
 * Old function, is now in the Selection constructor
 */
mozile.dom.Selection.getSelection = function () {
	if(mozile.window.getSelection) { //FF, Safari
		return mozile.window.getSelection();
	} else if (mozile.document.getSelection) { // Opera?
		return mozile.document.getSelection();
	} else if(mozile.document.selection) { // IE win
		return new mozile.dom.Selection();
	}
	return null;
}




if(mozile.browser.isIE) {
	mozile.require("mozile.dom.TreeWalker");
	mozile.require("mozile.dom.InternetExplorerRange");
	mozile.require("mozile.dom.InternetExplorerSelection");
}
else {
	mozile.dom.NodeFilter = NodeFilter;
	mozile.dom.TreeWalker = TreeWalker;
}





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