/** /ajax.js **/
/*

	Título..: VeryTinyAJAX2 0.2d, Wrapper JavaScript simple a funciones XMLHTTP para AJAX.
	Licencia: GPLv2 (http://www.gnu.org/licenses/gpl.txt)
	Autores.: Pablo Rodríguez Rey (mr -en- xkr -punto- es)
	          https://mr.xkr.es/
	          Javier Gil Motos (cucaracha -en- inertinc -punto- org)
	          http://cucaracha.inertinc.org/

	Agradecimientos a Cucaracha, por darme interés en el desarrollo de webs usando
	AJAX y proveerme del ejemplo básico con el que está desarrollada esta librería.

	Ejemplo de usos comunes:

		PETICIÓN COMPLETA (GET ajax=test POST data=hola+mundo):
			ajax({
				"get":{"ajax":"test"},
				"post":{"data":"hola mundo"},
				"always":function(){
				},
				"async":function(resultado){
					alert(adump(resultado.data));
				},
				"error":function(error){
					error.show();
				}
			});

		PETICIÓN ABREVIADA EQUIVALENTE:
			ajax("test", "hola mundo", function(resultado){
				alert(adump(resultado.data));
			});

*/

// información de versión
function ajaxVersion() { return("VeryTinyAJAX2/0.2c"); }

// comprobar si las peticiones AJAX están soportadas por este navegador
function ajaxEnabled() { return (httpObject()?true:false); }

// generar un nuevo objeto XMLHttpRequest
function httpObject() {
	var xmlhttp=false;
	try { xmlhttp=new ActiveXObject("Msxml2.XMLHTTP"); }
	catch (e) { try { xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); }
	catch (e) { try { xmlhttp=new XMLHttpRequest(); }
	catch (e) { xmlhttp=false; } } }
	return xmlhttp;
}

// cadena de estado de la petición
function httpStateString(state) {
	switch (state) {
	case 0: return "Uninitialized";
	case 1: return "Loading";
	case 2: return "Loaded";
	case 3: return "Interactive";
	case 4: return "Complete";
	case 5: return "Server Crashed";
	}
	return "";
}

// parsear cadena para ser enviada por GET/POST
function gescape(torg) {
	var d=""+torg;
	try { var d=d.replace(/%/gi,"%25"); } catch(e) {}
	try { var d=d.replace(/\"/gi,"%22"); } catch(e) {}
	try { var d=d.replace(/\\/gi,"%5C"); } catch(e) {}
	try { var d=d.replace(/\?/gi,"%3F"); } catch(e) {}
	try { var d=d.replace(/&/gi,"%26"); } catch(e) {}
	try { var d=d.replace(/=/gi,"%3D"); } catch(e) {}
	try { var d=d.replace(/\+/gi,"%2B"); } catch(e) {}
	try { var d=d.replace(/ /gi,"%20"); } catch(e) {}
	return d;
}

// hard escape: codifica aparte de los especiales,
// también aquellos cuyo ASCII sea <32 o >127
function hescape(t) {
	var s=gescape(t);
	var r="";
	var c;
	for (var i=0;i<s.length;i++) {
		c=s.charCodeAt(i);
		r+=(c<32 || c>127?"%"+c.toString(16):s.charAt(i));
	}
	return r;
}

// función auxiliar para crear cadenas PHP sin caracteres de control
function jescape(torg) {
	var d=""+torg;
	try { var d=d.replace(/\\/gi,"\\\\"); } catch(e) {}
	try { var d=d.replace(/\"/gi,"\\\""); } catch(e) {}
	try { var d=d.replace(/\n/gi,"\\n"); } catch(e) {}
	try { var d=d.replace(/\t/gi,"\\t"); } catch(e) {}
	try { var d=d.replace(/\f/gi,"\\f"); } catch(e) {}
	try { var d=d.replace(/\r/gi,"\\r"); } catch(e) {}
	return d;
}

// convertir variable JavaScript a JSON
function json(a, level) {
	if (JSON && JSON.stringify) return JSON.stringify(a);
	if (!level) level=0;
	if (a==null) return 'null';
	switch (typeof(a)) {
	case 'object':
		var s="";
		for (var i in a) s+=(s?",":"")+(a.length?'':'"'+jescape(i)+'":')+json(a[i],level+1);
		return (a.length?"[":"{")+s+(a.length?"]":"}");
	case 'boolean': return (a?'true':'false');
	case 'number': return a;
	case 'string': default: return '"'+jescape(a)+'"';
	}
	return null;
}

// petición AJAX
function ajax(data, realdata, func1, func2) {

	// si no está soportado, cancelar petición
	if (!ajaxEnabled()) return false;

	// convertir llamada abreviada en significado real: llamada en plano
	if (typeof(data) == "object" && typeof(realdata) == "function") {
		return ajax({
			"post":data, // datos que se enviarán por post
			"async":realdata, // función de retorno asíncrona
			"plain":true, // la petición es AJAX o es de texto plano/XML
			"showerrors":false // se mostrarán errores típicos
		});
	}

	// convertir llamada abreviada en significado real: llamada abreviada
	if (typeof(data) == "string" && typeof(func1) == "function") {
		if (typeof(func2) == "function") {
			return ajax({
				"ajax":data, // comando ajax
				"data":realdata, // datos que se enviarán
				"always":func1, // ejecutar esta función siempre (al terminar o al ocurrir un error)
				"async":func2, // función de retorno asíncrona
				"showerrors":false // se mostrarán errores típicos
			});
		} else {
			return ajax({
				"ajax":data, // comando ajax
				"data":realdata, // datos que se enviarán
				"async":func1, // función de retorno asíncrona
				"plain":(func2?true:false), // la petición es AJAX o es de texto plano/XML
				"showerrors":false // se mostrarán errores típicos
			});
		}
	}

	// preparar datos
	var http=httpObject();
	var async=(data.sync?false:true);
	var func=(async?data.async:data.sync);
	var always=data.always;
	var post="";
	var url=(typeof(data.url) == "undefined"?location.href:data.url);
	var urlalm="";
	var urlalmp=url.indexOf("#");
	if (urlalmp != -1) {
		urlalm=url.substring(urlalmp);
		url=url.substring(0, urlalmp);
	}

	// control de eventos HTTP
	function events(http) {

		// objeto resultado
		function result(is_error) {

			// estado de la petición y cadena de estado
			try { this.state=http.readyState; } catch(e) { this.state=5; }
			this.stateString=httpStateString(this.state);

			// indicar si se ha completado la operación
			this.complete=(http.readyState == 4?true:false);

			// código de protocolo del servidor
			try { this.status=http.status; } catch(e) { this.status=null; }

			// si el resultado es de tipo error, agregar objetos
			if (is_error) {
				this.error=(r.status
					?"Se ha encontrado el error "+r.status+" en el servidor."
					:"El servidor no responde a la petición!\nPruebe dentro de unos instantes."
				);
				this.show=function(){
					if (typeof(newerror) == "function") newerror(this.error);
					else alert(this.error);
				};
			}

			// preparar datos
			this.text=http.responseText; // datos recibidos en texto plano
			this.xml =http.responseXML; // datos recibidos en XML
			this.data=null; // ausencia de datos por defecto
			try { eval("this.data="+http.responseText); }
			catch(e) { this.error=true; } // datos recibidos en JSON son preparados
			this.http=http; // http, último objeto

		}

		// comprobar que la respuesta del servidor es la 200 (HTTP OK)
		var error=false;
		if (http.readyState == 4) error=(http.status != 200);

		// crear objeto resultado
		var r=new result(false);

		// devolver evento
		if (error) {
			if (data.error || data.showerrors) {
				var re=new result(true);
				switch (typeof(data.error)) {
				case "boolean": re.show(); break;
				case "string":
					if (typeof(newerror) == "function") newerror(data.error);
					else alert(data.error);
					break;
				case "function": data.error(re); break;
				}
				if (always) always(re);
				if (data.showerrors && re.status) re.show();
			}
		} else {
			if (r.complete) {
				if (data.showerrors && r.error && !data.plain) {
					var alertmsg="Error en buffer de salida: No se puede procesar la petición AJAX";
					if (typeof(newerror) == "function") newerror("<b>"+alertmsg+"</b><hr/>"+r.text);
					else alert(alertmsg+"\n\n"+r.text);
				}
				if (always) always(r);
				if (func) func(r);
			} else {
				if (data.events) data.events(r);
			}
		}
	
	}

	// gestión de timeout
	http.ontimeout=function(e){
		if (data.timeout) {
			var d={"timeout":e};
			if (always) always(d);
			data.error(d);
		}
	};

	// si se especifica acción, incluir parámetro GET ajax=acción
	if (data.ajax) url+=(url.indexOf("?")<0?"?":"&")+"ajax="+data.ajax;

	// si se especifican parámetros GET adicionales, incluirlos
	if (data.get) {
		if (typeof(data.get) == "object") {
			for (var i in data.get)
				url+=(url.indexOf("?")<0?"?":"&")+gescape(i)+"="+gescape(data.get[i]);
		} else {
			url+=(url.indexOf("?")<0?"?":"&")+data.get;
		}
	}

	// si se especifican datos por POST, incluirlos
	if (data.post) {
		if (typeof(data.post) == "object") {
			for (var i in data.post)
				post+=(post?"&":"")+gescape(i)+"="+gescape(data.post[i]);
		} else {
			post=data.post;
		}
	}

	// si hay datos prefijados, añadir al POST en formato JSON
	if (data.data) post+=(post?"&":"")+"data="+gescape(json(data.data));

	// petición HTTP
	url+=urlalm;
	http.open((post?"POST":"GET"), url, async);
	if (async) http.onreadystatechange=function(){ events(http); };
	if (data.progress) http.onprogress=data.progress;
	if (data.timeout) http.timeout=data.timeout;
	if (data.uploadProgress && http.upload) http.upload.onprogress=data.uploadProgress;
	http.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
	//try { http.setRequestHeader("Content-Length", (post?post.length:0)); } catch(e) {}
	http.send(post?post:null);
	if (!async) events(http);

	// completado correctamente
	return true;

}


/** /common.js **/
// common.js by mr.xkr v2 rev.4b

// check if a variable is set
function isset(v) { return (typeof(v) != "undefined"?true:false); }

// if not (a), then (b)
function ifnot(a, b) { return (a?a:b); }

// little functions to reduce code
function gid(id) { try { var rid=(typeof(id) == "object"?id:document.getElementById(id)); return rid; } catch(e) { return null; } }
function gidget(id) { return gid(id).innerHTML; }
function gidset(id, html) { var e=gid(id); try { e.innerHTML=(typeof(html) == "undefined"?"":html); } catch(e) { console.warn("gidset("+id+", html): "+e); } }
function gidval(id, data) { if (typeof(data) != "undefined") gid(id).value=(data == null?"":data); else return(gid(id).type=="checkbox"?(gid(id).checked?gid(id).value:""):gid(id).value); }
function gidvals(idsdata) { for (var i in idsdata) gidval(i,idsdata[i]); }
function giddel(id) { var d=gid(id); d.parentNode.removeChild(d); }
function gidmove(id_org, id_dst) { gid(id_dst).innerHTML=gid(id_org).innerHTML; gid(id_org).innerHTML=""; }
function alter(id) { gid(id).style.display=(gid(id).style.display == "none"?"block":"none"); }
function show(id) { gid(id).style.display="block"; }
function hide(id) { gid(id).style.display="none"; }
function cell(id) { gid(id).style.display="table-cell"; }
function visible(id) { gid(id).style.visibility="visible"; }
function hidden(id) { gid(id).style.visibility="hidden"; }
function isShow(id) { return(gid(id).style.display == "block"?true:false); }
function isVisible(id) { return(gid(id).style.display != "none"?true:false); }
function showSwitch(id) { gid(id).style.display=(gid(id).style.display != "none"?"none":""); }
function gidfocus(id) { if (gid(id)) { try { gid(id).select(); } catch(e) {}; try { gid(id).focus(); } catch(e) {}; } }

// set element is enabled
function gidenabled(id, enabled) {
	var e=gid(id);
	if (e) {
		if (enabled) e.removeAttribute("disabled");
		else e.setAttribute("disabled", "");
	}
}

// foreach implementation
function xforeach(a, f) {
	var r;
	if (typeof(a) == "object" && typeof(f) == "function") for (var i in a) {
		r=f(a[i], i, a);
		if (typeof(r) != "undefined") return r;
	}
	return null;
}

// ensure valid date in ISO format YYYY-MM-DD (deprecated)
function gidvalFecha(id) {
	var d=intval(gidval(id+"_d"));
	var m=intval(gidval(id+"_m"));
	var y=intval(gidval(id+"_y"));
	if (isNaN(d) || d<1 || d>31) return "";
	if (isNaN(m) || m<1 || m>12) return "";
	if (isNaN(y) || y<1900 || y>3000) return "";
	return y+"-"+(m<10?"0":"")+m+"-"+(d<10?"0":"")+d;
}

// obtener todos los datos de campos según su ID prefijados y/o sufijados
function gpreids(prefix, ids, sufix) {
	var ids=ids.split(" ");
	var a={};
	for (var i in ids) {
		var id=(prefix?prefix+"_":"")+ids[i]+(sufix?"_"+sufix:"");
		var e=gid(id);
		if (e) {
			var v=gidval(id);
			switch (e.type) {
			case "checkbox":
			case "radio":
				if (!e.checked) v="";
			}
			a[ids[i]]=v;
		}
	}
	return a;
}

// establecer todos los datos de campos según su ID prefijados y/o sufijados
function spreids(prefix, ids, values, sufix) {
	var ids=ids.split(" ");
	for (var i in ids)
		if (gid((prefix?prefix+"_":"")+ids[i]+(sufix?"_"+sufix:""))) {
			var o=gid((prefix?prefix+"_":"")+ids[i]+(sufix?"_"+sufix:""));
			var v=values[ids[i]];
			switch (o.type) {
			case "checkbox":
			case "radio":
				if (parseInt(v) || (v.length>0 && v!="0")) o.checked=true;
				break;
			case "select-one":
			case "select-multiple":
				for (var j=0; j<o.options.length; j++)
					if (o.options[j].value == v) {
						gidval(o, v);
						break;
					}
				break;
			default:
				gidval(o, v);
			}
		}
}

// template substitutions from an array of strings
function template(s, replaces) {
	if (replaces)
		for (var i in replaces)
			s=s.replace(new RegExp(i.replace(/([.?*+^$[\]\\(){}-])/g, "\\$1"),'g'), replaces[i]);
	return s;
}

// carga el contenido HTML de una capa y realiza reemplazos para usarla como template para ventanas o búsquedas AJAX
// por defecto, los reemplazos que hace son automáticamente de id y name con prefijo $
function gtemplate(id, replaces) {
	var s=gidget("template:"+id);
	s=s.replace(/ id=\$/gi," id=");
	s=s.replace(/ id=\'\$/gi," id='");
	s=s.replace(/ id=\"\$/gi,' id="');
	s=s.replace(/ name=\$/gi," name=");
	s=s.replace(/ name=\'\$/gi," name='");
	s=s.replace(/ name=\"\$/gi,' name="');
	if (replaces) s=template(s,replaces);
	return s;
}

// simplified creation of a new DOM element
function newElement(element, o) {
	var e=document.createElement(element);
	if (o) {
		if (o.id) e.id=o.id;
		if (o.class) e.className=o.class;
		if (o.html) e.innerHTML=o.html;
		if (o.type) e.type=o.type;
		if (o.title) e.title=o.title;
		if (o.style) e.style=o.style;
		if (o.value) e.value=o.value;
		if (o.attributes)
			for (var i in o.attributes)
				if (o.attributes[i]!==null)
					e.setAttribute(i, o.attributes[i]);
		if (o.properties)
			for (var i in o.properties)
				if (o.properties[i]!==null)
					e[i]=o.properties[i];
		if (o.styles)
			for (var i in o.styles)
				if (o.styles[i]!==null)
					e.style[i]=o.styles[i];
		if (o.events)
			for (var i in o.events)
				if (o.events[i]!==null)
					e.addEventListener(i, o.events[i]);
		if (o.childs)
			for (var i in o.childs)
				if (o.childs[i]!==null)
					e.appendChild(o.childs[i]);
	}
	return e;
}

// dump JavaScript variable tree
// Thanks to Binny V A (binnyva -at- hotmail -dot- com - binnyva.com) for adump implementation
function adump(arr, level) {
	if (!level) level=0;
	var s="";
	var t=""; for (var j=0; j < level; j++) t+="\t";
	try {
		if (typeof(arr) == 'object') {
			if (arr.nextSibling) return t+"{*}\n"; // NO devolver elementos internos del navegador
			for (var item in arr) {
				var value=arr[item];
				if (typeof(value) == 'object') {
					var size=0; for (var none in value) size++;
					s+=t+'"' + item + '" = '+typeof(value)+'('+size+'):\n';
					s+=adump(value,level+1);
				} else {
					s+=t+'"' + item + '" = '+typeof(value)+'("' + value + '")\n';
				}
			}
		} else {
			s="("+typeof(arr)+") "+arr;
		}
	} catch(e) {}
	return s;
}

// element positioning functions
function getTop(id) { var o=gid(id); var p=0; do { p+=o.offsetTop; } while (o=o.offsetParent); return(p); }
function getLeft(id) { var o=gid(id); var p=0; do { p+=o.offsetLeft; } while (o=o.offsetParent); return(p); }
function getScrollTop(id) { var o=gid(id); var p=0; do { p+=(o.scrollTop?o.scrollTop:0); } while (o=o.parentNode); return(p); }
function getScrollLeft(id) { var o=gid(id); var p=0; do { p+=(o.scrollLeft?o.scrollLeft:0); } while (o=o.parentNode); return(p); }
function getWidth(id) { return gid(id).offsetWidth; }
function getHeight(id) { return gid(id).offsetHeight; }

// element style change
function style(id, styles) { var o=gid(id); for (var i in styles) o.style[i]=styles[i]; }

// element className functions
function classAdd(id, c) {
	var a=gid(id); if (!a) return false;
	a=a.className.split(" ");
	for (var i in a)
		if (a[i] == c) return;
	gid(id).className=trim(gid(id).className+" "+c);
	return true;
}
function classDel(id, c) {
	var a=gid(id); if (!a) return false;
	a=a.className.split(" ");
	var cs="";
	for (var i in a)
		if (a[i] != c)
			cs+=" "+a[i];
	gid(id).className=trim(cs);
	return true;
} 
function classEnable(id, c, enabled) {
	return (enabled?classAdd(id, c):classDel(id, c));
}
function classSwap(id, c) {
	return (hasClass(id, c)?classDel(id, c):classAdd(id, c));
}
function hasClass(id, c){
	if (!gid(id) || !gid(id).className) return;
	var cs=gid(id).className.split(" ");
	return (cs.indexOf(c) != -1);
}

// document and window properties
function ieTrueBody() { return((document.compatMode && document.compatMode != "BackCompat")?document.documentElement:document.body); }
function scrollLeft() { return ieTrueBody().scrollLeft; }
function scrollTop() { return ieTrueBody().scrollTop; }
function windowWidth() { return (document.documentElement.clientWidth?document.documentElement.clientWidth:(window.innerWidth?window.innerWidth:document.body.clientWidth)); }
function windowHeight() { return (document.documentElement.clientHeight?document.documentElement.clientHeight:(window.innerHeight?window.innerHeight:document.body.clientHeight)); }
function documentWidth() { return Math.max(document.body.scrollWidth, document.body.offsetWidth, document.documentElement.clientWidth, document.documentElement.scrollWidth, document.documentElement.offsetWidth); }
function documentHeight() { return Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight); }

// natural properties of an image (real size)
function naturalWidth(idimg) {
	if (typeof(gid(idimg).naturalWidth) == "number") return gid(idimg).naturalWidth;
	else { var tmp=new Image(); tmp.src=gid(idimg).src; return tmp.width; }
}
function naturalHeight(idimg) {
	if (typeof(gid(idimg).naturalHeight) == "number") return gid(idimg).naturalHeight;
	else { var tmp=new Image(); tmp.src=gid(idimg).src; return tmp.height; }
}

// notify if an element is partial or fully visible in the view
function isIntoView(id, full) {
	var e=gid(id);
	if (!e || !e.getBoundingClientRect) return false;
	var r=e.getBoundingClientRect();
	if (!r) return false;
	return (isset(full) && full
		?(r.top >= 0) && (r.bottom <= window.innerHeight) // completely visible
		:r.top < window.innerHeight && r.bottom >= 0      // partially visible
	);
}

// callback if an element appears into the view
function appearIntoView(id, callback, o) {
	var o=o||{};
	var intoview=false;
	var check_appear=function(e){
		o.id=id;
		o.event=e;
		o.intoview=isIntoView(id, o.full);
		if ((e && e._forced) || (!intoview && o.intoview) || (intoview && !o.intoview)) {
			callback(o);
			if (!o.always) document.removeEventListener("scroll", check_appear);
		}
		intoview=o.intoview;
	};
	document.addEventListener("scroll", check_appear);
	if (o.always) check_appear({"_forced":true});
}

// set cursor
function setCursor(cursor) { document.body.style.cursor=(cursor?cursor:"auto"); }

// get element border width (horizontal)
function getBorderWidth(id) {
	var wext=(parseInt(getStyle(id, "border-left-width"))+parseInt(getStyle(id,"border-right-width")));
	return (!isNaN(wext)?wext:0);
}

// get element border height (vertical)
function getBorderHeight(id) {
	var wext=(parseInt(getStyle(id, "border-top-width"))+parseInt(getStyle(id,"border-bottom-width")));
	return (!isNaN(wext)?wext:0);
}

// get element top border height
function getBorderTopHeight(id) {
	var wext=parseInt(parseInt(getStyle(id, "border-top-width")));
	return (!isNaN(wext)?wext:0);
}

// get element bottom border height
function getBorderBottomHeight(id) {
	var wext=parseInt(parseInt(getStyle(id, "border-bottom-width")));
	return (!isNaN(wext)?wext:0);
}

// get element left border width
function getBorderLeftWidth(id) {
	var wext=parseInt(parseInt(getStyle(id, "border-left-width")));
	return (!isNaN(wext)?wext:0);
}

// get element right border width
function getBorderRightWidth(id) {
	var wext=parseInt(parseInt(getStyle(id, "border-right-width")));
	return (!isNaN(wext)?wext:0);
}

// get element padding width (horizontal)
function getPaddingWidth(id) {
	var wext=(parseInt(getStyle(id, "padding-left"))+parseInt(getStyle(id, "padding-right")));
	return (!isNaN(wext)?wext:0);
}

// get element padding height (vertical)
function getPaddingHeight(id) {
	var wext=(parseInt(getStyle(id, "padding-top"))+parseInt(getStyle(id, "padding-bottom")));
	return (!isNaN(wext)?wext:0);
}

// obtiene el estilo final computado de un elemento
function getStyle(id, styleprop) {
	var x=gid(id);
	if (x.currentStyle) { return x.currentStyle[styleprop]; }
	else if (window.getComputedStyle) { return document.defaultView.getComputedStyle(x,null).getPropertyValue(styleprop); }
	return null;
}

// element inner width (without margin/padding/border)
function crossInnerWidth(id) {
	var element=gid(id);
	try {
		if (window.getComputedStyle(element,""))
			return(element.clientWidth-parseInt(window.getComputedStyle(element,"").getPropertyValue("padding-left"))-parseInt(window.getComputedStyle(element,"").getPropertyValue("padding-right")));
	} catch(e) {
		return(element.clientWidth-parseInt(element.currentStyle.paddingLeft)-parseInt(element.currentStyle.paddingRight));
	}
}

// element inner height (without margin/padding/border)
function crossInnerHeight(id) {
	var element=gid(id);
	try {
		if (window.getComputedStyle(element,""))
			return(element.clientHeight-parseInt(window.getComputedStyle(element,"").getPropertyValue("padding-top"))-parseInt(window.getComputedStyle(element,"").getPropertyValue("padding-bottom")));
	} catch(e) {
		return(element.clientHeight-parseInt(element.currentStyle.paddingTop)-parseInt(element.currentStyle.paddingBottom));
	}
}

// image preloading
var imagePreloadList={};
function imagePreload(imageorlist) {
	var image_list=(typeof(imageorlist) == "string"?[imageorlist]:imageorlist);
	for (var i in image_list) {
		var image=image_list[i];
		imagePreloadList[image]={"loaded":false,"img":new Image()};
		imagePreloadList[image].img.src=image;
		imagePreloadList[image].img.onload=function(){ imagePreloadList[image].loaded=true; };
	}
}

// get scrollbar width (horizontal)
// thanks to Alexandre Gomes (Portugal)
// http://www.alexandre-gomes.com/?p=115
function scrollWidth() {

	var inner=document.createElement('p');
	inner.style.width='100%';
	inner.style.height='200px';

	var outer=document.createElement('div');
	outer.style.position='absolute';
	outer.style.top='0px';
	outer.style.left='0px';
	outer.style.visibility='hidden';
	outer.style.width='200px';
	outer.style.height='150px';
	outer.style.overflow='hidden';

	outer.appendChild(inner);
	document.body.appendChild(outer);

	var w1=inner.offsetWidth;
	outer.style.overflow='scroll';
	var w2=inner.offsetWidth;
	if (w1==w2) w2=outer.clientWidth;

	document.body.removeChild(outer);
	return(w1-w2);
}

// get cookie
function getCookie(name) {
	var nameEQ=name.replace(/=/gi,"_")+"=";
	var ca=document.cookie.split(';');
	for (i=0; i<ca.length; i++) {
		var c=ca[i];
		while (c.charAt(0) == ' ')
			c=c.substring(1, c.length);
		if (c.indexOf(nameEQ) == 0)
			return c.substring(nameEQ.length, c.length).replace(/\\\\/gi,"\\").replace(/\\n/gi,"\n").replace(/\\,/gi,";");
	}
	return "";
}

// set cookie
function setCookie(name, value, o) {
	var o=(Number.isFinite(o)?{"days":o}:o||{});
	if (o.days) o.expires=o.days*86400000;
	var expires="";
	if (o.expires) {
		var date=new Date();
		date.setTime(date.getTime()+o.expires);
		expires="; expires="+date.toGMTString();
	}
	document.cookie
		=name.replace(/=/gi,"").replace(/;/gi,"")
		+"="+(""+value).replace(/\\/gi,"\\\\").replace(/\n/gi,"\\n").replace(/;/gi,"\\,").replace(/;/gi,"_")
		+expires
		+"; path="+(o.path||"/")
		+"; sameSite="+(o.samesite||"Lax")
		+(o.secure?"; Secure":"")
	;
}

// delete cookie
function delCookie(name) {
	setCookie(name, "", {"expires":-1, "samesite":"Strict"});
	setCookie(name, "", {"expires":-1, "samesite":"Lax"});
	setCookie(name, "", {"expires":-1, "samesite":"None", "secure":true});
	setCookie(name, "", {"expires":-1, "samesite":"None"});
}

// delete all accesible cookies
function delAllCookies() {
	var cookies=document.cookie.split(";");
	for (var i=0; i<cookies.length; i++) {
		var cookie=cookies[i];
		var eqPos=cookie.indexOf("=");
		var name=(eqPos>-1?cookie.substr(0, eqPos):cookie);
		delCookie(name);
	}
}

// check for typical navigators
function isie() { return (navigator.userAgent.indexOf("MSIE") != -1); }
function ismoz() { return (navigator.userAgent.indexOf("Firefox") != -1 || navigator.userAgent.indexOf("Iceweasel") != -1); }
function ischrome() { return (navigator.userAgent.indexOf("Chrome") != -1); }

// hide all selects (only in IE)
function hideSelects(hidden) {
	if (!isie()) return;
	selects=document.getElementsByTagName("select");
	for (i=0; i<selects.length; i++) selects[i].style.visibility=(hidden?"hidden":"");
}

// search and run embeded <script>
function getrunjs(data) {
	runjs(getjs(data));
}

// search embeded <script>
function getjs(data) {
	scode="";
	while (true) {
		ss=data.toLowerCase().indexOf("<script>"); if (ss<0) break;
		es=data.toLowerCase().indexOf("<\/script>", ss+2); if (es<0) break;
		scode=scode+data.substring(ss+8, es);
		data=data.substring(0, ss)+data.substring(es+9);
	}
	return scode;
}

// include javascript from string (faster than eval)
function runjs(data) {
	if (!data) return;
	var escode=document.createElement("script");
	escode.setAttribute("type","text/javascript");
	escode.text=data;
	document.getElementsByTagName("body").item(0).appendChild(escode);
}

// include javascript from file
function includejs(filename, onload) {
	if (!filename) return;
	var escode=document.createElement("script");
	escode.setAttribute("type","text/javascript");
	escode.src=filename;
	if (onload) escode.onload=onload
	document.getElementsByTagName("body").item(0).appendChild(escode);
}

// convert string new lines to HTML <br /> tags
function nl2br(t) {
	try { return t.replace(/\n/gi, "<br />"); } catch(e) {}
	return t;
}

// id/object merge: de una lista de identificadores separadas por comas,
// mezclar sus datos con objetos JavaScript previamente existentes
function ioMerge(ids, root, obj) {
	ids=ids.split(",");
	for (i=0;i<ids.length;i++)
		if (gid(root+ids[i]))
			obj[ids[i]]=gidval(root+ids[i]);
	return(obj);
}

// object/object merge: de una lista de objetos separados por comas,
// copia los los datos del primero objeto en el segundo y devuelve este
function ooMerge(ids, o1, o2) {
	ids=ids.split(",");
	for (i=0;i<ids.length;i++)
		o2[ids[i]]=o1[ids[i]];
	return o2;
}

// añade CSS al documento (no funciona bien en IE6)
function cssAdd(css) {
	var style=document.createElement("style");
	style.type="text/css";
	if (style.styleSheet) style.styleSheet.cssText=css;
	else style.appendChild(document.createTextNode(css));
	document.getElementsByTagName("head")[0].appendChild(style);
}

// copia en profundidad de un objeto
function array_copy(o) {
	if (typeof o != "object" || o === null || o instanceof HTMLElement) return o;
	var r=(o instanceof Array?[]:{});
	for (var i in o) r[i]=array_copy(o[i]);
	return r;
}

// array_count: cuenta el número de elementos de un array asociativo
function array_count(a) {
	var c=0;
	try { if (a) for (var i in a) c++; } catch(e) {}
	return c;
}

// array_merge: mezcla arrays puros o asociativos en profundidad
function array_merge(a1, a2) {
	var a=array_copy(a1);
	var b=array_copy(a2);
	for (var i in b) {
		var e=b[i];
		if (typeof a[i] === "object" && typeof e === "object") {
			a[i]=array_merge(a[i], e);
		} else if (typeof a[i] === "array" && typeof e === "array") {
			a[i]=a[i].concat(e);
		} else {
			a[i]=e;
		}
	}
	return a;
}

// elimina un elemento en la posición del indice de un array
function array_delete(a, index) {
	var n=Array();
	for (var i in a)
		if (i != index)
			n[n.length]=a[i];
	return n;
}

// array_remove: elimina claves de un array asociativo
function array_remove(a1, a2) {
	var a=new Object();
	var clone;
	for (var i in a1) {
		clone=true;
		for (var j in a2)
			if (i == a2[j]) {
				clone=false;
				break;
			}
		if (clone) a[i]=a1[i];
	}
	return a;
}

// array_get: devuelve las claves de un array dada una lista de ellas
function array_get(a, list) {
	var o=new Object();
	for (var i in list)
		o[list[i]]=a[list[i]];
	return o;
}

// array_save: busca las claves de la lista en el segundo array y los mezcla con el primero
function array_save(a1, a2, list) {
	for (var i in list)
		a1[list[i]]=a2[list[i]];
	return a1;
}

// añade un objeto a un array plano
function array_push(a,o) {
	a.push(o);
}

// verifica si un array está vacío o no
function array_isclean(a) {
	if (!a) return true;
	for (var i in a)
		return false;
	return true;
}

// devuelve si un array es idéntico a otro (se usa ===)
function array_equals(a, b) {
	var c=0, d=0;
	for (var i in a) {
		if (a[i] !== b[i])
			return false;
		c++;
	}
	for (var i in b)
		d++;
	return (c == d?true:false);
}

// devolver un hash siempre, aunque el constructor sea un array
function array_hash(a) {
	for (var i in a) return a;
	return {};
}

// devuelve las claves de un hash en un nuevo array
function array_keys(a) {
	var b=[];
	for (var i in a) b.push(i);
	return b;
}

// convierte un hash/array para devolver siempre un array
function array_values(h) {
	var a=[];
	for (var i in h) a.push(h[i]);
	return a;
}

// verifica si un elemento está dentro del array
function in_array(e, a) {
	if (!a) return false;
	for (var i in a)
		if (a[i] === e) return true;
	return false;
}

// control de impresión, sobrecargar este método
function doprint() { window.print(); }

// init_fast
var init_fast_func=[];
function init_fast(add) {
	if (add) init_fast_func[init_fast_func.length]=add;
	else { for (var i in init_fast_func) { try { init_fast_func[i](); } catch(e) {} } }
}

// onload
var init_func=[];
var init_func_last=window.onload;
function init(add) {
	if (add) init_func[init_func.length]=add;
	else { for (var i in init_func) { try { init_func[i](); } catch(e) {} } }
}
window.onload=function(){
	try { init_func_last(); } catch(e) {}
	try { init(); } catch(e) {}
}

// onunload
var unload_func=[];
var unload_func_last=window.onunload;
function unload(add) {
	if (add) unload_func[unload_func.length]=add;
	else { for (var i in unload_func) { try { unload_func[i](); } catch(e) {} } }
}
window.onunload=function(){
	try { unload_func_last(); } catch(e) {}
	try { unload(); } catch(e) {}
}

// onresize
var resize_func=[];
var resize_func_last=window.onresize;
function resize(add) {
	if (add) resize_func[resize_func.length]=add;
	else { for (var i in resize_func) { try { resize_func[i](); } catch(e) {} } }
}
window.onresize=function(){
	try { resize_func_last(); } catch(e) {}
	try { resize(); } catch(e) {}
}

// onkeydown
var keydown_func=[];
var keydown_func_last=document.onkeydown;
function keydown(p) {
	if (typeof(p)=="function") keydown_func[keydown_func.length]=p;
	else { for (var i in keydown_func) { try { keydown_func[i](p); } catch(e) {} } }
}
document.onkeydown=function(we){
	try { keydown_func_last(we); } catch(e) {}
	try { keydown(we); } catch(e) {}
}

// onkeyup
var keyup_func=[];
var keyup_func_last=document.onkeyup;
function keyup(p) {
	if (typeof(p)=="function") keyup_func[keyup_func.length]=p;
	else { for (var i in keyup_func) { try { keyup_func[i](p); } catch(e) {} } }
}
document.onkeyup=function(we){
	try { keyup_func_last(we); } catch(e) {}
	try { keyup(we); } catch(e) {}
}

// onscroll
var scroll_func=[];
var scroll_func_last=document.onscroll;
function scroll(p) {
	if (typeof(p)=="function") scroll_func[scroll_func.length]=p;
	else { for (var i in scroll_func) { try { scroll_func[i](p); } catch(e) {} } }
}
document.onscroll=function(we){
	try { scroll_func_last(we); } catch(e) {}
	try { scroll(we); } catch(e) {}
}

// onmouseup
var mouseup_func=[];
var mouseup_func_last=document.onmouseup;
function mouseup(p) {
	if (typeof(p)=="function") mouseup_func[mouseup_func.length]=p;
	else { for (var i in mouseup_func) { try { mouseup_func[i](p); } catch(e) {} } }
}
document.onmouseup=function(we){
	if (!we) we=window.event;
	try { mouseup_func_last(we); } catch(e) {}
	try { mouseup(we); } catch(e) {}
}

// onmousedown
var mousedown_func=[];
var mousedown_func_last=document.onmousedown;
function mousedown(p) {
	var ret=null;
	if (typeof(p)=="function") mousedown_func[mousedown_func.length]=p;
	else {
		for (var i in mousedown_func) { try { ret=mousedown_func[i](p); } catch(e) {} }
		if (typeof(ret)!="undefined" && typeof(ret)!="null") return(ret);
	}
}
document.onmousedown=function(we){
	var ret=null;
	if (!we) we=window.event;
	try { ret=mousedown_func_last(we); } catch(e) {}
	try { ret=mousedown(we); } catch(e) {}
	if (typeof(ret)!="undefined" && typeof(ret)!="null") return(ret);
}

// onmousemove
var mousemove_func=[];
var mousemove_func_last=document.onmousemove;
function mousemove(p) {
	if (typeof(p)=="function") mousemove_func[mousemove_func.length]=p;
	else { for (var i in mousemove_func) { try { mousemove_func[i](p); } catch(e) {} } }
}
document.onmousemove=function(we){
	if (!we) we=window.event;
	try { mousemove_func_last(we); } catch(e) {}
	try { mousemove(we); } catch(e) {}
}

// mouse delta
var mousewheel_func=[];
function mousewheel(p) {
	if (typeof(p)=="function") mousewheel_func[mousewheel_func.length]=p;
	else { for (var i in mousewheel_func) { try { mousewheel_func[i](p); } catch(e) {} } }
}
function mousewheel_eventhandler(event) {
	var delta=0;
	if (!event) event=window.event; // IE
	if (event.wheelDelta) { // IE/Opera
		delta=event.wheelDelta/120;
		// In Opera 9, delta differs in sign as compared to IE.
		if (window.opera) delta=-delta;
	} else if (event.detail) { // Mozilla
		// In Mozilla, sign of delta is different than in IE. Also, delta is multiple of 3.
		delta=-event.detail/3;
	}
	// If delta is nonzero, handle it. Basically, delta is now positive
	// if wheel was scrolled up, and negative, if wheel was scrolled down.
	if (delta) {
		//handle(delta);
		var cancel=false;
		for (var i in mousewheel_func) { try { if (mousewheel_func[i](delta,event)) cancel=true; } catch(e) {} }
	}
	// Prevent default actions caused by mouse wheel. That might be ugly,
	// but we handle scrolls somehow anyway, so don't bother here..
	if (cancel) {
		if (event.preventDefault)
			event.preventDefault();
		event.returnValue=false;
	}
}
if (window.addEventListener) window.addEventListener('DOMMouseScroll', mousewheel_eventhandler, false); // DOMMouseScroll for Mozilla
window.onmousewheel=document.onmousewheel=mousewheel_eventhandler; // IE/Opera

// evento mouseenter
var mouseenter_cancellers_count=0;
var mouseenter_cancellers={};
function mouseenter(id, func, bubbling) {
	mouseenter_cancellers_count++;
	var id=gid(id);
	if (!id.id) {
		id.id="mouseenter_canceller_id_"+mouseenter_cancellers_count;
	}
	if (typeof id.onmouseleave=="object") {
		if (id.addEventListener) {
			id.addEventListener("mouseenter", func, (bubbling?bubbling:false));
		} else {
			id.onmouseenter=func;
		}
	} else {
		id.addEventListener("mouseover", function(event) {
			var id=this.id;
			if (mouseenter_cancellers[id]) {
				clearTimeout(mouseenter_cancellers[id]);
				mouseenter_cancellers[id]=false;
			} else {
				func(event);
			}
		}, bubbling);
		id.addEventListener("mouseout", function(event) {
			var id=this.id;
			mouseenter_cancellers[id]=setTimeout(function(){
				mouseenter_cancellers[id]=false;
			},1);
		}, bubbling);
	}
}

// evento mouseleave
var mouseleave_cancellers_count=0;
var mouseleave_cancellers={};
function mouseleave(id, func, bubbling) {
	mouseleave_cancellers_count++;
	var id=gid(id);
	if (!id.id) {
		id.id="mouseleave_canceller_id_"+mouseleave_cancellers_count;
	}
	if (typeof id.onmouseleave=="object") {
		if (id.addEventListener) {
			id.addEventListener("mouseleave", func, (bubbling?bubbling:false));
		} else {
			id.onmouseleave=func;
		}
	} else {
		id.addEventListener("mouseover", function(event) {
			var id=this.id;
			if (mouseleave_cancellers[id]) {
				clearTimeout(mouseleave_cancellers[id]);
				mouseleave_cancellers[id]=false;
			}
		}, bubbling);
		id.addEventListener("mouseout", function(event) {
			var id=this.id;
			mouseleave_cancellers[id]=setTimeout(function(){
				mouseleave_cancellers[id]=false;
				func(event);
			},1);
		}, bubbling);
	}
}

// habilitar/deshabilitar seleccionar texto en un elemento
function selectionEnabled(o,enable) {
	var o=gid(o);
	if (typeof o.onselectstart!="undefined") { if (enable) o.onselectstart=null; else { o.onselectstart=function(){ return false; } } } // IE
	else if (typeof o.style.MozUserSelect!="undefined") { o.style.MozUserSelect=(enable?"":"none"); } //Firefox
	else { if (enable) o.onmousedown=null; else { o.onmousedown=function(){ return false; } } } // all other navs
	o.style.cursor="default";
}

// get current decimal separator
function localeDecimalSeparator() {
	return (1.1).toLocaleString().substring(1, 2);
}

// parse number from locale string number
function localeNumber(n) {
	return parseFloat((""+n).replace(localeDecimalSeparator(), '.'));
}

// number to locale string
function numberLocale(n) {
	return (""+localeNumber(n)).replace('.', localeDecimalSeparator());
}

// entrada sólo numérica entera
function gInputInt(id, negatives, floating) {
	var input=gid(id);
	var lds=localeDecimalSeparator();
	input.style.textAlign="right";
	input.onkeyup=function(e){
		var c=e.keyCode;
		if (c==16) input.removeAttribute("data-key-shift");
		if (c==17) input.removeAttribute("data-key-control");
	};
	input.onkeydown=function(e){
		var c=e.keyCode;
		if (c>=35 && c<=39) return true;
		if (c==16) {
			input.setAttribute("data-key-shift","1");
			return true;
		}
		if (c==17) {
			input.setAttribute("data-key-control","1");
			return true;
		}
		if (c==8) return true;
		if (c==46) return true;
		if (c==116) return true;
		if (c==9) return true;
		if (c==13) return true;
		if (c==15) return true;
		if (input.getAttribute("data-key-control")=="1") {
			if (c==90) return true; // Ctrl+Z
			if (c==88) return true; // Ctrl+X
			if (c==67) return true; // Ctrl+C
			if (c==86) return true; // Ctrl+V
		}
		if (floating)
			if ((lds == "," && (c==188)) || (c==110 || c==190))
				return (this.value.indexOf(lds)==-1 && this.value.indexOf(".")==-1?true:false);
		if (negatives)
			if (c==109 || (c==173 && !input.getAttribute("data-key-shift")))
				return (this.value.indexOf("-")==-1?true:false);
		if (c>=48 && c<=57) return true;
		if (c>=96 && c<=105) return true;
		return false;
	};
	input.onblur=function(e){
		var lds=localeDecimalSeparator();
		var v=parseFloat(this.value.replace(lds, "."));
		v=(isNaN(v)?"":""+v);
		if (this.getAttribute("type") != "number") v=v.replace(".", lds);
		this.value=v;
	};
	input.onblur();
}

// entrada sólo numérica floatante
function gInputFloat(id, negatives) {
	gInputInt(id,negatives,true);
}

// comprobación autogrow
function autogrowcheck(id) {
	var e=gid(id), h=0, p=['padding-top', 'padding-bottom'];
	var s=window.getComputedStyle(e, null);
	for (var i in p) h+=parseInt(s[p[i]]);
	e.style.height='auto';
	e.style.height=(e.scrollHeight+(s["box-sizing"] == "border-box"?h:-h))+'px';
}

// textarea autogrowing
function autogrow(id) {
	var e=gid(id);
	e.style.resize='none';
	e.addEventListener("input", function(){ autogrowcheck(id); });
	autogrowcheck(id);
}

// eliminar espacios de una cadena
function trim(str) {
	return (""+str).replace(/^\s*|\s*$/g,"");
}

// elimina la ruta de un nombre de fichero completo,
// y también su sufijo, si se especifica y coincide
function basename(path, suffix) {
	var b=path.replace(/^.*[\/\\]/g, '');
	if (typeof(suffix) == 'string' && b.substr(b.length-suffix.length) == suffix)
		b=b.substr(0, b.length-suffix.length);
	return b;
}

// equivalente a br2nl en php
function br2nl(s) {
	return s.replace(/<br\s*\/?>/mg,"\n");
}

// mostrar un número en formato X.XXX,XX
function spf(n, f) {
	var n=parseFloat(n);
	if (isNaN(n)) return "";
	var f=f || 2;
	var e=Math.pow(10, f);
	var n=Math.round(n*e)/e;
	if (typeof(Intl) == "object" && Intl.NumberFormat) return ""+(new Intl.NumberFormat(undefined, {minimumFractionDigits:f}).format(n));
	return n.toFixed(f).replace(".", localeDecimalSeparator());
}

// mostrar un número en formato X.XXX
function spd(n) {
	var n=parseFloat(n);
	if (isNaN(n)) return "";
	if (typeof(Intl) == "object" && Intl.NumberFormat) return ""+(new Intl.NumberFormat(undefined, {maximumFractionDigits:0}).format(n));
	return n.toFixed(0).replace(".", localeDecimalSeparator());
}

// mostrar un número en formato X.XXX o X.XXX,XX (solo si tiene decimales)
function spn(n, f) {
	return (parseInt(n) == parseFloat(n)?spd(n):spf(n, f));
}

// asegurar una fecha en formato ISO
function isoEnsure(dt) {
	var m=dt.match(/\d{4}-[01]\d-[0-3]\d [0-2]\d:[0-5]\d:[0-5]\d/);
	return (m?m[0]:null);
}

// devuelve fecha y hora en formato ISO YYYY-MM-DD HH:II:SS desde fecha JavaScript (o fecha y hora actual)
function isoDatetime(f) { return isoDateTime(f); } // coherencia
function isoDateTime(f) {
	var
		f=f||new Date(),
		y=f.getFullYear(),
		m=f.getMonth()+1,
		d=f.getDate(),
		h=f.getHours(),
		i=f.getMinutes(),
		s=f.getSeconds()
	;
	return y+"-"+(m>9?"":"0")+m+"-"+(d>9?"":"0")+d
		+ " "+(h>9?"":"0")+h+":"+(i>9?"":"0")+i+":"+(s>9?"":"0")+s
	;
}

// devuelve fecha en formato ISO YYYY-MM-DD desde fecha JavaScript (o fecha actual)
function isoDate(f) {
	return isoDatetime(f).substring(0, 10);
}

// devuelve fecha en formato ISO HH:II:SS desde fecha JavaScript (o fecha actual)
function isoTime(f) {
	return isoDatetime(f).substring(11, 19);
}

// convierte una fecha Javascript a format SQL YYYY-MM-DD HH:II:SS (alias OBSOLETO)
function sqlFromDate(f) { return isoDatetime(f); }

// convierte una fecha SQL en formato YYYY-MM-DD con o sin HH:II:SS a formato JavaScript
function sqlDate(fecha) {
	var d=fecha.split(" ");
	var f=d[0].split("-");
	var h=(d.length>1?d[1].split(":"):[0,0,0]);
	try {
		return new Date(
			intval(f[0]), intval(f[1])-1, intval(f[2]),
			intval(h[0]), intval(h[1]), intval(h[2]),
		0);
	} catch(e) {
		return null;
	}
}

// convierte una cadena YYYY-MM-DD HH:II:SS a DD/MM/YYYY HH:II:SS
function sqlDateSP(d) {
	if (!d || (d.length!=10 && d.length!=19)) return "";
	return d.substring(8,10)+"/"+d.substring(5,7)+"/"+d.substring(0,4)+d.substring(10);
}

// convierte una cadena DD/MM/YYYY HH:II:SS a YYYY-MM-DD HH:II:SS
function spDateSQL(d) {
	if (!d || (d.length!=10 && d.length!=19)) return "";
	return d.substring(6,10)+"-"+d.substring(3,5)+"-"+d.substring(0,2)+d.substring(10);
}

// devuelve una fecha JavaScript en formato DD/MM/YYYY
function spDate(f) {
	var f=f || new Date();
	var dd=f.getDate(); if (dd<10) dd='0'+dd;
	var mm=f.getMonth()+1; if (mm<10) mm='0'+mm;
	var yyyy=f.getFullYear();
	return String(dd+"/"+mm+"/"+yyyy);
}

// devuelve una hora JavaScript en formato HH:II:SS
function spTime(f) {
	var f=f || new Date();
	var hh=f.getHours(); if (hh<=9) hh='0'+hh;
	var ii=f.getMinutes(); if (ii<=9) ii='0'+ii;
	var ss=f.getSeconds(); if (ss<=9) ss='0'+ss;
	return String(hh+":"+ii+":"+ss);
}

// devuelve la fecha actual en formato DD/MM/YYYY
function spDateNow() {
	return spDate(new Date());
}

// devuelve la hora actual en formato HH:II:SS
function spTimeNow() {
	return spTime(new Date());
}

// getElementsByClassName, implementación para IE (el único que no lo soporta)
if (typeof(getElementsByClassName) == "undefined") {
	function getElementsByClassName(oElm, strTagName, strClassName){
		var arrElements=(strTagName == "*" && oElm.all)? oElm.all :	oElm.getElementsByTagName(strTagName);
		var arrReturnElements=[];
		strClassName=strClassName.replace(/\-/g, "\\-");
		var oRegExp=new RegExp("(^|\\s)" + strClassName + "(\\s|$)");
		var oElement;
		for (var i=0; i<arrElements.length; i++) {
			oElement = arrElements[i];     
			if(oRegExp.test(oElement.className))
				arrReturnElements.push(oElement);
		}
		return arrReturnElements;
	}
}

// pasa cualquier cadena a entero/decimal, incluyendo números que comienzan con 0
function intval(t) { t=t+""; while (t.substring(0,1)=="0") t=t.substring(1); if (!t) t=0; return parseInt(t); }
function doubleval(t) { t=t+""; while (t.substring(0,1)=="0") t=t.substring(1); if (!t) t=0; return parseFloat(t); }

// devuelve el timestamp con resolución de milisegundos
function militime() { return new Date().getTime(); }

// devuelve parámetros GET (si no se especifican) o un parámetro GET de una URL o de la URL actual
function get(n, url) {
	var url=url || location.href;
	var i=url.indexOf("?");
	if (i !== -1) {
		url=url.substring(i+1);
		var i=url.indexOf("#");
		if (i !== -1) url=url.substring(0, i);
		var items=url.split("&");
		var k, v;
		if (typeof(n) === "undefined" || n === null) {
			var a={};
			for (var i=0; i<items.length; i++) {
				[k, v]=items[i].split("=");
				a[decodeURIComponent(k)]=(typeof(v) == "undefined"?"":decodeURIComponent(v));
			}
			return a;
		} else {
			for (var i=0; i<items.length; i++) {
				[k, v]=items[i].split("=");
				if (decodeURIComponent(k) === n) return (typeof(v) == "undefined"?"":decodeURIComponent(v));
			}
		}
	}
	return null;
}

// modifica parámetros (de una URL)
function alink(p, url) {
	var p=p||{};
	var url=url||location.href;
	var get={};
	var marker="";
	var i=url.indexOf("#");
	if (i !== -1) {
		marker=url.substring(i);
		url=url.substring(0, i);
	}
	var i=url.indexOf("?");
	if (i !== -1) {
		var g=url.substring(i+1).split("&");
		url=url.substring(0, i);
		for (var i in g) {
			[k, v]=g[i].split("=");
			get[decodeURIComponent(k)]=(typeof(v) == "undefined"?"":decodeURIComponent(v));
		}
	}
	for (var k in p) {
		var v=p[k];
		if (v === null) delete get[k];
		else get[k]=v;
	}
	var qs="";
	for (var k in get) {
		var v=get[k];
		qs+=(qs?"&":"")+encodeURIComponent(k)+(v===""?"":"="+encodeURIComponent(v));
	}
	return url+(qs?"?"+qs:"")+marker;
}

// evita la acción de carga de ficheros al arrastrarlos sobre la página o sobre un elemento de ella
function preventDefaultDrag(id) {
	if (typeof(id)=="undefined") var id=document.body;
	o=(typeof(id)=="object"?id:document.getElementById(id));
	o.addEventListener("dragover",function(event) { event.preventDefault(); },true);
	o.addEventListener("drop", function(event) { event.preventDefault(); }, false);
}

// phpjs: http://locutus.io/php/strip_tags/
function strip_tags(input, allowed) {
	allowed = (((allowed || '') + '').toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join('')
	var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi
	return input.replace(commentsAndPhpTags, '').replace(tags, function ($0, $1) {
		return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''
	});
}

// phpjs: tabla de traducciones HTML
function get_html_translation_table(table, quote_style) {
	var entities={}, hash_map={}, decimal;
	var constMappingTable={
		0:'HTML_SPECIALCHARS',
		1:'HTML_ENTITIES'
	}, constMappingQuoteStyle={
		0:'ENT_NOQUOTES',
		2:'ENT_COMPAT',
		3:'ENT_QUOTES'
	};
	var useTable={}, useQuoteStyle={};

	// translate arguments
	useTable=(!isNaN(table) ? constMappingTable[table] : table ? table.toUpperCase() : 'HTML_SPECIALCHARS');
	useQuoteStyle=(!isNaN(quote_style) ? constMappingQuoteStyle[quote_style] : quote_style ? quote_style.toUpperCase() : 'ENT_COMPAT');

	if (useTable !== 'HTML_SPECIALCHARS' && useTable !== 'HTML_ENTITIES') {
		throw new Error("Table: " + useTable + ' not supported');
	}

	entities['38']='&amp;';
	if (useTable === 'HTML_ENTITIES') {
		entities['160']='&nbsp;';
		entities['161']='&iexcl;';
		entities['162']='&cent;';
		entities['163']='&pound;';
		entities['164']='&curren;';
		entities['165']='&yen;';
		entities['166']='&brvbar;';
		entities['167']='&sect;';
		entities['168']='&uml;';
		entities['169']='&copy;';
		entities['170']='&ordf;';
		entities['171']='&laquo;';
		entities['172']='&not;';
		entities['173']='&shy;';
		entities['174']='&reg;';
		entities['175']='&macr;';
		entities['176']='&deg;';
		entities['177']='&plusmn;';
		entities['178']='&sup2;';
		entities['179']='&sup3;';
		entities['180']='&acute;';
		entities['181']='&micro;';
		entities['182']='&para;';
		entities['183']='&middot;';
		entities['184']='&cedil;';
		entities['185']='&sup1;';
		entities['186']='&ordm;';
		entities['187']='&raquo;';
		entities['188']='&frac14;';
		entities['189']='&frac12;';
		entities['190']='&frac34;';
		entities['191']='&iquest;';
		entities['192']='&Agrave;';
		entities['193']='&Aacute;';
		entities['194']='&Acirc;';
		entities['195']='&Atilde;';
		entities['196']='&Auml;';
		entities['197']='&Aring;';
		entities['198']='&AElig;';
		entities['199']='&Ccedil;';
		entities['200']='&Egrave;';
		entities['201']='&Eacute;';
		entities['202']='&Ecirc;';
		entities['203']='&Euml;';
		entities['204']='&Igrave;';
		entities['205']='&Iacute;';
		entities['206']='&Icirc;';
		entities['207']='&Iuml;';
		entities['208']='&ETH;';
		entities['209']='&Ntilde;';
		entities['210']='&Ograve;';
		entities['211']='&Oacute;';
		entities['212']='&Ocirc;';
		entities['213']='&Otilde;';
		entities['214']='&Ouml;';
		entities['215']='&times;';
		entities['216']='&Oslash;';
		entities['217']='&Ugrave;';
		entities['218']='&Uacute;';
		entities['219']='&Ucirc;';
		entities['220']='&Uuml;';
		entities['221']='&Yacute;';
		entities['222']='&THORN;';
		entities['223']='&szlig;';
		entities['224']='&agrave;';
		entities['225']='&aacute;';
		entities['226']='&acirc;';
		entities['227']='&atilde;';
		entities['228']='&auml;';
		entities['229']='&aring;';
		entities['230']='&aelig;';
		entities['231']='&ccedil;';
		entities['232']='&egrave;';
		entities['233']='&eacute;';
		entities['234']='&ecirc;';
		entities['235']='&euml;';
		entities['236']='&igrave;';
		entities['237']='&iacute;';
		entities['238']='&icirc;';
		entities['239']='&iuml;';
		entities['240']='&eth;';
		entities['241']='&ntilde;';
		entities['242']='&ograve;';
		entities['243']='&oacute;';
		entities['244']='&ocirc;';
		entities['245']='&otilde;';
		entities['246']='&ouml;';
		entities['247']='&divide;';
		entities['248']='&oslash;';
		entities['249']='&ugrave;';
		entities['250']='&uacute;';
		entities['251']='&ucirc;';
		entities['252']='&uuml;';
		entities['253']='&yacute;';
		entities['254']='&thorn;';
		entities['255']='&yuml;';
	}
	if (useQuoteStyle !== 'ENT_NOQUOTES') entities['34']='&quot;';
	if (useQuoteStyle === 'ENT_QUOTES') entities['39']='&#39;';
	entities['60']='&lt;';
	entities['62']='&gt;';

	// ascii decimals to real symbols
	for (decimal in entities) {
		if (entities.hasOwnProperty(decimal)) {
			hash_map[String.fromCharCode(decimal)] = entities[decimal];
		}
	}

	return hash_map;
}

// phpjs: convierte un texto a entidades HTML
function htmlentities(string, quote_style, charset, double_encode) {

	var hash_map = get_html_translation_table('HTML_ENTITIES', quote_style);
	symbol = '';
	string = string == null ? '' : string + '';

	if (!hash_map) return false;

	if (quote_style && quote_style === 'ENT_QUOTES') hash_map["'"] = '&#039;';

	if (!!double_encode || double_encode == null) {
		for (symbol in hash_map) {
			if (hash_map.hasOwnProperty(symbol)) {
				string = string.split(symbol).join(hash_map[symbol]);
			}
		}
	} else {
		string = string.replace(/([\s\S]*?)(&(?:#\d+|#x[\da-f]+|[a-zA-Z][\da-z]*);|$)/g, function (ignore, text, entity) {
			for (symbol in hash_map) {
				if (hash_map.hasOwnProperty(symbol)) {
					text = text.split(symbol).join(hash_map[symbol]);
				}
			}
			return text + entity;
		});
	}
	
	return string;
}

// abrir una ventana centrada
function windowOpen(url, pw, ph, o) {
	var o=o||{};
	var options={
		"toolbar":0,
		"location":0,
		"directories":0,
		"resizable":1,
		"scrollbars":1
	};
	if (typeof(pw) === "object") {
		var o=pw;
	} else {
		var w=parseInt(screen.width*pw);
		var h=parseInt(screen.height*ph);
		var l=parseInt((screen.width-w)/2);
		var t=parseInt((screen.height-h)/2.5);
		if (o.wratio) w=h*o.wratio;
		if (o.hratio) h=w*o.hratio;
	}
	if (o) for (var i in o) {
		if (o[i]===null || i=="name") delete options[i];
		else options[i]=o[i];
	}
	var p="";
	for (var i in options)
		p+=(p?",":"")+i+"="+options[i];
	return window.open(url, (o.name?o.name:""), p);
}

// convierte bytes a un string fácilmente legible
function bytesToString(bytes) { return sizeString(bytes); }
function sizeString(bytes) {
	if (bytes >= 1099511627776) return spf(bytes/1099511627776)+" TB";
	if (bytes >= 1073741824) return spf(bytes/1073741824)+" GB";
	if (bytes >= 1048576) return spf(bytes/1048576)+" MB";
	if (bytes >= 1024) return spf(bytes/1024)+" KB";
	return spd(bytes);
}

// copiar texto al portapapeles
function copyToClipboard(text) {
	var e=document.createElement("textarea");
	e.type="text";
	e.value=text;
	document.body.appendChild(e);
	e.select();
	e.focus();
	document.execCommand("copy");
	e.parentNode.removeChild(e);
}

// insertar texto en la posición del cursor o en sustitución de la selección de un input/textarea
function insertAtCursor(id, text) {
	var e=gid(id);
	if (document.selection) {
		e.focus();
		sel=document.selection.createRange();
		sel.text=text;
	} else if (e.selectionStart || e.selectionStart == '0') {
		var selectionEnd=e.selectionEnd;
		e.value=e.value.substring(0, e.selectionStart)+text+e.value.substring(selectionEnd, e.value.length);
		e.selectionEnd=selectionEnd+text.length;
	} else {
		e.value+=text;
	}
}


/** /newalert.js **/
/*

	newalert 0.1
	Creates modal dialogs using JS/HTML.
	* requires: common.js

	Examples:
		newalert("Hello world!");
		newwait("Please, wait while processing...");
		newwait_close();
		newalert({
			"ico":"images/ico48/ok.png",
			"title":"Modal Dialog",
			"msg":"This is an example dialog",
			"buttons":[
				{"caption":"Simple Wait Test","ico":"images/ico16/clock.png","action":function(newalert_id){
					newsimplewait();
					setTimeout(function(){ newwait_close(newalert_id); },1000);
				}},
				{"caption":"OK","ico":"images/ico16/ok.png","action":function(id, o){ newok("Test OK Dialog"); }},
				{"caption":"ERROR","ico":"images/ico16/error.png","action":function(id, o){ newerror("Test Error Dialog"); }},
				{"caption":"Close","ico":"images/ico16/cancel.png","action":function(id, o){ newalert_close(id) }},
				{"caption":"Alternate Close","ico":"images/ico16/cancel.png"}
			]
		});

*/

var _newalert=_newalert || {}; // setup
if (typeof _newalert.id == "undefined") _newalert.id="newalert_"; // base id
if (typeof _newalert.mobile == "undefined") _newalert.mobile=1000; // less than these pixels is considered mobile
var newalerts={};
var newalert_texts={
	"en":{
		"accept":"Accept",
		"cancel":"Cancel",
		"close":"Close",
		"wait":"Processing, please wait a moment..."
	},
	"es":{
		"accept":"Aceptar",
		"cancel":"Cancelar",
		"close":"Cerrar",
		"wait":"Proceso en curso, por favor espere..."
	}
};

function newalert_T(id) {
	var lang=document.documentElement.lang;
	if (!newalert_texts[lang]) lang="es";
	return (newalert_texts[lang][id]?newalert_texts[lang][id]:"["+id+"]");
}

function newalert_ismobile() {
	if (isset(_newalert.mobile)) {
		switch (_newalert.mobile) {
		case false: case true: return _newalert.mobile;
		case "auto": default:
		}
	}
	return (windowWidth()<_newalert.mobile); // auto
}

function newalert_open(id) {
	return newalerts[id];
}

function newalert_exec_button(id, i) {
	var activeElement=document.activeElement
	newalerts[id].buttons[i].action(id, newalerts[id].buttons[i]);
	if (activeElement != document.activeElement) newalerts[id].return_focus=false; // prevents action custom focus modification
}

function newalert_back_close(id, e) {
	if (!e.stopPropagation) return;
	if (!newalerts[id]) return;
	if (!newalerts[id].noclose) newalert_close(id);
	e.stopPropagation();
}

function newalert_body_click(id, e) {
	if (!e.stopPropagation) return;
	e.stopPropagation();
	if (newalerts[id] && newalerts[id].onclick) newalerts[id].onclick(id, e);
}

function newalert_change(o) {
	switch (o.action) {
	case "buttons_hide": hide(_newalert.id+o.id+"_cmds"); break;
	case "buttons_hide": hide(_newalert.id+o.id+"_cmds"); break;
	case "buttons_show": show(_newalert.id+o.id+"_cmds"); break;
	}
}

function newalert_resize(o) {
	var o=o||{};
	if (o.forced || _newalert.last_ismobile!=newalert_ismobile()) {
		_newalert.last_ismobile=newalert_ismobile();
		var body_noscroll_windows=0;
		var body_full_windows=0;
		for (var id in newalerts) {
			var newalert=newalerts[id];
			if (!newalert.nomobile) body_noscroll_windows++;
			if (newalert.full) body_full_windows++;
		}
		var body_noscroll=(body_noscroll_windows && _newalert.last_ismobile);
		classEnable(document.body, "newalert_window_body_mobile", body_noscroll);
		classEnable(document.body, "newalert_window_body_desktop", !body_noscroll);
		for (var id in newalerts) {
			var newalert=newalerts[id];
			if (gid(_newalert.id+id+"_back")) gid(_newalert.id+id+"_back").className=newalerts[id].backClass();
		}
	}
}

function newalert(o) {
	var index_default=0;
	if (typeof(o) == "string") {
		var id="";
		var msg=o;
		var buttons=null;
		var o={};
	} else {
		var id=(o.id?o.id:"");
		var msg=(o.msg?o.msg:"");
		var buttons=o.buttons;
	}
	if (newalerts[id]) newalert_remove(id, true);
	newalerts[id]=o;
	if (buttons == null) buttons=[{"caption":newalert_T("accept")}];
	newalerts[id].backClass=function(){
		var use_mobile=(!this.nomobile && newalert_ismobile());
		return (use_mobile?"newalert_mobile":(this.full?"newalert_full":"newalert_desktop"));
	};
	newalerts[id].return_focus=document.activeElement;
	newalerts[id].return_function=o.func;
	newalerts[id].buttons=[];
	if (newalerts[id].transition_timer) {
		clearTimeout(newalerts[id].transition_timer);
		newalerts[id].transition_timer=null;
	}
	newalerts[id].active=true;
	var b=document.createElement("div");
	b.setAttribute("class", "newalert_background"+(o.class?" "+o.class:""));
	b.setAttribute("id", _newalert.id+id+"_bg");
	b.style.display="block";
	var d=document.createElement("div");
	d.setAttribute("class", "newalert_container"+(o.class?" "+o.class:""));
	d.setAttribute("id", _newalert.id+id);
	var hasicon=(o.ico || o.icoclass);
	var cols=(hasicon?"colspan='2'":"");
	s="<table id='"+_newalert.id+id+"_back' class='"+newalerts[id].backClass()+"' cellpadding='0' cellspacing='0' width='100%' height='100%'><tr><td align='center' valign='middle' onClick='javascript:newalert_back_close(\""+id+"\", event);'>";
	if (o.body) s+=o.body;
	else {
		s+="<table id='"+_newalert.id+id+"_table'"
				+" class='"+(o.className?o.className:"newalert")+"'"
				+" cellpadding='0' cellspacing='0'"
				+(o.width?" width='"+o.width+"'":"")
				+(o.height?" height='"+o.width+"'":"")
				+(o.style?" style='"+o.style+"'":"")
			+" onClick='javascript:newalert_body_click(\""+id+"\", event);'>"
			+(o.title == null?"":"<tr height='1'><th "+cols+" class='newalert_title'>"+(o.title?o.title:"")+"</th></tr>")
			+"<tr>"
				+(hasicon
					?"<td class='newalert_icon' id='"+_newalert.id+id+"_icon'>"
					+(o.icoclass
						?"<div class='"+o.icoclass+"'></div>"
						:"<img class='newalert_icon_img' src='"+o.ico+"' alt='' />"
					)
					+"</td>"
					:""
				)
				+"<td class='newalert_body "+(msg?"newalert_body_msg":"newalert_body_nomsg")+"'>"
					+"<div class='newalert_frame' id='"+_newalert.id+id+"_frame'>"
						+"<div class='newalert_content' id='"+_newalert.id+id+"_content'>"
							+msg
						+"</div>"
					+"</div>"
				+"</td>"
			+"</tr>"
		;
		if (buttons.length) {
			s+="<tr height='1'>"
					+"<td "+cols+" id='"+_newalert.id+id+"_cmds_td'>"
						+"<div id='"+_newalert.id+id+"_cmds' "+cols+" class='newalert_cmds'>"
			;
			for (var i in buttons) {
				if (!buttons[i].id) buttons[i].id=_newalert.id+id+"_cmd_"+i;
				if (buttons[i].default) index_default=i;
				if (buttons[i].caption) {
					s+=" <button id='"+buttons[i].id+"' class='cmd"+(buttons[i].class?" "+buttons[i].class:"")+"'"
						+" onClick='javascript:"+(buttons[i].action
							?(typeof(buttons[i].action)=="function"
								?"newalert_exec_button(\""+id+"\","+i+");"
								:buttons[i].action
							)
							:"newalert_close(\""+id+"\")"
						)
						+";'>"
							+(buttons[i].ico?"<span class='icon' style='background-image:url(\""+buttons[i].ico+"\")'>":"")
								+buttons[i].caption
							+(buttons[i].ico?"</span>":"")
						+"</button>"
					;
				}
			}
			newalerts[id].buttons=buttons;
			s+"</div></td></tr>";
		}
		s+="</table>";
	}
	s+="</td></tr></table>";
	d.innerHTML=s;
	document.body.appendChild(b);
	document.body.appendChild(d);
	if (o.notransition) {
		classAdd(_newalert.id+id+"_bg", "newalert_background_transition_none");
		classAdd(_newalert.id+id, "newalert_container_transition_none");
	} else {
		newalerts[id].transition_timer=setTimeout(function(){
			classAdd(_newalert.id+id+"_bg", "newalert_background_transition_in");
			classAdd(_newalert.id+id, "newalert_container_transition_in");
		}, 20);
	}
	_newalert.openWindows++;
	// resize event
	if (!isset(_newalert.last_ismobile)) {
		_newalert.last_ismobile=newalert_ismobile();
		window.addEventListener("resize", function(){ newalert_resize({"auto":true}); }, false);
	}
	newalert_resize({"forced":true});
	// focus first button
	try { gid(_newalert.id+id+"_cmd_"+index_default).focus(); } catch(e) {}
	// return window information
	return {
		"id":id,
		"o":o,
		"close":function(){ newalert_close(id); }
	};
}

function newwait(o) {
	var o=(typeof o == "string"?{"msg":o}:o||{});
	o.id=(o.id != null?o.id:"wait");
	o.nomobile=true;
	o.noclose=true;
	o.msg=(o.msg?o.msg:newalert_T("wait"));
	if (!isset(o.icoclass)) o.icoclass="newalert_ico newalert_ico_busy";
	if (!isset(o.buttons)) o.buttons=[];
	newalert(o);
}

function newsimplewait() {
	newalert({
		"id":"wait",
		"noclose":true,
		"nomobile":true,
		"noshadow":true,
		"body":"<div class='newalert_simplewait'></div>",
		"buttons":[]
	});
}

function newalert_gen(kind, o, action) {
	if (typeof o == "string") o={"msg":o};
	_newalert.genc=(_newalert.genc?_newalert.genc:0)+1;
	newalert(array_merge({
		"id":"newalert_gen_"+kind+_newalert.genc,
		"icoclass":"newalert_ico newalert_ico_"+kind,
		"buttons":[{"caption":newalert_T("accept")}],
		"onclose":function(){ if (action) action(); }
	}, o));
}

function newok(o, action) { newalert_gen("ok", o, action); }
function newwarn(o, action) { newalert_gen("warn", o, action); }
function newerror(o, action) { newalert_gen("error", o, action); }

function newalert_remove(id, notransition) {
	if (!id) var id="";
	if (!newalerts[id]) return;
	newalerts[id].active=false;
	if (newalerts[id].notransition) notransition=true;
	if (gid(_newalert.id+id)) {
		var s=document.createElement('p').style, supportsTransitions='transition' in s;
		if (supportsTransitions && !notransition) {
			newalerts[id].transition_timer=setTimeout(function(){
				if (newalerts[id]) newalerts[id].transition_timer=null;
				if (gid(_newalert.id+id+"_bg")) {
					classDel(_newalert.id+id+"_bg", "newalert_background_transition_in");
					classAdd(_newalert.id+id+"_bg", "newalert_background_transition_out");
					gid(_newalert.id+id+"_bg").addEventListener("transitionend", function(){
						if (gid(_newalert.id+id+"_bg")) {
							gid(_newalert.id+id+"_bg").parentNode.removeChild(gid(_newalert.id+id+"_bg"));
							newalert_resize({"force":true});
						}
					}, true);
				}
				if (gid(_newalert.id+id)) {
					classDel(_newalert.id+id, "newalert_container_transition_in");
					classAdd(_newalert.id+id, "newalert_container_transition_out");
					gid(_newalert.id+id).addEventListener("transitionend", function(){
						if (gid(_newalert.id+id)) {
							delete newalerts[id];
							if (gid(_newalert.id+id)) gid(_newalert.id+id).parentNode.removeChild(gid(_newalert.id+id));
							newalert_resize({"forced":true});
						}
					}, true);
				}
			}, 20);
		} else {
			if (newalerts[id].transition_timer) {
				clearTimeout(newalerts[id].transition_timer);
				delete newalerts[id].transition_timer;
			}
			delete newalerts[id];
			if (gid(_newalert.id+id)) gid(_newalert.id+id).parentNode.removeChild(gid(_newalert.id+id));
			if (gid(_newalert.id+id+"_bg")) gid(_newalert.id+id+"_bg").parentNode.removeChild(gid(_newalert.id+id+"_bg"));
			newalert_resize({"forced":true});
		}
	}
}

function newalert_close(id) {
	if (!id) var id="";
	if (newalerts[id]) {
		try { if (newalerts[id].return_focus) newalerts[id].return_focus.focus(); } catch(e) {};
		if (newalerts[id].onclose) newalerts[id].onclose(id, newalerts[id]);
		try { if (newalerts[id].return_function) newalerts[id].return_function(id); } catch(e) {};
	}
	newalert_remove(id);
}

function newwait_close(id) {
	newalert_close(id?id:'wait');
}


/** /xwidgets.js **/
/*

	xWidgets.
	Simple HTML widgets for a better life!

*/
var xwidgets={

	widgets:{},

	/*

		xwidgets.selector()
		HTML ComboBox.

		Example:

			var selector=new xwidgets.selector({
				"id":"element",
				"items":[
					{"caption":"Default", "value":""},
					{"caption":"Item 1", "value":"1"},
					{"caption":"Item 2", "value":"2"},
					{"caption":"Item 3", "value":"3"}
				],
				"value":"",
				"onchange":function(selector, item){
					alert(item.value);
				}
			});

		Parameters:

			id:string (required)
				Element or Element identifier.

			items:object (optional)
				List of items.

			item:object
				Return current item

			value:integer|string (optional)
				Select an item by its index/key (if defined).

			render (optional)
				Render item.
				:function(self, item, index, selected)
					Renderer item using custom function.
					Parámeters:
						self - same class
						item - item to render
						key - item key (can be null if empty caption)
						selected - is item selected?
					Return:
						HTML string or DOM Element

		Events:

			onchange(self, item)
				Fires on item selection change.

	*/
	selector:function(o) {
		var self=this;
		self.o=o;
		if (!isset(self.o.value)) self.o.value=null;

		// select by index
		self.select=function(index){
			if (isset(self.o.value)) for (var i in self.o.items) if (self.o.value === self.o.items[i].value) {
				classDel(self.items[i], "widget_selector_item_active");
				break;
			}
			classAdd(self.items[index], "widget_selector_item_active");
			self.o.value=self.o.items[index].value;
			if (self.o.onchange) self.o.onchange(self, self.item());
		};

		// get selected item
		self.item=function(){
			for (var i in self.o.items) if (self.o.value === self.o.items[i].value) {
				return self.o.items[i];
			}
			return false;
		};

		// get/set value
		self.value=function(value){
			if (isset(value)) {
				self.o.value=value;
				if (self.o.onchange) self.o.onchange(self, self.o);
				self.refresh();
			}
			return self.o.value;
		};

		// refresh element
		self.refresh=function(){

			// render items
			self.items=[];
			for (var index in self.o.items) {
				var item=self.o.items[index];
				var down=(isset(self.o.down)?self.o.down(self, item):false);
				var selected=(self.o.value === item.value);
				var caption=item.caption;
				if (self.o.render) caption=self.o.render(self, item, index, selected);
				var child=(
					typeof(caption) == "function"
					?caption
					:newElement("div", {
						"class":"widget_selector_item_caption",
						"html":caption
					})
				);
				self.items.push(newElement("div", {
					"class":"widget_selector_item"+(selected?" widget_selector_item_active":""),
					"attributes":{
						"tabindex":0, // selectable
						"data-index":index
					},
					"properties":{
						"onkeypress":function(e){
							if (e.keyCode == 32 || e.keyCode == 13) {
								var index=this.getAttribute("data-index");
								self.select(index);
								e.preventDefault();
							}
						},
						"onclick":function(){
							var index=this.getAttribute("data-index");
							self.select(index);
						}
					},
					"childs":(child?[child]:[])
				}));
			}

			// widget container
			self.widget=newElement("div", {
				"class":"noselect",
				"childs":self.items
			});

			// selector
			self.selector=newElement("span", {
				"class":"widget_selector",
				"childs":[self.widget]
			});

			// render
			gidset(self.o.id, "");
			gid(self.o.id).appendChild(self.selector);

		};

		// refresh
		self.refresh();

		// register self
		xwidgets.widgets[self.o.id]=self;

	},

	/*

		xwidgets.hsearch()
		HTML SearchBox.

		Example:

			var hsearch=new xwidgets.hsearch({
				"id":"element",
				"render":function(self, item){
					return (item
						?"<b>"+item.caption+"</b> ("+item.id+")"
						:"- Please, select an option -"
					);
				},
				"search":function(self, search){
					var items=[
						{"id":1, "caption":"First Option"},
						{"id":2, "caption":"Second Option"}
					];
					var r=[];
					for (var i in items) {
						var item=items[i];
						if (!search || self.searchWords(item.caption, search)) {
							r.push(self.optionElement({
								"html":"<b>"+item.caption+"</b> ("+item.id+")",
								"item":item
								//"onselect":function(self){ self.select(item); }
							}));
						}
					}
					return r;
				}
			});
			//alert(hsearch.item());

		Parameters:

			id:element (required)
				Element or element identifier.

			render:function(self, item) (required)
				returns Rendered element or HTML string.

			search:function(self, search) (required)
				returns Rendered element or HTML string of options.

			item:any (optional)
				Sets initial item.

		Methods:

			.close()
				Close search.

			.open()
				Open search.

			.init()
				Initialize (called at startup).
	
			.item(item):any
				Get/Set current item.

			.optionElement(options):element
				Creates a clickable option element.

				options:object (required)

					Parameters:

						class:string (optional)
							CSS class/classes to apply.

						item:any (optional, required if no onselect)
							Item to be selected.

						html:string (optional, required if no child)
							HTML content.

						child:array (optional, required if no html)
							Array of child elements.

					Events:

						onselect(self, options) (optional, required if no item)
							Fired on element click/selection.

			.refresh()
				Refresh item.

			.search()
				Invocate search.

			.searchWords(text, search):boolean
				Do a natural case insensitive word search in the specified text.

			.select(item)
				Sets item selection and close search (if opened).

		Properties:

			.opened:boolean (readonly)
				Returns current dropdown state.

		Events:

			onchange(self, item)
				Fires on item change.

			onclose(self)
				Fires on close search.

			onopen(self)
				Fires on open search.

			onselect(self, item)
				Fires on item selection.

	*/
	"hsearch":function(o) {
		var self=this;
		self.o=o;

		self.select=function(item){
			var item=(typeof(item) != "undefined"?item:null);
			self.item(item);
			if (self.o.onselect) self.o.onselect(self, item);
			setTimeout(function(){
				self.close();
			},1);
		};

		self.open=function(){
			self.opened=true;
			classAdd(self.o.id, "hsearch_open");
			gidfocus(self._search);
			self.search();
			if (self.o.onopen) self.o.onopen(self);
		};

		self.closeChecked=function(){
			setTimeout(function(){
				//alert(document.activeElement+"/"+gid(self.o.id).matches(':focus-within'));
				if (!gid(self.o.id).matches(':focus-within')) self.close();
			}, 1);
		};

		self.close=function(){
			if (self.opened) {
				self.opened=false;
				classDel(self.o.id, "hsearch_open");
				if (self.o.onclose) self.o.onclose(self);
			}
		};

		self.search=function(){
			if (typeof(self.o.search) !== "function") return false;
			var s=self.o.search(self, gid(self._search).value);
			gidset(self._results, (s?(typeof s == "string"?s:""):(self.o.noresults?self.o.noresults:"<i>Sin resultados</i>")));
			if (s instanceof Array) {
				for (var i=0; i < s.length; i++) gid(self._results).appendChild(s[i]);
			} else if (typeof s != "string") {
				gid(self._results).appendChild(s);
			}
			classEnable(self._pop, "hsearch_empty", !s);
		};

		self.searchWords=function(text, search){
			var w=search.toLowerCase().split(" ");
			if (!w.length) return false;
			for (var i in w)
				if (text.toLowerCase().indexOf(w[i]) == -1)
					return false;
			return true;
		};

		self.item=function(item){
			if (typeof(item) != "undefined") {
				self.o.item=item;
				self.refresh();
				if (self.o.onchange) self.o.onchange(self, item);
			}
			return self.o.item;
		};

		self.refresh=function(){
			var e=self.o.render(self, self.o.item);
			gidset(self._item, (typeof e == "string"?e:""));
			if (e && typeof e != "string") gid(self._item).appendChild(e);
		};

		self.optionElement=function(o){
			function select() {
				if (o.onselect) o.onselect(self, o);
				if (o.item) self.select(o.item);
			}
			return newElement("div", {
				"class":"cmb_item"+(o.class?" "+o.class:""),
				"attributes":{
					"tabindex":"0",
				},
				"properties":{
					"onclick":function(){ select(); },
					"onkeypress":function(){ if (event.keyCode == 13) select(); }
				},
				"html":(o.html?o.html:""),
				"child":(o.child?o.child:"")
			});
		};

		self.value=function(){
			if (self.o.key) {
				var item=self.item();
				return (typeof(item) == "object"?item[self.o.key]:null);
			}
			return null;
		};

		self.init=function(){

			// ensure requisites
			if (!self.o.render) return console.error("hsearch: render method required.");

			// defaults
			self.o.key=self.o.key||"id";

			classAdd(self.o.id, "hsearch");

			gid(self.o.id).onclick=function(){
				if (!self.opened) self.open();
			};

			self._item=newElement("div", {
				"class":"hsearch_item",
				"attributes":{
					"tabindex":"0",
				},
				"properties":{
					"onclick":function(){
						self.open();
					},
					"onkeypress":function(){
						if ([13,32].includes(event.keyCode) && self.canopen) {
							self.open();
							event.preventDefault();
						}
					},
					"onkeydown":function(){
						if ([9,16,13].includes(event.keyCode)) return;
						if (self.canopen) self.open();
					},
					"onfocus":function(){
						self.canopen=true;
					},
					"onblur":function(){
						//console.log("blur:"+this.contains(document.activeElement));
						self.canopen=false;
					}
				}
			});

			self._pop=newElement("div", {
				"class":"hsearch_pop",
				"attributes":{
					"tabindex":"0",
				},
				"properties":{
					"onblur":function(){
						self.closeChecked();
						//console.log("blur:"+gid(self.o.id).matches(':focus-within'));
					}
				}
			});

			self._search=newElement("input", {
				"class":"hsearch_input",
				"attributes":{
					"type":"text",
					"value":""
				},
				"properties":{
					"ondblclick":function(){
						this.value="";
						self.search();
					},
					"oninput":function(){
						self.search();
					},
					"onkeyup":function(){
						if (event.keyCode == 27 && self.opened) self.close();
					},
					"onblur":function(){
						self.closeChecked();
					}
				}
			});

			self._search_container=newElement("div", {
				"class":"hsearch_input_container",
				"childs":[self._search]
			});

			self._results=newElement("div");

			gidset(self.o.id, "");
			self._pop.appendChild(self._results);
			gid(self.o.id).appendChild(self._search_container);
			gid(self.o.id).appendChild(self._item);
			gid(self.o.id).appendChild(self._pop);

			self.refresh();

		};

		self.init();

	},

	/*

		xwidgets.hcombo()
		HTML ComboBox.

		Example:

			var items=[
				{"id":1,"caption":"Option oen"},
				{"id":3,"caption":"Option two"},
				{"id":4,"caption":"Option four (no three)"},
				{"id":5,"caption":"Option five"},
				{"id":6,"caption":"Option six"},
				{"id":7,"caption":"Option seven"},
				{"id":8,"caption":"Option eight"},
				{"id":9,"caption":"Option nine"},
				{"id":10,"caption":"Option ten"}
			];

			var hcombo=new xwidgets.hcombo({
				id:"element",
				items:items,
				key:"id",
				render:"caption",
				empty:"- Select one or more -",
				multiple:true,
				index:2,
				keys:5
			});
			//alert(hcombo.index());

			var hcombo=new xwidgets.hcombo({
				id:"element",
				items:items,
				key:"id",
				caption:function(self, items){
					if (!items || !array_count(items)) return null;
					var h="";
					for (var i in items)
						h+=(h?", ":"")+items[i].caption;
					return h;
				},
				render:function(self, item, index, selected){
					return "* <b>"+item.caption+"</b>";
				},
				multiple:true,
				index:[2,3] //values:[4,5]
			});
			//alert(hcombo.values());

		Parameters:

			id:string (required)
				Element or Element identifier.

			items:object (optional)
				List of items.

			caption:string (optional)
				Render caption using key string as caption.

			caption:function(item, index) (optional)
				Renderer caption. Returns HTML.

			disabled:boolean (opcional)
				Set disabled field.

			readonly:boolean (opcional)
				Set read-only field.

			render (optional)
				Render item.
				:string
					Render using specified field as caption.
				:function(self, item, index, selected)
					Renderer item using custom function.
					Parámeters:
						self - same class
						item - item to render
						key - item key (can be null if empty caption)
						selected - is item selected?
					Return:
						HTML string.

			multiple:boolean (optional)
				True to enable multiple selection.

			empty:boolean (optional)
				Enable empty item (null item).

			search (optional)
				Enable search.
				:boolean
					Uses caption as searchable text.
				:string
					Uses custom field as searchable text.
				:function(self, item, index, search)
					Uses custom function.
					If string returned, uses it for searchable text.
					If boolean returned, uses it as match.

			key:string (optional)
				Set the field containing the key.

			keys:array (optional)
				Requires parameter key defined.
				Selects several items by its key.

			index (optional)
				:integer
					Select one item by its index.
				:array
					Selects several items by its index.

			value:integer|string (optional)
				Select an item by its index/key (if defined).

			values:array (optional)
				Selects several items by its index/key (if defined).

		Methods:

			.id()
				Returns identifier_string/element.

			.id(element|string)
				Sets new identifier_string/element for render.

			.focus()
				Focus combobox.

			.open()
				Open dropdown.

			.close()
				Close dropdown.

			.swap()
				Conmutes open/close dropdown.

			.select(index)
				Select an item by its index. If multiple and items is already selected, unselects it.
				Return: true if selected, false if not selected, null if not an item.

			.unselect()
				Un select all items.

			.isselected(index)
				Returns if an item is selected by its index.

			.renderCaption()
				Returns HTML for the current caption.

			.renderItem(item, index)
				Returns HTML for an item.

			.indexFirst()
				Get first index for all items (including empty item).

			.indexLast()
				Get last index for all items (including empty item).

			.count()
				Count number of visible elements (including empty item).

			.clear()
				Clear items from dropdown combo.

			.disable(disabled)
				Enable/Disable combo.

			.add(item)
				Adds one item to the dropdown combo at the end.

			.replace(item, index)
				Replaces one item into desired position.

			.focusedItem()
				Get current index of focused item (including empty item).

			.focusItem(index)
				Set focus to the item by index.

			.focusSelectedItem()
				Focus last selected item, or false if not selected.

			.focusFirstItem()
				Focus first item (including empty item).

			.focusLastItem()
				Focus last item (including empty item).

			.focusPrevItem()
				Focus previous item (including empty item).

			.focusNextItem()
				Focus next item (including empty item).

			.itemElement(item, index)
				Get item DOM element.

			.refresh()
				Refresh all.

			.refreshCaption()
				Refresh only caption.

			.refreshItem(index)
				Refresh item by index.

			.refreshItems()
				Refresh all items.

			.hasText(haystack, needle)
				Natural search a needle by words in a text haystack.
				Return: true if found, false if not.

			.readonly(readonly)
				Enable/Disable read-only.

			.resize()
				Resize event.

			.key([key:string])
				Get/Set the field containing the key.

			.keys([keys:string/array])
				Selects one/several items by its key.
				Return: selected keys.

			.index([indexes:integer/array])
				Select one/several items by its index.
				Return: selected index (single)/indexes (multiple).

			.values([keys:string/array])
				Get/Set selected keys by index or key, if defined.
				Return: items by index or key, if defined..

			.search([search:string])
				Get/set search value
				Return: search value.

			.destroy()
				Frees resources.

			.init()
				Initialize combobox (called at startup).

		Properties:

			.opened:boolean (readonly)
				Returns current dropdown state.

		Events:

			onselect(self, item, index)
				Fires on item selection.

			onclick(self, item, index)
				Fires on item click. If returns false, cancel item selection.

	*/
	hcombo:function(o){
		var self=this;

		// public vars
		self.opened=false;

		// get/set id
		self.id=function(id){
			if (isset(id)) self.o.id=id;
			return self.o.id;
		};

		// focus combobox
		self.focus=function(){
			if (self.e.cmb_input) {
				self.e.cmb_input.select();
				self.e.cmb_input.focus();
			} else {
				self.e.cmb_group.focus();
			}
		};

		// focus searchbox
		self.focusSearch=function(){
			if (self.e.cmb_search) {
				self.e.cmb_search.select();
				self.e.cmb_search.focus();
				return true;
			}
			return false;
		};

		// get/set disabled state
		self.disabled=function(disabled){
			self.o.disabled=disabled;
			self.close();
			self.refresh();
		};

		// get/set readonly state
		self.readonly=function(readonly){
			self.o.readonly=readonly;
			self.close();
			self.refresh();
		};

		// open combobox
		self.open=function(){
			if (self.o.disabled || self.o.readonly) return false;
			var first_time=(self.openedtimes?false:true);
			if (!self.openedtimes) self.openedtimes=0;
			self.openedtimes++;
			self.opened=true;
			if (self.closeTimeout) clearTimeout(self.closeTimeout);
			classAdd(self.e.cmb_group, "cmb_open");
			self.resize();
			if (first_time) {
				self.e.cmb_items.scrollTop=0;
				self.focusSearch();
				self.update({"focusSearch":true});
			}
			return true;
		};

		// close combobox
		self.close=function(focused){
			self.closeTimeout=setTimeout(function(){
				self.opened=false;
				delete self.closeTimeout;
				classDel(self.e.cmb_group, "cmb_open");
				if (focused) self.focus();
			}, 1);
		};

		// swap open/close
		self.swap=function(focused){
			if (self.opened) self.close(focused);
			else self.open(focused);
		};

		// select by index
		self.select=function(index){
			if (!isset(index)) return false;
			var item=null;
			if (!isNaN(index) && self.e.items[index] || (!index && self.o.empty && !self.o.multiple)) {
				if (!self.o.multiple) self.unselect();
				self.o.selectedindex=index;
				if (!self.o.selected) self.o.selected=[];
				if (isset(self.o.selected[index])) delete self.o.selected[index];
				else self.o.selected[index]=self.o.options[index];
				item=self.o.selecteditem=(self.o.selected[index]?self.o.selected[index]:false);
				if (self.o.key) {
					var lastitem=(index > self.o.options.length-1?null:self.o.options[index]);
					var key=(lastitem === null?"":(self.o.key?lastitem[self.o.key]:index));
					if (item) {
						self.o.selectedkeys[key]=item;
						var found=false;
						for (var i in self.o.selecteditems) if (self.o.selecteditems[i][self.o.key] == key) {
							found=true;
							break;
						}
						if (!found) self.o.selecteditems.push(item);
					} else {
						delete self.o.selectedkeys[key];
						for (var i in self.o.selecteditems) if (self.o.selecteditems[i][self.o.key] == key) {
							delete self.o.selecteditems[i];
							self.o.selecteditems=array_values(self.o.selecteditems);
							break;
						}
					}
				}
				self.refreshItem(index);
				self.refreshCaption();
				if (self.o.editable && self.e.cmb_input) {
					var value=strip_tags(self.renderCaption());
					if (typeof(self.o.editable) == "function") value=self.o.editable(self, item, value);
					self.e.cmb_input.value=value;
				}
				if (self.o.onselect) self.o.onselect(self, item, index);
				if (self.o.onchange) self.o.onchange(self, item, index);
				if (self.o.input) gidval(self.o.input, self.value());
			}
			return item;
		};

		// select first item
		self.selectFirst=function(){
			if (self.count()) if (!self.isselected(0)) self.select(0);
		};

		// select last item
		self.selectLast=function(){
			var last=self.count()-1;
			if (last >= 0) if (!self.isselected(last)) self.select(last);
		};

		// unselect all items
		self.unselect=function(){
			self.o.selected=[];
			self.o.selectedkeys={};
			self.o.selecteditems=[];
			delete self.o.selectedindex;
			delete self.o.selecteditem;
			if (self.e.items)
				for (var i in self.e.items)
					self.refreshItem(i);
		};

		// returns if an item is selected by its index
		self.isselected=function(index){
			if (!isset(index)) return false;
			return (self.o.selected && self.o.selected[index]?true:false);
		};

		// render caption
		self.renderCaption=function(){
			var html="";
			try {
				var selected=(self.o.key && self.o.multiple?self.o.selecteditems:self.o.selected);
				if (typeof(self.o.caption) == "function") {
					html=self.o.caption(self, (self.o.multiple?selected:self.o.selecteditem));
				} else if (typeof(self.o.caption) == "string") {
					if (selected)
						for (var i in selected)
							if (selected[i])
								html+=(html?", ":"")+selected[i][self.o.caption];
					if (!html && self.o.empty) html=null;
				} else {
					if (selected)
						for (var i in selected)
							if (selected[i])
								html+=(html?", ":"")+self.renderItem(selected[i]);
					if (!html && self.o.empty) html=null;
				}
				if (html === null) html=(self.o.editable?"":self.o.emptyCaption);
			} catch(e) {
				html="!caption: "+e;
			}
			return html;
		};

		// render item
		self.renderItem=function(item, index){
			if (item === null || index === null) return self.o.emptyCaption;
			var html="";
			try {
				if (typeof(self.o.render) == "function") {
					html=self.o.render(self, item, index, self.isselected(index));
				} else if (typeof(self.o.render) == "string") {
					html=(item?(isset(item[self.o.render])?item[self.o.render]:""):"");
				} else {
					html=(isset(item)?item:"");
				}
				if (self.o.itemclass) html="<div class='"+self.o.itemclass+"'>"+html+"</div>";
			} catch(e) {
				html="!item("+index+"): "+e;
			}
			return html;
		};

		// return first index
		self.indexFirst=function(){
			if (self.o.empty && !self.o.del) return null;
			if (self.e.items) for (var i in self.e.items) if (i !== "null") return parseInt(i);
			return false;
		};

		// return last index
		self.indexLast=function(){
			var index=false;
			if (self.e.items) for (var i in self.e.items) if (i !== "null") index=i;
			return parseInt(index);
		};

		// get aproximate page size
		self.pageSize=function(){
			// get first element height
			var min=5;
			var eh=false;
			var wh=self.e.cmb_items.offsetHeight;
			for (var i in self.e.items) {
				var e=self.e.items[i];
				eh=e.offsetHeight;
				break;
			}
			if (eh>0 && wh>0) {
				var ps=Math.floor(wh/eh)-1;
				return (ps>1?ps:1);
			}
			return 10;
		};

		// get focused item index
		self.focusedItem=function(){
			if (self.o.empty && document.activeElement == self.e.items[null]) return null;
			if (self.e.items)
				for (var i in self.e.items)
					if (document.activeElement == self.e.items[i])
						return parseInt(i);
			return false;
		};

		// focus an item
		self.focusItem=function(index){
			if (!isset(self.e.items[index])) return false;
			self.e.items[index].focus();
			return true;
		};

		// focus last selected item
		self.focusSelectedItem=function(){
			if (isset(self.o.selectedindex)) return self.focusItem(self.o.selectedindex);
			return false;
		};

		// focus first item
		self.focusFirstItem=function(){
			var i=self.indexFirst();
			if (i !== false) self.focusItem(i);
			return false;
		};

		// focus last item
		self.focusLastItem=function(){
			var i=self.indexLast();
			if (i !== false) self.focusItem(i);
			return false;
		};

		// focus previous item
		self.focusPrevItem=function(index){
			var index=index || self.focusedItem();
			var last=false;
			if (self.e.items) {
				if (index === 0 && self.o.empty) {
					self.focusItem(null);
				} else {
					for (var i in self.e.items) if (i !== "null") {
						if (i == index) return (last === false?false:self.focusItem(last));
						last=i;
					}
				}
			}
			return false;
		};

		// focus next item
		self.focusNextItem=function(index){
			var index=(isset(index)?index:self.focusedItem());
			var found=false;
			if (self.e.items) {
				if (index === null && self.o.empty) {
					self.focusItem(0);
				} else {
					for (var i in self.e.items) if (i !== "null") {
						if (found) return self.focusItem(i);
						found=(i == index);
					}
				}
			}
			return false;
		};

		// get item DOM element
		self.itemElement=function(item, index){
			return newElement("div", {
				"class":"cmb_item"+(self.isselected(index)?" cmb_item_selected":"")+(item?"":" cmb_item_empty"),
				"attributes":{
					"tabindex":"0",
					"data-index":(index >= 0?index:"")
				},
				"events":{
					"mousedown":function(){
						this.focus();
					},
					"keypress":function(e){
						var index=this.getAttribute("data-index");
						var r=true;
						index=(index === null?null:parseInt(index));
						switch (e.keyCode) {

						case 13:
							if (!self.isselected(index)) self.select(index);
							if (self.o.onclick) r=self.o.onclick(self, item, index);
							if (!isset(r) || r) {
								self.focus();
								self.close();
							}
							break;

						case 32:
							self.select(index);
							if (self.o.onclick) r=self.o.onclick(self, item, index);
							if (!isset(r) || r) {
								if (!self.o.multiple) {
									self.close();
									self.focus();
								}
								self.focusSelectedItem();
								if (self.o.multiple) self.focusNextItem();
							}
							break;

						}
						e.stopPropagation();
						e.preventDefault();
					},
					"click":function(){
						var index=this.getAttribute("data-index");
						var r=true;
						self.select(index);
						if (self.o.onclick) r=self.o.onclick(self, item, index);
						if (!isset(r) || r) {
							if (self.o.multiple) {
								self.focusItem(index);
							} else {
								self.focus();
								self.close();
							}
						}
					}
				},
				"html":self.renderItem(item, index)
			});
		};

		// get/set items
		self.items=function(items){
			if (isset(items)) {
				self.o.items=items;
				self.refreshItems();
			}
			return self.o.items;
		};

		// count number of visible elements
		self.count=function(){
			return self.o.options.length;
		};

		// clear items
		self.clear=function(){
			if (!self.o.items) self.o.items=[];
			if (self.e.items) for (var index in self.e.items) {
				if (self.e.items[index]) self.e.items[index].parentNode.removeChild(self.e.items[index]);
				delete self.e.items[index];
			}
			self.o.selected=[];
			self.o.options=[];
			self.o.keyindex={};
		};

		// add an item (option if index defined)
		self.add=function(item, index){
			var isnew=!isset(index);
			if (isnew) self.o.items.push(item);
			var index=(isnew || !index?self.count():index);
			var key=(item === null?"":(self.o.key?item[self.o.key]:index));
			var replace=isset(self.o.options[index]);
			//alert("add "+index+" key "+key+" item "+adump(item));
			if (replace) self.replace(item, index);
			else {
				self.o.keyindex[key]=index;
				self.o.options[index]=item;
				self.e.items[index]=self.itemElement(item, index);
				self.e.cmb_items.appendChild(self.e.items[index]);
			}
			return self.e.items[index];
		};

		// replace an option by its index
		self.replace=function(item, index){
			var key=(item === null?"":(self.o.key?item[self.o.key]:index));
			self.o.keyindex[key]=index;
			self.o.options[index]=item;
			if (isset(self.o.options[index])) {
				var rindex=(self.o.empty && !self.o.multiple?index-1:index);
				if (isset(self.o.items[rindex])) self.o.items[rindex]=item;
				self.refreshItem(index);
				return true;
			}
			return false;
		};

		// delete an item by its index
		self.del=function(index){
			var oindex=index;
			var index=(self.o.empty && !self.o.multiple?index-1:index);
			if (index >=0 && index < self.count()) {
				if (isset(self.o.items[index])) {
					var lastindex=self.o.selectedindex;
					delete self.o.items[index];
					self.o.items=array_values(self.o.items);
					delete self.o.selectedindex;
					delete self.o.selecteditem;
					self.refreshItems();
					self.refreshCaption();
					return true;
				}
			}
			return false;
		};

		// refresh combobox caption
		self.refreshCaption=function(){
			if (self.e.cmb_caption) gidset(self.e.cmb_caption, self.renderCaption());
		};

		// refresh an item
		self.refreshItem=function(index){
			if (!self.e.items[index]) return;
			var item=self.itemElement(self.o.options[index], index);
			self.e.items[index].parentNode.replaceChild(item, self.e.items[index]);
			self.e.items[index]=item;
		};

		// check if an item is visible
		self.visibleItem=function(item, index){
			var visible=true;
			if (self.o.search || self.o.filter) {
				var input=(self.e.cmb_search?self.e.cmb_search:self.e.cmb_input);
				if (input) {
					var search=input.value;
					if (search.length) {
						var filter=self.o.search || self.o.filter;
						var text="";
						switch (typeof(filter)) {
						case "function": // use custom function
							text=filter(self, item, index, search);
							break;
						case "string": // use custom field
							text=item[filter];
							break;
						case "boolean": // use item renderer (by default)
						default:
							text=self.renderItem(item, index);
							break;
						}
						// search text
						if (text === true || text === false) visible=text;
						else if (text && text.length && !self.hasText(text, search)) visible=false;
					}
				}
			}
			return visible;
		};

		// refresh combobox items
		self.refreshItems=function(){
			// clear items
			self.clear();
			// add empty item (if defined)
			if (self.o.empty && !self.o.multiple && !self.o.del) self.e.empty=self.add(null, null);
			// add items from array
			if (self.o.items) for (index in self.o.items) {
				var item=self.o.items[index];
				var nextindex=self.count();
				if (self.visibleItem(item, nextindex)) self.add(item, nextindex);
			}
			// add AJAX items
			if (self.o.ajaxdata && self.o.ajaxdata.data) {
				for (index in self.o.ajaxdata.data) {
					var item=self.o.ajaxdata.data[index];
					var nextindex=self.count();
					self.add(item, null);
				}
			}
			// select keys again
			if (self.o.key && self.o.selectedkeys) self.keys(array_keys(self.o.selectedkeys));
		};

		// natural search
		self.naturalSearch=function(text){
			return text
				.toLowerCase()
				.replace(/á/g,"a")
				.replace(/é/g,"e")
				.replace(/í/g,"i")
				.replace(/ó/g,"o")
				.replace(/ú/g,"u")
				.replace(/Á/g,"a")
				.replace(/É/g,"e")
				.replace(/Í/g,"i")
				.replace(/Ó/g,"o")
				.replace(/Ú/g,"u")
			;
		};

		// check if a value has all words inside
		self.hasText=function(haystack, needle) {
			var haystack=self.naturalSearch(haystack);
			var search=self.naturalSearch(needle).split(" ");
			var found=true;
			for (var i in search)
				if (haystack.search(new RegExp(search[i], "i")) == -1)
					return false;
			return true;
		};

		// refresh combo elements
		self.refresh=function(){

			// main element
			if (self.o.editable) {

				// caption input
				self.e.cmb_input=newElement("input", {
					"class":"cmb_input",
					"attributes":{
						"type":"text",
						"placeholder":"",
						"value":(isset(self.o.value)?self.o.value:"")
					},
					"events":{
						"focus":function(){
							classAdd(self.e.cmb_group, "cmb_focus");
						},
						"input":function(){
							// combo items
							if (!self.o.multiple) self.unselect();
							self.open();
							self.refreshItems();
							self.updateTimed();
						},
						"blur":function(){
							classDel(self.e.cmb_group, "cmb_focus");
						}
					}
				});

			} else {

				// caption container
				self.e.cmb_caption=newElement("span", {
					"class":"cmb_caption"
				});

				// combo container
				self.e.cmb_combo=newElement("div", {
					"class":"cmb_combo",
					"html":"&nbsp;",
					"childs":[self.e.cmb_caption]
				});

			}

			// search input
			self.e.cmb_search=(self.o.search?newElement("input", {
				"class":"cmb_search",
				"attributes":{
					"type":"text",
					"placeholder":(typeof(self.o.search) == "object" && self.o.search.placeholder?self.o.search.placeholder:"..."),
					"value":""
				},
				"events":{
					"click":function(e){
						e.preventDefault();
					},
					"dblclick":function(e){
						this.value="";
						self.refreshItems();
						self.updateTimed();
					},
					"focus":function(e){
						this.select();
						e.preventDefault();
					},
					"input":function(){
						self.refreshItems();
						self.updateTimed();
					},
					"blur":function(e){
						//e.open();
					},
					"keydown":function(e){
						switch (e.keyCode) {
						case 9: // tab
							self.open();
							self.focusSelectedItem();
							e.preventDefault();
							break;

						case 13: // enter
							if (self.count()) {
								self.select(self.indexFirst());
							}
							// no break
						case 27: // escape
							self.focus();
							self.close();
							break;

						case 33: // page up
						case 38: // up
							if (self.o.valignTop) {
								self.open();
								if (!self.focusSelectedItem()) self.focusLastItem();
							} else {
								self.focus();
								self.close();
							}
							e.stopPropagation();
							e.preventDefault();
							break;

						case 34: // page down
						case 40: // down
							if (self.o.valignTop) {
								self.focus();
								self.close();
							} else {
								self.open();
								if (!self.focusSelectedItem()) self.focusFirstItem();
							}
							e.stopPropagation();
							e.preventDefault();
							break;

						}
					}
				}
			}):null);

			// combobox
			self.e.cmb=newElement("div", {
				"class":"cmb",
				"childs":[(self.e.cmb_input?self.e.cmb_input:self.e.cmb_combo)],
				"events":{
					"mousedown":function(e){
						if (self.e.cmb_input) {
							if (!self.opened) {
								self.update();
								self.open();
							}
						} else {
							self.swap(true);
							if (self.opened) {
								self.update();
								if (!self.focusSearch())
									self.focus();
							}
							e.preventDefault();
						}
					}
				}
			});

			// set disabled attribute
			if (self.o.disabled) self.e.cmb.setAttribute("disabled", "");
			else self.e.cmb.removeAttribute("disabled");

			// set readonly attribute
			if (self.o.readonly) self.e.cmb.setAttribute("readonly", "");
			else self.e.cmb.removeAttribute("readonly");

			// items
			self.e.cmb_items=newElement("div", {
				"class":"cmb_items",
				"html":self.o.html,
				"events":{
					"mousedown":function(e){
						e.preventDefault();
					}
				}
			});

			// options
			self.e.cmb_options=newElement("div", {
				"class":"cmb_options",
				"childs":[
					self.e.cmb_search,
					self.e.cmb_items
				],
				"events":{
					"mousedown":function(e){
						//e.preventDefault();
					}
				}
			});

			// dropdown
			self.e.cmb_dropdown=newElement("div", {
				"class":"cmb_dropdown",
				"childs":[self.e.cmb_options],
				"events":{
					"mousedown":function(e){
						//e.preventDefault();
					}
				}
			});

			// fill items
			self.refreshItems();

			// general group for elements
			self.e.group=newElement("div", {
				"class":"group",
				"childs":[self.e.cmb]
			});

			// actions
			self.e.actions=[];
			if (!self.o.actions) self.o.actions=[];

			// add action
			if (self.o.add) self.o.actions.push(array_merge({
				"class":"cmd cmd_add",
				"html":"+",
				"action":function(self, action, index){
					if (self.o.disabled || self.o.readonly) return;
					if (isset(self.o.onadd)) self.o.onadd(self, action, index);
					else if (typeof(self.o.add) == "function") self.o.add(self, action, index);
				}
			}, (typeof(self.o.add) !== "object"?{}:self.o.add)));

			// delete action
			if (self.o.del) self.o.actions.push(array_merge({
				"class":"cmd cmd_del",
				"html":"⨯",
				"action":function(self, action, index){
					if (self.o.disabled || self.o.readonly) return;
					self.unselect();
					self.refreshCaption();
					self.focus();
					if (isset(self.o.ondel)) self.o.ondel(self, action, index);
					else if (typeof(self.o.del) == "function") self.o.del(self, action, index);
				}
			}, (typeof(self.o.del) !== "object"?{}:self.o.del)));

			// create actions
			if (self.o.actions) for (var i in self.o.actions) {
				(function(action, index){
					var a=array_copy(action);
					a["class"]=(isset(action["class"])?action["class"]:"cmd");
					delete a["action"];
					self.e.actions[index]=newElement("button", array_merge({
						"attributes":{
							"tabindex":(self.o.tabindex?self.o.tabindex+index+1:0),
						},
						"properties":{
							"onclick":function(){
								if (action.action) action.action(self, action, index);
							}
						}
					}, a));
					self.e.group.appendChild(self.e.actions[index]);
				})(self.o.actions[i], i);
			}

			// combobox group
			self.e.cmb_group=newElement("div", {
				"class":"cmb_group"
					+(self.o.checkboxes?" cmb_checkboxes":"")
					+(self.o.radios?" cmb_radios":"")
					+(self.o.class?" "+self.o.class:"")
				,
				"attributes":{
					"tabindex":(self.o.editable?"-1":(self.o.tabindex?self.o.tabindex:"0"))
				},
				"events":{
					"focus":function(e){
						//console.log("focus");
					},
					"focusin":function(e){
						//console.log("focusin "+e.target);
						if (self.opened) self.open();
					},
					"focusout":function(e){
						//console.log("focusout "+e.target);
						self.close();
					},
					"mousedown":function(e){
					},
					"keydown":function(e){
						var index=self.focusedItem();
						switch (e.keyCode) {
						case 9: // tab
							if (self.opened) {
								self.focus();
								self.close();
								e.preventDefault();
							}
							break;

						case 27: // escape
							self.focus();
							self.close();
							break;

						case 38: // up
							if (self.o.valignTop) {
								self.open();
								if (index === false) {
									if (!self.focusSearch()) // if search enabled, focus search
										if (!self.focusSelectedItem()) // else, focus selected item
											self.focusLastItem(); // else, first item
								} else self.focusPrevItem(index);
							} else {
								if (index === false) {
									self.open();
									if (!self.focusSelectedItem())
										self.focusFirstItem();
								} else {
									if (index !== self.indexFirst()) {
										self.focusPrevItem(index);
									} else {
										if (self.e.cmb_search) {
											self.focusSearch();
										} else {
											self.close();
											self.focus();
										}
									}
								}
							}
							e.preventDefault();
							break;

						case 40: // down
							if (self.o.valignTop) {
								if (index === false) {
									self.open();
									if (!self.focusSelectedItem())
										self.focusLastItem();
								} else {
									if (index != self.indexLast()) {
										self.focusNextItem(index);
									} else {
										if (self.e.cmb_search) {
											self.focusSearch();
										} else {
											self.close();
											self.focus();
										}
									}
								}
							} else {
								if (!self.opened) self.open();
								if (index === false) {
									if (!self.focusSearch()) // if search enabled, focus search
										if (!self.focusSelectedItem()) // else, focus selected item
											self.focusFirstItem(); // else, first item
								} else self.focusNextItem(index);
							}
							e.preventDefault();
							break;

						case 36: // home 
							if (index !== false) self.focusFirstItem();
							break;

						case 35: // end
							if (index !== false) self.focusLastItem();
							break;

						case 33: // page up
							if (index !== false) for (var i=0; i<self.pageSize(); i++) self.focusPrevItem();
							e.preventDefault();
							break;

						case 34: // page down
							if (index !== false) for (var i=0; i<self.pageSize(); i++) self.focusNextItem();
							e.preventDefault();
							break;

						}
					}
				},
				"childs":[self.e.group, self.e.cmb_dropdown]
			});

			// window/combo resize event
			if (typeof(ResizeObserver) == "function") self.resizeobserver=new ResizeObserver(self.resize).observe(self.e.cmb);
			if (typeof(window.addEventListener) == "function") window.addEventListener("resize", self.resize);

			// add all to the combo container
			classAdd(self.o.id, "cmb_container");
			gidset(self.o.id, "");
			gid(self.o.id).appendChild(self.e.cmb_group);

			// refresh initial caption
			self.refreshCaption();

		};

		// scroll top relative to parent
		self.parentScrollTop=function(){
			return (self.o.parent && gid(self.o.parent)?gid(self.o.parent).scrollTop:scrollTop());
		};

		// resize event
		self.resize=function(){

			// checks
			if (!gid(self.o.id)) return false;
			var parent=(self.o.parent && gid(self.o.parent)?self.o.parent:false);

			// initial setup
			var maxWidth=(parent?getLeft(parent)+getWidth(parent):windowWidth());
			var maxHeight=(parent?getTop(parent)+getHeight(parent):windowHeight());
			if (maxWidth > windowWidth()) maxWidth=windowWidth();
			if (maxHeight > windowHeight()) maxHeight=windowHeight();

			// calculate best alignment
			var alignRight, valignTop;
			switch (self.o.align) {
			case "left": alignRight=false; break;
			case "right": alignRight=true; break;
			default: alignRight=(getLeft(self.o.id) > (maxWidth / 2)); // calcular
			}
			switch (self.o.valign) {
			case "top": valignTop=true; break;
			case "bottom": valignTop=false; break;
			default: valignTop=((getTop(self.o.id) - self.parentScrollTop()) > (maxHeight / 2)); // calcular
			}

			// set class alignments
			classEnable(self.o.id, "cmb_align_left",    !alignRight);
			classEnable(self.o.id, "cmb_align_right",    alignRight);
			classEnable(self.o.id, "cmb_valign_top",     valignTop);
			classEnable(self.o.id, "cmb_valign_bottom", !valignTop);

			// calculate dimensions to optimize selection
			var margin=(isset(self.o.margin)?parseInt(self.o.margin):20);
			var inputWidth=getWidth(self.e.cmb);
			var inputHeight=getHeight(self.e.cmb);
			var resultsWidth=(alignRight
				?getLeft(self.e.cmb)+inputWidth-margin
				:maxWidth-margin-getLeft(self.e.cmb)
			);
			var resultsHeight=-inputHeight-margin;
			if (valignTop) {
				var bigger=(!parent || self.parentScrollTop() > getTop(parent)?self.parentScrollTop():getTop(parent));
				resultsHeight+=(getTop(self.e.cmb)-bigger);
			} else {
				resultsHeight+=maxHeight-(getTop(self.e.cmb)-self.parentScrollTop());
			}

			// set sizes
			self.e.cmb_options.style.minWidth=inputWidth+"px";
			self.e.cmb_options.style.maxWidth =parseInt(resultsWidth  < inputWidth ?inputWidth :resultsWidth )+"px";
			self.e.cmb_options.style.maxHeight=parseInt(resultsHeight < inputHeight?inputHeight*2:resultsHeight)+"px";

			// save alignments
			self.o.alignRight=alignRight;
			self.o.valignTop=valignTop;

			// ok
			return true;

		};

		// get/set key name
		self.key=function(key){
			if (isset(key)) self.o.key=key;
			return self.o.key;
		}

		// get/set selected keys
		self.keys=function(keys){
			if (!self.o.key) return false;
			if (isset(keys)) {
				if (keys instanceof Array) {
					for (var i in keys)
						self.keys(keys[i]);
				} else if (isset(keys) || (keys === null && self.o.empty)) {
					var index=self.o.keyindex[keys];
					if (!self.isselected(index)) self.select(index);
				}
			}
			var a=[];
			if (self.o.selected)
				for (var index in self.o.selected)
					if (self.o.selected[index])
						a.push(self.o.selected[index][self.o.key]);
			return a;
		};

		// get/set selected index/indexes
		self.index=function(indexes){
			if (isset(indexes)) {
				if (indexes instanceof Array) {
					for (var i in indexes)
						if (!isNaN(indexes[i]))
							self.select(indexes[i]);
				} else if (!isNaN(indexes)) {
					if (!self.isselected(indexes)) self.select(indexes);
				}
			}
			var a=[];
			if (self.o.multiple) {
				if (self.o.selected)
					for (var index in self.o.selected)
						if (self.o.selected[index])
							a.push(index);
			} else {
				a=self.o.selectedindex;
			}
			return a;
		};

		// get/set single/multiple
		self.multiple=function(multiple){
			if (isset(multiple)) {
				if (self.o.multiple != multiple) {
					self.o.multiple=multiple;
					if (!multiple) {
						var lastindex=self.o.selectedindex;
						var lastselected=array_count(self.o.selected);
						self.unselect();
						self.refreshItems();
						if (lastselected && isset(lastindex)) self.select(1+parseInt(lastindex));
						else if (self.o.empty) self.select(0);
					} else {
						self.refreshItems();
					}
					self.refreshCaption();
				}
			}
			return self.o.multiple;
		};

		// get available options
		self.options=function(){
			return self.o.options;
		};

		// get selected options
		self.selected=function() {
			return (self.o.multiple
				?(self.o.key?self.o.selecteditems:self.o.selected)
				:self.o.selecteditem
			);
		};

		// get/set item
		self.item=function(item){
			if (isset(item) && item !== null) {
				self.o.selecteditem=item;
				self.o.selected=[item];
				self.o.selectedkeys={};
				if (self.o.key) self.o.selectedkeys[item[self.o.key]]=item;
				self.refreshCaption();
				if (self.o.onchange) self.o.onchange(self, item);
				if (self.o.input) gidval(self.o.input, self.value());
			}
			return self.o.selecteditem;
		};

		// get/set input value or get selected keys/indexes
		self.value=function(value){
			if (self.e.cmb_input) {
				if (isset(value)) self.e.cmb_input.value=value;
				return self.e.cmb_input.value;
			} else {
				if (isset(value)) self.keys(value);
				var iks=self.keys();
				if (iks) {
					if (self.o.multiple) {
						return iks;
					} else {
						if (iks[0]) return iks[0];
					}
				}
			}
			return null;
		};

		// get selected items with its key/set with its keys/indexes
		self.values=function(values) {
			if (isset(values)) {
				if (self.o.key) self.keys(values);
				else self.index(values);
			}
			var a={};
			var selected=(self.o.key?self.o.selecteditems:self.o.selected);
			for (var index in selected) a[(self.o.key?selected[index][self.o.key]:index)]=selected[index];
			return a;
		};

		// timed update clear timer
		self.updateTimerClear=function(){
			if (self.updateTimer) {
				clearTimeout(self.updateTimer);
				delete self.updateTimer;
			}
		};

		// timed update
		self.updateTimed=function(noothercheck){
			self.updateTimerClear();
			self.updateTimer=setTimeout(function(){
				self.update();
			}, 250);
		};

		// get/set additional AJAX data
		self.data=function(d){
			if (d) self.o.data=d;
			return self.o.data;
		};

		// get/set search value
		self.search=function(s){
			var search_input=(self.e.cmb_search?self.e.cmb_search:(self.e.cmb_input?self.e.cmb_input:false));
			if (isset(s)) {
				search_input.value=s;
				self.refreshItems();
				self.update();
			}
			return search_input.value;
		};

		// update AJAX data
		self.update=function(o){
			var o=o||{};
			self.updateTimerClear();
			if (self.o.ajax) {
				var first_request=(self.o.requested?false:true);
				self.o.requested=true;
				var r={
					"search":(self.e.cmb_search?self.e.cmb_search.value:(self.e.cmb_input?self.e.cmb_input.value:"")),
					"visible":(self.o.visible?self.o.visible:50)
				};
				var er=self.data();
				if (er) r=array_merge(r, er);
				if (self.o.ajaxrequest) r=self.o.ajaxrequest(self, r);
				ajax(self.o.ajax, r, function(){}, function(r){
					if (r.data.err) newerror(r.data.err);
					if (r.data.ok) {
						// update ajax data
						self.o.ajaxdata=r.data;
						// refresh item list
						self.refreshItems();
						// if first request, autoselect item
						if (first_request) self.firstselect();
						// focus search, if requested
						if (o.focusSearch) self.focusSearch();
					}
				});
			}
		};

		// destroy
		self.destroy=function(){
			window.removeEventListener("resize", self.resize);
			if (self.resizeobserver) {
				self.resizeobserver.disconnect();
				delete self.resizeobserver;
			}
		};

		// aux: first item/index/key/value selection (called twice, on refresh and on update)
		self.firstselect=function(){
			if (self.o.item) {
				self.o.selecteditem=self.o.item;
				if (self.o.key) self.o.selectedkeys[self.o.item[self.o.key]]=self.o.item;
				if (self.e.cmb_caption) gidset(self.e.cmb_caption, self.renderItem(self.o.item));
			} else {
				if (isset(self.o.index)) self.index(self.o.index); else if (!self.o.editable && !self.o.multiple) self.index(0);
				if (isset(self.o.keys)) self.keys(self.o.keys);
				if (isset(self.o.value)) self.value(self.o.value);
			}
		};

		// startup
		self.init=function(o){
			if (self.o) self.destroy();
			self.e={"items":{}};
			self.o=array_copy(o);
			if (self.o.ajaxitem) self.o.ajaxitems=[self.o.ajaxitem];
			if (self.o.ajaxitems) self.o.ajaxdata={"data":self.o.ajaxitems};
			self.o.html=(isset(self.o.html)?self.o.html:gidget(self.o.id));
			self.unselect();
			self.o.multiple=self.o.multiple || false;
			self.o.emptyCaption="<div class='cmb_caption_empty'>"+(typeof(self.o.empty) == "string"?self.o.empty:"- Seleccione -")+"</div>";
			self.refresh();
			if (!self.o.item && (isset(self.o.index) || isset(self.o.keys) || isset(self.o.value))) self.update(); // if no item or no index/key/value is provided, no update is needed
			else self.o.requested=true; // if not, mark as requested, to prevent automatic item selection
			self.firstselect();
			// register self
			xwidgets.widgets[self.o.id]=self;
			if (o.focus) self.focus();
		};

		// automatic startup
		self.init(o);

	}

};


/** kernel.js **/
var kernel={


	// acción AJAX general
	"ajax":function(action, data, process, always){
		kernel.wait(true);
		ajax(action, data, function(){
			newwait_close();
			kernel.wait(false);
			if (always) always();
		},function(r){
			if (!process(r.data)) {
				if (r.data.err) newerror(r.data.err);
			}
		});
	},

	// iconos
	"icon":function(icon){
		return "<span class='fa fa-"+icon+"'></span>";
	},

	// mensaje de espera
	"wait":function(op){
		if (op && !newalert_open("wait")) {
			if (typeof(op) == "string") newwait(op);
			else newsimplewait();
		} else newwait_close();
	},

	// información de depuración
	"infodebug":function() {
		newalert({
			"ico":"images/ico48/ok.png",
			"title":"Información de depuración",
			"msg":gtemplate("rendering_time"),
			"buttons":[
				{"caption":"Cerrar","ico":"images/ico16/oculto.png"}
			]
		});
	},

	// aux: strip_tags (original by Kevin van Zonneveld - http://kevin.vanzonneveld.net)
	"strip_tags":function(input, allowed) {
	  allowed = (((allowed || "") + "").toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join(''); // making sure the allowed arg is a string containing only tags in lowercase (<a><b><c>)
	  var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi,
	    commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi;
	  return input.replace(commentsAndPhpTags, '').replace(tags, function ($0, $1) {
	    return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : '';
	  });
	},

	// getComputedStyle
	"getComputedStyle":function(id, property, where){
		var where=where||null;
		if (window.getComputedStyle
			&& window.getComputedStyle(gid(id), where)
		)
			return window.getComputedStyle(gid(id), where).getPropertyValue(property);
		return false;
	},
	
	// getMatchedStyle gets a CSS property value: thanks to Qtax on StackOverflow
	"getMatchedStyle":function(id, property){
		var elem=gid(id);
		if (!elem.style.getPropertyValue) return;
		var val=elem.style.getPropertyValue(property);
		if (elem.style.getPropertyPriority(property)) return val;
		if (!window.getMatchedCSSRules) return getStyle(elem, property);
		var rules=window.getMatchedCSSRules(elem);
		for (var i=rules.length;i-->0;) {
			var r=rules[i];
			var important=r.style.getPropertyPriority(property);
			if (val == null || important) {
				val=r.style.getPropertyValue(property);
				if (important) break;
			}
		}
		return val;
	},

	// URL anchor functions
	"anchor":{

		// get URL anchor
		"get":function(){
			var url=location.href;
			var p=url.indexOf("#");
			return (p != -1?url.substring(p+1):"");
		},

		// set URL anchor replacing state
		"set":function(anchor){
			var anchor=anchor||"";
			var url=location.href;
			var p=url.indexOf("#");
			if (p != -1) url=url.substring(0, p);
			history.replaceState({}, window.title, url+(anchor.length?"#"+anchor:""));
		},

		// navigate to anchor
		"navigate":function(anchor){
			var anchor=anchor||"";
			var url=location.href;
			var p=url.indexOf("#");
			if (p != -1) url=url.substring(0, p);
			var url=url+"#"+(anchor.length?anchor:"");
			if (location.href != url) {
				location.href=url;
				if (!anchor.length) kernel.anchor.set("");
			}
		}

	},

	// URL anchor functions
	"url":{

		// add URL state
		"add":function(url){
			history.pushState({}, null, url);
		},

		// get URL
		"get":function(){
			return location.href;
		},

		// set URL replacing state
		"set":function(url){
			history.replaceState({}, window.title, url);
		},

		// navigate to URL
		"navigate":function(url){
			location.href=url;
		}

	},

	// autenticación
	"auth":{
		
		// autorización
		"wait":function(visible){
			if (visible) {
				newwait({
					"id":"auth_wait",
					"ico":"images/ico48/secure.png",
					"msg":"Preparando inicio de sesión..."
				});
			} else {
				newalert_close("auth_wait");
			}
		},
		"close":function(){
			if (data.auth_type=="CAS") {
				newwait({
					"ico":"images/ico48/secure.png",
					"msg":"Cerrando sesión SSO de <b>"+data.user.email+"</b>..."
				});
				kernel.auth._close_iframe=document.createElement("iframe");
				style(kernel.auth._close_iframe,{
					"position":"fixed",
					"display":"block",
					"top":"0px",
					"left":"0px",
					"width":"1px",
					"height":"1px",
					"visibility":"hidden"
				});
				//style(kernel.auth._close_iframe, {"width":"400px","height":"300px","visibility":"visible"});
				document.body.appendChild(kernel.auth._close_iframe);
				kernel.auth._close_iframe.src=data.me+"?ajax=cas.logout";
				kernel.auth._close_iframe.setAttribute("onload","javascript:kernel.auth.closeIFrame();");
			} else {
				kernel.auth.finish();
			}
		},
		"closeIFrame":function(){
			if (kernel.auth._close_iframe) {
				kernel.auth._close_iframe.parentNode.removeChild(kernel.auth._close_iframe);
				delete kernel.auth._close_iframe;
				kernel.auth.finish();
			}
		},
		"finish":function(){
			newwait({
				"ico":"images/ico48/secure.png",
				"msg":"Cerrando sesión local de <b>"+data.user.email+"</b>..."
			});
			ajax({
				"url":"head.php",
				"ajax":"unauth",
				"async":function(r){
					if (r && r.data && r.data.ok) location.reload();
					else newerror("Ha ocurrido un error no especificado y no se ha podido cerrar sesión en este momento.<br /><br />Por favor, vuelva a intentarlo transcurridos unos segundos, y si el problema persiste notifíquelo a incidencias en Dumbo o llamando al 4222.");
				}
			});
		},
		
		// suplantación
		"suplantar":function(){
			newalert({
				"ico":"images/ico48/secure.png",
				"msg"
					:"<p>Usuario a suplantar:<br/>"
					+"<input id='suplantar_search' class='txt' type='text' value='' style='width:440px;' /></p>"
					+"<p>"
						+"<label class='checkbox'>"
							+"<input id='suplantar_replace' type='checkbox' /> <span>Sustituir acceso de usuario autenticado por el seleccionado</span>"
						+"</label>"
					+"</p>"
					+"<p id='suplantar_usuarios' style='width:440px;height:280px;overflow:auto;'></p>"
				,
				"buttons":[
					{"caption":"Cancelar","ico":"images/ico16/cancel.png"}
				]
			});
			gid("suplantar_search").onkeyup=function(){
				kernel.auth.suplantarSearchTimed(this.value);
			}
			kernel.auth.suplantarSearchTimed();
			gid("suplantar_search").focus();
		},
		"suplantarSearchTimed":function(search) {
			if (this.suplantarSearchTimer) clearTimeout(this.suplantarSearchTimer);
			this.suplantarSearchTimer=setTimeout(function(){ kernel.auth.suplantarSearch(search); },100);
		},
		"suplantarSearch":function(search){
			ajax("suplantar.search",{
				"search":search
			},function(){
				//newwait_close();
			},function(r){
				if (r.data.err) newerror(r.data.err);
				if (r.data.ok) {
					var h="";
					for (var i in r.data.personas) {
						var persona=r.data.personas[i];
						h+="<div><span class='a' onClick='javascript:kernel.auth.suplantarSelect("+persona.id+")'>"
									+persona.nombre+" "+persona.apellidos
									+(persona.email?" <span style='color:#888;'>("+persona.email+")</span>":"")
							+"</span></div>"
						;
					}
					gidset("suplantar_usuarios",h);
				}
			});
		},
		"suplantarSelect":function(usuario){
			kernel.auth.suplantarUsuario(usuario, null, gid("suplantar_replace").checked);
		},
		"suplantarUsuario":function(usuario, newurl, reemplazar) {
			newwait({"ico":"secure","msg":"Suplantando usuario..."});
			ajax("suplantar",{
				"usuario":usuario,
				"replace":(reemplazar?true:false)
			},function(){
				newwait_close();
			},function(r){
				if (r.data.err) newerror(r.data.err);
				if (r.data.ok) {
					if (newurl) {
						newwait("Redirigiendo...");
						location.href=newurl;
					} else {
						newwait("Actualizando página...");
						var i=location.href.indexOf("supplant=");
						if (i!=-1) location.href=location.href.substring(0,i-1);
						else location.reload();
					}
				}
			});
		},
		"suplantarUMtvNoticias":function(newurl) {
			newwait({"ico":"secure","msg":"Suplantando UMtv Noticias..."});
			ajax("suplantar.umtvnoticias",{},function(){
				newwait_close();
			},function(r){
				if (r.data.err) newerror(r.data.err);
				if (r.data.ok) {
					newwait("Actualizando página...");
					location.reload();
				}
			});
		}
		
	}, // auth
	
	// funciones de página
	"page":{
		
		"pageid":"xpage",
		"backid":"fondo",
		
		"tdebug":function(msg){
			if (!kernel.page._tdebug_div) {
				kernel.page._tdebug_div=document.createElement("div");
				style(kernel.page._tdebug_div, {"left":"0px","top":"0px","position":"fixed","display":"block","zIndex":"999999","background":"#F44","color":"#FFF","padding":"9px","fontSize":"1.2em"});
				document.body.appendChild(kernel.page._tdebug_div);
			}
			gidset(kernel.page._tdebug_div, msg);
		},
		
		"isMobileCSS":function(){
			if (!kernel.page._ismobile_div) {
				kernel.page._ismobile_div=document.createElement("div");
				kernel.page._ismobile_div.className='ismobile';
				document.body.appendChild(kernel.page._ismobile_div);
			}
			var res=kernel.getComputedStyle(kernel.page._ismobile_div, "content", ":after");
			//kernel.page.tdebug("TEST:"+res);
			return (res.indexOf("mobile")!=-1?true:false);
		},
		
		// scroller con animación
		"scroller":{
			"images":[],
			"actual":0,
			"interval":4000,
			"animationInterval":1500,
			"animationFastInterval":300,
			"recheckInterval":250,
			"pause":function(){
				clearTimeout(kernel.page.scroller.timerNext);
				kernel.page.scroller.timerNext=false;
			},
			"resume":function(){
				kernel.page.scroller.timedNext();
			},
			"onSlide":function(action){
				kernel.page.scroller.onSlideAction=action;
			},
			"onSlideEvent":function(o){
				if (!o) var o={"next":kernel.page.scroller.actual};
				if (kernel.page.scroller.onSlideAction) {
					o.image=kernel.page.scroller.images[o.next];
					o.images=kernel.page.scroller.images;
					kernel.page.scroller.onSlideAction(o);
				}
			},
			"setActual":function(actual){
				kernel.page.scroller.pause();
				kernel.page.scroller.actual=actual % kernel.page.scroller.images.length;
				kernel.page.scroller.resume();
				kernel.page.scroller.refresh();
				kernel.page.scroller.onSlideEvent();
			},
			"setLoaded":function(){
				var index=parseInt(this.getAttribute("data-index"));
				kernel.page.scroller.images[index].loaded=true;
				kernel.page.scroller.refresh();
			},
			"setImages":function(images){
				kernel.page.scroller.images=[];
				for (var i in images) {
					var image=images[i];
					var img=new Image();
					img.src=images[i].src;
					img.setAttribute("data-index", kernel.page.scroller.images.length);
					image.img=img;
					image.loaded=false;
					kernel.page.scroller.images.push(image);
					img.onload=kernel.page.scroller.setLoaded;
				}
				kernel.page.refresh();
			},
			"setBackground":function(o){
				//if (!kernel.page.scroller.images) return;
				var o=o||{};
				var actual=kernel.page.scroller.images[o.index];
				var iw=actual.img.width;
				var ih=actual.img.height;
				if (iw && ih) {
					var mobile=kernel.page.isMobileCSS();
					var t=getTop(kernel.page.backid);
					var w=getWidth(kernel.page.backid);
					var h=getHeight(kernel.page.backid);
					var rih=w*ih/iw;
					var cexcess=(rih-h)/2;
					var p=scrollTop()/(h+t);
					if (p<0) p=0; if (p>1) p=1;
					var d=parseInt(h*p)-cexcess;
					style(o.id, {
						"backgroundImage":"url("+actual.img.src+")",
						"opacity":(mobile?1:1-p),
					});
					var fixed=(mobile || cexcess<0);
					style(o.id, {
						"backgroundImage":"url("+actual.img.src+")",
						"backgroundPosition":(fixed?"center center":"0px "+(mobile?0:d)+"px"),
						//"opacity":(mobile?1:(1-p*0.5))*o.opacity,
						"opacity":(mobile?1:o.opacity),
						"backgroundSize":(fixed?"cover":"100%")
					});
				}
			},
			"scroll":function(){
				if (kernel.page.backid && gid(kernel.page.backid)) {
					kernel.page.scroller.setBackground({
						"id":kernel.page.backid,
						"index":kernel.page.scroller.actual,
						"opacity":1
					});
					kernel.page.scroller.animateRefresh();
				}
				if (scrollTop()) classAdd(kernel.page.pageid, "xpage_scrolled");
				else classDel(kernel.page.pageid, "xpage_scrolled");
			},
			"refresh":function(){
				kernel.page.scroller.scroll();
			},
			"animateRefresh":function(){
				if (!kernel.page.scroller.animation) return;
				var p=((new Date())-kernel.page.scroller.animation.started) / kernel.page.scroller.animation.interval;
				if (p>=1) p=1;
				kernel.page.scroller.setBackground({
					"id":kernel.page.scroller.animation.div,
					"index":kernel.page.scroller.animation.index,
					"opacity":(1-((1-p)*(1-p)*(1-p)))
				});
				if (p>=1) kernel.page.scroller.animateStop();
			},
			"animateStop":function(){
				if (kernel.page.scroller.animation) {
					kernel.page.scroller.actual=kernel.page.scroller.animation.index;
					giddel(kernel.page.scroller.animation.div);
					delete kernel.page.scroller.animation;
					kernel.page.scroller.refresh();
					kernel.page.scroller.timedNext();
				}
			},
			"animate":function(o){
				if (!kernel.page.scroller.animation) {
					kernel.page.scroller.onSlideEvent({"next":o.index});
					kernel.page.scroller.animation={
						"index":o.index,
						"started":new Date(),
						"interval":(o.interval?o.interval:kernel.page.scroller.animationInterval)
					};
					var next=kernel.page.scroller.images[o.index];
					kernel.page.scroller.animation.div=document.createElement("div");
					style(kernel.page.scroller.animation.div,{
						"position":"absolute",
						"top":"0px",
						"bottom":"0px",
						"left":"0px",
						"right":"0px",
						"background":"url("+next.src+")"
					});
					gid(kernel.page.backid).appendChild(kernel.page.scroller.animation.div);
					kernel.page.scroller.refresh();
					kernel.page.scroller.timedRefresh();
				}
			},
			"timedNext":function(o){
				var o=o||{};
				if (kernel.page.scroller.timerNext) clearTimeout(kernel.page.scroller.timerNext);
				kernel.page.scroller.timerNext=setTimeout(function(){
					delete o.recheck;
					delete kernel.page.scroller.timerNext;
					if (!kernel.page.scroller.images) return;
					var next=(kernel.page.scroller.actual+1)%kernel.page.scroller.images.length;
					if (next!=kernel.page.scroller.actual) {
						if (!kernel.page.scroller.images || !kernel.page.scroller.images[next]) return;
						if (!kernel.page.scroller.images[next].loaded) {
							o.recheck=true;
							kernel.page.scroller.timedNext(o);
							return;
						}
						kernel.page.scroller.animate({"index":next});
					}
				}, (o.recheck?kernel.page.scroller.recheckInterval:kernel.page.scroller.interval));
			},
			"timedRefresh":function(){
				if (kernel.page.scroller.timerRefresh) clearTimeout(kernel.page.scroller.timerRefresh);
				kernel.page.scroller.timerRefresh=setTimeout(function(){
					delete kernel.page.scroller.timerRefresh;
					kernel.page.scroller.scroll();
					kernel.page.scroller.timedRefresh();
				}, (kernel.page.scroller.animation?1:kernel.page.scroller.recheckInterval));
			},
			"auto":function(){
				if (!kernel.page.backid && gid(kernel.page.backid)) return;
				var url=trim(kernel.getComputedStyle(gid(kernel.page.backid),"background-image"));
				if (url.substring(0,4)=="url(") {
					url=url.substring(4);
					if (url.substring(url.length-1)==")") url=url.substring(0, url.length-1);
				}
				url=url.replace(/\"/g,'');
				url=url.replace(/\'/,'');
				if (url) kernel.page.scroller.setImages([{"src":url}]);
			},
			"prev":function(){
				kernel.page.scroller.animateStop();
				var prev=(kernel.page.scroller.actual?kernel.page.scroller.actual-1:kernel.page.scroller.images.length-1);
				if (prev!=kernel.page.scroller.actual) {
					kernel.page.scroller.animate({
						"index":prev,
						"interval":kernel.page.scroller.animationFastInterval
					});
				}
			},
			"next":function(){
				kernel.page.scroller.animateStop();
				var next=(kernel.page.scroller.actual+1)%kernel.page.scroller.images.length;
				if (next!=kernel.page.scroller.actual) {
					kernel.page.scroller.animate({
						"index":next,
						"interval":kernel.page.scroller.animationFastInterval
					});
				}
			},
			"enableSwipe":function(){
				var
					thresholdX=100, // required min distance in X traveled to be considered swipe
					thresholdY=300, // required max distance in Y traveled to be considered swipe
					allowedTime=400, // maximum time allowed to travel that distance
					touchsurface=gid(kernel.page.backid),
					startX,
					startY,
					startTime
				;
				touchsurface.addEventListener('touchstart', function(e){
					var touchobj=e.changedTouches[0];
					startX=touchobj.pageX;
					startY=touchobj.pageY;
					startTime=new Date().getTime();
					//e.preventDefault();
				}, false);
				touchsurface.addEventListener('touchmove', function(e){
					//e.preventDefault()
				}, false);
				touchsurface.addEventListener('touchend', function(e){
					var touchobj=e.changedTouches[0];
					var dist=startX-touchobj.pageX;
					var elapsedTime=(new Date().getTime() - startTime);
					if (
						elapsedTime <= allowedTime
						&& Math.abs(dist) >= thresholdX
						&& Math.abs(touchobj.pageY - startY) <= thresholdY
					) {
						if (dist>0) kernel.page.scroller.next();
						else kernel.page.scroller.prev();
					}
					//e.preventDefault();
				}, false);
			},
			"init":function(){
				scroll(kernel.page.scroller.scroll);
				resize(kernel.page.scroller.scroll);
				kernel.page.scroller.timedNext();
				kernel.page.scroller.timedRefresh();
			}
		},

		// toolboxes
		"toolbox":{
			"isvisible":false,
			"toolboxes":{
				"info":{"menu":"info"},
				"buscar":{"menu":"buscar","div":"buscador","focus":"busqueda"},
				"usuario":{"menu":"usuario"}
			},
			"show":function(id){
				classAdd("menu_id_"+id,"toolbox_instant");
				classAdd("toolbox","toolbox_instant");
				kernel.page.toolbox.swap(id);
				setTimeout(function(){
					classDel("menu_id_"+id,"toolbox_instant");
					classDel("toolbox","toolbox_instant");
				},1);
			},
			"swap":function(id){
				var toolbox=kernel.page.toolbox.toolboxes[id];
				if (kernel.page.toolbox.isvisible) {
					var last=kernel.page.toolbox.toolboxes[kernel.page.toolbox.isvisible];
					classDel("toolbox","toolbox_visible");
					classDel("toolbox","toolbox_menu_"+last.menu);
					classDel("menu_id_"+last.menu,"toolbox_visible");
					classDel("menu_id_"+last.menu,"toolbox_menu_"+last.menu);
					hide(last.div);
				}
				if (kernel.page.toolbox.isvisible && kernel.page.toolbox.isvisible==id) {
					delete kernel.page.toolbox.isvisible;
				} else {
					kernel.page.toolbox.isvisible=id;
					classAdd("toolbox","toolbox_visible");
					classAdd("toolbox","toolbox_menu_"+toolbox.menu);
					classAdd("menu_id_"+toolbox.menu,"toolbox_visible");
					classAdd("menu_id_"+toolbox.menu,"toolbox_menu_"+toolbox.menu);
					show(toolbox.div);
					if (toolbox.focus) gidfocus(toolbox.focus);
				}
			}
		},

		// búsqueda
		"search":{
			"go":function(search){
				newwait("Buscando <b>"+htmlentities(search)+"</b>...");
				location.href=alink({"search":search},"videos");
			},
			"setInputTextEvents":function(input){
				var input=gid(input);
				if (!input.addEventListener) return false;
				input.addEventListener("keypress",function(e){
					var e=e||window.event;
					if (e.keyCode==13) kernel.page.search.go(this.value);
				},false);
			},
			"init":function(){
				kernel.page.search.setInputTextEvents("busqueda");
			}
		},
		"refresh":function(){
			kernel.page.scroller.refresh();
		},
		
		// gestión de idiomas
		"idiomas":{

			// crear una dataURL conteniendo una imágen generada por canvas del código de idioma especificado
			"icono":function(codigo) {
				if (!data.idiomas) return "";
				var idioma=data.idiomas[codigo];
				if (!idioma) return "";
				var canvas=document.createElement('canvas');
				canvas.width="18";
				canvas.height="12";
				if (canvas.getContext) {
					var ctx=canvas.getContext('2d');
					try { ctx.imageSmoothingEnabled=true; } catch(e) {
						try { ctx.mozImageSmoothingEnabled=true; } catch(e) {};
					};
					ctx.fillStyle="#"+idioma.color;
					ctx.fillRect(0,0,18,12);
					ctx.fillStyle="#FFF";
					ctx.textAlign="center";
					ctx.font="10px Arial";
					ctx.fillText(idioma.id.toUpperCase(),9,9);
					return canvas.toDataURL();
				}
				return "";
			}

		},

		// mensaje de cookies
		"cookiesEnabled":function(){
			return (getCookie("cookies_ok")?true:false);
		},
		"cookiesOk":function(){
			setCookie("cookies_ok","ok",30);
			var cmsg=gid("cookiesMessage");
			var bmsg=gid("cookiesMessageBody");
			var start=(new Date().getTime());
			var height=getHeight(cmsg);
			var updateCookieOpacity=function(){
				var actual=(new Date().getTime());
				var o=1-((actual-start)/750);
				o=o*o*o;
				if (o<=0) o=0;
				//bmsg.style.opacity=o;
				cmsg.style.height=parseInt(o*height)+"px";
				if (o) setTimeout(updateCookieOpacity,1);
				else cmsg.parentNode.removeChild(cmsg);
			};
			updateCookieOpacity();
		},
			
		// control de la orientación
		"orientation":{
			"isRotated":function(){
				if (window.orientation)
					if (screen.width>screen.height)
						if (Math.abs(window.orientation)==90)
							return true;
				return false;
			},
			"update":function(){
				var rotated=kernel.page.orientation.isRotated();
				classEnable("xpage", "xpage_rotated", rotated);
			},
			"init":function(){
				window.addEventListener("orientationchange", function(){
					kernel.page.orientation.update();
				}, false);
				kernel.page.orientation.update();
			}
		},
		
		// incializar página
		"init":function(){
			//delCookie("cookies_ok");
			kernel.page.orientation.init();
			kernel.page.search.init();
			kernel.page.scroller.init();
			kernel.page.refresh();
			if (data.menu_submenu) kernel.page.toolbox.show("info");
		}
		
	},

	// búsqueda de objeto multimedia
	"tvumSearchs":[],
	"tvumSearch":function(o) {
		var self=this;
		self.o=o;
		self.newalertid="tvum_search_"+index;
		self.res={};

		var index=kernel.tvumSearchs.length;
		kernel.tvumSearchs.push(self);

		self.close=function(){
			newalert_close(self.newalertid);
		};

		self.select=function(i){
			if (self.o.onselect)
				if (self.o.onselect(self.res[i])) {
					newalert_close(self.newalertid);
				}
		};

		self.search=function(fast){
			var search=gidval("tvum_search_txt");
			if (self.searchTimer) clearTimeout(self.searchTimer);
			self.searchTimer=setTimeout(function(){
				self.searchTimer=null;
				gidset("tvum_search_stat", "Buscando...");
				ajax("tvum.search",{"search":search,"callback":"kernel.tvumSearchs["+index+"].select"},function(){
					gidset("tvum_search_stat","OK");
				},function(r){
					if (r.data.ok) {
						self.res=r.data.res;
						gidset("tvum_search_stat","<b>"+r.data.num+"</b> resultados"+(r.data.limit<r.data.num?" (solo se muestran <b>"+r.data.limit+"</b>)":"")+".");
						gidset("tvum_search_res",r.data.html);
					}
				});
			}, (fast?1:300));
		};

		newalert({
			"id":self.newalertid,
			"title":"Seleccionar Objeto Multimedia",
			"msg"
				:"<div><table width='100%'><tr><td width='1'>Buscar:</td><td><input class='txt' id='tvum_search_txt' type='text' onKeyUp='javascript:kernel.tvumSearchs["+index+"].search();' style='width:99%;' /></td></tr></table></div>"
				+"<p id='tvum_search_stat' style='margin-top:2px;text-align:right;'>&nbsp;</p>"
				+"<div id='tvum_search_res' style='width:600px;height:400px;overflow:auto;'></div>"
			,
			"buttons":[
				{"caption":(self.o.onunset?"Quitar":null),"ico":"images/ico16/trash.png","action":function(id){
					newalert_close(id);
					if (self.o.onunset) self.o.onunset(self);
				}},
				{"caption":"Cancelar","ico":"images/ico16/cancel.png","action":function(id){
					newalert_close(id);
					if (self.o.oncancel) self.o.oncancel(self);
				}}
			]
		});

		self.search(true);
		gidfocus("tvum_search_txt");

	},

	// suscripción a un objeto multimedia
	"subscribeOM":function(om_id){
		var temp="suscripcion";
		var eventos=["om_general","om_videos","om_comentarios","om_ficheros"];
		newsimplewait();
		// consultar suscripciones
		ajax("subscribe.get",{
			"om":om_id
		},function(){
			newwait_close();
		},function(r){
			if (r.data.err) newerror(r.data.err);
			if (r.data.ok) {
				// diálogo de suscripciones
				newalert({
					"id":temp,
					"ico":"images/ico48/alarm.png",
					"title":"Modificar avisos"+(r.data.om.titulo?": "+r.data.om.titulo:""),
					"msg"
						:"<div>Por favor, selecciona los eventos a los que desee suscribirse:</div>"
						+"<ul style='list-style-type:none;'>"
						+"	<li><input id='suscripcion_om_general' type='checkbox' /> <label for='suscripcion_om_general' />Notificame de modificaciones: lugar, título, descripción, etc.</label></li>"
						+(r.data.om.directo
							?"		<li><input id='suscripcion_om_videos' type='checkbox' /> <label for='suscripcion_om_videos' />Notificame nuevos videos asociados a este directo</label></li>"
							:""
						)
						+"	<li><input id='suscripcion_om_comentarios' type='checkbox' /> <label for='suscripcion_om_comentarios' />Notificame de nuevos comentarios</label></li>"
						+"	<li><input id='suscripcion_om_ficheros' type='checkbox' /> <label for='suscripcion_om_ficheros' />Notificame de cambios en imágenes y/o ficheros adjuntos</label></li>"
						+"</ul>"
						+"<p style='width:480px;font-size:0.8em;'>"
						+"	<u>AVISO:</u> No utilice el sistema de suscripciones como su agenda de avisos absoluta, si no como"
						+"	complemento a ella, puesto que el envío de correo electrónico de notificación es susceptible a fallos de red"
						+"	o filtros de correo basura que pueden hacer mermar su fiabilidad, pero ajenos al sistema."
						+"</p>"
					,
					"buttons":[
						{"caption":"Aceptar","ico":"images/ico16/ok.png","action":function(){
							var notificar={};
							for (var i in eventos)
								notificar[eventos[i]]=(gid("suscripcion_"+eventos[i]) && gid("suscripcion_"+eventos[i]).checked);
							newwait("Suscribiéndose...");
							ajax("subscribe.update",{
								"om":om_id,
								"notificar":notificar
							},function(){
								newwait_close();
							},function(r){
								if (r.data.err) newerror(r.data.err);
								if (r.data.ok) {
									newwait({
										"id":"avisos_ok",
										"ico":"images/ico48/ok.png",
										"msg":"Avisos modificados correctamente."
									});
									newalert_close(temp);
									setTimeout(function(){ newalert_close("avisos_ok"); },1000);
								}
							});
						}},
						{"caption":"Cancelar","ico":"images/ico16/cancel.png"}
					]
				});
				// actualizar checkboxes
				for (var i in eventos)
					if (gid("suscripcion_"+eventos[i]))
						gid("suscripcion_"+eventos[i]).checked=(r.data.suscripciones[eventos[i]]?true:false);
			}
		});
	}
	
};


// funciones de videoayuda
var videoayuda={
	"temp":"videoayuda",
	"play":function(o) {
		var title=(o.title?o.title:"Videoayuda");
		if (!o.id) alert("Videoayuda no especificada en el parámetro id!");
		newalert({
			"id":videoayuda.temp,
			"title":title,
			"msg":"<div id='"+videoayuda.temp+"'>"+title+"</b>...</div>",
			"buttons":[
				{"caption":"Aceptar","ico":"images/ico16/ok.png","action":function(){
					videoayuda.end();
				}}
			]
		});
		swfobject.embedSWF("videoayudas/"+o.id+".swf", videoayuda.temp, "960", "598", "9.0.0");
	},
	"end":function(){
		newalert_close(videoayuda.temp);
		/*alert(1);
		tvum.stop(videoayuda.temp);
		alert(2);
		//tvum.flowplayer(videoayuda.temp).unload();
		tvum.flowplayer(videoayuda.temp).close();
		alert(3);
		var pruebas=function(){
			if (!this.contador) this.contador=1;
			else this.contador++;
			alert(tvum.flowplayer(videoayuda.temp).getState());
			setTimeout(pruebas,1000);
			if (this.contador<3) newalert_close(videoayuda.temp);
		};
		pruebas();*/
	}
};


/** extra/cuentaatras.js **/
// clase auxiliar para realizar y/o mostrar una cuenta atrás simple
function CuentaAtras(o) {
	var a=this;
	a.o=o;
	a.actualizar=function(){
		if (o.hasta) {
			if (o.hasta_evento && (new Date()>=o.hasta))
				o.hasta_evento(o);
			if (gid(o.id)) {
				var time=parseInt((new Date().getTime())/1000);
				var rest=parseInt(o.hasta.getTime()/1000)-time;
				if (rest>0) {
					var d=parseInt(rest/86400);
					var s=rest-d*86400;
					var h=parseInt(s/3600); s-=h*3600;
					var m=parseInt(s/60); s-=m*60;
					var html=(d>0?(d==1?d+" día ":d+" días "):"")+(d || h?h+"h ":"")+(d || h || m?m+"m ":"")+s+"s";
					if (gidget(o.id)!=html) gidset(o.id,html);
				} else {
					gidset(o.id,(o.hasta_texto?o.hasta_texto:"&nbsp;"));
				}
			}
			setTimeout(a.actualizar,500);
		}
	};
	a.actualizar();
}


