/* 
Loader.
A powerful MooTools Request wrapper with style.

To be able to use the "spinner" loadStyle, you'll need to make sure you have loaded the 
cvi_busy_lib.js library in your header. Without this, the Loader class will still work, you just 
won't be able to use loadStyle:'spinner' as an option. cvi_busy_lib.js creates the spinner recipients using
the canvas tag and requires no images whatsoever. See http://www.netzgesta.de/busy/ for more info & to download. 

________
OPTIONS:
	type 					-- 	'single' or 'multi' -- defaults to single. 
								Whether this Loader loads a single source into a single recipient container, 
								or multiple sources into a single recipient container.
								NOTE: If 'multi' is used, the 'source' option MUST contain an array of JSON objects. See examples below.
	source 					-- 	The file you want to load via ajax.
	loadCSS				 	-- 	(boolean, defaults to true)
								Whether or not to load the source file's associated CSS.
	 							NOTE: To work, the CSS file must be named the same as the source file, except for the extension. (e.g., Content123.html, Content123.css)
	loadJS				 	-- 	(boolean, defaults to true)
								Whether or not to load the source file's associated JS.
	 							NOTE: To work, the JS file must be named the same as the source file, except for the extension. (e.g., Content123.html, Content123.js)
	assetsCache				--  (boolean, defaults to true) Whether to append a query string to the JS and CSS files that are loaded (see loadCSS and loadJS above). To avoid caching.
	loadStyle 				-- 	How to visually display the load. Can be left blank for no effect. Other options are 'spinner' & 'fade'.
	loadMethod				-- 	'replace' , 'append' , 'appendTop' -- defaults to 'replace'.
								Whether to replace the recipient container with the newly ajaxed content, or append to it. 
								'appendTop' puts the contents above the existing content.
	spinnerOnElement 		-- 	If you want the spinner and it's overlay to occur on an recipient OTHER THAN the "recipient" recipient.
								Takes an recipient id in the form of: 'MyDiv'.
	spinnerOptions 			-- 	The visual properties of the spinner itself (size/shape/etc.) See http://www.netzgesta.de/busy/ for more info.
	spinnerOverlayOptions 	-- 	The visual properties of the spinner overlay. See http://www.netzgesta.de/busy/ for more info.
	fadeDuration 			-- 	The duration of the fade effect if 'fade' is chosen as the loadStyle.
	onComplete 				-- 	What to do when the Request is complete.
	onSuccess 				-- 	What to do when the Request is successfully completed.
	onFailure 				-- 	What to do if the ajax request fails.
	ajaxOptions 			-- 	Can take all of the available MooTools ajax request options. see http://www.mootools.net/docs/core/Request/Request
	logging					--  (boolean, defaults to false) Shows log messages via console.log (if present) (helpful for debugging/developing).
_________
EXAMPLES:
	var myLoader = new Loader($('RecipientDiv'), {
		source: 'getme.html'
		,loadStyle: 'spinner'   
		(see above for other available options)
	});
	
	var newMultiLoader = new Loader($('RecipientDiv'), {
		type: 'multi'
		,source: ["{div:'ContainerId1', source:'loaded1.html'}", "{div:'ContainerId2', source:'loaded2.html'}"]
		,loadStyle: 'spinner'   
		,onComplete: function() {
			alert('Do Something!');
		}
	});

	var mySources = [
		 "{div:'ContainerId1', source:'loaded1.html'}"
		,"{div:'ContainerId2', source:'loaded2.html'}"
		,"{div:'ContainerId3', source:'loaded3.html'}"
		,"{div:'ContainerId4', source:'loaded4.html'}"
	];
	var newMultiLoader = new Loader($('RecipientDiv'), {
		type: 'multi'
		,source: mySources
		,loadStyle: 'spinner'   
		,spinnerOnElement: 'RecipientDiv'
	});
*/

var Loader = new Class({
	Implements: [Events, Options, Log],

	options: {
		type: 'single',			// single || multi
		source: '',
		loadCSS: true,
		loadJS: true,
		assetsCache: true,
		loadStyle: '',			// spinner || fade || ''
		loadMethod: 'replace',	// replace || append || appendTop
		spinnerOnElement: '',	// 'MyDivId'
		spinnerOptions: {
			color:'#000',
			iradius:6,
			weight:2,
			size:26,
			type:'tube',		// circle || oval || polygon || rectangle || tube
			count: 12,			// 5 - 36
			speed: 96 			// 30 - 1000
		},
		spinnerOverlayOptions: {
			color:'#fff',
			opacity:0.4,
			text:'loading...',
			style:'font-size:11px; font-family: Helvetica, Arial, sans-serif; color:#666; position:relative; left:4px;'
		},
		fadeDuration: 200,
		onComplete: function() {},
		onSuccess: function() {},
		onFailure: function() {},
		ajaxOptions: {},
		logging: false
	},

  	initialize : function(recipient, options){
		this.recipient = $(recipient);
		this.setOptions(options);
		this.source = this.options.source;
		this.loadCSS = this.options.loadCSS;
		this.loadJS = this.options.loadJS;
		this.assetsCache = this.options.assetsCache;

		if (this.options.spinnerOnElement == '') {
			this.spinnerOnElement = this.recipient;
		} else {
			this.spinnerOnElement = $(this.options.spinnerOnElement);
		}

		if (this.options.type == 'single') {
			this.loadSingle();
		} else if (this.options.type == 'multi') {
			this.loadMulti();
		}
		if (this.options.logging) {
			this.enableLog();
			this.log('load initialized...');
		}
	},

	loadSingle: function(){
		var theRecipientDiv = this.recipient;
		var fileName = this.options.source.substr(0,this.options.source.lastIndexOf('.'));
		var noCache = 'nc=' + new Date().getTime();

		if (this.options.loadStyle == 'spinner') {
			if (Browser.Engine.trident) {
				var recipientCoordinates = this.recipient.getCoordinates();
				var tempSpinnerHolder = new Element('div', {
					'id': 'tempSpinnerHolderFor'+this.recipient.id
					,'html': '&nbsp;'
					,'styles': {
						'position': 'absolute'
						,'top': recipientCoordinates.top
						,'left': recipientCoordinates.left
						,'height': recipientCoordinates.height
						,'width': recipientCoordinates.width
					}
				}).inject($$('body')[0], 'bottom');
				this.loadingSpinner = getBusyOverlay(tempSpinnerHolder, this.options.spinnerOverlayOptions, this.options.spinnerOptions);
			} 
			else {
				this.loadingSpinner = getBusyOverlay(this.spinnerOnElement, this.options.spinnerOverlayOptions, this.options.spinnerOptions);
			}
		} else if (this.options.loadStyle == 'fade') {
			this.recipient.fade('hide');
		}

		if (this.loadCSS != false) {
			if (this.assetsCache) {
				var theCSS = Asset.css (fileName + ".css" + "?" + noCache);
			} else if (!this.assetsCache) {
				var theCSS = Asset.css (fileName + ".css");
			}
		}	

		if (this.options.loadMethod == 'append' || this.options.loadMethod == 'appendTop') {
			var theModuleDivId = 'InnerWrapperFor'+this.recipient.id;
			var theModuleDiv = new Element('div', {
				 'id': theModuleDivId
				,'style': 'min-height:'+(this.options.spinnerOptions.size+50)+'px;'
			});
			var theRecipientDiv = theModuleDiv;
			
			if (this.options.loadMethod == 'append') {
				theModuleDiv.inject(this.recipient, 'bottom');
			} else if (this.options.loadMethod == 'appendTop') {
				theModuleDiv.inject(this.recipient, 'top');
			}
		}
		
		this.ajax = this.ajax || new Request.HTML();
		this.ajax.setOptions({
			url: this.options.source,
			method: 'get',
			update: theRecipientDiv,
			evalScripts: true,
			noCache: true,
			onFailure: function(responseText) {
				if (this.options.logging) {	this.log('error loading: '+this.options.source); };
				if ((this.options.loadStyle == 'spinner') && (this.options.spinnerOnElement == '')) {
					this.loadingSpinner.remove();
				}
				this.fireEvent('onFailure');
			}.bind(this),
			onComplete: function() {
				if (this.loadJS != false) {
					if (this.assetsCache) {
						var theJS = Asset.javascript (fileName + ".js" + "?" + noCache);
					} else if (!this.assetsCache) {
						var theJS = Asset.javascript (fileName + ".js");
					}
				}	
								
				if (this.options.loadStyle == 'fade') {
					var fadeInFx = new Fx.Tween(this.recipient, {duration:this.options.fadeDuration});
					fadeInFx.start('opacity','1');
					this.fireEvent('onComplete');
				} else {
					this.fireEvent('onComplete');
				}
			}.bind(this),
			onSuccess: function(responseTree, responseElements, responseHTML, responseJavaScript) {
				this.fireEvent('onSuccess', arguments);
				if (Browser.Engine.trident) {
					if ($('tempSpinnerHolderFor'+this.recipient.id) != null) {
						$('tempSpinnerHolderFor'+this.recipient.id).destroy();
					}
				};
				if (this.options.logging) {	
					this.log('loaded:\n\t— '+this.options.source); 
					if (this.loadJS != false) {
						this.log('\t— '+fileName + ".js"); 
					}
					if (this.loadCSS != false) {
						this.log('\t— '+fileName + ".css"); 
					}
				};
			}.bind(this)
		}).setOptions(this.options.ajaxOptions);
		this.ajax.send();
	},


	loadMulti: function(){
		var sourceArray = this.options.source;
		var numToLoad = sourceArray.length;
		for(var sourceIndex = 0; sourceIndex < sourceArray.length; sourceIndex++) {

			var JSON = eval('('+sourceArray[sourceIndex]+')');
			var theModuleDivId = JSON.div;
			var theModuleDiv = new Element('div', {
				 'id': theModuleDivId
				,'style': 'min-height:'+(this.options.spinnerOptions.size+50)+'px;'
			});
			theModuleDiv.inject(this.recipient, 'bottom');

			var theSource = JSON.source;
			var multiLoaderThis = this;
			var multiLoader = new Loader(theModuleDiv, {
				source: theSource
				,type: 'single'
				,loadCSS:    			this.options.loadCSS
				,loadJS:				this.options.loadJS
				,loadStyle:				this.options.loadStyle            
				,spinnerOnElement:		this.options.spinnerOnElement     
				,spinnerOptions:		this.options.spinnerOptions       
				,spinnerOverlayOptions:	this.options.spinnerOverlayOptions
				,fadeDuration:			this.options.fadeDuration         
				,ajaxOptions:			this.options.ajaxOptions          
				,onComplete: function() {
					numToLoad--;
					if (numToLoad == 0) {
						multiLoaderThis.fireEvent('onComplete');
					}
				}
				,onSuccess: function() {
					if (multiLoaderThis.options.logging) { multiLoaderThis.log('loaded: '+this.options.source); }
				}
				,onFailure: function() {
					if (multiLoaderThis.options.logging) {
						multiLoaderThis.log('error loading: '+this.options.source);
					}

					numToLoad--;
					if (numToLoad == 0) {
						multiLoaderThis.fireEvent('onFailure');
						if ((this.options.loadStyle == 'spinner') && (this.options.spinnerOnElement == '')) {
							this.loadingSpinner.remove();
						}
					}
				}
			});
		}
		var fadeInFx = new Fx.Tween(this.recipient, {duration:this.options.fadeDuration});
		fadeInFx.start('opacity','1');
	}
});



/* 
HOW-TO:

var newSubmitter = new Submitter({
	handler: 'URL of action handler'
	,loadStyle: 'spinner'   
	,onSuccess: function() {
		
	}
	(see below for other available options)
});

*/
var Submitter = new Class({
	Implements: [Events, Options],
	options: {
		handler: '',
		type: 'JSON',   // JSON || HTML
		loadStyle: '', // spinner || fade
		spinnerOnElement: '',
		spinnerOptions: {
			color:'#000',
			radius:6,
			weight:2,
			size:26,
			type:'rectangle' // circle|oval|polygon|rectangle|tube
		},
		spinnerOverlayOptions: {
			color:'#fff',
			opacity:0.4,
			text:'loading...',
			style:'font-size:11px; font-family: Helvetica, Arial, sans-serif; color:#666; position:relative; left:4px;'
		},
		fadeDuration: 200,
		onSuccess: function(){
			// alert('Done.');
		},
		onFailure: function(){
			//alert('I failed.');
		},
		ajaxOptions: {}
	},
  	initialize : function(options){
		this.setOptions(options);
		this.handler = this.options.handler;
		this.spinnerOnElement = $(this.options.spinnerOnElement);
		
		if (this.spinnerOnElement == '') {
			this.spinnerOnElement = this.recipient;
		};
		
		if (this.options.loadStyle == 'spinner') {
			this.recipient.setStyle('min-height', '100px');
			this.loadingSpinner = getBusyOverlay(this.spinnerOnElement, this.options.spinnerOverlayOptions, this.options.spinnerOptions);
		} else if (this.options.loadStyle == 'fade') {
			this.recipient.fade('hide');
		};
		this.submitIt();
	},

	submitIt: function(){
		if (this.options.type == 'JSON') {
			this.ajax = new Request.JSON();
			this.ajax.setOptions({
			    url: this.options.handler,
				onSuccess: function(responseJSON, responseText) {
				    responseText = '(' + responseText + ')';
				    if(responseText != "()")
					    this.responseJSON = eval(responseText);
					//this.responseJSON = eval(responseText);
					//alert(this.responseJSON);//.RichTextModuleContentOut);
					if (this.options.loadStyle == 'spinner') {
						this.loadingSpinner.remove();
					} else if (this.options.loadStyle == 'fade') {
						var fadeInFx = new Fx.Tween(this.recipient, {duration:this.options.fadeDuration});
						fadeInFx.start('opacity','1');
					};
					this.fireEvent('onSuccess');
				}.bind(this)
			}).setOptions(this.options.ajaxOptions);
			this.ajax.send('ms='+ new Date().getMilliseconds());
		} else if (this.options.type == 'HTML') {
			this.ajax = new Request.HTML();
			this.ajax.setOptions({
			    url: this.options.handler,
				onSuccess: function(responseTree, responseElements, responseHTML, responseJavaScript) {
					if (this.options.loadStyle == 'spinner') {
						this.loadingSpinner.remove();
					} else if (this.options.loadStyle == 'fade') {
						var fadeInFx = new Fx.Tween(this.recipient, {duration:this.options.fadeDuration});
						fadeInFx.start('opacity','1');
					};
					this.fireEvent('onSuccess');
				}.bind(this)
			}).setOptions(this.options.ajaxOptions);
			this.ajax.send('ms='+ new Date().getMilliseconds());
		}
	}
});



/* --------------------------------------------------------------------
//  populate Select Boxes Options
// --------------------------------------------------------------------
*/
function populateSelectBoxesOptions(oArg) 
{
        var selectBoxSelectorText = oArg.selectBoxSelectorText;
        var selectBoxLabel = oArg.selectBoxLabel;
        var dataServerUrl = oArg.dataServerUrl;
        var dataSource = oArg.dataSource;
        var dataValueField = oArg.dataValueField;
        var dataDisplayField = oArg.dataDisplayField;
        var dataSortingField = oArg.dataSortingField;
        var selectedValue = oArg.selectedValue;
        var querystringParameters = oArg.querystringParameters;
	    var onCompletion = oArg.onSuccess;
		var connectionString = oArg.connectionString;
		var retainCases = oArg.retainCases;

		if(retainCases == 'true') { //User wants to retain cases, dont lowercase anything
			if(dataValueField != null && dataDisplayField == null) dataDisplayField = dataValueField;
	        if(dataDisplayField != null && dataValueField == null) dataValueField = dataDisplayField;
	        if(selectBoxLabel == null) selectBoxLabel = '(Select ' + dataDisplayField + ')';
	        if(dataSortingField == null) dataSortingField = dataDisplayField;

	        if(dataDisplayField != null) dataDisplayField = dataDisplayField;
	        if(dataValueField != null) dataValueField = dataValueField;
		} else {
			//User did not specify case retention or does not want it, lowercase everything
			if(dataValueField != null && dataDisplayField == null) dataDisplayField = dataValueField.toLowerCase();
	        if(dataDisplayField != null && dataValueField == null) dataValueField = dataDisplayField.toLowerCase();
	        if(selectBoxLabel == null) selectBoxLabel = '(Select ' + dataDisplayField + ')';
	        if(dataSortingField == null) dataSortingField = dataDisplayField;

	        if(dataDisplayField != null) dataDisplayField = dataDisplayField.toLowerCase();
	        if(dataValueField != null) dataValueField = dataValueField.toLowerCase();
		}

       
        if(dataServerUrl == null) dataServerUrl = '/utilities/SelectBoxJson.aspx';
        dataServerUrl += "?DataSource=" + dataSource;
        dataServerUrl += "&DataValueField=" + dataValueField;
        dataServerUrl += "&DataDisplayField=" + dataDisplayField;
		if(connectionString != null) dataServerUrl += "&ConnectionString=" + connectionString;
        if(dataSortingField != null) dataServerUrl += "&DataSortingField=" + dataSortingField;
        if(querystringParameters != null) dataServerUrl += "&" + querystringParameters;
        //alert(dataServerUrl);
        
        var sender = new Request({
            onSuccess: function(responseText, responseXML){
                if(responseText != '') {
                    var json = eval('(' + responseText + ')');
                    $$(selectBoxSelectorText).each(function(el) {
                            var selectBox = $(el.id);
                            
                            var selectedValue = '';
                            if(selectBox != null){
                                var selectedIndex = selectBox.selectedIndex;
                                if(selectedIndex >= 0) {
                                    selectedValue = selectBox.options[selectedIndex].value;
                                }
                            }   

                            selectBox.innerHTML = '';

                            // add first (label) option
                            if(selectBoxLabel != null) {
                                var option = document.createElement('option');
                                option.value='';
                                option.innerHTML=selectBoxLabel;
                                selectBox.appendChild(option);
                            }
                            //json.Records.length
                            for(var i=0; i<json.Records.length; i++){
                                var option = document.createElement('option');
                                option.value = json.Records[i][dataValueField];
                                if(selectedValue && selectedValue == option.value){
                                    option.selected=true;
                                }
								if(retainCases == 'true')
									option.innerHTML = json.Records[i][dataDisplayField];
								else
                                	option.innerHTML = json.Records[i][dataDisplayField.toLowerCase()];
                                selectBox.appendChild(option);
                            }
                       
                    });   
                }   
    	        if(onCompletion != null) onCompletion();    

            }
        }).send({
            data: 'query=true',
            url: dataServerUrl
        });
}


//Get value of query string key quickly
function $Q(key) {
    var paramList = new Array();
    var urlparts = new Array();
    urlparts = document.URL.split('?');
    if (urlparts.length > 1)
    {
        var params = new Array();
        params = urlparts[1].split('&');
        
        for ( i = 0; i < params.length; i++)
        {
        
          var sparts = new Array();
          sparts= params[i].split('=');
          
          paramList[sparts[0].toLowerCase()]= sparts[1];
          
        }
    }
    return paramList[key.toLowerCase()];
}





function urlencode( str ) {
    // http://kevin.vanzonneveld.net
    // +   original by: Philip Peterson
    // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    // +      input by: AJ
    // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    // +   improved by: Brett Zamir
    // %          note: info on what encoding functions to use from: http://xkr.us/articles/javascript/encode-compare/
    // *     example 1: urlencode('Kevin van Zonneveld!');
    // *     returns 1: 'Kevin+van+Zonneveld%21'
    // *     example 2: urlencode('http://kevin.vanzonneveld.net/');
    // *     returns 2: 'http%3A%2F%2Fkevin.vanzonneveld.net%2F'
    // *     example 3: urlencode('http://www.google.nl/search?q=php.js&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:en-US:unofficial&client=firefox-a');
    // *     returns 3: 'http%3A%2F%2Fwww.google.nl%2Fsearch%3Fq%3Dphp.js%26ie%3Dutf-8%26oe%3Dutf-8%26aq%3Dt%26rls%3Dcom.ubuntu%3Aen-US%3Aunofficial%26client%3Dfirefox-a'
                             
    var histogram = {}, tmp_arr = [];
    var ret = str.toString();
    
    var replacer = function(search, replace, str) {
        var tmp_arr = [];
        tmp_arr = str.split(search);
        return tmp_arr.join(replace);
    };
    
    // The histogram is identical to the one in urldecode.
    histogram["'"]   = '%27';
    histogram['(']   = '%28';
    histogram[')']   = '%29';
    histogram['*']   = '%2A';
    histogram['~']   = '%7E';
    histogram['!']   = '%21';
    histogram['%20'] = '+';
    
    // Begin with encodeURIComponent, which most resembles PHP's encoding functions
    ret = encodeURIComponent(ret);
    
    for (search in histogram) {
        replace = histogram[search];
        ret = replacer(search, replace, ret); // Custom replace. No regexing
    }
    
    // Uppercase for full PHP compatibility
    return ret.replace(/(\%([a-z0-9]{2}))/g, function(full, m1, m2) {
        return "%"+m2.toUpperCase();
    });
    
    return ret;
}

function urldecode( str ) {
    // Decodes URL-encoded string  
    // 
    // version: 907.503
    // discuss at: http://phpjs.org/functions/urldecode
    // +   original by: Philip Peterson
    // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    // +      input by: AJ
    // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    // +   improved by: Brett Zamir (http://brett-zamir.me)
    // +      input by: travc
    // +      input by: Brett Zamir (http://brett-zamir.me)
    // +   bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    // +   improved by: Lars Fischer
    // +      input by: Ratheous
    // %          note 1: info on what encoding functions to use from: http://xkr.us/articles/javascript/encode-compare/
    // *     example 1: urldecode('Kevin+van+Zonneveld%21');
    // *     returns 1: 'Kevin van Zonneveld!'
    // *     example 2: urldecode('http%3A%2F%2Fkevin.vanzonneveld.net%2F');
    // *     returns 2: 'http://kevin.vanzonneveld.net/'
    // *     example 3: urldecode('http%3A%2F%2Fwww.google.nl%2Fsearch%3Fq%3Dphp.js%26ie%3Dutf-8%26oe%3Dutf-8%26aq%3Dt%26rls%3Dcom.ubuntu%3Aen-US%3Aunofficial%26client%3Dfirefox-a');
    // *     returns 3: 'http://www.google.nl/search?q=php.js&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:en-US:unofficial&client=firefox-a'
    
    var hash_map = {}, ret = str.toString(), unicodeStr='', hexEscStr='';
    
    var replacer = function(search, replace, str) {
        var tmp_arr = [];
        tmp_arr = str.split(search);
        return tmp_arr.join(replace);
    };
    
    // The hash_map is identical to the one in urlencode.
    hash_map["'"]   = '%27';
    hash_map['(']   = '%28';
    hash_map[')']   = '%29';
    hash_map['*']   = '%2A';
    hash_map['~']   = '%7E';
    hash_map['!']   = '%21';
    hash_map['%20'] = '+';
    hash_map['\u00DC'] = '%DC';
    hash_map['\u00FC'] = '%FC';
    hash_map['\u00C4'] = '%D4';
    hash_map['\u00E4'] = '%E4';
    hash_map['\u00D6'] = '%D6';
    hash_map['\u00F6'] = '%F6';
    hash_map['\u00DF'] = '%DF';
    hash_map['\u20AC'] = '%80';
    hash_map['\u0081'] = '%81';
    hash_map['\u201A'] = '%82';
    hash_map['\u0192'] = '%83';
    hash_map['\u201E'] = '%84';
    hash_map['\u2026'] = '%85';
    hash_map['\u2020'] = '%86';
    hash_map['\u2021'] = '%87';
    hash_map['\u02C6'] = '%88';
    hash_map['\u2030'] = '%89';
    hash_map['\u0160'] = '%8A';
    hash_map['\u2039'] = '%8B';
    hash_map['\u0152'] = '%8C';
    hash_map['\u008D'] = '%8D';
    hash_map['\u017D'] = '%8E';
    hash_map['\u008F'] = '%8F';
    hash_map['\u0090'] = '%90';
    hash_map['\u2018'] = '%91';
    hash_map['\u2019'] = '%92';
    hash_map['\u201C'] = '%93';
    hash_map['\u201D'] = '%94';
    hash_map['\u2022'] = '%95';
    hash_map['\u2013'] = '%96';
    hash_map['\u2014'] = '%97';
    hash_map['\u02DC'] = '%98';
    hash_map['\u2122'] = '%99';
    hash_map['\u0161'] = '%9A';
    hash_map['\u203A'] = '%9B';
    hash_map['\u0153'] = '%9C';
    hash_map['\u009D'] = '%9D';
    hash_map['\u017E'] = '%9E';
    hash_map['\u0178'] = '%9F';

    for (unicodeStr in hash_map) {
        hexEscStr = hash_map[unicodeStr]; // Switch order when decoding
        ret = replacer(hexEscStr, unicodeStr, ret); // Custom replace. No regexing
    }
    
    // End with decodeURIComponent, which most resembles PHP's encoding functions
    ret = decodeURIComponent(ret);

    return ret;
}