/**
 * Implementation of basically and commonly useful JS code.
 *
 * This file provides many commonly useful JS features like browser detection,
 * document tree traversal, dynamic style and JS code inclusion, reference
 * obfuscation, AnX-client (for JSON-based RPCs) and more ...
 *
 *
 * This file is distributed as part of toxA.CMS!
 *
 * @author Thomas Urban <soletan@toxa.de>
 * @package client-fx
 * @copyright 2005-2007, toxA IT-Dienstleistungen, Berlin
 *
 */


/**
 * Implementation of client-side AnX node.
 *
 * This class implements a client-side node for AnX-based communication with a
 * toxA.CMS-based server site.
 *
 * This class is tested to work with
 *   - MS Internet Explorer 6
 *   - MS Internet Explorer 7
 *   - Firefox 2.0
 *   - Opera 9.1
 *
 * It includes these protocols and features:
 *   - JSON
 *   - JSON RPC
 *
 * In addition to special support for AnX this class tries to provide common
 * support for Ajax-like client-side implementations and is prepared to include
 * other protocols like SOAP and URL-encoded form data. These features are
 * neither fully implemented nor tested, currently.
 *
 * @param mixed serverScript either URL of a script used to send RPC to or
 *              boolean value with these semantics: false selects HTTP-based
 *              connection to server while true prefers HTTPS-based connection,
 *              if server site is supporting proper base URL and thus supporting
 *              HTTPS at all.
 *
 *
 * @example

// first declare processor for result
function processorFunc( node )
{
	if ( node )
	{
		if ( !node.error )
			// call succeeded
			alert( "result is " + node.result );
		else
			alert( "error is " + node.error );
	}
}

// now call for remote procedure
var anx = new AnXRequest();
if ( !anx.Call( processorFunc, 'module:class::method',
				'arg1', 2, [ 'arg3.1', 'arg3.2' ] ) )
	alert( "failed to call" );

 */

function AnXRequest( serverScript )
{
	// constructor


	/*
	 * properties of class
	 */

	// public:
	this.result    = null;	// will contain result of call
	this.error     = null;	// will be set on error
	this.processor = null;	// optional function being called to process result

	// private:
	var node       = null;	// internally managed instance of XMLHttpRequest
	var mode       = 'json';// mode used to encode and transfer call data
	var baseURI    = null;	// URI to server's AnX peer to be used

	var wrapper    = this;	// can't use "this" in ResponseHandler() ...

	var used_id    = null;



	// select proper URL of script
	if ( typeof serverScript == 'string' )
		baseURI = serverScript;
	else
	{
		// derive from current context

		pos = location.pathname.indexOf( '/src/' );
		if ( pos > 0 )
			baseURI = location.pathname.substring( 0, pos );
		else
			baseURI = '';

		baseURI = location.protocol + '//' + location.host + baseURI +
				  '/src/anx.php';

	}



	/**
	 * Response handler for asynchronously processed requests.
	 */

	this.ResponseHandler = function()
	{

		if ( node.readyState != 4 )
			return;


		if ( node.status == 200 )
		{
			// server indicates success

			wrapper.result = null;
			wrapper.error  = null;

			// parse response depending on current mode
			switch ( mode )
			{

				case 'json' :
					var temp;

					try
					{
						temp = wrapper.FromJSON( node.responseText);
					}
					catch ( e )
					{
						temp = false;
					}

					if ( !temp || !temp.id )
						wrapper.error    = 'json';
					else
					{

						if ( !wrapper.CheckID( temp.id ) )
							wrapper.error = 'noid';
						else
						{
							wrapper.error  = temp.error;
							wrapper.result = temp.result;
						}

					}
					break;

				case 'soap' :
					wrapper.result = node.responseXML;
					break;

				default :
					wrapper.error = 'format';

			}
		}
		else
			wrapper.error = node.status;


		/*
		 * check for optionally selected processor function and call
		 * if set, otherwise write any error to window's status bar
		 */
		if ( typeof wrapper.processor == 'function' )
			wrapper.processor( wrapper );
		else
			if ( wrapper.error )
				window.status = "AJAX request failed (" +
								wrapper.error + ")";

	}



	/**
	 * Get internally managed request node.
	 *
	 * This method automatically creates a new node, if this is the first
	 * call for retrieving internal node, any previous call failed or a
	 * previously created node was dropped using DropNode().
	 */

	this.GetNode = function()
	{

		if ( !node )
		{

			// create XMLHttpRequest instance depending on current browser
			if ( window.XMLHttpRequest )
			{
				// browser has native support for XMLHttpRequest

				node = new XMLHttpRequest();

			}
			else if ( window.ActiveXObject )
			{
				// browser supports XMLHttpRequest through ActiveX, only

				try
				{
					node = new ActiveXObject( 'Msxml2.XMLHTTP' );
				}
				catch ( e )
				{
					try
					{
						node = new ActiveXObject( 'Microsoft.XMLHTTP' );
					}
					catch ( e )
					{
						node = null;
					}
				}
			}

			if ( !node )
			{
				this.error = 'no-ajax';
				return false;
			}


			node.onreadystatechange = this.ResponseHandler;

		}


		return node;

	}


	/**
	 * Sends request to server script.
	 *
	 * Any optionally provided query and data is included on request.
	 *
	 */

	this.Request = function( query, data )
	{

		if ( !node )
			// internally managed node wasn't prepared before --> prepare now
			this.GetNode();



		// select optimum request method depending on whether
		// data is provided to be included with request or not
		if ( data && data.length > 0 )
			method = 'POST';
		else
		{
			method = 'GET';
			data   = null;
		}

		// if caller provided query, include separating ampersand
		if ( query && ( query.length > 0 ) )
			query = '&' + query;

		// compile URL open URL of peer script
		var url = baseURI + "?ism=off&m=" + mode + query



		// open asynchronous connection to server
		node.open( method, url, true );

		// customize request as required for current mode
		switch ( mode )
		{

			case 'json' :
				node.setRequestHeader( "Content-Type", "application/json" );
				break;

			case 'soap' :
//				node.setRequestHeader( "Content-Type", "text/xml" );
				this.error = "no-soap";
				return false;

			default :
				node.setRequestHeader( "Content-Type",
											"application/x-www-url-encoded" );

		}

		// perform request
		node.send( data );


		return node;

	}


	/**
	 * Drops internally managed request node and releases any related memory.
	 */

	this.DropNode = function()
	{
		delete node;
		node = null;
	}



	/**
	 * Selects JSON mode.
	 *
	 * JSON-mode is selected by default.
	 */

	this.JSON = function()
	{
		mode = 'json';
	}

	/**
	 * Selects SOAP mode.
	 */

	this.SOAP = function()
	{
		mode = 'soap';
	}

	/**
	 * Retrieves currently selected mode.
	 */

	this.GetMode = function()
	{
		return mode;
	}


	/**
	 * Retrieves URI of server script to be used.
	 */

	this.GetBaseURI = function()
	{
		return baseURI;
	}


	/**
	 * Adds provided ID to list of expected response IDs.
	 */

	this.SetID = function( id )
	{

		if ( !used_id )
			used_id = new Array;

		used_id.push( id );

	}


	/**
	 * Checks if provided ID is listed to be expected.
	 *
	 * If the ID is found on internally managed list, it is removed from list
	 * and method returns true. Otherwise it returns false.
	 */

	this.CheckID = function( id )
	{

		if ( used_id && ( used_id.length > 0 ) )
			for ( var i = 0; i < used_id.length; i++ )
				if ( used_id[i] == id )
				{

					// remove found ID from list
					used_id.splice( i, 1 );

					return true;

				}


		return false;

	}
}

/**
 * Converts provided data object into its JSON-representation.
 *
 * @param mixed any object, array or scalar value
 * @return string the JSON-representation of provided value, false on error
 */

AnXRequest.prototype.ToJSON = function( data )
{

	switch ( typeof data )
	{

		case 'boolean' :
			return data ? 'true' : 'false';

		case 'string' :
			data = data.replace( /\\/, '\\\\' );
			data = data.replace( /"/, '\\"' );		// '
			data = data.replace( /\n/, '\\n' );
			data = data.replace( /\r/, '\\r' );
			data = data.replace( /\t/, '\\t' );
			data = data.replace( /\f/, '\\f' );
			data = data.replace( /\x08/, '\\b' );
			return '"' + data + '"';

		case 'number' :
			return String( data );

		case 'object' :
			var i, name;
			var assoc = false;
			var out   = '';


			for ( i in data )
				if ( typeof i != 'number' )
					if ( i.search( /^\d+$/ ) < 0 )
					{
						assoc = true;
						break;
					}

			for ( i in data )
			{

				if ( assoc )
				{

					if ( i.search( /^\w+$/, i ) )
						name = i;
					else
						name = this.ToJSON( String( i ) );

					name += ':';

				}
				else
					name = '';


				if ( out.length > 0 )
					out += ',';

				out += name + this.ToJSON( data[i] );

			}

			if ( assoc )
				return '{' + out + '}';
			else
				return '[' + out + ']';

		case 'undefined' :
			return 'null';

	}

	return null;

}

/**
 * Converts back data from its provided JSON representation.
 *
 * Due to intention of JSON this method is quite simple and returns any
 * scalar or structured value represented by provided string containing
 * JSON-code.
 *
 * @param string data the string containing JSON-code of value
 * @return the decoded value, false on error
 */

AnXRequest.prototype.FromJSON = function( data )
{
	return eval( '(' + data + ')' );
}


/**
 * Converts provided data into URL-encoded representation according to
 * MIME-type application/x-www-url-encoded, which is commonly used to post
 * form data to server.
 *
 * @param object/scalar value to be encoded, an object may have scalar
 *                      properties, only!
 * @return string the FORM-representation of provided data
 */

AnXRequest.prototype.ToFORM = function( data, sub )
{

	switch ( typeof data )
	{

		case 'boolean' :
			if ( !sub )
				return false;

			return data ? '1' : '0';

		case 'number' :
			if ( !sub )
				return false;

			data = String( data );

		case 'string' :
			if ( !sub )
				return false;

			data = encodeURIComponent( data );
			return data.replace( / /, '+' );

		case 'object' :
			if ( sub )
				return false;

			var i, name;
			var out = '';

			for ( i in data )
			{

				if ( out.length > 0 )
					out += '&';


				if ( typeof i == 'number' )
					name = 'N_' + String( i );
				else
					name = this.ToFORM( String( i ), true );


				out += name + this.ToFORM( data[i], true );

			}

		case 'undefined' :
			if ( !sub )
				return false;

			return 'null';

	}


	return null;

}

/**
 * Calls method available on server.
 *
 * Beside arguments id and method any further argument is passed to called
 * method on server. Examples for using this feature are:
 *
 *  node.Call( true, 'user:getList' );
 *  node.Call( 123, 'form:checkValue', valueToBeChecked );
 *  node.Call( false, 'status:countClick', clickedURL );
 *  node.Call( function( node ) { ... }, 'getAge', 'name of person' );
 *
 * @param mixed id integer ID to be explicitly assigned with call, true to
 *                 get a random ID assigned automatically, false to call
 *                 method as notification or a function to be called on
 *                 receiving response (ID is then assigned randomly).
 * @param string method full name of method to be called, optionally
 *                      including library selection.
 */

AnXRequest.prototype.Call = function( id, method )
{

	if ( !method || ( method.length == 0 ) )
		return false;


	if ( typeof id == 'function' )
	{
		this.processor = id;
		id             = true;
	}

	if ( typeof id != "number" )
	{
		if ( id )
			// caller explicitly requested to use random ID
			// (e.g. by providing true)
			id = Math.floor( Math.random() * Math.pow( 2, 31 ) + 1 );
		else
			// caller explicitly requested to send notification,
			// thus ID must be null
			id = null;
	}

	this.SetID( id );


	// check for module selection included with method name and extract
	var module, p;

	p = method.indexOf( ':' );
	if ( p >= 0 )
	{

		module = method.substr( 0, p );
		method = method.substr( p + 1 );

		if ( p > 0 )
			module = 'l=' + encodeURIComponent( module );

	}
	else
		module = '';



	// get provided parameters (skip ID and method in first two arguments)
	var params, i, method, data;

	params = new Array();

	if ( arguments && arguments.length )
		for ( i = 2; i < arguments.length; i++ )
			params.push( arguments[i] );
	else
		for ( i = 2; i < AnXRequest.Call.arguments.length; i++ )
			params.push( AnXRequest.Call.arguments[i] );




	// prepare request data depending on current mode
	var mode = this.GetMode();

	switch ( mode )
	{

		case 'json' :
			data = this.ToJSON( {
								"method" : method,
								"params" : params,
								"id"     : id
								} );
			break;

		case 'soap' :
			return false;

		default :
			data = this.toFORM( data, null );

	}




	/*
	 * send request now
	 */

	// create internally managed node
	if ( !this.GetNode() )
		return false;

	if ( !this.Request( module, data ) )
		return false;


	return true;

}











/**
 * start separate namespace for common basic methods specific to toxA.CMS
 *
 * toxA() contains several methods used to commonly manage JavaScript code
 * in toxA.CMS.
 *
 */

function toxA_Engine()
{

	var classHandlers    = new Object();

	this.addClassHandler = function( className, func, initialData, before )
	{

		// initially prepare the list of handlers on selected class
		if ( !classHandlers[className] || !classHandlers[className] )
			classHandlers[className] = new Array();


		// traverse existing list of handlers for the one given here
		var i, s = classHandlers[className].length;
		for ( i = 0; i < s; i++ )
			if ( classHandlers[className][i]["function"] == func )
				break;


		if ( i < s )
		{
			// handler is already attached
			// --> check whether to move it or not

			if ( before && ( i > 0 ) )
				// got explicit request to attach BEFORE all others
				// --> handler isn't first currently
				//     --> need to move
				needMove = true;
			else if ( ( typeof before != "undefined" ) && ( i < s - 1 ) )
				// got explicit request to attach AFTER all others
				// --> handler isn't last currently
				//     --> need to move
				needMove = true;
			else
				needMove = false;


			if ( needMove )
			{
				// need to move handler ...

				// get current record on handler
				var handler = classHandlers[className][i];

				// update data if provided
				if ( typeof initialData != "undefined" )
					handler["data"] = initialData;

				// detach handler from current position
				this.classHandlers[className].splice( i, 1 );

				// and re-attach
				if ( before )
					classHandlers[className].unshift( handler );
				else
					classHandlers[className].push( handler );

			}
			else
				// handler does not need to be moved in list
				if ( typeof initialData != "undefined" )
					// update its data with provided record
					classHandlers[className][i]["data"] = initialData;


		}
		else
		{
			// handler wasn't attached before

			handler = {
						"data"     : initialData,
						"function" : func
						};


			if ( before )
				classHandlers[className].unshift( handler );
			else
				classHandlers[className].push( handler );

		}


		return true;

	}

	this.dropClassHandler = function( className, func )
	{

		if ( !classHandlers[className] || !classHandlers[className] )
			return true;

		// traverse existing list of handlers for the one given here
		var i, s = classHandlers[className].length;
		for ( i = 0; i < s; i++ )
			if ( classHandlers[className][i]["function"] == func )
				break;


		if ( i < s )
			classHandlers[className].splice( i, 1 );


		return true;

	}

	this.getClassHandlerData = function( className, func )
	{

		if ( !classHandlers[className] || !classHandlers[className] )
			return false;

		// traverse existing list of handlers for the one given here
		var i, s = classHandlers[className].length;
		for ( i = 0; i < s; i++ )
			if ( classHandlers[className][i]["function"] == func )
				break;


		if ( i < s )
			return classHandlers[className][i]['data'];


		return false;

	}

	this.callClassHandler = function( className, mode, node )
	{

		if ( !classHandlers[className] || !classHandlers[className] )
			return;

		// traverse existing list of handlers for the one given here
		var i, s = classHandlers[className].length;
		for ( i = 0; i < s; i++ )
			if ( typeof classHandlers[className][i]["function"] == "function" )
				classHandlers[className][i]["data"] =
										classHandlers[className][i]["function"](
										node, mode,
										classHandlers[className][i]["data"],
										className );

	}

	this.handleClassesInTree = function( node, mode )
	{

		if ( !node )
			return;

		if ( !node.parentNode )
			return;


		var sub = node.firstChild;
		while ( sub )
		{

			if ( !sub.parentNode )
				break;

			if ( sub.nodeType == 1 )
			{

				// invoke special list of node-related handlers using meta-class
				this.callClassHandler( '__node_early', mode, sub );


				// get all class names of current element
				// node and call related handlers ...
				classes = this.getClasses( sub );
				for ( i = 0; i < classes.length; i++ )
					if ( classes[i] )
						this.callClassHandler( classes[i], mode, sub );


				// invoke special list of node-related handlers using meta-class
				this.callClassHandler( '__node_in_between', mode, sub );


				// descend into structure
				this.handleClassesInTree( sub, mode );


				// invoke special list of node-related handlers using meta-class
				this.callClassHandler( '__node_late', mode, sub );

			}


			sub = sub.nextSibling;

		}
	}


	// extracts all classes of given node
	this.getClasses = function( node )
	{

		// get all class names of given DOM element as array
		classes = node.getAttribute( 'class' );
		if ( !classes )
			if ( node.className )
				classes = node.className;

		if ( classes )
		{

			classes = classes.split( " " );
			if ( classes )
				if ( classes.length > 0 )
					return classes;

		}


		return new Array();

	}

	// detects if given node is in named classed (among others)
	this.isInClass = function( node, className )
	{

		// get all class names of given DOM element as array
		classes = this.getClasses( node );

		for ( var i = 0; i < classes.length; i++ )
			if ( classes[i] == className )
				return true;


		return false;

	}



	var __markDocumentHasLoaded = false;

	this.markDocumentLoaded = function()
	{

		if ( __markDocumentHasLoaded )
			return false;

		__markDocumentHasLoaded = true;

		return true;

	}




	this.dynRef = function( address, label, classname )
	{

		if ( address && address.length )
		{

			// join array elements to get string, then replace
			// any occurence of '?' by '@'
			var temp = address.join( '' );
			var mailadr = temp.replace( /\?/, '@' );

			if ( !label || !label.length ) label = mailadr;
			if ( !classname ) classname = "email";

			document.write( "<a class=\"" + classname + "\" href=\"mailto:" +
							mailadr + "\"\n>" + label + "</a>" );

		}
	}


	this.focusFirstEdit = function( node, firstEmpty )
	{

		if ( ( ( node.nodeName == "INPUT" ) &&
			 node.getAttribute( "type" ).match( /^text|password$/i ) ) ||
			 ( node.nodeName.match( /SELECT|TEXTAREA/i ) ) )
			if ( !node.disabled && !node.readOnly )
				if ( !firstEmpty || ( node.value.length == 0 ) )
				{

					node.select();
					node.focus();

					return true;

				}


		if ( node.childNodes && ( node.childNodes.length > 0 ) )
		{

			var child = node.firstChild;

			while ( child && child.parentNode )
				if ( this.focusFirstEdit( child, firstEmpty ) )
					return true;
				else
					child = child.nextSibling;

		}


		return false;

	}





	/*
	 * Methods used to dynamically include further JavaScript code/CSS styles
	 */

	this.includedFiles = new Array();

	this.qualifyScriptURL = function( url )
	{

		if ( url.indexOf( '/' ) == -1 )
			// tries to build URLs similar to server-side method jsref()
			url = './file.php?n=static%3A' + url + '&d=inline&t=js';

		return url;

	}

	this.qualifyStyleURL = function( url )
	{

		if ( url.indexOf( '/' ) == -1 )
			// tries to build URLs similar to server-side method cssref()
			url = './file.php?n=static%3A' + url + '&d=inline&t=css';

		return url;

	}

	this.includeScript = function( scriptURL, codeOnCompletion )
	{

		scriptURL = this.qualifyScriptURL( scriptURL );

		head = document.getElementsByTagName( "HEAD" ).item( 0 );
		if ( head )
		{

			script = document.createElement( "script" );
			script.setAttribute( "type", "text/javascript" );
			script.setAttribute( "src", scriptURL );

			if ( codeOnCompletion )
			{

				// tie code to current node
				script.codeOnCompletion = codeOnCompletion;

				// MSIE-way ...
				script.onreadystatechange = function()
				{
					if ( ( this.readyState == "complete" ) ||
						 ( this.readyState == "loaded" ) )
						if ( this.codeOnCompletion )
						{
							if ( typeof this.codeOnCompletion == 'function' )
								this.codeOnCompletion();
							else if ( this.codeOnCompletion.length > 0 )
									eval( this.codeOnCompletion );
						}
				}

				// FF2/Opera9 way ...
				if ( script.addEventListener )
					script.addEventListener( 'load', function()
					{
						if ( this.codeOnCompletion )
						{
							if ( typeof this.codeOnCompletion == 'function' )
								this.codeOnCompletion();
							else if ( this.codeOnCompletion.length > 0 )
								eval( this.codeOnCompletion );
						}
					}, false );

			}


			head.appendChild( script );


			this.includedFiles[this.includedFiles.length] = scriptURL;


			return true;

		}


		return false;

	}

	this.includeScriptOnce = function( scriptURL, codeOnCompletion )
	{

		scriptURL = this.qualifyScriptURL( scriptURL );

		for ( i = 0; i < this.includedFiles.length; i++ )
			if ( this.includedFiles[i] == scriptURL )
			{

				if ( codeOnCompletion )
					eval( codeOnCompletion );

				return true;

			}


		return this.includeScript( scriptURL, codeOnCompletion );

	}

	this.includeStyle = function( styleURL )
	{

		styleURL = this.qualifyStyleURL( styleURL );

		head = document.getElementsByTagName( "HEAD" ).item( 0 );
		if ( head )
		{

			style = document.createElement( "link" );
			style.setAttribute( "rel", "stylesheet" );
			style.setAttribute( "type", "text/css" );
			style.setAttribute( "href", styleURL );

			head.appendChild( style );


			this.includedFiles[this.includedFiles.length] = styleURL;


			return true;

		}


		return false;

	}

	this.includeStyleOnce = function( styleURL )
	{

		styleURL = this.qualifyStyleURL( styleURL );

		for ( i = 0; i < this.includedFiles.length; i++ )
			if ( this.includedFiles[i] == styleURL )
				return true;


		return this.includeStyle( styleURL );

	}






	/*
	 * Methods used to manage localization in client-side JavaScript code.
	 */

	var localeMap     = new Object();	// hash of l10ns
	var localeMapAdds = new Array();	// keys of l10ns to be retrieved

	var retriever     = null;			// instance of AnX client for retrieval

	// registers l10ns like setLocalized, in addition demands retrieving proper
	// l10ns from server ... if delayedRetrieval is true, retrieval is delayed
	// and must be initiated by call of __retrieveLocalizedDelayed()
	this.wantLocalized = function( lookups, delayedRetrieval )
	{

		for ( var prop in lookups )
		{
			localeMap[prop] = lookups[prop];
			if ( retriever )
				localeMapAdds.push( prop );
		}

		if ( ( localeMapAdds.length > 0 ) && !delayedRetrieval )
			__retrieveLocalized( localeMapAdds );

	}

	// looks up hash of registered l10ns for given key
	this.getLocalized = function( lookup )
	{
		return localeMap[lookup];
	}

	// registers l10ns, records is a hash of key => l10n mappings with latter
	// giving default l10n string
	this.setLocalized = function( records )
	{
		if ( records )
			for ( var lookup in records )
				localeMap[lookup] = records[lookup];
	}

	// is called to process AnX return on retrieving l10n
	this.__processRetrievedLocalizations = function( node )
	{
		if ( node )
			if ( !node.error )
				toxA.setLocalized( node.result );
	}

	// starts retrieving all given localizations, if omitted re-retrieves ALL
	// registered localizations ...
	this.__retrieveLocalized = function( lookups )
	{

		if ( retriever )
		{
			// retriever exists - check if it's running

			var node = retriever.GetNode();
			if ( node )
				if ( ( node.readyState > 0 ) && ( node.readyState < 4 ) )
					// there IS a running request -> don't start another
					// @todo schedule re-invocation for trying later again
					return;

		}
		else
			retriever = new AnXRequest();


		if ( !lookups )
		{
			// caller omitted list of keys to retrieve -> re-retrieve all l10ns

			lookups = new Array();

			for ( var name in localeMap )
				lookups.push( name );

		}


		if ( lookups.length > 0 )
			// call for remote procedure retrieving l10ns
			retriever.Call( this.__processRetrievedLocalizations,
							'getLocalized', lookups );

	}

	// starts retrieving recently demanded localizations
	this.__retrieveLocalizedDelayed = function()
	{

		if ( localeMapAdds )
			if ( localeMapAdds.length > 0 )
				this.__retrieveLocalized( localeMapAdds );

	}





	// code of browser detection derived from:
	//   http://www.quirksmode.org/js/detect.html
	this.browserDetect = {

		init: function ()
		{

			if ( this.browser )
				return;

			this.browser = this.searchString( this.dataBrowser )
								|| "An unknown browser";

			this.version = this.searchVersion( navigator.userAgent )
								|| this.searchVersion( navigator.appVersion )
								|| "an unknown version";

			this.OS      = this.searchString( this.dataOS )
								|| "an unknown OS";

		},

		searchString: function( data )
		{

			for ( var i = 0; i < data.length; i++ )
			{

				var dataString = data[i].string;
				var dataProp   = data[i].prop;

				this.versionSearchString = data[i].versionSearch ||
										   data[i].identity;

				if (dataString)
				{

					if ( dataString.indexOf( data[i].subString ) != -1 )
						return data[i].identity;

				}
				else if ( dataProp )
					return data[i].identity;

			}
		},

		searchVersion: function( dataString )
		{

			var index = dataString.indexOf( this.versionSearchString );

			if ( index == -1 )
				return;

			return parseFloat( dataString.substring( index +
							   this.versionSearchString.length + 1 ) );

		},

		dataBrowser: [
			{ 	string        : navigator.userAgent,
				subString     : "OmniWeb",
				versionSearch : "OmniWeb/",
				identity      : "OmniWeb"
			},
			{
				string        : navigator.vendor,
				subString     : "Apple",
				identity      : "Safari"
			},
			{
				prop          : window.opera,
				identity      : "Opera"
			},
			{
				string        : navigator.vendor,
				subString     : "iCab",
				identity      : "iCab"
			},
			{
				string        : navigator.vendor,
				subString     : "KDE",
				identity      : "Konqueror"
			},
			{
				string        : navigator.userAgent,
				subString     : "Firefox",
				identity      : "Firefox"
			},
			{
				string        : navigator.vendor,
				subString     : "Camino",
				identity      : "Camino"
			},
			{		// for newer Netscapes (6+)
				string        : navigator.userAgent,
				subString     : "Netscape",
				identity      : "Netscape"
			},
			{
				string        : navigator.userAgent,
				subString     : "MSIE",
				identity      : "Explorer",
				versionSearch : "MSIE"
			},
			{
				string        : navigator.userAgent,
				subString     : "Gecko",
				identity      : "Mozilla",
				versionSearch : "rv"
			},
			{ 		// for older Netscapes (4-)
				string        : navigator.userAgent,
				subString     : "Mozilla",
				identity      : "Netscape",
				versionSearch : "Mozilla"
			}
		],

		dataOS : [
			{
				string    : navigator.platform,
				subString : "Win",
				identity  : "Windows"
			},
			{
				string    : navigator.platform,
				subString : "Mac",
				identity  : "Mac"
			},
			{
				string    : navigator.platform,
				subString : "Linux",
				identity  : "Linux"
			}
		]

	};


	this.detectBrowser = function()
	{

		this.browserDetect.init();

		this.browserName    = this.browserDetect.browser;
		this.browserVersion = this.browserDetect.version;
		this.browserOS      = this.browserDetect.OS;

	}
}


var toxA = new toxA_Engine();

toxA.detectBrowser();




toxA_onDocumentLoaded = function()
{

	if ( !toxA.markDocumentLoaded() )
		return;


	toxA.__retrieveLocalized()


	var bodies = document.getElementsByTagName( "BODY" );
	if ( bodies )
		if ( bodies.length )
		{

			// invoke special list of document-related handlers
			// using meta-class
			toxA.callClassHandler( '__document_early', 'onDocumentLoaded',
								   bodies[0] );

			toxA.handleClassesInTree( bodies[0], 'onDocumentLoaded');

			// invoke special list of document-related handlers
			// using meta-class
			toxA.callClassHandler( '__document_late', 'onDocumentLoaded',
								   bodies[0] );


			// handlers might have registered more l10ns for retrieval
			toxA.__retrieveLocalizedDelayed()


			if ( document.__toxA_focusThisNode )
				toxA.focusFirstEdit( document.__toxA_focusThisNode,
									 document.__toxA_focusFirstEmpty );
			else
				toxA.focusFirstEdit( bodies[0] );

		}
}

toxA_onWindowLoaded = function()
{

	var bodies = document.getElementsByTagName( "BODY" );
	if ( bodies )
		if ( bodies.length )
		{

			// invoke special list of document-related handlers
			// using meta-class
			toxA.callClassHandler( '__document_early', 'onWindowLoaded',
								   bodies[0] );

			// invoke special list of document-related handlers
			// using meta-class
			toxA.callClassHandler( '__document_late', 'onWindowLoaded',
								   bodies[0] );

		}
}





/*
 * implement croos browser "document.onload" event
 */

// document.onload for Mozilla/Firefox
if ( document.addEventListener )
	document.addEventListener( "DOMContentLoaded", toxA_onDocumentLoaded,
								false );

// document.onload for Internet Explorer (based on http://dean.edwards.name/weblog/2006/06/again/)
/*@cc_on @*/
/*@if (@_win32)
document.write( "<script id=__toxA_msie_ondocload_helper defer src=javascript:void(0)><"+"/script>" );
var toxA_global_script = document.getElementById( "__toxA_msie_ondocload_helper" );
toxA_global_script.onreadystatechange = function()
{
	if ( this.readyState == "complete" )
		toxA_onDocumentLoaded();
};
/*@end @*/

// document.onload for Safari (based on http://dean.edwards.name/weblog/2006/06/again/)
if ( /WebKit/i.test( navigator.userAgent ) )
{
	var toxA_Safari_onDocLoad_timer = setInterval( function()
	{
		if ( /loaded|complete/.test( document.readyState ) )
		{
			clearInterval( toxA_Safari_onDocLoad_timer );
			toxA_onDocumentLoaded();
		}
	}, 10 );
}

// document.onload for all else browsers
window.toxAPostLoading = window.onload ? window.onload : function() {};
window.onload = function()
{
	toxA_onDocumentLoaded();
	toxA_onWindowLoaded();
	window.toxAPostLoading();
}








/*
 * Implements feature to warn user of potential data loss on clicking links
 * not inside selected threads of current document's HTML/XML tree.
 *
 * E.g. used by macro wizard, if user haven't closed form data.
 */

function toxA_SafeLinkAreas()
{
}

toxA_SafeLinkAreas.installProperty = function( node, mode, data )
{

	if ( mode == "onDocumentLoaded" )
		document.unloadWarningFunction = function() { return true; };
}

// marks current node to be parent of a "safe link area"
toxA_SafeLinkAreas.handleSafeLinkAreaMarkers = function( node, mode, data )
{

	if ( mode == "onDocumentLoaded" )
	{

		if ( !toxA.getLocalized( 'SAFE_LINKS_ALERT_MSG' ) )
			toxA.wantLocalized( { 'SAFE_LINKS_ALERT_MSG' : "ATTENTION!\n\n" +
							"You might loose unsaved changes made on current\n"+
							"page by changing to another one.\n\n" +
							"Click OK to confirm changing nevertheless!" }, 1 );


		// mark current node as it starts (or continues) "safe link area"
		node.isSafeLinkAreaNode  = "set";

		document.unloadWarningFunction = function()
		{

			msg = toxA.getLocalized( 'SAFE_LINKS_ALERT_MSG' );
			if ( !window.confirm( msg ) )
				return false;

			return true;

		}

	}
}

/*
 * distributes mark of a "safe link area" node to all its
 * descending elements and prepares all acting elements in
 * that area to prevent warning on unloading page
 */
toxA_SafeLinkAreas.handleSafeLinkAreaNodes = function( node, mode, data )
{

	if ( mode != "onDocumentLoaded" )
		return;


	if ( node.isSafeLinkAreaNode )
		node.isSafeLinkAreaNode = "set";
	else
		if ( node.parentNode )
			if ( node.parentNode.isSafeLinkAreaNode )
				node.isSafeLinkAreaNode = "set";


	if ( !node.isSafeLinkAreaNode )
	{
		// node belongs to safe link area
		// --> prepend click/submit handlers by setting related flag

		if ( node.nodeName == 'A' )
		{
			// A-tag act on clicks

			href = node.getAttribute( "href" );

			if ( href )
				// MSIE prepends local anchor references by current URL
				if ( href.substr( 0, location.href.length ) == location.href )
					href = href.substr( location.href.length );

			if ( href )
				if ( href.charAt( 0 ) != "#" )
				{

					node.toxAPostClickHandler = node.onclick ? node.onclick :
																function() {};

					node.onclick = function()
									{
										if ( !document.unloadWarningFunction() )
											return false;
										return node.toxAPostClickHandler();
									}

				}
		}
		else if ( node.nodeName == 'FORM' )
		{
			// FORM-tags act on submission

			node.toxAPostSubmitHandler = node.onsubmit ? node.onsubmit :
														function() {};

			node.onsubmit = function()
							{
								if ( !document.unloadWarningFunction() )
									return false;
								return node.toxAPostSubmitHandler();
							}

		}
	}
}

toxA.addClassHandler( 'js-safe-links-area',
					  toxA_SafeLinkAreas.handleSafeLinkAreaMarkers, 0 );
toxA.addClassHandler( '__node_in_between',
					  toxA_SafeLinkAreas.handleSafeLinkAreaNodes, 0 );
toxA.addClassHandler( '__document_early',
					  toxA_SafeLinkAreas.installProperty, 0 );







/*
 * Generic JS Loader
 *
 * Many JS-based features are made available in separate script files.
 * The following code provides an even more generic interface to methods
 * of class toxA_Engine available for traversing document tree and processing
 * a subset of nodes selected by their assigned class names.
 *
 * This API supports keeping valid XHTML code with load latencies more depending
 * on client-side functionality actually required in a page.
 *
 */

// set of indicators supported out-of-the-box
//
// (This map may be overloaded/extended using same structure in document-bound
//  variable document.__toxA_genericLoaderMap_override!)
toxA.genericLoaderMap =
{

	// include DynUI e.g. to render several effects (tooltips, popup menus etc.)
	'js-use-dynui'  :
	{
		'data'        : null,
		'maxCount'    : 1,
		'process'     : function() { document.__toxA_usingDynUI = true; },
		'scripts'     : { 'dynui.js'  : null },
		'styles'      : { 'dynui.css' : null },
		'postprocess' : null
	},

	// automatically focus first contained (empty) input field
	'js-auto-focus' :
	{
		'maxCount'    : 1,
		'process'     : function( node, mode, data, className )
						{
							document.__toxA_focusThisNode = node;
							document.__toxA_focusFirstEmpty = toxA.isInClass(
												node, 'js-auto-focus-empty' );
						}
	},

	// scroll down to tagged object
	'js-scroll-here' :
	{
		'maxCount'    : 1,
		'condition'   : function( node, mode )
						{
							if ( mode == 'onDocumentLoaded' )
								if ( node )
									if ( node.parentNode )
										return true;
						},
		'process'     : function( node ) { return node; },
		'postprocess' : function()
						{
							var node = toxA.getClassHandlerData(
														'js-scroll-here',
														toxA_genericLoader );
							if ( !node )
								return;

							if ( !node.nodeType )
								return;


							if ( !( typeof DynUI == 'function' ) )
								toxA.includeScriptOnce( 'dynui.js', this );
							else
							{

								var pos = DynUI.getPosition( node );
								if ( pos )
									if ( pos.length )
									{

										var top = pos[1] - 5;
										if ( top < 0 )
											top = 0;

										window.scrollTo( 0, top );

									}
							}
						}
	},

	// replace corners of tagged object with rounded-corner images
	'js-use-rcorners' :
	{
		'data'        : [],
		'condition'   : function( node, mode )
						{
							if ( mode == 'onDocumentLoaded' )
								if ( node )
									if ( node.parentNode )
										if ( node.nodeName == 'DIV' )
											return true;
						},
		'process'     : function( node, mode, data )
						{
							data.push( node );
							return data;
						},
		'postprocess' : function( node, mode, data )
						{
							if ( mode != 'onDocumentLoaded' )
								return;

							var nodes = toxA.getClassHandlerData(
															'js-use-rcorners',
															toxA_genericLoader);
							if ( nodes )
								if ( nodes.length )
								{

									document.toxA_RCorners_nodes = nodes;

									toxA.includeScriptOnce(
												'./file.php?n=rcorners.js&t=js',
												'toxA_RCorners_compute();' );

								}
						}
	},

	// replace tagged object by third-party rich text editor component
	'js-use-rte' :
	{
		'data'        : [],
		'condition'   : function( node, mode )
						{
							if ( mode == 'onDocumentLoaded' )
								if ( node )
									if ( node.parentNode )
										if ( ( node.nodeName == 'TEXTAREA' ) ||
											 ( node.nodeName == 'DIV' ) )
											return true;
						},
		'process'     : function( node, mode, data )
						{
							data.push( node );
							return data;
						},
		'postprocess' : function( node, mode )
						{
							if ( mode != 'onDocumentLoaded' )
								return;

							nodes = toxA.getClassHandlerData( 'js-use-rte',
															toxA_genericLoader);
							if ( nodes )
								if ( nodes.length )
								{

									document.toxA_RTE_nodes = nodes;

									toxA.includeScriptOnce(
											'./file.php?n=rteglue%3Af.js&b=' +
											toxA.browserName,
											'toxA_RTE_bind();' );

								}
						}
	}

};



// handler used to process node matching one of the registered indicators
function toxA_genericLoader( node, mode, data, className )
{

	if ( !className )
		return data;

	var record = toxA.genericLoaderMap[className];
	if ( !record )
		return data;


	// check for related function used to check whether inclusions are demanded
	// this time or not ...
	if ( typeof record['condition'] == 'function' )
	{

		var condition = record['condition']( node, mode, data, className );
		if ( !condition )
			// condition failed -> don't process this time
			return data;

	}


	// provide option to select maximum count of nodes being processed this way
	if ( record['maxCount'] > 0 )
		if ( record['count'] >= record['maxCount'] )
			// reached maximum count of processed nodes before -> skip this time
			return data;

	if ( toxA.genericLoaderMap[className]['count'] )
		toxA.genericLoaderMap[className]['count']++;
	else
		toxA.genericLoaderMap[className]['count'] = 1;


	// handle adjustment of attached data
	if ( typeof record['process'] == 'function' )
		data = record['process']( node, mode, data, className );


	// include all listed style definition files
	if ( record['styles'] )
		for ( var file in record['styles'] )
		{

			var handler = record['styles'][file];
			if ( !handler )
				handler = function() {};

			toxA.includeStyleOnce( file, handler );

		}

	// include all listed script files
	if ( record['scripts'] )
		for ( var file in record['scripts'] )
		{

			var handler = record['scripts'][file];
			if ( !handler )
				handler = function() {};

			toxA.includeScriptOnce( file, handler );

		}


	// add provided separate handler to be invoked when document has been loaded
	if ( record['postprocess'] && ( record['count'] == 1 ) )
		toxA.addClassHandler( '__document_late', record['postprocess'], 0 );


	return data;

}


// override map according to available customization table
if ( document.__toxA_genericLoaderMap_override )
{

	var override = document.__toxA_genericLoaderMap_override

	for ( var name in override )
	{

		if( toxA.genericLoaderMap[name] )
			// exists: overload all first-level records given in override
			for ( var sub in override[name] )
				 toxA.genericLoaderMap[name][sub] = override[name][sub];
		else
			// additional description -> transfer to list
			toxA.genericLoaderMap[name] = override[name];

	}
}


// register class handler for each listed class
for ( var name in toxA.genericLoaderMap )
	toxA.addClassHandler( name, toxA_genericLoader,
						  toxA.genericLoaderMap[name]['data'] );



/* EOF */