var sval=0;
var scombo="";//the text entry
var sidcombo=-1;//the select value
var sparcombo="";//the parent id for the combo
var nneg=0;
var doNotBlock = false;
/**
 * Provides a means for onClick code to cancel the block as needed.
 * Just set doNotBlock to true and the next call to blocking will be ignored.
 * 
 * @return none
 */
function myBlockUI() {
	if (!doNotBlock) {
		jQuery('div.block').block({
			message: 'Processing ...',
			centerX: true, centerY: false, css:{ top:'10px'}, overlayCSS:{backgroundColor:'#000',opacity:0.3}
		});
	} else {
		doNotBlock = false;
	}
}
function myUnBlockUI() {
	jQuery('div.block').unblock();
}
/**
 * Submits form to save this row for the summary display settings, but only if isadmin is true.
 * 
 * @param col
 * @param row
 * @param isadmin
 */
function setSummary(prj, code, cols) {
	var msg = "Which column (1 to " + (cols-1) + ") should end with project '" + prj + "'?";
	var col = prompt(msg, "1");
	if (col === null) {
		return false;
	} else if (0 + col < 1) {
		col = 1;
	} else if (0 + col >= cols) {
		col = cols - 1;
	}
	e = document.getElementById('summarycol');
	e.value = col;
	e = document.getElementById('summarycode');
	e.value = code;
	document.summary.submit();
//	alert("col: "+col+", code:'"+code+"'");
	return false;
}
/**
 * A combo has a complicated structure and naming convention
 * It consists of a 'div' enclosing the entire entity, name/id = idcombo.
 * Within this div are the following:
 * 	label for the select list, name/id = 'lab' + idcombo.
 * 	select list, name/id = 'sel' + idcombo.
 * 
 * When you start a combo, you provide the id (iddata) of the parent data.
 * Two fields are expected based on this id.
 * 	input field, name/id = iddata. This is where either the select option is stored
 * 	or the "other" text is stored.
 * 	hidden input field, name/id = 'id' + iddata. This is where the selected index is 
 * 	stored. If the "other" option is used, the index is 0.
 */
function startCombo(idcombo, iddata) {
	//Occurs when the button is pressed to choose from the select list.
	var d = document.getElementById(iddata);
	var i = document.getElementById('id' + iddata);
	var c = document.getElementById("sel" + idcombo);
	saveCombo(d);//store the current input fields
	var s = c.size;
	c.size = 0;
	c.options[c.selectedIndex].selected = false;
	//look for the text in the options to set selected index
	c.options[findComboMatch(c, scombo)].selected = true;
	c.size = s;
	//Now, move it under the text field
//TODO: this findpos doesn't work in firefox
//	var pos = findPos(d);
	var pos = makePos("divData", d);
	c.parentNode.style.left = pos[0] + "px";
	c.parentNode.style.top = pos[1] + "px";
	c.parentNode.style.position = "absolute";
	c.parentNode.style.display = "block";
	c.focus();
}
function endContact(e) {
	endCombo(e);
	validateContact(sparcombo);
}
function endCombo(e) {
	//Occurs when a selection is made in the related select list.
	//Save the combo text in the text input field.
	//Save the combo value in the related id field.
	//close the combo and move focus back to the text input field.
	var t = document.getElementById(sparcombo);
	var i = document.getElementById('id' + sparcombo);
	i.value = e.options[e.selectedIndex].value;
	t.value = e.options[e.selectedIndex].text;
	e.parentNode.style.display = "none";
	t.focus();
}
function findComboMatch(c, t) {
	//Searches the options in select combo c for text of t and returns index for first match
	//Returns 0 for no match.
	var m = 0;
	//look for the text in the options to set selected index
	for (var i=1; i<c.options.length; i++) {
		if (c.options[i].text == t) {
			m = i;
			break;
		}
	}
	return m;
}
function updCombo(e, cid) {
	//Occurs when the text part of the combo is changed by the user.
	//Set the related id field if the text matches one of the combo entries.
	//e is the text field
	//cid is the id of the combo
	var i = document.getElementById('id' + e.id);
	var c = document.getElementById('sel' + cid);
	m = findComboMatch(c,e.value);
	i.value = c.options[m].value;
	
}
function saveCombo(e, doSelect) {
	//save the current text and id data prior to opening the combo select
	//also save the id of the related input field.
	scombo = getElemVal(e);
	sidcombo = getVal('id' + e.id);
	sparcombo = e.id;
	if (doSelect) {
		e.select();
	}
}
function saveVal(e) {
	if (e.className.indexOf("recalc") == -1) {
		return;
	}
	sval = toNum(e.value);
	e.select();
}
function addAmt(id, amt) {
	var e = document.getElementById(id);
	//Do not add to input fields, only to plain cells.
	if (e.tagName.toLowerCase() != "input") {
		val = parseInt(toNum(e.innerHTML)) + amt;
		e.innerHTML = toDisplay(val, e.className);
	}
}
function replaceAmt(id, amt) {
	var e = document.getElementById(id);
	if (e.tagName.toLowerCase() == "input") {
		e.value = toDisplay(amt, e.className);
	} else {
		e.innerHTML = toDisplay(amt, e.className);
	}
}
function getParent(id) {
	var t = id.substr(0,id.length-1);
	t = id.substr(0,t.lastIndexOf(":")+1);
	return t;	
}
function fixDistributions(e, diff) {
	//
	//Called when a program level vote is cast or
	//	the children change causing a change in distribution.
	//DOES NOT APPLY TO SUPPLEMENTALS
	//Changes the distributions for the children
	//PHP equivalent
	//	foreach ($prjs as &$prj) {
	//		$pgm = &$pgms[getParent($prj['code'])];
	//		if ($pgm['vdis'] > 0) {
	//			$prj['fdis'] = $prj['fdis_nov'] + $pgm['vdis']*($prj['fdis_nov']/$pgm['fdis_nov']);
	//			$prj['fpct'] = $prj['fdis']/$prj['reqfund'];
	//			$prj['freq'] = $prj['reqfund']-$prj['fdis'];
	//		}
	//	}
	var lid, lparent;
	if (e.id.indexOf("vsub") != -1) {
		lid = 'vsub_';
	} else 	if (e.id.indexOf("vsup") != -1) {
		lid = 'vsup_';
	} else {
		return;
	}
	if (e.className.indexOf("prj") != -1) {
		//modify the saved distribution info
		lparent = getParent(e.id);
		tVals[e.id.replace(lid,'fdis_nov_')] += diff;
		tVals[lparent.replace(lid,'fdis_nov_')] += diff;
		diff = 0;//This will redistribute the program funds without changing them
	} else if (e.className.indexOf("pgm") != -1){
		lparent = e.id;
	}
	//Now iterate over the children and update the values.
	lparent = lparent.replace(lid,'');
	//save the distribution change.
	tVals['vdis_all_'+lparent] += diff;
	pgm_vdis = tVals['vdis_all_'+lparent]; 
	pgm_nov = tVals['fdis_nov_'+lparent];
//	pgm_freq = getVal('reqfund_'+lparent);
	pgm_freq = tVals['reqfundopen_'+lparent];
	for (i in tVals['children_'+lparent]) {
		lchild = lparent + tVals['children_'+lparent][i];
		prj_nov = tVals['fdis_nov_'+lchild];
		prj_freq = getVal('reqfund_'+lchild);
		prj_fdis = getVal('fdis_'+lchild);
		if (pgm_nov == 0) {
			//We have voted a distribution but no votes have been cast for the projects
			//Use Funds Required to distribute
			if (pgm_freq > 0.01) {
				nfdis = prj_nov + roundNumber((pgm_vdis*prj_freq/pgm_freq),2);
			} else {
				nfdis = 0;
			}
		} else {
			nfdis = prj_nov + roundNumber((pgm_vdis*prj_nov/pgm_nov),2);
		}
		if (prj_freq > 0.01) {
			nfpct = prj_freq == 0? 0: nfdis/prj_freq;
		} else {
			nfpct = 0;
		}
		nfreq = prj_freq - nfdis;
		replaceAmt("fdis_" + lchild, nfdis);
		replaceAmt("fpct_" + lchild, nfpct);
		replaceAmt("freq_" + lchild, nfreq);
	}
}
function bubbleUp(id, diff) {
	//
	//This takes a change in a vote field and distributes it wherever it also applies.
	//It gets added to all parents (except a pgm subscription which is an enterable distribution),
	//It gets factored into funds distribution, % funded and remaining (except a supplemental vote),
	//It gets factored into the company subscription summary (adjSubscription),
	//
	lineage = id.split(":");
	lparent = "";
	issup = (id.indexOf("vsup") != -1);
	for (var i=0; i<lineage.length-1; i++) {
		lparent += lineage[i] + ":";
		addAmt(lparent, diff);
		//Adjust the other columns (except for supplemental changes)
		if (true || !issup) {
			lbase = lparent.substring(lparent.indexOf("_"));
			addAmt("freq" + lbase, -diff);
			addAmt("fdis" + lbase, diff);
			a = getVal("reqfund" + lbase);
			b = getVal("fdis" + lbase);
			replaceAmt("fpct" + lbase, b/a);
		}
	}
	//Adjust the Subscription totals
	adjSubscription(id, diff);
}
function adjSubscription(id, diff) {
	if (id.indexOf("vsub") != -1 || id.indexOf("vsub") != -1) {
		//Adjust Voted Subscription
		addAmt("subvote", diff);
		addAmt("subrem", -diff);
	} if (id.indexOf("vsup") != -1) {
		//Adjust Supplemental Amount
		addAmt("supvote", diff);
	}
}
function validateContact(id) {
	//Be sure there is a contact if there are votes.
	var b = id.substr(id.indexOf("_"));
	var e = document.getElementById("contact"+b);
	e.className = e.className.replace(' invalid', '');
	var c = trim(getElemVal(e, false));
	if (e.tagName.toLowerCase().indexOf('input') == -1) {
		return true;
	} else if (c != "" || (getInpVal("vsub"+b) == 0 && getInpVal("vsup"+b) == 0)) {
		//Contact is OK
		return true;
	} else {
		e.className += " invalid";
		return false;
	}
}
function validate(fname) {
	if (nneg) {
		alert("Negative Votes Are not Permitted");
		doNotBlock = true;
		return false;
	}
	if (getVal("subrem") < 0) {
		msg = "ERROR!" + "\n";
		msg += "\n" + "You have voted more than your company's Subscription Funds available.";
		msg += "\n" + "Please reduce the Subscription Funds Voted accordingly."; 
		msg += "\n" + "\n" + "Should you wish to vote amounts higher than your company's Subscription Funds,";
		msg += "\n" + "you may enter those votes in your company's Supplemental Funds Voted column.";
		alert(msg);
		doNotBlock = true;
		return false;
	}
	f = document.getElementById(fname);
	contok = true;
	for (i = 0; i < f.elements.length; i++) {
		e = f.elements[i];
		if (e.id.substr(0,7) == 'contact') {
			if (!validateContact(e.id)) {
				contok = false;
			}
		}
	}
	if (!contok) {
		msg = "ERROR!" + "\n";
		msg += "\n" + "You have voted funds to project without designating a potential";
		msg += "\n" + "project team representative from your company.  You must provide"; 
		msg += "\n" + "a point of contact for each project you vote funds to.";
		alert(msg);
		doNotBlock = true;
		return false;
	}
	if (getVal("subrem") > 0) {
		msg = "WARNING!" + "\n";
		msg += "\n" + "Your company still has Subscription Funds remaining to vote.";
		msg += "\n" + "Please hit CANCEL if you wish to vote these dollars now,"; 
		msg += "\n"+ "or hit OK to submit the ballot as is.";
		if (!confirm(msg)) {
			doNotBlock = true;
			return false;
		}
	}
	doNotBlock = false;
	return true;
	
}
function goodLogin(data, status) {
	//If we get a good ajax return, we will come here.
	//clean up the ajax and act on the returned data
	var e;
	var ret = qdbticket(data, status);
	if (ret.errcode != 0) {
		//unblock
		myUnBlockUI();
		//report problem
		e = document.getElementById("qdberror");
		e.innerHTML = ret.errtext;
	} else {
		//post back the ticket and userid
		e = document.getElementById("ticket");
		e.value = ret.ticket;
		e = document.getElementById("userid");
		e.value = ret.userid;
//		e = document.getElementById("qdberror");
//		e.innerHTML = "ticket: " + ret.ticket + ", userid: " + ret.userid;
		var frm = e.parentNode;
		frm.action = "vote.php?" + window.location.search.substring(1);
		frm.submit();
	}
	return false;
}
function gotoVote(byr, dbtype) {
	var h = "vote.php?byr=" + byr + "&dbtype=" + dbtype;
	window.location = h;
	return false;
}
function voteLock(byr, dbtype, underpct) {
	var h = "vote.php?byr=" + byr + "&dbtype=" + dbtype + "&fcn=votelock";
	var e = document.getElementById(underpct);
	if ("" + e.value != "") {
		h += "&under=" + e.value;
	}
	window.location = h;
	return false;
}
function lockVote(byr, dbtype, ltype, prj) {
	
	var msg = "WARNING!" + "\n";
	var mok;
	var fcn;
	switch (ltype) {
		case "unlock":
			msg += "\n" + "Project " + urldecode(prj) + " will be UNLOCKED for " + byr + ".";
			mok = " unlock the project.";
			fcn = "unlock";
			break;
		case "lock":
			msg += "\n" + "Project " + urldecode(prj) + " will be LOCKED for " + byr + ".";
			msg += "\n" + "All program level votes related to this project will be distributed.";
			mok = " lock the project.";
			fcn = "retfunds&rettype=none";
			break;
		case "over":
			msg += "\n" + "Project " + urldecode(prj) + " will be LOCKED for " + byr + ".";
			msg += "\n" + "All program level votes related to this project will be distributed.";
			msg += "\n" + "All overfunding will be returned to those who have voted for this project.";
			mok = " lock the project and return the funds.";
			fcn = "retfunds&rettype=over";
			break;
		case "all":
		case "under":
			msg += "\n" + "Project " + urldecode(prj) + " will be LOCKED for " + byr + ".";
			msg += "\n" + "All program level votes related to this project will be distributed.";
			msg += "\n" + "All funding will be returned to those who have voted for this project.";
			mok = " lock the project and return the funds.";
			fcn = "retfunds&rettype=under";
			break;
	}
	msg += "\n" + "Please hit CANCEL if you do not want to do this,"; 
	msg += "\n"+ "or hit OK to" + mok;
	msg += "\n";
	if (confirm(msg)) {
		doNotBlock = false;
		window.location = "vote.php?byr=" + byr + "&dbtype=" + dbtype + "&fcn=" + fcn + "&lcode=" + prj;
		return false;
	} else {
		doNotBlock = true;
	}
	return false;
}
function primeVote(byr, dbtype, prj) {
	
	var msg = "WARNING!" + "\n";
	msg += "\n" + "All " + byr + " voting for Project " + urldecode(prj) + " will be UPLOADED to PRIME.";
	msg += "\n   Program will be created if necessary.";
	msg += "\n   Project will be created if necessary.";
	msg += "\n   Voted funding will be transferred.";
	msg += "\n   Team assignments will be made for persons known to PRIME.";
	mok = " upload the voting.";
	msg += "\n" + "Please hit CANCEL if you do not want to do this,"; 
	msg += "\n"+ "or hit OK to upload the voting.";
	msg += "\n";
	if (confirm(msg)) {
		doNotBlock = false;
		window.location = "vote.php?byr=" + byr + "&dbtype=" + dbtype + "&fcn=toprime&phase=1&prjcode=" + prj;
		return false;
	} else {
		doNotBlock = true;
	}
	return false;
}
function distrVote(byr, dbtype, pgm) {
	
	var msg = "WARNING!" + "\n";
	if (pgm == "all") {
		msg += "\n" + "All program level subscription votes for " + byr + " will be distributed to the project level.";
	} else {
		msg += "\n" + urldecode(pgm) + " program level subscription votes for " + byr + " will be distributed to the project level.";
	}
	msg += "\n" + "Please hit CANCEL if you do not want to do this,"; 
	msg += "\n"+ "or hit OK to distribute the votes.";
	msg += "\n";
	if (confirm(msg)) {
		doNotBlock = false;
		window.location = "vote.php?byr=" + byr + "&dbtype=" + dbtype + "&fcn=distr&distrcode=" + pgm;
		return false;
	} else {
		doNotBlock = true;
	}
	return false;
}
function castVote() {
	if (validate("castvote")) {
		document.castvote.submit();
	}
	return false;
}
function updContact(e, cid) {
	updCombo(e, cid);
	validateContact(e.id);
}
function updVal(e) {
	validateContact(e.id);
	if (e.className.indexOf("recalc") == -1) {
		return;
	}
	nval = parseInt(toNum(e.value));
	if (nval < 0) {
		if (e.className.indexOf("invalid") == -1) {
			e.className += " invalid";
			nneg++;
		}
	} else {
		if (e.className.indexOf("invalid") != -1) {
			e.className = e.className.replace(' invalid', '');
			nneg--;
		}
	}
	diff = nval - parseInt(sval);
	//move the change up and across the totals
	bubbleUp(e.id, diff);
	//fix the distributions
	fixDistributions(e, diff);
	//save the new value
	e.value = toDisplay(e.value, e.className);
}
//////////////////////////////////////////////////////////////////////////////////////////////
//These are general use functions (pretty much)
//////////////////////////////////////////////////////////////////////////////////////////////
function toDisplay(num, cl) {
	if (cl.indexOf('pct') != -1) {
		num = toPct(num, true);
	} else if (cl.indexOf('money') != -1) {
		num = toMoney(num, false);
	} else {
		//val = tVals[t];
	}
	return num;
}
function toPct(num, addSym) {
	num = toNum(num);
	if(isNaN(num)) {
		num = "0";
	}
	num = Math.round(num*100);
	if (addSym) {
		num += "%";
	}
	return num;
}
function toMoney(num, addCents) {
	num = toNum(num);
	if(isNaN(num)) {
		num = "0";
	}
	sign = (num == (num = Math.abs(num)));
	num = Math.floor(num*100+0.50000000001);
	cents = num%100;
	num = Math.floor(num/100).toString();
	if(cents<10) {
		cents = "0" + cents;
	}
	for (var i = 0; i < Math.floor((num.length-(1+i))/3); i++) {
		num = num.substring(0,num.length-(4*i+3))+','+
			num.substring(num.length-(4*i+3));
	}
	if (addCents) {
		num = (((sign)?'':'-') + '$' + num + '.' + cents);
	} else {
		num = (((sign)?'':'-') + '$' + num);
	}
	return num;
}
function trim(str, chars) {
	return ltrim(rtrim(str, chars), chars);
}
function ltrim(str, chars) {
	chars = chars || "\\s";
	return str.replace(new RegExp("^[" + chars + "]+", "g"), "");
} 
function rtrim(str, chars) {
	chars = chars || "\\s";
	return str.replace(new RegExp("[" + chars + "]+$", "g"), "");
}
function getStyle(el,styleProp,isEl) {
	if (isEl) {
		var x = el;
	} else {
		var x = document.getElementById(el);
	}
	if (x.currentStyle)
		var y = x.currentStyle[styleProp];
	else if (window.getComputedStyle)
		var y = document.defaultView.getComputedStyle(x,null).getPropertyValue(styleProp);
	return y;
}
function toggleMe(id) {
	e = document.getElementById(id);
	e.style.display = getStyle(id,'display') == "none" ? "block" : "none";
}
function makePos(eid, c) {
	//Places the position halfway down from eid's top and c's width in.
	var e = document.getElementById(eid);
	curleft = parseInt(e.offsetLeft + e.offsetWidth - c.offsetWidth);
	curtop = parseInt(e.offsetTop + e.offsetHeight/2);
	return [curleft,curtop];
}
//TODO: this findpos doesn't work in firefox
//even jquery doesn't work
function findPos(e) {
	var lp="";
	var curleft = curtop = 0;
	if (e.offsetParent) {
		do {
			curleft += e.offsetLeft - e.scrollLeft;
			curtop += e.offsetTop - e.scrollTop;
//			lp += "<br /><hr />offsetLeft:"+e.offsetLeft+" offsetTop:"+e.offsetTop+"<br />";
//			lp += "scrollLeft:"+e.scrollLeft+" scrollTop:"+e.scrollTop+"<br />";
		} while (e = e.offsetParent);
	}
//	jserr(lp);
	return [curleft,curtop];
}
function toNum(val) {
	val = val.toString().replace(/\$|\,/g,'');
	if(isNaN(val) || val == "") {
		val = "0";
	}
	return val;
}
function roundNumber(num, dec) {
	var result = Math.round( Math.round( num * Math.pow( 10, dec + 1 ) ) / 10 ) / Math.pow(10,dec);
	return result;
}
function getElemVal(e, onlyInput) {
	var isInput = e.tagName.toLowerCase() == "input";
	if (e.className.indexOf('money') == -1) {
		if (isInput) {
			return trim(e.value, " ");
		} else if (onlyInput) {
			return "";
		} else {
			return trim(e.innerHTML, " ");
		}
	} else {
		if (isInput) {
			return toNum(e.value);
		} else if (onlyInput) {
			return 0;
		} else {
			return toNum(e.innerHTML);
		}
	}
}
function getVal(id) {
	var e = document.getElementById(id);
	return getElemVal(e, false);
}
function getInpVal(id) {
	var e = document.getElementById(id);
	return getElemVal(e, true);
}
function jserr(msg) {
	var jerr=document.getElementById("jerr");
	jerr.innerHTML = msg;
}
function deleteVotes(byr, dbtype) {

	var msg = "WARNING!" + "\n";
	msg += "\n" + "You have asked that ALL " + byr + " VOTES BE DELETED.";
	msg += "\n" + "THIS CANNOT BE UNDONE!";
	var msg1 = "\n\nARE YOU REALLY SURE!!!\n";
	var msg2 = "\n" + "Please hit OK if you wish to delete all of these votes,"; 
	msg2 += "\n"+ "or hit CANCEL to keep these votes.";
	if (!confirm(msg + msg2)) {
		doNotBlock = true;
		return false;
	}
	if (!confirm(msg + msg1 + msg2)) {
		doNotBlock = true;
		return false;
	}
	doNotBlock = false;
	window.location = "vote.php?byr=" + byr + "&dbtype=" + dbtype + "&fcn=deletevotes";
	return false;
}
function copyToTest(byr, dbtype) {

	var msg = "WARNING!" + "\n";
	msg += "\n" + "You have asked to copy the CURRENT " + byr + " votes to the TEST votes.";
	msg += "\n" + "This will delete any " + byr + " TEST votes.";
	msg += "\n" + "THIS CANNOT BE UNDONE!";
	var msg1 = "\n\nARE YOU REALLY SURE!!!\n";
	var msg2 = "\n" + "Please hit OK if you wish to COPY these votes,"; 
	msg2 += "\n"+ "or hit CANCEL to keep the current test votes.";
	if (!confirm(msg + msg2)) {
		doNotBlock = true;
		return false;
	}
	if (!confirm(msg + msg1 + msg2)) {
		doNotBlock = true;
		return false;
	}
	doNotBlock = false;
	window.location = "vote.php?byr=" + byr + "&dbtype=" + dbtype + "&fcn=copydb";
	return false;
}
function urldecode( 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 (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
	// %          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 histogram = {}, 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 histogram is identical to the one in urlencode.
	histogram["'"]   = '%27';
	histogram['(']   = '%28';
	histogram[')']   = '%29';
	histogram['*']   = '%2A';
	histogram['~']   = '%7E';
	histogram['!']   = '%21';
	histogram['%20'] = '+';
	histogram['\u00DC'] = '%DC';
	histogram['\u00FC'] = '%FC';
	histogram['\u00C4'] = '%D4';
	histogram['\u00E4'] = '%E4';
	histogram['\u00D6'] = '%D6';
	histogram['\u00F6'] = '%F6';
	histogram['\u00DF'] = '%DF'; 
	histogram['\u20AC'] = '%80';
	histogram['\u0081'] = '%81';
	histogram['\u201A'] = '%82';
	histogram['\u0192'] = '%83';
	histogram['\u201E'] = '%84';
	histogram['\u2026'] = '%85';
	histogram['\u2020'] = '%86';
	histogram['\u2021'] = '%87';
	histogram['\u02C6'] = '%88';
	histogram['\u2030'] = '%89';
	histogram['\u0160'] = '%8A';
	histogram['\u2039'] = '%8B';
	histogram['\u0152'] = '%8C';
	histogram['\u008D'] = '%8D';
	histogram['\u017D'] = '%8E';
	histogram['\u008F'] = '%8F';
	histogram['\u0090'] = '%90';
	histogram['\u2018'] = '%91';
	histogram['\u2019'] = '%92';
	histogram['\u201C'] = '%93';
	histogram['\u201D'] = '%94';
	histogram['\u2022'] = '%95';
	histogram['\u2013'] = '%96';
	histogram['\u2014'] = '%97';
	histogram['\u02DC'] = '%98';
	histogram['\u2122'] = '%99';
	histogram['\u0161'] = '%9A';
	histogram['\u203A'] = '%9B';
	histogram['\u0153'] = '%9C';
	histogram['\u009D'] = '%9D';
	histogram['\u017E'] = '%9E';
	histogram['\u0178'] = '%9F';

	for (unicodeStr in histogram) {
		hexEscStr = histogram[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;
}

