/** * 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 * @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( "" + label + "" ); } } 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( "