API Docs for:
Show:

File: latest/dom.js

/**
 * Daniel15 JavaScript Framework - By Daniel15, 2011
 * http://dl.vc/jsframework
 * Feel free to use any of this, but please link back to my site (dan.cx)
 */

/**
 * DOM methods
 * @class DOM
 * @static
 * @module JSFramework
 * @submodule DOM
 */
var DOM =
{
	/**
	 * Cache of ElementWrappers
	 * @property cache
	 * @type Array
	 * @private
	 */
	cache: [],
	/**
	 * Name of the attribute used to store an element ID number in the cache 
	 * @property JS_ELEMENT_ID
	 * @final
	 */
	JS_ELEMENT_ID: 'js-element-id',
	
	/**
	 * Create and return a new wrapped DOM element
	 * @method create
	 * @param	{String}  tag                Tag name to create (eg. "span")
	 * @param	{Object}  [properties={}]    Properties to set on the new element
	 * @param	{Boolean} [wrap=true]        Whether to wrap the element or not. Optional, default is true
	 * @param   {Object}  [attributes={}]    Attributes to set on the new element
	 * @return  {ElementWrapper|HTMLElement} The new element (or a wrapper if wrap is true)
	 * @example
	var newEl = DOM.create('div',
	{
		id: 'hello-world',
		className: 'awesome',
		innerHTML: 'This is a test!'
	}); // Creates <div id="hello-world" class="awesome">This is a test!</div>
	 */
	create: function(tag, properties, wrap, attributes)
	{
		if (wrap == undefined)
			wrap = true;
			
		var el = document.createElement(tag);
		Util.extend(el, properties);
		if (attributes)
		{
			for (var i in attributes)
			{
				if (attributes.hasOwnProperty(i))
				{
					el.setAttribute(i, attributes[i]);
				}
			}
		}
		return wrap ? DOM.wrap(el) : el;
	},
	
	/**
	 * Wrap an element with useful functions. This creates an instance of {{#crossLink "ElementWrapper"}}{{/crossLink}} 
	 * that "wraps" the passed element. The `$` method can be used as a shortcut to this.
	 * @method wrap
	 * @param	{HTMLElement|ElementWrapper|String} el Either the ID of the element, or the element itself
	 * @return	{ElementWrapper} An ElementWrapper instance
	 * @example
	var containerEl = DOM.wrap('container'); // Gets the element with an ID of "container"
	var containerEl = $('container');        // Exact same as above - $ is an alias for DOM.wrap
	 */
	wrap: function(el)
	{
		if (!el)
			return null;
			
		// If it's already wrapped, just return the element
		if (el instanceof ElementWrapper)
			return el;
		
		// If it's a string, assume it's an ID
		if (typeof(el) == "string")
			el = document.getElementById(el);
			
		// Check if a wrapper was already created for this
		var elId = el.getAttribute(this.JS_ELEMENT_ID);
		if (elId)
			return this.cache[elId];
			
		// Otherwise, create a new wrapper and cache it
		var wrapper = new ElementWrapper(el);
		el.setAttribute(this.JS_ELEMENT_ID, this.cache.push(wrapper) - 1);
		return wrapper;
	},
	
	/**
	 * Wrap all the passed elements
	 * @method wrapAll
	 * @param	{Array of HTMLElement} input Array of elements
	 * @return	{Array of ElementWrapper} Array of wrapped elements
	 * @example
	// Get all DIVs on the page, and create an ElementWrapper for each
	var wrapped = DOM.wrapAll(document.getElementsByTagName('div'));
	 */
	wrapAll: function(input)
	{
		return new ElementWrapperList(input);
	},
	
	// Set below ElementWrapper, here just to make YUIDoc happy
	/**
	 * Body of the page
	 * @property body
	 * @type ElementWrapper
	 */
	body: null 
}

/**
 * Class that wraps DOM elements and provides additional functionality. Should not be constructed
 * directly - Instead use {{#crossLink "DOM/wrap"}}{{/crossLink}} or {{#crossLink "DOM/create"}}{{/crossLink}}
 * @class ElementWrapper
 * @constructor
 * @module JSFramework
 * @submodule DOM
 */
function ElementWrapper(element)
{
	/**
	 * The element being wrapped
	 * @property {HTMLElement} element
	 * @private
	 */
	this.element = element;
}

ElementWrapper.prototype =
{
	/**
	 * Gets the internal element ID for this element. This is *not* the id in the HTML!
	 * @method getElementId
	 * @return {String} The element ID
	 */
	getElementId: function()
	{
		return this.element.getAttribute(DOM.JS_ELEMENT_ID);
	},
	
	/**
	 * Set a property on this element
	 * @method set
	 * @param {String} key   Property name
	 * @param {String} value Value to set
	 * @chainable
	 * @example
	var containerEl = $('container');
	containerEl.set('id', 'foobar'); // Changes ID of #container
	 */
	set: function(key, value)
	{
		this.element[key] = value;
		return this;
	},
	
	/**
	 * Get a property on this element
	 * @method get
	 * @param {String} key Property name
	 * @return {String} Property value
	 */
	get: function(key)
	{
		return this.element[key];
	},
	
	/**
	 * Insert a DOM node after this node
	 * @method insertAfter
	 * @param  {ElementWrapper} Wrapped DOM node
	 * @chainable
	 * @example
	// Insert a new element after #container
	var containerEl = $('container');
	var newEl = DOM.create('div', { id: 'helloWorld' });
	containerEl.insertAfter(newEl);
	 */
	insertAfter: function(newNode)
	{
		this.element.parentNode.insertBefore(newNode.element, this.element.nextSibling);
		return this;
	},
	
	/**
	 * Append some HTML to the element
	 * @method append
	 * @param  {String}  html  HTML to append
	 * @chainable
	 * @example
	$('container').append('<p>More content</p>');
	 */
	append: function(html)
	{
		var newContent = document.createElement('div');
		newContent.innerHTML = html;
		while (newContent.firstChild)
		{
			this.element.appendChild(newContent.firstChild);
		}
		return this;
	},
	
	/**
	 * Get the position of this element
	 * @method getPosition
	 * @return	{Object} X and Y values
	 */
	getPosition: function()
	{
		var obj = this.element,
			x = 0,
			y = 0;
		do
		{
			x += obj.offsetLeft;
			y += obj.offsetTop;
		}
		while (obj = obj.offsetParent);
		
		return {x: x, y: y};
	},
	
	/**
	 * Set a CSS style on this element
	 * @method setStyle
	 * @param	{String} style  Style to set
	 * @param	{mixed}  value  New style value
	 * @chainable
	 * @example
	$('container').setStyle('backgroundColor', 'red');
	 */
	setStyle: function(style, value)
	{
		// TODO: Support converting hyphenated-keys to camelCase
		this.element.style[style] = value;
		return this;
	},
	
	/**
	 * Set multiple CSS styles at once
	 * @method setStyles
	 * @param {Object} styles  Styles to set
	 * @chainable
	 * @example
	$('container').setStyles({
		backgroundColor: red,
		fontSize: '18px'
	});
	 */
	setStyles: function(styles)
	{
		for (var key in styles)
		{
			this.setStyle(key, styles[key]);
		}
		return this;
	},
	
	/**
	 * Check if this element has the specified CSS class name
	 * @method hasClass
	 * @param	{String} name Class name to look for
	 * @return	{Boolean} Whether this element has this class name as one of its classes
	 * @example
	// HTML contains: <div id="test" class="hello">...</div>
	$('test').hasClass('blah'); // false
	$('test').hasClass('hello'); // true
	 */
	hasClass: function(name)
	{
		return (' ' + (this.element || this).className.toUpperCase() + ' ').indexOf(' ' + name.toUpperCase() + ' ') > -1;
	},
	
	/**
	 * Add the specified class name to this element
	 * @method addClass
	 * @param	{String} name Class name to add
	 * @chainable
	 */
	addClass: function(name)
	{
		this.element.className += ' ' + name;
		return this;
	},
	
	/**
	 * Remove the specified class name from this element
	 * @method removeClass
	 * @param	{String} name Class name to remove
	 * @chainable
	 */
	removeClass: function(name)
	{
		var newClass = (' ' + this.element.className + ' ').replace(' ' + name + '', '');
		this.element.className = newClass.trim();
		return this;
	},
	
	////////////////////////////////////////////////////////////////////////////////////////////////
	
	// TODO: Clean up the below blocks of code. Remove duplication.
	
	/**
	 * Get the first child element by tag name, or null if there is no matching child
	 * @method firstByTag
	 * @param	{String} tag Tag name to get
	 * @param	{Boolean} [wrap=true] Whether to wrap the element or not. Optional, default is true
	 * @return	{ElementWrapper|HTMLElement} The element, or a wrapper around it if wrap is true.
	 */
	firstByTag: function(tag, wrap)
	{
		if (wrap == undefined)
			wrap = true;
			
		var els = this.getByTag(tag, false);
		return els && els[0] && (wrap ? DOM.wrap(els[0]) : els[0]);
	},
	
	/**
	 * Get all children by tag name
	 * @method getByTag
	 * @param	{String} tag Tag name to get
	 * @param	{Boolean} [wrap=true] Whether to wrap the elements or not. Optional, default is true
	 * @return	{Array of ElementWrapper|Array of HTMLElement} The elements, or wrappers around them if wrap is true.
	 */
	getByTag: function(tag, wrap)
	{
		if (wrap == undefined)
			wrap = true;
			
		var els = this.element.getElementsByTagName(tag);
		return wrap ? DOM.wrapAll(els) : els;
	},
	
	/**
	 * Get the first child element by class name, or null if there is no matching child
	 * @method firstByClass
	 * @param	{String} className Class name to look for
	 * @param	{Boolean} [wrap=true] Whether to wrap the element or not. Optional, default is true
	 * @return	{ElementWrapper|HTMLElement} The element, or a wrapper around it if wrap is true.
	 */
	firstByClass: function(className, wrap)
	{
		if (wrap == undefined)
			wrap = true;
			
		var els = this.getByClass(className, false);
		return els && els[0] && (wrap ? DOM.wrap(els[0]) : els[0]);
	},
	
	/**
	 * Get all children by class name
	 * @method getByClass
	 * @param	{String} className Class name to look for
	 * @param	{Boolean} [wrap=true] Whether to wrap the elements or not. Optional, default is true
 	 * @return	{Array of ElementWrapper|Array of HTMLElement} The elements, or wrappers around them if wrap is true.
	 */
	getByClass: function(className, wrap)
	{
		if (wrap == undefined)
			wrap = true;
			
		var els = this._getByClass(this.element, className);			
		return wrap ? DOM.wrapAll(els) : els;
	},
	
	/**
	 * Internal function for getting elements by class name. Don't use externally (use getByClass
	 * instead)
	 * @method _getByClass
	 * @param  {HTMLElement} el        Element to search the children of
	 * @param  {String}      className Name of the class to search for
	 * @return {Array of HTMLElement} The matching elements
	 * @private
	 */
	_getByClass: (function()
	{
		var tempEl = document.createElement('div');
		// Normal browsers
		if ('getElementsByClassName' in tempEl)
		{
			return function(el, className)
			{
				return el.getElementsByClassName(className);
			}
		}
		// IE8 supports querySelectorAll but not getElementsByClassName
		if ('querySelectorAll' in tempEl)
		{
			return function(el, className)
			{
				return el.querySelectorAll('.' + className);
			}
		}
		// IE 6 and 7... Naive polyfill
		// TODO: Unit test this!!
		return function(el, className)
		{
			var els = el.all || el.getElementsByTagName('*');
			var result = [];
			var hasClass = ElementWrapper.prototype.hasClass;
			
			for (var i = 0, count = els.length; i < count; i++)
			{
				if (hasClass.call(els[i], className))
				{
					result.push(els[i]);
				}
			}
			
			return result;
		}
	})(),
	
	/**
	 * Get the first child element by a selector or null if there is no matching child. Incomplete
	 * as there's not yet full IE support.
	 * @method firstBySelector
	 * @param	{String} selectors Selector to use
 	 * @param	{Boolean} [wrap=true] Whether to wrap the elements or not. Optional, default is true
	 * @return	The element, or a wrapper around it if wrap is true.
	 * @beta
	 */
	firstBySelector: function(selectors, wrap)
	{
		if (wrap == undefined)
			wrap = true;
		
		// TODO: Polyfill for IE
		var el = this.element.querySelectorAll(selectors);
		return el && (wrap ? DOM.wrap(el) : el);
	},
	
	/**
	 * Get all children by selector. Incomplete as there's not yet full IE support.
	 * @method getBySelector
	 * @param	{String} selectors Selector to use
 	 * @param	{Boolean} [wrap=true] Whether to wrap the elements or not. Optional, default is true
 	 * @return	{Array of ElementWrapper|Array of HTMLElement} The elements, or wrappers around them if wrap is true.
 	 * @beta
	 */
	getBySelector: function(selectors, wrap)
	{
		if (wrap == undefined)
			wrap = true;
			
		// TODO: Polyfill for IE
		var els = this.element.querySelectorAll(selectors);
		return wrap ? DOM.wrapAll(els) : els;
	},
	
	////////////////////////////////////////////////////////////////////////////////////////////////
	
	/**
	 * Get all the children of this element
	 * @method children
	 * @return {Array of ElementWrapper} The children
	 */
	children: function()
	{
		return DOM.wrapAll(this.element.children);
	},
	
	/**
	 * Get the parent element of this element. If a tag name is passed, get the first parent that
	 * matches this tag name, traversing the tree
	 * @method parent
	 * @param	{String} [tagName=null] Name of tag to look for, or null to just get the first ancestor
	 * @return {ElementWrapper} The parent element
	 */
	parent: function(tagName)
	{
		if (!tagName)
			return DOM.wrap(this.element.parentNode);
			
		tagName = tagName.toUpperCase();
		var parent = this.element.parentNode;
		while (parent && parent.nodeName.toUpperCase() != tagName)
			parent = parent.parentNode;
			
		return DOM.wrap(parent);
	},
	
	/**
	 * Get the sibling element before this one
	 * @method previous
	 * @return {ElementWrapper} The previous element, or null if there's none.
	 */
	previous: (function()
	{
		var tempEl = document.createElement('div');
		if ('previousElementSibling' in tempEl)
		{
			return function()
			{
				return DOM.wrap(this.element.previousElementSibling);
			}
		}
		else
		{
			return function()
			{
				var sibling = this.element;
				while (sibling = sibling.previousSibling)
				{
					if (sibling.nodeType === 1)
					{
						return DOM.wrap(sibling);
					}
				}
				return null;
			}
		}
	})(),
	
	/**
	 * Get the sibling element after this one
	 * @method next
	 * @return {ElementWrapper} The next element, or null if there's none.
	 */
	next: (function()
	{
		var tempEl = document.createElement('div');
		if ('nextElementSibling' in tempEl)
		{
			return function()
			{
				return DOM.wrap(this.element.nextElementSibling);
			}
		}
		else
		{
			return function()
			{
				var sibling = this.element;
				while (sibling = sibling.nextSibling)
				{
					if (sibling.nodeType === 1)
					{
						return DOM.wrap(sibling);
					}
				}
				return null;
			}
		}
	})(),
	
	/**
	 * Remove this element from the DOM.
	 * @method remove
	 */
	remove: function()
	{
		// Ensure the wrapper is removed from the cache
		DOM.cache[this.element.getAttribute(DOM.JS_ELEMENT_ID)] = null;
		
		// Actually delete it
		this.element.parentNode.removeChild(this.element);
		delete this.element;
	},
	
	/**
	 * Add a child to the start of this element
	 * @method prependChild
	 * @param	{ElementWrapper|HTMLElement} New element to add
	 * @chainable
	 */
	prependChild: function(newNode)
	{
		if (newNode instanceof ElementWrapper)
			newNode = newNode.element;
		
		this.element.insertBefore(newNode, this.element.firstChild);
		return this;
	},
	
	/**
	 * Clone this element
	 * @method cloneNode
	 * @param  {Boolean} deep Whether to perform a deep clone
	 * @return {ElementWrapper} The cloned node
	 */
	cloneNode: function(deep)
	{
		return new ElementWrapper(this.element.cloneNode(deep));
	},
	
	/***** Normal DOM method wrappers *****/
	/**
	 * Set the value of the specified attribute
	 * @method setAttribute
	 * @param  {String} attribute Attribute to set
	 * @param  {mixed} value Value to set attribute to
	 * @chainable
	 */
	setAttribute: function(attribute, value)
	{
		this.element.setAttribute(attribute, value);
		return this;
	},
	/**
	 * Get the value of the specified attribute
	 * @method getAttribute
	 * @param  {String} attribute Attribute to get
	 * @return {mixed}  Attribute's value
	 */
	getAttribute: function(attribute)
	{
		return this.element.getAttribute(attribute);
	},
	/**
	 * Append a new child to the end of this element
	 * @method appendChild
	 * @param  {ElementWrapper|HTMLElement} newNode Node to append
	 * @chainable
	 */
	appendChild: function(newNode)
	{
		if (newNode instanceof ElementWrapper)
			newNode = newNode.element;
		
		this.element.appendChild(newNode);
		return this;
	},
	
	/**
	 * Remove the specified child from this element
	 * @method removeChild
	 * @param  {ElementWrapper|HTMLElement} node Child node to remove
	 * @chainable
	 */
	removeChild: function(node)
	{
		if (node instanceof ElementWrapper)
			node = node.element;
		
		this.element.removeChild(node);
		return this;
	}
}

// Body is used frequently
DOM.body = DOM.wrap(document.body);
// Helper function (a la Prototype, MooTools, jQuery, etc.)
function $(el) { return DOM.wrap(el); }

/**
 * List containing multiple elements, similar to an array. Allows calling methods like setStyle and
 * addClass, which automatically call this method on every element in the list. Immutable.
 * Available methods include:
 * 
 * - {{#crossLink "ElementWrapper/addClass"}}{{/crossLink}}
 * - {{#crossLink "ElementWrapper/remove"}}{{/crossLink}}
 * - {{#crossLink "ElementWrapper/removeClass"}}{{/crossLink}}
 * - {{#crossLink "ElementWrapper/set"}}{{/crossLink}}
 * - {{#crossLink "ElementWrapper/setAttribute"}}{{/crossLink}}
 * - {{#crossLink "ElementWrapper/setStyle"}}{{/crossLink}}
 * - {{#crossLink "ElementWrapper/setStyles"}}{{/crossLink}}
 * 
 * @class ElementWrapperList
 * @constructor
 * @module JSFramework
 * @submodule DOM
 */
function ElementWrapperList(items)
{
	// Add the items from the passed array
	for (var i = 0, count = items.length; i < count; i++)
		this[i] = DOM.wrap(items[i]);
	
	/**
	 * Number of items in this list
	 * @property length
	 * @type Integer
	 */
	this.length = items.length;
}

ElementWrapperList.prototype = {
	/**
	 * Convert this list into a proper array.
	 * @method toArray
	 * @return	{Array}	An array with the same elements as this list
	 */
	toArray: function()
	{
		return Array.prototype.slice.call(this);
	}
};

// Add all the functions that would make sense if called on a list of elements
(function()
{
	var toAdd = ['set', 'setAttribute', 'remove', 'addClass', 'removeClass', 'setStyle', 'setStyles'];
	
	for (var i = 0, count = toAdd.length; i < count; i++)
	{
		var func = toAdd[i];
		// Create a wrapper for this function
		ElementWrapperList.prototype[func] = (function(functionName)
		{
			return function()
			{
				for (var i = 0; i < this.length; i++)
				{
					this[i][functionName].apply(this[i], arguments);
				}
				return this;
			}
		})(func);
	}
})();