// handler for "mouseover" event
function riff_over(e) {
	e.stopPropagation();
	if(typeof preloads == 'undefined'){return false;}
	var source = e.target;
	var sname = source.src.replace(/.*\/(\w+)_[a-z]{2,3}\.([a-z]{3})$/,"$1");
	source.src = preloads['o'+sname].src; 
	return true;
}

// handler for "mouseout" event
function riff_out(e) {
	e.stopPropagation();
	if(typeof preloads == 'undefined'){return false;}
	var source = e.target;
	var sname = source.src.replace(/.*\/(\w+)_[a-z]{2,3}\.([a-z]{3})$/,"$1");
	source.src = preloads['n'+sname].src; 
	return true;
}

// handler for buttonbar "click" event
function riff_show(e) {
	e.stopPropagation();
	var source = e.target;
	return riff_display(source.id);
}

function riff_display(button_id) {
	var clayer = document.forms[0].currLayer;
	var old = document.getElementById(clayer.value);
	if (!old) { return false; }
	var source = document.getElementById(button_id);
	var layerID = button_id + 'Area';
	var rxLayer = /^Layer[1-9]{1}Area$/;
	var rxButton = /.*\/(\w+)_[a-z]{2,3}\.([a-z]{3})$/;
	var divs = document.getElementById('content').getElementsByTagName('div');
	
	for (var n=0; n < divs.length; n++) {
		if (!divs[n].id || !(rxLayer.test(divs[n].id))) { continue; }
		if (divs[n].id != layerID) {
			divs[n].style.display = "none";
		} else {
			divs[n].style.display = "block";
		}
	}
// Make old active button normal
	var oname = old.src.replace(rxButton,"$1");
	old.src = preloads['n'+oname].src;
	Event.add(old, 'mouseover', riff_over);
	Event.add(old, 'mouseout', riff_out);
	Event.add(old, 'click', riff_show);
// Make new active button
	var sname = source.src.replace(rxButton,"$1");
	source.src = preloads['a'+sname].src;
	Event.remove(source, 'mouseover', riff_over);
	Event.remove(source, 'mouseout', riff_out);
	Event.remove(source, 'click', riff_show);
// Set current layer
	clayer.value = button_id;
	return true;
}

// called at end of init(), establishes default layer visibility
function riff_setup() {
// obtain current layer from hidden field
	var clayer = document.forms[0].currLayer; if (!clayer) return;
	if (clayer.value == '') clayer.value = 'Layer1';
// get default layer and button
	var showLayer = $(clayer.value+'Area');
	var defaultButton = $(clayer.value);
	if (showLayer) { showLayer.style.display = "block"; }
	if (!defaultButton || !defaultButton.src) { return false; }
// parse name, modify default button source
	var dname = defaultButton.src.replace(/.*\/(\w+)_[a-z]{2,3}\.([a-z]{3})$/,"$1");
	defaultButton.src = preloads['a'+dname].src;
// remove events from default button
	Event.remove(defaultButton, 'mouseover', riff_over);
	Event.remove(defaultButton, 'mouseout', riff_out);
	Event.remove(defaultButton, 'click', riff_show);
	return true;
}

// initialises RIFF framework; called onload
function riff_init() {
	preloads = {}; // GLOBAL
	var x = document.getElementById('content').getElementsByTagName('img');
	var xid,xname,xsrc,xdir,xtn,bbar,cid;
	// Regexen
	var brx = /^Layer[1-9]{1}$/; // buttonbar id
	var orx = /^(\D*)[0-9]?$/; 	 // img_click array key
	var nrx = /.*\/(\w+)_[a-z]{2,3}\.([a-z]{3})$/; 	// name of image file
	
	// boolean, if "img_click" hash is present
	var hash_present = (typeof img_click != 'undefined');
	
	for (var i=0; i < x.length; i++) 
	{
		xid = x[i].id;
		if (!xid) { continue; } // if it doesn't have an id, skip this element
		xsrc = x[i].src;
		xname = xsrc.replace(nrx,"$1");
		xdir = xsrc.substring(0,xsrc.lastIndexOf('/')+1);
		xtn  = xsrc.substring(xsrc.lastIndexOf('.')+1);
		bbar = brx.test(xid);
		
		preloads['n'+xname] = new Image;
		preloads['n'+xname].src = xdir + xname + '_up.' + xtn;   // Normal
		preloads['o'+xname] = new Image;
		preloads['o'+xname].src = xdir + xname + '_dwn.' + xtn;  // Over
		preloads['a'+xname] = new Image;
		preloads['a'+xname].src = xdir + xname + '_off.' + xtn;  // Active tab, blue
		
		Event.add(x[i], 'mouseover', riff_over);
		Event.add(x[i], 'mouseout', riff_out);
		
		if (bbar) {
			Event.add(x[i], 'click', riff_show);
		} else if (hash_present) {
			cid = xid.replace(orx,"$1");
			if (img_click[cid]) { Event.add(x[i], 'click', img_click[cid]); }
		}
	}
	// load form_event array, if it exists
	DESTINY.Event.load((typeof form_event != 'undefined') ? form_event : null);
	
	return riff_setup();
}

// capability testing preferred, convenience object (parsing userAgent where it can't be helped)
Browser = {
    IE:     !!(window.attachEvent && !window.opera),
    Opera:  !!window.opera,
    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1
};

/* FireBug (http://getfirebug.com/) */
if (!window.console || !console.firebug) {
	var names = ["debug", "info", "warn", "error", "assert", "profile", "profileEnd",
		"dir", "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace"];
	if (window.console && !console.firebug) {
		names.splice(0,7); // webkit supports basic methods
	} else {
		window.console = {}; // IE and Opera need help
		window.console.log = function() { alert(arguments.concat('\n')); };
	}
	for (var i = 0; i < names.length; ++i) window.console[names[i]] = function() {};
}

// shortcut document.getElementById(), lifted from prototype.js (slightly modified)
function $(element) {
	if (arguments.length > 1) {
		for (var i = 0, elements = [], length = arguments.length; i < length; i++)
			elements.push($(arguments[i]));
		return elements;
	}
	if ('string' == typeof element)
		element = document.getElementById(element);
	return element;
}

// 'string'.strip(), remove whitespace surrounding text
String.prototype.strip = function() {
	return this.replace(/^\s+/, '').replace(/\s+$/, '');
};

// prototype the indexOf method for arrays; this method is native in Gecko 1.8+
if (!Array.prototype.indexOf) {
	Array.prototype.indexOf = function(searchElement, fromIndex) {
		var l = this.length, i = 0;
		if (fromIndex) {
			i = fromIndex;
			if (i < 0) { i += l; if (i < 0) { i = 0; } }
		}
		while (i < l) {
			if (this[i] === searchElement) { return i; }
			i++;
		}
		return -1;
	};
}

/**	document.getElementsByClassName
 *		@param	cls		(string) className
 *		@param	node	(object) document, or specific DOM parent [optional] *OR* id (string)
 *		@param	tag		(string) specific tagName, or "*" for all [optional]
 *	@return	an array of matching element nodes
**/
if (typeof document.getElementsByClassName == 'undefined') document.getElementsByClassName = function(cls, node, tag) {
	var classElements = [];
	var t = tag || "*", n = (node)? $(node):document.body;
	var els = n.getElementsByTagName(t.toUpperCase());
	var pattern = new RegExp("(^|\\s)"+cls+"(\\s|$)");
	for (var i = 0, elm; elm = els[i]; i++){
		if (pattern.test(elm.className)) classElements.push(elm);
	}
	return classElements;
};

/*	buttonbar replacement

======= at the end of the form's script file =========================

DESTINY.clicks = {
	'id': handler, // no arguments to handler
	'id': function() { return handler('argument'); }, // try to avoid
	...
};

DESTINY.events = [
	{ id: 'unique', type: 'click/change/focus/blur/...', fn: handler },
	...
];

DESTINY.initialize();
// functions passed to initialize will be called onload, in order

======= in place of the old "buttonbar" ==============================

<ul id="tabs">
	<li>
		<a href="#Layer1">Layer 1 Title</a>
		<a href="#Layer2">Layer 2 Title</a>
		<a href="#Layer3">Layer 3 Title</a>
		<a href="#Layer4">Layer 4 Title</a>
		...
	</li>
	<!--- You may optionally have multiple rows --->
	<li>
		...
	</li>
	...
</ul>
	
*/
DESTINY = {
	// 'instance' variables, populated in this.loader()
	layer_areas: [],
	layer_tabs: [],
	preloads: {},
	
	tab_wrapper: null,
	max_tab_offset: null,
	current_layer_obj: null,
	
	// optional, provided by script prior to calling this.initialize()
	clicks: null,
	events: null,
	
	initialize: function() {
		// load the whole shebang
		this.Event.add(window, 'load', DESTINY.loader);
		
		// if functions passed, load them too
		if (arguments.length) {
			for (var a = 0; a < arguments.length; a++) {
				if ('function' == typeof arguments[a])
					this.Event.add(window, 'load', arguments[a]);
			}
		}
	},
	
	loader: function(e) {
		
		/*	load form events before tabs to allow 'overloading' a tab's onclick
		
			To 'overload' a tab's onclick (call a function before the tab is switched),
			simply give the tab anchor an id and reference that id in the events array
			
			This will cause it to fire before tab_onclick, as it is attached first
		*/
		Event.load(DESTINY.events);
		
		// load navigation tabs
		DESTINY.load_tabs();
		
		// setup image onclicks
		DESTINY.load_images();
	},
	
	load_tabs: function() {
		var self = this;
		
		self.tab_wrapper = $('tabs');
		
		// if the tab bar doesn't exist, don't bother doing tab stuff
		if (self.tab_wrapper) {
			
			// load instance variables (layerXArea divs, tab anchors)
			var divs = $('content').getElementsByTagName('div');
			
			for (var d = 0, div; div = divs[d]; d++) {
				// ignore divs without an id
				if (!div.id) continue;
				
				// if the id matches the pattern, store the div in a sparse array
				var dmatch = /^Layer([1-9])Area$/.exec(div.id);
				if (!!dmatch) self.layer_areas[dmatch[1]] = div;
			}
			
			// obtain tabs, preparing them if necessary
			var tabs = DSI.prepare_tabs(),
				max_width = self.max_tab_offset,
				wrapper_width = self.tab_wrapper.offsetWidth,
				TabFS  = parseInt(Get.style(tabs[0],'font-size'), 10),
				max_em = parseFloat((max_width / TabFS).toFixed(2));
			
			// set tab widths to max tab width
			for (var n = 0; n < tabs.length; n++) {
				tabs[n].style.width = max_em + "em";
			}
			
			var li = self.tab_wrapper.getElementsByTagName('li')[0];
			
			var total_tab_width = (tabs.length * max_width) +
				(tabs.length * ((Browser.IE) ? tabs.length : 2));
			
			if (total_tab_width < wrapper_width) {
				// add right padding to center buttons
				var li_pad_percent = (
					(((wrapper_width - total_tab_width) / 2) / wrapper_width) * 100
				).toFixed(2) + "%";
				
				li.style.paddingRight = li_pad_percent;
			} else {
				// widen wrapper to prevent line break between tabs
				var DocFS = parseInt(Get.style($('content'),'font-size'), 10),
					wrap_em = (
						((total_tab_width - wrapper_width) / DocFS) +
						(wrapper_width / DocFS) +
						0.25 /* weird fudge, avoid line break in Gecko */
					).toFixed(2) + 'em';
				
				$('content').style.width = wrap_em;
			}
			
			// event handler for tab navigation
			Event.add(li, 'click', DSI.tab_onclick);
			
			// append float clearing li
			DSI.Class.add(self.tab_wrapper.appendChild(document.createElement('li')), 'clear');
			
			// determine the current visible layer, defaulting to 'Layer1'
			self.current_layer_obj = document.forms[0]['currLayer'] || { value: "Layer1" };
			
			// show the default or specified layer
			DSI.toggle_layer();
			
			tabs = li = null; // cleanup
		} // end if (tabs)
		
	},
	
	load_images: function() {
		var self = this;
		
		var clicks = self.clicks, images = $('content').getElementsByTagName("img"),
			pre = self.preloads,  mouse_ptr = DSI.img_mouse_handler;
		
		/*	All rollover images MUST have an id, and it MUST be unique
			The standard way to do this with images of the same name is to
				append a number to the id in sequence (submit must always be submit1)
		*/
		for (var i = 0, img; img = images[i]; i++) {
			if (!img.id) continue;
			
			// the key is the id without the number at the end
			var key = img.id.match(/^(\D+)/)[1];
			
			// if the key exists in the clicks array, attach event handler
			if (clicks && clicks[key]) Event.add(img, 'click', clicks[key]);
			
			// only preload the rollover image once
			if (!pre[key]) {
				var path = img.src.split('/');
				var file = path.pop().split("."); // 0 = filename, 1 = extension
				
				// each key of pre[] is an object with an 'up' and 'dwn' property
				var pk = pre[key] = {};
				
				pk.up = {}; // pk.up is a simple object, the image is already loaded
				pk.up.src = img.src;
				
				// this transforms 'file_up.gif' into 'file_dwn.gif'
				path.push(file[0].substring(0, file[0].lastIndexOf('_')) + '_dwn.' + file[1]);
				
				pk.dwn = new Image; // a new Image 'preloads' into the browser cache
				pk.dwn.src = path.join('/');
			}
			
			// add event listeners for rollover states of all images
			Event.add(img, 'mouseover', mouse_ptr);
			Event.add(img, 'mouseout',  mouse_ptr);
		}
		
		images = img = mouse_ptr = null; // cleanup
	},
	
	// prevent trailing-comma issues, keep at end
	z: null
};

/*		addEvent() Reloaded
 *
 *	written by Dean Edwards, 2005
 *	with input from Tino Zijdel - crisp@xs4all.nl
 *
 *	http://dean.edwards.name/weblog/2005/10/add-event/
 *	http://therealcrisp.xs4all.nl/upload/addevent_dean.html

	Moved to DESTINY object 9/27/2007
	
**/
Event = DESTINY.Event = {
	add: function(element, type, handler) {
		element = $(element);
		if (element.addEventListener)
			element.addEventListener(type, handler, false);
		else {
			if (!handler.$$guid) handler.$$guid = DESTINY.Event.guid++;
			if (!element.events) element.events = {};
			var handlers = element.events[type];
			if (!handlers){
				handlers = element.events[type] = {};
				if (element['on' + type]) handlers[0] = element['on' + type];
				element['on' + type] = DESTINY.Event.handle;
			}
			
			handlers[handler.$$guid] = handler;
		}
	},
	guid: 1,
	
	remove: function(element, type, handler) {
		if (element.removeEventListener)
			element.removeEventListener(type, handler, false);
		else if (element.events && element.events[type] && handler.$$guid)
			delete element.events[type][handler.$$guid];
	},
	
	handle: function(event) {
		event = event || DESTINY.Event.fix(window.event);
		var returnValue = true;
		var handlers = this.events[event.type];

		for (var i in handlers){
			if (!Object.prototype[i]){
				this.$$handler = handlers[i];
				if (this.$$handler(event) === false) returnValue = false;
			}
		}

		if (this.$$handler) this.$$handler = null;

		return returnValue;
	},
	
	fix: function(event) {
		event.preventDefault  = DESTINY.Event.fix.preventDefault;
		event.stopPropagation = DESTINY.Event.fix.stopPropagation;
		event.target = event.srcElement;
		switch (event.type) {
			case 'mouseover': event.relatedTarget = event.fromElement;	break;
			case 'mouseout' : event.relatedTarget = event.toElement;	break;
		}
		return event;
	},
	
	load: function(events) {
		if (!events || events.length == 0) return;
		
		for (var e = 0, ev; ev = events[e]; e++) {
			if (typeof ev['id'] == 'string') {
				// wrap in try/catch blocks to avoid erroring on id typos or nonexistence
				try { this.add($(ev['id']), ev['type'], ev['fn']); } catch(er){ };
			} else {
				// an array of strings, each id getting the same type and handler
				var f = ev['id'].length;
				while (f--) {
					try { this.add($(ev['id'][f]), ev['type'], ev['fn']); } catch(er){ };
				}
			}
		}
	}
};
// set function object properties etc.
DESTINY.Event.fix.preventDefault  = function(){ this.returnValue = false; };
DESTINY.Event.fix.stopPropagation = function(){ this.cancelBubble = true; };

if (!window.addEventListener) {
	// This little snippet fixes the problem that the onload attribute on the body-element
	// will overwrite previous attached events on the window object for the onload event
	document.onreadystatechange = function() {
		if (window.onload && window.onload != DESTINY.Event.handle) {
			DESTINY.Event.add(window, 'load', window.onload);
			window.onload = DESTINY.Event.handle;
		}
	};
}

/**	Get.(x)	=> 'singleton' namespace
 *	DOM traversal functions
 *		sibling
 *		child
 *		children
 *		ancestor
**/
var Get = {
	/** Get.sibling
	 *		@param	oSib	(object) originating node
	 *		@param	sDir	(string) direction, "next" or "previous"
	 *	@return the next/previous sibling non-text/comment element node
	**/
	sibling: function(oSib, sDir) {
		var prop = sDir + 'Sibling';
		while ((oSib = oSib[prop])) { 
			if (oSib.nodeType == 1)
				return oSib;
		}
		return null;
	},
	/** Get.child
	 *		@param	oParent		(object) parent DOM node
	 *		@param	sTagName	(string) desired child node tagName
	 *	@return	first node matched successfully, else null
	**/
	child: function(oParent, sTagName) {
		var kids = oParent.childNodes,
			tag  = sTagName.toUpperCase();
		for (var x=0, kid; kid = kids[x]; x++) {
			if (kid.nodeType == 1 &&
				kid.nodeName == tag)
					return kid;
		}
		return null;
	},
	/** Get.children
	 *		@param	oParent		(object) parent DOM node
	 *	@return	a normalised array of childNodes of oParent
	**/
	children: function(oParent) {
		var arr = [], child,
			children = oParent.childNodes;
		for (var x=0; child = children[x]; x++) {
			if (typeof child == 'object' &&
				child.nodeType == 1)
					arr.push(child);
		}
		return arr;
	},
	/** Get.ancestor
	 *		@param	obj	(object) originating node
	 *		@param	tag	(string) desired ancestor node tagName
	 *	@return first node matched successfully, else null
	**/
	ancestor: function(obj, tag) {
		var name = tag.toUpperCase();
		while ((obj = obj.parentNode)) {
			if (obj.nodeName == name)
				return obj;
		}
		return null;
	},
	/** Get.style
		getComputedStyle / currentStyle analogue
		
		WARNING:
			IE returns what the style SHOULD BE (non-computed)
			All other browsers return what it IS (computed)
		
		http://www.robertnyman.com/2006/04/24/get-the-rendered-style-of-an-element/
	**/
	style: function(oElm, strCssRule) {
		var strValue = "";
		if (document.defaultView && document.defaultView.getComputedStyle) {
			strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
		} else if (oElm.currentStyle) {
			strCssRule = strCssRule.replace(/\-(\w)/g, function(strMatch, p1) {
			    return p1.toUpperCase();
			});
			strValue = oElm.currentStyle[strCssRule];
			// check for non-pixel values (+ font keywords)
			if (/(em|ex|pt|%|small|medium|large)$/.test(strValue)) {
				strValue = DSI.convertToPixels(strValue, oElm);
			}
		}
		return strValue;
	},
	
	z: null
};

/*	do_action
		initiate form submission with corresponding action
	
	first argument = (string) form action
	optional second argument: (string) carrier
		=> use to pass simple variables, like rowid, between page loads
	
	This form submission is not action-prejudiced (unlike doEntry)
*/
function do_action(a) {
	if (!a || a == '') { return; }
	var f = document.forms[0];
	if (f['carrier'])
		f['carrier'].value = arguments[1] || '';
	f['formaction'].value = a;
	f.submit();
}
// do_simple_action - wrapper to allow simple event assignment
function do_simple_action(e) {
	try {
		do_action(e.target.id.match(/^(\D+)\d*$/)[1]);
	} catch(er) {
		console.log('Simple Action Failed');
	};
}

/*	do_method -- unobtrusive create/get/update/delete event handler
	
	Setup:
		Used in tandem with img_click action buttons.
		--	Do not pass arguments to this function via anonymous function syntax.
			All necessary variables are self-discovered and fail safely.
		
		Image button element must have an ancestor with an id matching the pattern
			"(var)_disp" or "(var)_edit". The (var) is of course variable, and the
			suffixes correspond to "display" and "editable" areas, the two most 
			common places action buttons are found.
		
		--	As ids _must_ be unique throughout a document, the ancestor id is 
			generally best placed on the encompassing table/fieldset tag, rather 
			than table rows (tr).
			==	There are, of course, exceptions. (viz. #upper_static's "save" button)
		
	Return:
		A formaction string corresponding to Method (verb) and Target (noun), separated
			by an underscore. More than one underscore may exist in the button's id;
			The CF action parsing will consider all of the text before the _first_
			underscore to be the method, and all text _after_ the first underscore
			to be the target.
			
		This formaction is passed to do_action, with _no_ carrier.
	
	Test:
		Use do_method_stub to test do_method-generated calls to do_action.
			This is useful when form backend logic is incomplete.
*/
function do_method(e) {
	var method_string;  e.stopPropagation(); // stops event from bubbling
	if (method_string = get_method_string(this)) do_action(method_string);
}
// do_method stub
function do_method_stub(e) {
	return console.log('do_action("'+ get_method_string(this) +'")');
}
// shared method utility
function get_method_string(obj) {
	var parent = obj.parentNode;	// establish initial parent node of current element
	var rx = /^(\w+)_(edit|disp)$/;	// this pattern is used to identify the proper parent
	
	while (parent) {	// traverse document structure upwards
		if (rx.test(parent.id)) break;	// if matching id found, break out of loop
		parent = parent.parentNode;	// go up one level of hierarchy if test failed
	}
	if (!parent) return undefined; // if no parent, get out of Dodge
	
	var method = /^([a-zA-Z_-]+)\d*/.exec(obj.id)[1]; // this element's id fragment (e.g. "update")
	var target = rx.exec(parent.id)[1];	// the parent element's id fragment (e.g. "activity")
	
	return (method + '_' + target);	// ex: "update_activity"
}

DSI = DESTINY.Methods = {
	/*	toggle_element

		This event handler is designed to work with any arbitrary show/hide scheme,
		as long as the naming conventions are followed. This also enables multiple
		instances in the same page, as long as the targets are unique.

		For multiple sets of buttons for the same target, append a number to the id
		of the button toggles, but _not_ the target.

		ex:
			To toggle a block with the id 'vote', there must exist (CSS shorthand):

				#show_vote ------- "Show ..." image (or anchor)
				#hide_vote ------- "Hide ..." image (or anchor)
				#vote ------------ target table, div, p, etc (block level element)

				form.vote_toggle - input type="hidden" in document.forms[0] (first)

			On the initial page load, the 'remove' class must be applied to the target
			as well as the #hide_vote button, with the vote_toggle field = 'hide'.

			Subsequent page loads will conditionally assign classes appropriately.

		JS setup example:
			// togglers are images
			DESTINY.clicks = {
				...
				'hide_vote': toggle_handler,
				'show_vote': toggle_handler
			}

			// OR - togglers are anchors
			DESTINY.events [
				...
				{ id: ['hide_vote','show_vote'], type: 'click', fn: toggle_handler }
			];

		CF setup example:

			<cfparam name="form.vote_toggle" default="hide">
			<cfscript>
				vote_visible = form.vote_toggle EQ 'show';
				vote_toggle_class['show'] = iif(vote_visible, de(' remove'), de(''));
				vote_toggle_class['hide'] = iif(vote_visible, de(''), de(' remove'));
			</cfscript>
			...
			<img src="images/show_vote_up.gif" id="show_vote" alt="Show Vote Information"
				class="rectbutton#vote_toggle_class['show']#">
			<img src="images/hide_vote_up.gif" id="hide_vote" alt="Hide Vote Information"
				class="rectbutton#vote_toggle_class['hide']#">		

			<table class="formtable tableCollapsed#vote_toggle_class['hide']#" id="vote">...
			...
			<input type="hidden" name="vote_toggle" value="#form.vote_toggle#">
	*/
	toggle_element: function(e) {
		// info[0] => 'hide' or 'show', the target state
		// info[1] => the id of the element hidden or shown
		var info = this.id.split('_'),
			hiding = !!(info[0] == 'hide'),
			target_id = info[1].replace(/^(\D+)/, "$1"),
			reveal_button = $((hiding ?'show':'hide') + '_' + target_id);

		// store target state in hidden field
		document.forms[0][target_id + '_toggle'].value = info[0];

		// whenever a click occurs, it is always hiding the button clicked
		DSI.Class.add(this, 'remove');

		// likewise, it is always 'showing' the other toggle button
		DSI.Class.remove(reveal_button, 'remove');

		// if hiding the target, add the class 'hide', otherwise remove it
		window[(hiding ?'add':'remove') + 'Class']($(target_id), 'remove');

		// workaround FF bug with FCKeditor
		if (!hiding) switchEditors($(target_id), 'on');
	},
	
	// mouseover/mouseout handler for image buttons
	img_mouse_handler: function(e) {
		var self  = e.target,
			name  = self.id.match(/^(\D+)/)[1],
			state = (e.type == 'mouseover') ? "dwn" : "up";
		
		// avoid stupid error if mouseover occurs during load
		if ("undefined" != typeof DESTINY)
			self.src = DESTINY.preloads[name][state].src;
	},
	
	// handler for layer toggling onclick
	tab_onclick: function(e) {
		if (e.target.nodeName == 'LI') return;
		
		// ensure tab is the anchor
		var tab = (e.target.nodeName.toLowerCase() == 'a') ?
			       e.target :  Get.ancestor(e.target, 'a');
		
		try {
			// remove 'focus ring'
			tab.blur();
		
			// stop link from being followed, event propagation
			e.preventDefault();
			e.stopPropagation();
		} catch(e) {
			// if cursor is inside rich text editor when tab clicked, it seems to barf
			// coddle IE thusly, stopping link etc.
			e.returnValue = false;
			e.cancelBubble = true;
		}
		
		// if it is not the current tab, fire toggle_layer
		if (!DSI.Class(tab,'current'))
			DSI.toggle_layer(tab.hash.match(/(\d)/)[1]);
		
		// coddle IE in cases where rich text editor hoses things
		return false;
	},
	
	// switch the currently visible layer
	toggle_layer: function(id) {
		// obtain current layer
		var current = DESTINY.current_layer_obj,
			cid = current.value.match(/(\d)/)[1];
		
		// if no id passed, default to current id
		id || (id = cid);
		
		if (cid != id) {
			// switching to a different layer, hide current
			DESTINY.layer_areas[cid].style.display = 'none';
			// protect from error if using user-hostile 'sub' tabs
			if (DESTINY.layer_tabs[cid])
				DSI.Class.remove(DESTINY.layer_tabs[cid], 'current');
		}
		// show the target layer
		DESTINY.layer_areas[id].style.display = 'block';
		// highlight current tab if present
		if (DESTINY.layer_tabs[id])
			DSI.Class.add(DESTINY.layer_tabs[id], 'current');
		
		current.value = 'Layer'+id; // save state
	},
	
	// append spans wrapping text to make the buttons look good
	nest_spans: function(parent) {
		// obtain text inside anchor
		var text = parent.removeChild(parent.firstChild);
		
		// strip extraneous spaces from text
		text.nodeValue = text.nodeValue.strip();
		
		// create 'root' span
		var root = document.createElement('span');
		
		// append three nested spans
		for (var i = 0, nested = root; i < 3; i++) {
		    nested = nested.appendChild(document.createElement('span'));
		}
		// insert textnode into deepest nested span
		nested.appendChild(text);
		
		// append root to parent node
		parent.appendChild(root);
	},
	
	prepare_tabs: function() {
		var tabs = $('tabs').getElementsByTagName('A');
		
		// may or may not be called from body (after #tabs, faster on huge pages)
		if (DESTINY.max_tab_offset !== null) return tabs; // TODO: make work in frickin IE
		
		for (var t = 0, tab, max_offset = 0; tab = tabs[t]; t++) {
			
			// store the anchor (tab) in a sparse array, analogous to layer_areas
			DESTINY.layer_tabs[tab.hash.match(/(\d)/)[1]] = tab;
			
			// set accesskey (Alt + Number in Windows, Control + Number in OS X)
			tab.setAttribute('accessKey', t+1);
			
			// avoid null title issues
			tab.setAttribute('title', ((tab.getAttribute('title'))?
				tab.getAttribute('title')+' ':'') + '('+(t+1)+')');
			
			// insert nested spans
			DSI.nest_spans(tab);
			
			// prepare max_offset for possible 'centering' later
			if (max_offset < tab.offsetWidth)
				max_offset = tab.offsetWidth;
		}
		
		DESTINY.max_tab_offset = max_offset;
		
		return tabs;
	},
	
	// helper for Get.style() in IE
	// adapted from http://blog.stchur.com/2006/09/20/converting-to-pixels-with-javascript/
	convertToPixels: function(str, context) {
		if (/px$/.test(str)) return parseInt(str, 10);
		
		var tmp = document.createElement('div');
		tmp.style.visbility = 'hidden';
		tmp.style.position = 'absolute';           
		tmp.style.lineHeight = '0';          
		
		if (/%$/.test(str)) {
			// percentages are relative to parent
			context = context.parentNode || context;
			tmp.style.height = str;
		} else if (/(small|medium|large)$/.test(str)) {
			// font keywords
			tmp.style.fontSize = str;
			tmp.style.lineHeight = '1';
			tmp.appendChild(document.createTextNode('M'));
		} else {
			tmp.style.borderStyle = 'solid';
			tmp.style.borderBottomWidth = '0';
			tmp.style.borderTopWidth = str;
		}
		
		if (!context) context = document.body;
		
		context.appendChild(tmp);
		var px = tmp.offsetHeight;
		context.removeChild(tmp);
		
		return px + 'px';
	},
	
	z: null
};

// common form interactions
DSI.Form = {
	/*	Paths object configuration
		
		Before DESTINY.initialize is called, page-specific paths may be set with this syntax:
		DSI.Form.paths.print = '../print/print_hrtf.cfm';
		
		DSI.Form.paths.print is the only value without a default;
			unless set, DSI.Form.print will not function
		
		'search' requires the 'formid' form field to be present, which is in the default template
	*/
	paths: {
		print:   null,
		filecab: "../fas/review2.cfm",
		search:  "/fas/form_find.cfm" // path from application root (aq)
	}, 
	classify_readonly: function() {
		if (document.forms[0].rofl && document.forms[0].rofl.value == 'true') {
			// text inputs
			var elms = $('content').getElementsByTagName('INPUT');
			for (var e = 0, elem; elem = elms[e]; e++) {
				if (elem && elem.type == 'text' && elem.readOnly == true)
					DSI.Class.add(elem,'readonly');
			}
			// textareas
			var texs = $('content').getElementsByTagName('TEXTAREA');
			for (var t = 0, text; text = texs[t]; t++) {
				if (text && text.readOnly == true)
					DSI.Class.add(text,'readonly');
			}
		}
	},
	control: function(e) {
		DSI.Form.controller(this.id.match(/^(\D+)\d*$/)[1]);
	},
	controller: function(target) {
		switch (target.toLowerCase()) {
			case 'return':	return doEntry('inbox');
			case 'review':	return doEntry(DSI.Form.paths.filecab);
			case 'submit':	return doEntry('Approve');
			case 'save':	return doEntry('Save');
		}
	},
	pop: function(path, name) {
		var d = document.forms[0], href = path + '?seq=' + d.seq.value + '&form=' + d.formid.value;
		window.open(href, name,'width=640,height=400,scrollbars=yes,resizable=yes,left=50,top=50');
	},
	print: function(e) {
		if (DSI.Form.paths.print === null) return;
		var d = document.forms[0],
			href = DSI.Form.paths.print + '?forms=' + d.seq.value + '&formid=' + d.formid.value;
		window.open(href,'Print','width=640,height=400,scrollbars=yes,resizable=yes,left=50,top=50');
	},
	search: function(e) {
		switch(e.type) {
		case 'click':
			var path = DSI.Form.paths.search + "?form_type=" + document.forms[0]['formid'].value;
			location.href = location.href.replace(/\/\w+\/\w+\.cfm\??[\w&=]*$/,path);
			break;
		case 'change':
			if (this.value.strip() == '')
				location.href = location.href.replace(/(\?.*)$/,''); /* nuke querystring */
			else
				getEntry(location.pathname.substring(location.pathname.lastIndexOf('/')+1));
			break;
		}
	},
	status: function(e) {
		DSI.Form.pop('../fas/fas_status.cfm','Status');
	}
};

DSI.Class = function(obj, cls) {
	return new RegExp("(^|\\s)" + cls + "(\\s|$)").test(obj.className);
};
DSI.Class.add = function(obj, cls) {
	if (this(obj, cls)) return;
	if (obj.className != '')
		obj.className += ' ' + cls;
	else
		obj.className = cls;
};
DSI.Class.remove = function(obj, cls) {
	if (this(obj, cls)) {
		var oldClass = obj.className || '',
			rx = new RegExp('\\s?'+ cls +'\\b','g');
		obj.className = oldClass.replace(rx,'');
	}
};

// backwards compatibility API (THESE ARE GOING AWAY SOON)
window.addEvent = DESTINY.Event.add;
window.removeEvent = DESTINY.Event.remove;
window.hasClass = function(){ return DSI.Class.apply(DSI.Class, arguments); };
window.addClass = function(){ return DSI.Class.add.apply(DSI.Class, arguments); };
window.removeClass = function(){ return DSI.Class.remove.apply(DSI.Class, arguments); };

