/**
 * Part of JMF Library core
 * @requires JMF, JMF.Events, JMF.Exception
 * SVN: $Id: htmlelement.js 6163 2008-08-14 14:36:31Z mjezierski $
 * 
 * This file contains extensions to HTML objects.
 * TODO:
 * handle custom events on dom nodes
 */

JMF.registerLib('JMF.HTMLElement','$Id: htmlelement.js 6163 2008-08-14 14:36:31Z mjezierski $');

/**
 * Extends HTML object
 * Namespace for DOM object extensions
 * @param {Mixed} obj Id string, node reference or css selector search string
 * @return {Mixed} Extended HTML node or array of nodes
 * Selector search string format:
 * :C:class name:tag
 * C - means that class selectors will be searches. In future there also be other possible search methods
 * class name - css class name that will be searched for
 * tag - tag name or * for any tags. Specyfing tag name will cause s earch to be much faster
 * Most of DOM extensions support arrays of nodes
 */
JMF.HTMLElement = function(obj) {
	if((typeof(obj) === 'object' && obj !== null && obj.__jmfExtension)||obj.nodeType === 9) {
		return obj;
	}
   obj = JMF.HTMLElement.find(obj);
   if(obj) {
	   JMF.HTMLElement.extend(obj);
	   if(obj instanceof Array) {
	   	obj.each(function(elem){JMF.HTMLElement.extend(elem);});
	   }
   }      	
   return obj;	
};

/**
 * Creates DOM Element
 * @param {String} elem Element name, default is div
 * @return {Object} DOM Object
 */
JMF.HTMLElement.create = function(elem) {
	if(elem == 'canvas' && JMF.browser.IE) {
	  if(!window.CanvasRenderingContext2D) {
	  	  throw 'JMF: Canvas in IE requires canvas compatibility library';
	  }
	  var d = document.createElement('div');
	  var c = document.createElement('canvas');
	  d.appendChild(c);
	  c = G_vmlCanvasManager.initElement(c);
	  return JMF.$H(c);	
	} 
	
	return JMF.HTMLElement(document.createElement(elem || 'div'));
};

/**
 * Creates text node
 * @param {String} text Text to insert into node, default ''
 * @return {Object} Text node
 */
JMF.HTMLElement.createText = function(text) {
	return document.createTextNode(text || '');
};

//set it to true to perform strict innerHTML check
//it works only in fireox/opera
JMF.HTMLElement.strictInnerHTML = false;
/**
 * Extends given object prototype.
 * Internal function. JMF.HTMLElement should be used instead
 * @param {Object} obj DOM node
 * @return {Object} Extended dom node
 */
JMF.HTMLElement.extend = function(obj) {
   obj.__jmfExtension = true;
   for(var i in JMF.HTMLElement.prototype) {
      if(undefined === obj[i]) {
         obj[i] = JMF.HTMLElement.prototype[i];
      }
   }
   return obj;
};

JMF.HTMLElement.attrMap = (function(){
	if(JMF.browser.IE) {
		return {
			'class':'className'
		};
	}
	return {
		'className':'class'
	};
})();

/**
 * Finds DOM node(s) in document
 * Internal function. JMF.HTMLElement shoud be used instead
 * @param {Mixed} obj DOM node or search string
 * @return {Mixed} DOM node or array of DOM sodes
 * @throws {JMF.Exception} JMF.Exception.EX_INVPARAMS when search string cannot be properly parsed
 */
JMF.HTMLElement.find = function(selector, ctx) {
   //if selector is an element just return it
   if(typeof selector === 'object') {
      return selector;
   }

   //handle old api
   if(/^\:C\:/.test(selector)) {
      JMF._dbg.warn('You are using old api version. Use "." instead of ":C:": '+selector);
      selector = selector.replace('#','.').replace(/^:C:/,'');
   }

   //assure that context is properly set
   ctx = ctx || document;      
   var creg = /([*a-z]*)([#:.]?)([^\s<>]*)/i;
   var prs = creg.exec(selector);

   //parse error
   if(null === prs) {
      JMF._dbg.error('Invalid or not supported selector: '+selector);
      return null;
   }   
   
   //tag name
   var e = prs[1] || '*';
   //operation 
   var o = prs[2];
   //parameter
   var p = prs[3];
   
   var ret = null;
   switch(o) {
      case '.': 
        ret = JMF.$H.prototype.getElementsByClassName(p,e,ctx);
        break;
      case '#':
         ret = JMF.HTMLElement.prototype.getElementById(p,e,ctx);
        break;
      default:
         JMF._dbg.warn('Invalid selector or old api call. Trying to use "#" (getElementById): '+selector);
         ret = JMF.HTMLElement.prototype.getElementById(selector,'*',ctx);
   }
   return ret;
};

JMF.HTMLElement.prototype = {
	/**
	 * Handles cross browser setting of css string.
	 * Unifies float and opacity values
	 * @param {String} cssString Css style string
	 * @return {Mixed} Node or array of nodes  
	 */
	css:function(cssString) {
	   if(this instanceof Array) {
	   	this.each(function (elem) {elem.css(cssString);});
	   	return this;
	   }
	
	   if(!cssString) {
	      return this;
	   }
	   
	   cssString = cssString.split(';');
	   var styleObject = {};
	   var entry;
	
	   for(var i=0;i<cssString.length;i++) {
	      entry = (/([a-z\-]*):(.{1,})/i.exec(cssString[i])||[]);
	      if(entry[1]) {
	         //reformat to javascript property
	         styleObject[entry[1].replace(/-([a-zA-Z]){1}/,function(s){if(s) {return s.charAt(1).toUpperCase();}return '';})] = entry[2];
	      }
	   }
	 
	   this.cssStyle(styleObject);
	   return this;
	},
	/**
	 * Handles cross browser style object setting
	 * @param {Object} styleObject Object with style properties ({prop:value}), formated in JS manner eg. borderColor
	 * @return {Mixed} Node or array of nodes
	 */
   cssStyle:function(styleObject) {
	   if(this instanceof Array) {
	      this.each(function (elem) {elem.cssStyle(styleObject);});
	      return this;
	   }
	
	   if(!styleObject) {
	      return this;
	   }
	   
	   var styleProperty;
	   
	   for(var i in styleObject) {
	      //omit prototypes and invalid values
	      if((!styleObject.prototype || styleObject.prototype[i]) && (typeof( styleObject[i]) === 'string' || typeof (styleObject[i]) === 'number')) {
	            if(i === 'float') {
	               styleProperty = JMF.browser.IE?'styleFloat':'cssFloat';	
	            } else {
	            	styleProperty = i;
	            }
	
	            if(i === 'opacity' && JMF.browser.IE) {
	            	styleProperty = 'filter';
	            	styleObject[i] = 'alpha(opacity='+(Math.round(styleObject[i]*100))+')';
	            }
	         try {            
	            this.style[styleProperty] = styleObject[i];
	         } catch(e){} 
	      }
	   }
	   return this; 
	},

	/**
	 * Sets node attributes. For IE's sake it does not use set attribute method
	 * @param {Object} attrHash Object with attributes {attrName, attrValue}
	 * @return {Mixed} Node or array of nodes
	 */
   attr:function(attrHash) {
	   if(this instanceof Array) {
	      this.each(function (elem) {elem.attr(attrHash);});
	      return this;
	   }
	   
	   var attrProp;
      /*jslint forin:false*/
	   for(var i in attrHash) {
         if(!attrHash.hasOwnProperty(i)) {
         	continue;
         }
         
	      if('innerHTML' === i) {
	      	this.iHTML(attrHash[i]);
	      	continue;
	      }
	      //map browser quirks
         attrProp = JMF.HTMLElement.attrMap[i] || [i];

	      if((!attrHash.prototype || !attrHash.prototype[i]) && ((typeof(attrHash[i]) !== 'object' || attrHash[i] === null) && typeof (attrHash[i]) !== 'function')) {
	         //this[attrProp] = attrHash[i];
	         this.setAttribute(attrProp,attrHash[i]);
	      }
	   }
	   /*jslint forin:true*/
	   return this;
	},
   /**
    * Removes attribute from element
    * @param {String} attr Name of attribute to remove
    */
   rmAttr:function(attr) {
	   this.removeAttribute(attr);
	   return this;	
	},
	/**
	 * Adds event listener
	 * @param {String} evt Event name. It should not contain 'on' prefix
	 * @param {Function} handler Event handler function
	 * @return {Mixed} Node or array of nodes
	 */
   addListener:function(evt,handler) {
	   if(this instanceof Array) {
	      this.each(function (elem) {elem.addListener(evt,handler);});
	      return this;
	   }
	
	   JMF.$EE.addListener(this,evt,handler);
	   return this;
	},
	/**
	 * Removes event listener
	 * @param {String} evt Event name. It should not contain 'on' prefix
	 * @param {Function} handler Event handler function
	 * @return {Mixed} Node or array of nodes
	 */
   removeListener:function(evt,handler) {
	   if(this instanceof Array) {
	      this.each(function (elem) {elem.removeListener(evt,handler);});
	      return this;
	   }
	   
	   JMF.$EE.removeListener(this,evt,handler);
	   return this;
	},
	/**
	 * Sets css class 
	 * @param {String} className Css class name
	 * @return {Mixed} Node or array of nodes
	 */
   cssClass:function(className) {
	   if(this instanceof Array) {
	      this.each(function (elem) {elem.cssClass(className);});
	      return this;
	   }
	   this.className = className;
	},
	/**
	 * Returns element bounds
	 * Note that this function does not support array of nodes 
	 * @param {String} className Css class name
	 * @return {Object} Object with element bounds (top, left, right, bottom, width, height, clientWidth, clientHeight)
	 */
   getBounds:function() {
	   var obj = this;
	   var bounds = {top:0,left:0};
	   
	   do {
	   	bounds.top += obj.offsetTop;
	   	bounds.left += obj.offsetLeft;
	   } while ((obj = obj.offsetParent));
	   
	   bounds.width = this.offsetWidth;
	   bounds.height = this.offsetHeight;
	   bounds.clientWidth = this.clientWidth;
	   bounds.clientHeight = this.clientHeight;
	   
	   bounds.right = bounds.left + bounds.width;
	   bounds.bottom = bounds.top + bounds.height;
	   return bounds;
   },
	/**
	 * Forces repaint on element. This is necessery for some operations in Gecko based browsers
	 * Note that this function does not support array of nodes.
	 */
   repaint:function() {
		this.cssStyle({opacity:'0.99'});
		var instance = this;
		setTimeout(function(){instance.cssStyle({opacity:'1'});});
		return this;
   },
	/**
	 * Sets innerHTML property
	 * This function should be used with most care due to deprecation of innerHTML property in XHTML
	 * @param {String} innerHTML
	 * @return {Mixed} Node or array of nodes
	 */
	iHTML:function(innerHTML) {
	    if(this instanceof Array) {
	      this.each(function (elem) {elem.iHTML(innerHTML);});
	      return this;
	   }
	   this.clear();
	      
	   //pseudo standard code EXPERIMENTAL
	   if(window.DOMParser && JMF.HTMLElement.strictInnerHTML) {
	   	var dp = new DOMParser();
	   	var doc = dp.parseFromString('<div xmlns="http://www.w3.org/1999/xhtml">' + innerHTML + '</div>', 'application/xhtml+xml');
	   	if(doc.firstChild.tagName == 'parsererror') {
	   		JMF._dbg.error(doc.firstChild.firstChild.nodeValue);
	   		JMF._dbg.log(innerHTML);
	   		throw new JMF.Exception(JMF.Exception.EX_INVPARAMS,doc.firstChild.firstChild.nodeValue);
	   	}
	   	for(var i=0;i<doc.firstChild.childNodes.length;i++) {
	         this.appendChild(document.importNode(doc.firstChild.childNodes[i],true));   		
	   	}
	   } else {
	      this.innerHTML = innerHTML;
	   }
	   return this;
	},
	/**
	 * Clears all children
	 * @member JMF.HTMLElement
	 */
	clear:function() {
	    if(this instanceof Array) {
	      this.each(function (elem) {elem.clear();});
	      return this;
	   }
	
		var fc;
	   while((fc = this.firstChild)) {
	   	this.removeChild(fc);
	   }
	   return this;
	},
	/**
	 * Gets element by id relatively to curren node
	 * @member JMF.HTMLELement
	 * @param {String} id Searched element id
	 * @param {tag} tag Tag name for filtering searched elements
	 * @param {HTMLNode} ctx Context to search in 
	 * @return {Object} Node or null
	 */
	getElementById:function(id,tag,ctx) {
	   ctx = ctx || this;
	   tag = tag || '*';
	
	   if(ctx.nodeType === 9 && tag === '*') {
	      return  document.getElementById(id);
	   }
	   
	   var fnodes = ctx.getElementsByTagName(tag), ret = [];
      var i = fnodes.length;
      while(i--) {
         if(fnodes[i].getAttribute('id') === id) {
            return JMF.$H.extend(fnodes[i]);
         }
      }
	   return null;  
	},
	/**
	  * Check wheteher element is inside other element
	  * This function is called by  mouseenter/mouseleave event handlers  
     * @member JMF.HTMLELement
     * @param {Object} event 
     * @param {Object} elem
     * @return {bool}
  */
  isIn:function(elem) {
	  while ( elem && elem.nodeType === 1 && elem !== this ) {
         elem = elem.parentNode; 
	   }
	   return this === elem;
   },
   /**
    * Returns node collection width given css class
    * @param className {String} class name
    * @param tag {String} tag to search for
    * @param ctx {HTMLNode} context to be searched
    */
   getElementsByClassName:function(className,tag,ctx) {
	   tag = (tag || '*').toLowerCase();
	   ctx = ctx || this;
	   if(ctx._getElementsByClassName || (ctx.nodeType === 9 && ctx.getElementsByClassName)) {
	      var fn = ctx._getElementsByClassName||ctx.getElementsByClassName;
	      var fnodes = Array.prototype.slice.call(fn.call(ctx,className));
	      fnodes.each(function(e){JMF.$H.extend(e);});
	      if(tag === '*') {
	         return fnodes;
	      }
	      return fnodes.filter(function(e){return e.tagName.toLowerCase()=== tag;});
	   }
	   
	   var ret = [],
	       nodes,
	       rx=new RegExp('(^|\\s)'+className+'(\\s|$)');
	       
	   nodes = ctx.getElementsByTagName(tag);
	   for(var i=0,len=nodes.length;i<len;i++) {
	      if((nodes[i].className||'').match(rx) && (tag === '*' || nodes[i].tagName.toLowerCase() === tag)) {
	         ret.push(JMF.$H.extend(nodes[i]));
	      }
	   }
	   return ret;
	}
};

JMF.$H = JMF.HTMLElement;



//store original version of getElementsByClassName
if(window.HTMLElement && HTMLElement.prototype.getElementsByClassName) {
	HTMLElement.prototype._getElementsByClassName = HTMLElement.prototype.getElementsByClassName; 
} 