/* -- BEGIN: DP namespace ----------------------------------------------- */
var DP = {

	// boolean; to enable console debugging, pass in "DEBUG" as a query param
	DEBUG : function(){
		var qs = window.location.search.toQueryParams();
//		console.log("Object.keys(qs).indexOf(\"DEBUG\") = " + Object.keys(qs).indexOf("DEBUG"));
		return (Object.keys(qs).indexOf("DEBUG") > -1) || false; // NOTE: make sure this is false for production
	}(),

	// tiny debug wrapper (being nice to IE/Win)
	debug : function(msg) {
		// bail out if debugging is off
		if (!this.DEBUG) { return; }

		// accept an array of strings and use them as lines
		if (msg instanceof Array) {
			msg = msg.join("\n");
		}

		try {
			// ff and safari go here
			console.log(msg);

		} catch(e) {
			// IE gets this

			// open the win if no win exist
			if (!this.debug_win) {
				this.debug_win = window.open("","","");
				this.debug_win.document.open();
			}

			// simple HTML regex
			msg = msg.replace(/</g, "&lt;");
			msg = msg.replace(/>/g, "&gt;");

			// user may have closed the window, so reopen if necessary
			try {
				this.debug_win.document.write("");
			} catch(e) {
				this.debug_win = window.open("","","");
				this.debug_win.document.open();			
			}

			// finally output the msg
			this.debug_win.document.write("<pre style='font-size: 80%; margin: .5em 0 2em 0;'>" + msg + "</pre>");

		}
	} // END: debug()

};

/* ------------------------------------------------------------------------ */

// settings that are useful for all of our features
DP.GLOBALS = {
	active_class : "Active",
	expanded_class : "Expanded",
	disabled_class : "Disabled",
	deleted_class : "Deleted",
	selected_class : "Selected",
	hover_class : "Hover",
	link_class : "Link",
	dhtml_link_class : "LinkDHTML",
	img_path : "/images/",
	ajax_update_delay : 100 // ms
};

/* ------------------------------------------------------------------------ */

// each new "feature" goes here
DP.Features = {};

DP.Feature = Class.create();
DP.Feature.prototype = {
	initialize : function(options) {
		/* 
		options is a hash of:
			Version
			CONFIG
			initialize
			setupElements
			public_methods
		*/

		// FIXME: check for bad version strings
		var feature_name = options.Version.split(";")[0];

		var new_feature = {};
		new_feature[feature_name] = {
			Version : options.Version,

			CONFIG : options.CONFIG || {},

			elements : {},
			store : function(id, item, overwrite) {	
				if (typeof(this.elements[id]) === 'undefined') {
					this.elements[id] = item;
					return true;

				} else if (overwrite === true) {
					DP.debug(["hash \"" + id + "\" already exists in elements... OVERWRITING", "this.elements[" + id + "] = " + this.elements[id].toString() ]);
					this.elements[id] = item;
					return true;

				} else {
					DP.debug(["hash \"" + id + "\" already exists in elements", "this.elements[" + id + "] = " + this.elements[id].toString() ]);
					return false;
				}
			}, // END: store()

			remove : function(id) {
				if (typeof(this.elements[id]) !== 'undefined') {
					delete this.elements[id];
					return true;

				} else {
					DP.debug("hash \"" + id + "\" not found");
					return false;
				}
			},

			initialize : function() {
//				console.log('initializing ' + this.Version);

				// FIXME: this could use some refactoring

				// patch in the public methods
				if (typeof(options.public_methods) == 'object')
					this.applyMethods(options.public_methods);

				// run the initialize call immediately
				if (typeof(options.initialize) == 'function')
					options.initialize.call(this);

				// then run setupElements on DOM ready
				if (typeof(options.setupElements) == 'function') {
					// if domReady wasn't called yet, set up our function
					if (!Event._domReady.done) {
						Event.onReady(this.setupElements.bind(this));

					// if it's after domReady, then just call our function
					} else {
						this.setupElements.bind(this);
					}
				}

			}, // END: initialize()

			setupElements : function(root_node) {
				// accept an aribitrary root_node (for use with AJAX updates)
				root_node = $(root_node) || document;

//				console.log(this.Version + ": setupElements(" + root_node + ")");

				options.setupElements.call(this, root_node);

			}, // END: setupElements()

			tearDownElements : function() {
				this.elements = {};
			},

			applyMethods : function(methods) {
				// FIXME: prevent public_methods from overriding methods above
				for (var m in methods)
					this[m] = methods[m];
			}
		}; // END: new_feature

		Object.extend(new_feature[feature_name], Event.Listener);
		Object.extend(new_feature[feature_name], Event.Publisher);

		// extend DP.Features and initialize the new feature immediately
		Object.extend(DP.Features, new_feature)[feature_name].initialize();
	}
}; // END: DP.Feature.prototype

/* ------------------------------------------------------------------------ */

DP.Widgets = {};

DP.Widget = Class.create();
DP.Widget.prototype = {
	activate : function() {
		DP.debug("sorry, activate() is not implemented on this class");
	},
	
	node : null
}; // END: DP.Widget.prototype

/* ------------------------------------------------------------------------ */

// this holds the event broker from the event mixin
DP.EventBroker = {};
Object.extend(DP.EventBroker, Event.Broker);

/* ------------------------------------------------------------------------ */

DP.Errors = {};

DP.Error = Class.create();
DP.Error.prototype = {
	initialize : function(error_msg){ },

	alert : function(msg){
		if (DP.DEBUG) alert(msg);
	}
};

DP.Errors.AjaxError = Class.create();
Object.extend(Object.extend(DP.Errors.AjaxError.prototype, DP.Error.prototype), {
	initialize : function(){
		var args = $A(arguments);
		var msg = args.shift();

		this.alert(msg);

		DP.debug("AjaxError: " + msg);
		DP.debug(args.shift());
	}
});

/* ------------------------------------------------------------------------ */

DP.reapplyFeatures = function(root_node){
//	DP.debug("reapplying all Features starting at node " + root_node.id);
	for (var f in DP.Features)
		DP.Features[f].setupElements(root_node);
};

/* ------------------------------------------------- END: DP namespace -- */


/* ======================================================================== */


/* -- BEGIN: tabs widget -------------------------------------------------- */

DP.Widgets.TabSet = Class.create();
Object.extend(DP.Widgets.TabSet.prototype, DP.Widget.prototype);
Object.extend(DP.Widgets.TabSet.prototype, {
	initialize : function(tabset) {

		if (tabset.hasClassName("Static")) return;

		// loop over the anchors in the tabnav
		$$S(tabset, '.TabNav A').each(function(anchor, index){
			// create a new tab and store it in order
			this.tabs.push(new DP.Widgets.Tab(anchor, this.tabs));
		}.bind(this));

		// turn on the first tab by default
		this.tabs[0].activate();

	}, // END: initialize()

	// stores the tabs associated with this tabset
	tabs : []
});

/* ------------------------------------------------------------------------ */

DP.Widgets.Tab = Class.create();
Object.extend(DP.Widgets.Tab.prototype, DP.Widget.prototype);
Object.extend(DP.Widgets.Tab.prototype, {
	CONFIG : {
		tabnav_prefix: "tab-nav_",
		tab_prefix : "tab_"
	},

	initialize : function(anchor, all_tabs) {
		this.id = anchor.href.getHash();
		this.nav_node = $(anchor.parentNode);
		this.tab_node = $(this.CONFIG['tab_prefix'] + this.id);
		this.all_tabs = all_tabs;

		// attach the event handler
		anchor.observe('click', this.activate.bindAsEventListener(this), false);
		anchor.onclick = Prototype.False; // fix for dumb older versions of safari
	},

	id : null,
	nav_node : null,
	tab_node : null,
	all_tabs : [],

	activate : function(e) {
		if (e) {
			// get the anchor from the event and blur it to remove the outline
			Event.element(e).blur();
		}

		// bail if this tab is already active
		if (this.nav_node.hasClassName(DP.GLOBALS["active_class"])) {
			return;
		}

		// (de)activate the tabs
		this.all_tabs.each(function(tab){
			if (this.id == tab.id) {
				tab.nav_node.addClassName(DP.GLOBALS["active_class"]);
				tab.tab_node.addClassName(DP.GLOBALS["active_class"]);
				tab.tab_node.show();
			} else {
				tab.nav_node.removeClassName(DP.GLOBALS["active_class"]);
				tab.tab_node.removeClassName(DP.GLOBALS["active_class"]);
				tab.tab_node.hide();
			}
		}.bind(this));
	}
});

/* ---------------------------------------------------- END: tabs widget -- */


/* -- BEGIN: revealer widget ---------------------------------------------- */

DP.Widgets.Revealer = Class.create();
// FIXME: make "activate"
Object.extend(DP.Widgets.Revealer.prototype, {
	initialize : function(el) {
		DP.debug("new Revealer: " + el);

		// keep track of the node
		this.node = el;

		// attach event handlers
		el.observe('click', this.toggle.bind(this), false);
		el.onclick = Prototype.False; // fix for dumb older versions of safari
	}, // END: initialize()

	node : null,
	
	// toggle the Revealer
	toggle : function(e) {

		DP.debug("toggling: " + this.node.id);

		var anchor = Event.element(e);

		// blur the anchor so it doesn't have the outline around it
		anchor.blur();

		var dt = anchor.up();
		dt.next('dd').toggle();
		dt.toggleClassName(DP.GLOBALS['active_class']);

	}, // END: toggle()

	activate : this.toggle

});

/* ------------------------------------------------ END: revealer widget -- */


/* -- BEGIN: toggle box widget -------------------------------------------- */

DP.Widgets.ToggleBox = Class.create();
Object.extend(DP.Widgets.ToggleBox.prototype, DP.Widget.prototype);
Object.extend(DP.Widgets.ToggleBox.prototype, {
	CONFIG : {
		class_name : "Toggle",
		header_class_name : "ToggleHeader",
		content_class_name : "ToggleContent",
		element_prefix : "toggle_",
		static_box_class : "Static"
	},

	initialize : function(el) {
		// sanity check
		if (!$(el)) {
			DP.debug(["ToggleBox: bad element passed to constructor", 'el = ' + el]);
			return;
		}

		this.listenForEvent(DP.EventBroker, 'togglebox_close', true, "toggleboxEvent");
		this.listenForEvent(DP.EventBroker, 'togglebox_open', true, "toggleboxEvent");

		// keep track of the DOM node
		this.anchor_node = el;

		// copy the toggle method to the DOM node for convenience
		this.anchor_node.toggle = this.toggle;

		// attach event handlers
		el.observe('click', this.toggle.bind(this), false);
		el.onclick = Prototype.False; // fix for dumb older versions of safari

	}, // END: initialize

	// holds the DOM node of the anchor
	anchor_node : null,

	toggleboxEvent : function(e) {
		if ( e.event_data.data['togglebox_id'] == (this.CONFIG['element_prefix'] + this.anchor_node.href.getHash()) )
			this.toggle();
	},

	// this gets called via a method on a DOM node or by HashHandler
	toggle : function(e) {
		if (e) {
			Event.stop(e);
			var anchor_node = Event.element(e);

		} else {
			var anchor_node = this.anchor_node;
		}

		// blur the anchor so it doesn't have the outline around it
		anchor_node.blur();

		var header = anchor_node.up(1);

		// do the toggle!
		header.next('div').toggle();
		header.up().toggleClassName(DP.GLOBALS['active_class']);

	}, // END: toggle()

	// this is meant to be called by an A with rel="HashHandler"
	activate : function() {
		var hash = this.anchor_node.href.getHash();

		// get the actual Toggle Box
		var target_box = this.anchor_node.parentNode.parentNode.parentNode;

		// only activate it if it isn't active already
		if (!target_box.hasClassName(DP.GLOBALS['active_class'])) {
			this.toggle();
		}

		// fancy scroll to effect
		new Effect.ScrollTo(target_box);
	} // END: activate

});
Object.extend(DP.Widgets.ToggleBox.prototype, Event.Listener);

/* ---------------------------------------------- END: toggle box widget -- */


/* -- BEGIN: row widget --------------------------------------------------- */

DP.Widgets.Row = Class.create();
Object.extend(DP.Widgets.Row.prototype, DP.Widget.prototype);
Object.extend(DP.Widgets.Row.prototype, {
	CONFIG : {
		table_class : "Listing",
		link_class : "LinkDHTML",
		edit_link_rel : "EditItem",
		show_link_rel : "ShowItem",

		row_selector : "TR",
		row_id_suffix : "_details",

		editing_class     : "Editing",
		editing_row_class : "EditRow",
		details_class     : "ShowingDetails",
		details_row_class : "DetailsRow"
	},

	TYPES : {
		ShowItem : {
			row_class : "ShowingDetails",
			next_row_class : "DetailsRow",
			url_prop : "details_url"
		},
		EditItem : {
			row_class : "Editing",
			next_row_class : "EditRow",
			url_prop : "edit_url"
		}
	},

	row : null,
	mode : null,

	initialize : function(row) {
		// save a ref to the row's DOM node
		this.row = row;

		// get all of the anchors in the row with rel attributes and attach event handlers
		$$S(this.row, "A." + this.CONFIG['link_class']).each(function(anchor) {
			var mode = anchor.getAttribute("rel");
			if (!mode) { return; }

			// store the URL of the link in the right property
			this[this.TYPES[mode].url_prop] = anchor.href;

			anchor.observe('click', this.toggle.bindAsEventListener(this, "hello"), false);
		}.bind(this));

		// these rows are going to listen to events
		this.listenForEvent(DP.EventBroker, 'row_open', true, "onReceiveEvent"); // unused right now
		this.listenForEvent(DP.EventBroker, 'row_close', true, "onReceiveEvent");

		// ...and also generate events
		DP.EventBroker.registerEventsPublisher('row_open', this); // unused right now
		DP.EventBroker.registerEventsPublisher('row_close', this);

	}, // END: initialize()

	oppositeMode : function() {
		if (this.mode == "ShowItem")
			return "EditItem";
		else if (this.mode == "EditItem")
			return "ShowItem";
	},

	// this gets triggered when a link gets clicked
	// events get sent to onReceiveEvent
	toggle : function() {
		if (arguments[0] && typeof(arguments[0]) != 'string') {
			var e = arguments[0];
			Event.stop(e);

			var anchor = Event.element(e);
			anchor.blur();

			this.mode = anchor.getAttribute("rel");

		} else {
			this.mode = (arguments[0] == 'edit') ? "EditItem" : "ShowItem";
		}

		// bail out if there are already requests going on.
		if (Ajax.activeRequestCount > 0) {
			DP.debug('sorry, there are active ajax requests.');
			return;
		}

		// ensure that we are in a known 'mode'
		if (!Object.keys(this.TYPES).include(this.mode)) {
			DP.debug("unknown mode: '" + this.mode + "', aborting AJAX request!");
			return;
		}

		if (this.toggleRow())
			this.retrieveData();

	}, // END: toggle()

	toggleRow : function(forced_state) {
		var next_row = this.row.next(this.CONFIG['row_selector']);

		// we have a forced state
		if (forced_state) {
//			console.log(this.row.id + " should execute a " + forced_state);

			if (forced_state == "row_open") {
				// we're only allowed to open all details, not all editors
				this.mode = "ShowItem";

			} else if (forced_state == "row_close") {
				next_row.remove();
				this.row.toggleClassName(this.TYPES[this.mode].row_class);
				this.mode = null;
				return false;

			} else {
//				console.log("unknown state :" + forced_state);
				return false;
			}

		} // END: if forced state

		// if we're going into edit mode, tell all of the other rows to close
		// (only the rows already in edit mode will close)
		if ( (this.mode == "EditItem") && !forced_state ) {
			this.dispatchEvent('row_close', { row_id : 'ALL', excluding: this.row.id });
		}

		// if we retrieved the desired row already, just show it (don't re-request it)
		if ( next_row && next_row.hasClassName(this.TYPES[this.mode].next_row_class) ) {
			next_row.remove();
			this.row.toggleClassName(this.TYPES[this.mode].row_class);
			this.mode = null;
			return false;
		}

		// let's check and see if the next row might be the opposite of what we want to do.
		// if the other row already in place, remove it!
		var opposite_mode = this.oppositeMode();
		if ( next_row && next_row.hasClassName(this.TYPES[opposite_mode].next_row_class) ) {
			next_row.remove();
			this.row.removeClassName(this.TYPES[opposite_mode].row_class);
		}

		if (forced_state == "row_open") {
			this.requestData();
		} else {
			return true;
		}

	}, // END: toggleRow()

	onReceiveEvent : function(e) {
		var e_data = e.event_data;
		var forced_state = e_data.event_name;

		// if the event should exclude this row, ignore it
		if (e_data.data.excluding == this.row.id) { return;	}

		if ( (this.mode == "EditItem") && 
				(e_data.data.row_id == "ALL") &&
				(forced_state == "row_close") ) {
			this.toggleRow(forced_state);

		} else if ( (this.mode == "ShowItem") && 
				(e_data.data.row_id == "ALL") && 
				(e_data.event_target.mode != "EditItem") ) {
			this.toggleRow(forced_state);

		} else if (this.mode == null) {
//			console.log(this.row.id + " is closed");
		}

		// if the destination row id and this row id match
		if ( e_data.data.row_id == this.row.id ) {
			this.toggleRow(forced_state);
		}

	}, // END: onReceiveEvent

	activate : function(mode) {
		this.toggle(mode);
		new Effect.ScrollTo(this.row);
	},

	_event_source_id : function() {
		return this.row.id;
	},

	// TODO: refactor this section out into method(s) of an "AJAX Widget"
	retrieveData : function() {
		// make the request
		new Ajax.Request(this[this.TYPES[this.mode].url_prop], { 
			method: 'get', 

			// this is needed to get correct-colored rows for row-striping
			parameters: { class_name: (this.row.hasClassName("Odd") ? "Odd" : "Even") }, 

			// insert the row
			onSuccess : this.insertContent.bind(this),

			// handle exceptions
			onException : function(){
				throw new DP.Errors.AjaxError("AJAX exception raised.", arguments);
			},

			// let the user know if something went wrong
			onFailure : function() {
				throw new DP.Errors.AjaxError("AJAX request failed.", arguments);
			}
		});

	}, // END: retrieveData()

	insertContent : function(request) {
		// switch this row to the right mode
		this.row.toggleClassName(this.TYPES[this.mode].row_class);

		// insert the new data immediately after this row
		new Insertion.After(this.row, request.responseText);

		// update the newly added content with all of our JS event handlers, etc.
		DP.reapplyFeatures(this.row.next(this.CONFIG['row_selector']));
	} // END: insertContent()

});
Object.extend(DP.Widgets.Row.prototype, Event.Listener);
Object.extend(DP.Widgets.Row.prototype, Event.Publisher);

/* ----------------------------------------------------- END: row widget -- */


/* -- BEGIN: image radio input widget ------------------------------------- */

DP.Widgets.ImageRadioInput = Class.create();
Object.extend(DP.Widgets.ImageRadioInput.prototype, DP.Widget.prototype);
Object.extend(DP.Widgets.ImageRadioInput.prototype, {
	initialize : function(input) {
		// NOTE: stupid safari doesn't work with labels transferring click 
		// events to INPUTs, so we have to monitor the onclick event of the IMG
		input.next().down().observe('click', this.activate.bindAsEventListener(this));
		// input.observe('click', this.activate.bindAsEventListener(this), false);

		this.node = input;
		this.all_nodes = $$S(input.up("ul"), "INPUT");

		if (input.checked) {
			input.up('LI').addClassName(DP.GLOBALS['selected_class']);
		}
	},

	node : null,
	all_nodes : null,

	activate : function() {
		this.all_nodes.each(function(input){
			if (this.node.id == input.id) {
				input.up('LI').addClassName(DP.GLOBALS['selected_class']);
				input.checked = true;
				// input.focus();
			} else {
				input.up('LI').removeClassName(DP.GLOBALS['selected_class']);
			}
		}.bind(this));
	}
});

/* --------------------------------------- END: image radio input widget -- */


/* -- BEGIN: FileUploadField ---------------------------------------------- */

DP.Widgets.FileUploadField = Class.create();
Object.extend(DP.Widgets.FileUploadField.prototype, {
	CONFIG : {
		remove_file_text : "Remove this file from your upload list",
		file_class : "file",
		queued_files : "queued-files",
		button_text : 'Add file to upload queue',
		file_input_class : "FileInput",
		text_input_class : "TextInput",
		field_selector : "DIV.Field", 
		valid_upload_types : $w("pdf")
	},

	initialize : function(field) {
		// create and append a button
		field.appendChild(this.createButton());

		// store some useful nodes
		this.field_node = field;
		this.all_fields = field.up().immediateDescendants();

		// listen for events from the existing files
		this.listenForEvent(DP.EventBroker, 'remove_file', true, "onReceiveEvent");
	},

	field_node : null,
	file_node : null,
	all_fields : [],

	parseFileName : function(filename) {
		// this needs to be tested on all platforms
		return filename.substr(filename.lastIndexOf("/") + 1);
	},

	parseFileExtension : function(filename) {
		var ext = filename.substr(filename.lastIndexOf(".") + 1);
		return this.CONFIG['valid_upload_types'].include(ext) ? ext : "generic";
	},

	createButton : function(){
		var but = document.createElement('button');
		but.type = "button";
		but.appendChild(document.createTextNode(this.CONFIG['button_text']));

		but.observe('click', this.buttonClickHandler.bindAsEventListener(this));

		return but;
	},

	buttonClickHandler : function(){

		var file_input = Event.element(arguments[0]).previous('INPUT.' + this.CONFIG['file_input_class']);

		// bail out if the file field is blank
		if (file_input.value.blank()) { return; }

		var desc_input = file_input.next('INPUT.' + this.CONFIG['text_input_class']);
		var file_node = this.createFileNode({ name: this.parseFileName(file_input.value), id: file_input.id, desc: desc_input.value });

		// append the new file into the listing
		var li = document.createElement('li');
		li.appendChild(file_node);
		this.file_node = $(this.CONFIG['queued_files']).appendChild(li);

		// hide the current field
		this.field_node.hide();

		DP.Features.MeetingFiles.showNextField();

	}, // END: buttonClickHandler

	createFileNode : function(data) {
		var file_node = document.createElement('div');
		file_node.className = this.CONFIG["file_class"];

		var img_node = document.createElement('img');
		img_node.src = DP.GLOBALS['img_path'] + 'action_stop.gif';
		img_node.className = "Icon Link";
		img_node.title = this.CONFIG['remove_file_text'];

		Event.observe(img_node, 'click', this.removeFileFromList.bindAsEventListener(this), false);

		file_node.appendChild(img_node);

		var icon_node = document.createElement('img');
		icon_node.src = DP.GLOBALS['img_path'] + 'icon.file.' + this.parseFileExtension(data.name) + '.s.gif';
		icon_node.className = "Icon";
		file_node.appendChild(icon_node);

		file_node.appendChild(document.createTextNode(" " + data.name));

		// desc is optional
		if (data.desc) {
			var desc_node = document.createElement('div');
			desc_node.className = "Description";
			desc_node.appendChild(document.createTextNode(data.desc));

			file_node.appendChild(desc_node);
		}

		// stash the member id in the DOM element for easy removal of member later
		file_node.input_id = data.id;

		return file_node;
	},

	clearInputs : function() {
		$$S(this.field_node, "INPUT").each(function(input){
			input.clear();
		});
	},

	removeFileFromList : function() {
		this.clearInputs();
		DP.Features.MeetingFiles.removeOverLimitMsg();
		this.all_fields.invoke('hide');
		this.field_node.show();
		this.file_node.remove();
	},

	onReceiveEvent : function(){
		var available_files = DP.Features.MeetingFiles.countAvailableFiles();
//		console.log("available_files = " + available_files);

		if (available_files < 0 && this.file_node) {
			this.clearInputs();
			this.file_node.remove();
			this.file_node = null;
		}
	}
});
Object.extend(DP.Widgets.FileUploadField.prototype, Event.Listener);

/* ------------------------------------------------ END: FileUploadField -- */


/* -- BEGIN: HelpTip widget ----------------------------------------------- */

DP.Widgets.HelpTip = Class.create();
Object.extend(DP.Widgets.HelpTip.prototype, DP.Widget.prototype);
Object.extend(DP.Widgets.HelpTip.prototype, {
	CONFIG : {
		help_tip_url_prefix : "/help_tips/",
		new_tip_class : "NewTip",
		help_tip_title : "Help with this field"
	},

	initialize : function(span) {
		this.node = span;

		span.observe('click', this.activate.bind(this));

		// WORKAROUND: this is used so we can delete the detail win when we're done with it (via the DOM node)
		this.node.helptip_obj = this;
	},

	node : null,
	detail_win : null,

	activate : function() {
		// don't open the window again
		if (this.detail_win) return;

		var new_tip = this.node.hasClassName(this.CONFIG['new_tip_class']);

		var url = this.CONFIG['help_tip_url_prefix'];
		url += (new_tip) ? "new?name=" + encodeURIComponent(this.node.id) : this.node.id;

		new Ajax.Request(url, {
			method : "get",

			onSuccess : function(transport){
				this.detail_win = new DP.Widgets.DetailPopup(this.node, this.node.title, transport.responseText);
			}.bind(this),

			onFailure : function(transport){
				// FIXME: catch the error better
				throw new DP.Errors.AjaxError('request failed!');
			}
		});
	}

});

/* ------------------------------------------------- END: HelpTip widget -- */


/* -- BEGIN: DetailPopup widget ------------------------------------------- */

DP.Widgets.DetailPopup = Class.create();
Object.extend(DP.Widgets.DetailPopup.prototype, DP.Widget.prototype);
Object.extend(DP.Widgets.DetailPopup.prototype, {
	CONFIG : {
		details_html : new Template([
			'<div class="DetailsShadow"><!-- do not remove --></div>',
			'<div class="DetailsContent">',
				'<h5>#{title}</h5>',
				'<div class="DetailsBody">',
				'#{body}',
				'</div>',
			'</div>',
			'<div class="DetailsArrow"><!-- do not remove --></div>',
			'<iframe id="DetailsIframe" src="javascript:void(0);" frameborder="0" scrolling="no"></iframe>'
		].join("\n"))
	},

	initialize : function(el, title, body_html) {
		// sanity check
		if ( !$(el) || (typeof(body_html) != 'string') || (body_html == "") ) return;

		// store the node we're attaching the popup to
		this.anchor_node = el;

		// use our content
		this.html_content = this.CONFIG['details_html'].evaluate({ 
			'title' : title, 
			'body' : body_html
		});

		// insert it into the DOM
		this.create();

		// these boxes are going to listen to events
		// this.listenForEvent(DP.EventBroker, 'detail_show', true, "onReceiveEvent"); // unused right now
		this.listenForEvent(DP.EventBroker, 'detail_close', true, "onReceiveEvent");

		// ...and also generate events
		// DP.EventBroker.registerEventsPublisher('detail_show', this); // unused right now
		// DP.EventBroker.registerEventsPublisher('detail_hide', this);
	},

	anchor_node : null, // the node our DetailPopup is "attached" to

	node : null,

	node_shadow : null,
	node_content : null,
	node_arrow : null,

	html_content : "",

	create : function() {
		// create the DIV with html_content
		var div = document.createElement('div');
		div.className = "Details";
		div.innerHTML = this.html_content;

		// append it to the DOM
		var parent = $('PageWrapper');
		parent.appendChild(div);
		this.node = parent.lastChild;

		this.reapplyFeatures();

		// get the pieces parts so we can make it look good
		var popup_parts = this.node.immediateDescendants();
		this.node_shadow  = popup_parts[0];
		this.node_content = popup_parts[1];
		this.node_arrow   = popup_parts[2];
		this.node_iframe  = popup_parts[3];

		// move it into place and display it
		this.positionPopup();
		this.node.setStyle({ 'visibility' : 'visible' });
	},

	reapplyFeatures : function() {
		DP.reapplyFeatures(this.node);

		// so we can close the window
		$$S(this.node, 'A').each(function(anchor){
			var rel = anchor.getAttribute('rel');
			if (rel == "CloseTip") {
				anchor.observe('click', this.destroy.bindAsEventListener(this));
			} else if (rel == "EditItem") {
				anchor.observe('click', this.editItem.bindAsEventListener(this));
			}
		}.bind(this));
	},

	positionPopup : function() {
		var offset_parent = $('PageWrapper');
		var top_offset = Position.cumulativeOffset(offset_parent)[1];

		var box_height = this.node_content.getHeight();
		var box_width = this.node_content.getWidth();

		var viewport_height = document.documentElement. clientHeight; // window.innerHeight || document.body.clientHeight;
		var scroll_offset_v = Position.realOffset(document.body)[1];

//		var parent_offset_t = this.anchor_node.offsetParent.offsetTop;
		var parent_offset_t = $('BodyWrapper').offsetTop + $('MainColumn').offsetTop;
		var parent_offset_w = this.node.offsetParent.offsetWidth;

		var anchor_offset_t = Position.cumulativeOffset(this.anchor_node)[1] - scroll_offset_v;
		var anchor_offset_l = Position.positionedOffset(this.anchor_node)[0];
		var anchor_offset_w = this.anchor_node.offsetWidth;

		var arrow_offset_h = 6;
		var arrow_offset_v = 6;
		var arrow_top = arrow_offset_v;

		var box_offset_t = 6;
		var box_offset_l = 10;
		var box_top = anchor_offset_t - top_offset + scroll_offset_v - box_offset_t;

		// DP.debug([
		// 	"box_height = " + box_height,
		// 	"box_width = " + box_width,
		// 	"",
		// 	"parent_offset_t = " + parent_offset_t,
		// 	"parent_offset_w = " + parent_offset_w,
		// 	"",
		// 	"anchor_offset_t = " + anchor_offset_t,
		// 	"anchor_offset_l = " + anchor_offset_l,
		// 	"anchor_offset_w = " + anchor_offset_w,
		// 	"",
		// 	"viewport_height = " + viewport_height,
		// 	"scroll_offset_v = " + scroll_offset_v
		// ].join("\n"));

		// compensate for the box being positioned off-screen vertically
		// FIXME: compensate for the viewport being too small for the popup
		if ((anchor_offset_t + box_height) > viewport_height) {
			var over_amt = anchor_offset_t + box_height - viewport_height;
			box_top -= over_amt;
			arrow_top += over_amt;
		}

		// try and put the box on the right-hand side of the element
		var box_left = anchor_offset_l + anchor_offset_w + box_offset_l;
		var arrow_left = -arrow_offset_h; // relative to the box left
		var class_name = "Details Right";

		// compensate for the box being positioned off-page horizontally
		// and switch it to the left-hand side of the element
		if ( (box_left + box_width) > parent_offset_w ) {
			box_left = anchor_offset_l - box_width - box_offset_l;
			arrow_left += box_width + arrow_offset_h - 1;
			class_name = "Details Left";
		}

		// DP.debug([
		// 	"box_left = " + box_left,
		// 	"box_top = " + box_top,
		// 	"arrow_left = " + arrow_left,
		// 	"arrow_top = " + arrow_top,
		// 	"class_name = " + class_name
		// ].join("\n"));

		// move the details box into position
		this.node.setStyle({ 'top' : box_top + "px", 'left' : box_left + "px" });
		this.node.className = class_name;

		// set the shadow height
		this.node_shadow.setStyle({ height : box_height + "px" });

		// position the callout arrow
		this.node_arrow.setStyle({ top : arrow_top + "px", left : arrow_left + "px" });

		// position the IFRAME
		this.node_iframe.setStyle({ 'top' : box_top + "px", 'left' : box_left + "px", 'width' : box_width + "px", height : box_height + "px" });
	},

	destroy : function(e) {
		if (e) Event.stop(e);

		$(this.node).remove();

		delete this.anchor_node.helptip_obj.detail_win;
	},

	editItem : function(e) {
		if (e) Event.stop(e);

		new Ajax.Request(Event.element(e).href, {
			method : 'get',

			onSuccess : function(transport){
				this.html_content = this.CONFIG['details_html'].evaluate({
					'title' : "Edit: " + this.anchor_node.title,
					'body' : transport.responseText
				});

				$(this.node).remove();

				this.create();
			}.bind(this),

			onFailure : function(transport){
				// FIXME: catch the error better
				throw new DP.Errors.AjaxError('request failed!');
			}
		});

	},

	onReceiveEvent : function(e) {
		if (e.event_data.event_name == 'detail_close') {
			// FIXME: this needs to be conditional
			this.anchor_node.removeClassName('NewTip');

			this.destroy(e);
		}
	}	
});
Object.extend(DP.Widgets.DetailPopup.prototype, Event.Listener);
Object.extend(DP.Widgets.DetailPopup.prototype, Event.Publisher); // NOTE: not sure about if it has to publish events

/* --------------------------------------------- END: DetailPopup widget -- */


/* -- BEGIN: AJAXForm ----------------------------------------------------- */

DP.Widgets.AJAXForm = Class.create();
Object.extend(DP.Widgets.AJAXForm.prototype, DP.Widget.prototype);
Object.extend(DP.Widgets.AJAXForm.prototype, Event.Publisher);
Object.extend(DP.Widgets.AJAXForm.prototype, {
	CONFIG : { },

	initialize : function(f) {
		this.node = f;
		this.node.observe('submit', this.submitHandler.bind(this));

		DP.EventBroker.registerEventsPublisher('detail_close', this);
	},

	node : null,

	submitHandler : function(e) {
		Event.stop(e);

		var params = Form.serialize(this.node);

		this.node.disable();

		new Insertion.Bottom(this.node.parentNode, "<p class=\"ProgressMessage\">submitting...</p>");

		new Ajax.Request(this.node.action, {
			method : 'post',
			parameters : params,

			onSuccess : function(){
				// FIXME: should send data about removing classname
				this.dispatchEvent('detail_close');
			}.bind(this),

			onError : function() {
				// FIXME: handle errors
			}
		});
	}

});

/* ------------------------------------------------------- END: AJAXForm -- */


/* -- BEGIN: FormValidation ----------------------------------------------- */

DP.Widgets.FormValidation = Class.create();
Object.extend(DP.Widgets.FormValidation.prototype, {
	CONFIG : {
		validation_class : 'Validate',
		validation_class_prefix : 'validate',
		class_regex : /^validate\-([\w-_]+)/,
		error_class : "FormValidationError"
	},

	initialize : function(f) {
		this.node = f;

		this.node.observe('submit', this.submitHandler.bind(this));
	},

	node : null,
	validation_errors : [],
	same_named_inputs : [],

	submitHandler : function(e) {
		if (!this.validate()) {
			Event.stop(e);

			new Insertion.Before($$S(this.node, "DIV.FormAction")[0], "<div class=\"" + this.CONFIG['error_class'] + "\">Please correct the errors in this form<\/div>");

			// var error_msg = document.createElement('div');
			// error_msg.className = this.CONFIG['error_class'];
			// error_msg.appendChild(document.createTextNode("Please correct the errors in this form"));
			// 
			// var form_action = $$S(this.node, "DIV.FormAction")[0];
			// var parent = form_action.parentNode;
			// parent.insertBefore(error_msg, form_action);
		}
	},

	clearValidationErrors : function() {
		$$S(this.node, "." + this.CONFIG['error_class']).invoke('remove');
	},

	validate : function() {
		this.clearValidationErrors();

		// reject all form elements with no NAME attrs
		// also reject the ones without validation classes
		var all_form_els = $A(this.node.elements).reject(function(el){

			// go through class names attached to element and see if any match our regex
			// which means we'd want to validate them
			var should_validate = $(el).classNames().toArray().any(function(class_name){
				return class_name.match(this.CONFIG['class_regex']);
			}.bind(this));

			return (el.name == undefined) || el.name.empty() || !should_validate;
		}.bind(this));

		// find all of the form element names
		var unique_form_element_names = all_form_els.pluck('name').uniq();

		var form_els = unique_form_element_names.map(function(el_name){
			return all_form_els.find(function(el){
				return el_name == el.name;
			});
		});

		this.validation_errors = form_els.map(function(el){
			var validator = el.classNames().toArray().find(function(class_name){
				return class_name.match(this.CONFIG['class_regex']);
			}.bind(this));

//			console.log(validator);

			// find and execute the matching validator
			if (Object.keys(this.validators).include(validator)) {
				return this.validators[validator](el);

			} else {
				DP.debug('no validator found for ' + validator);
				return true;
			}
		}.bind(this));

		this.validation_errors = this.validation_errors.reject(function(item){
			return item === true;
		});

		return (this.validation_errors.length < 1);
	},

	validators : {
		'validate-is-not-blank' : function(el) {
			return el.value.blank() || el.value.empty();
		},

		'validate-is-selected' : function(el) {
			var msg = "Please select at least one of the following";

			var inputs = $A($$('INPUT')).reject(function(input){
				return (input.name != el.name);
			});

			var is_valid = $A(inputs).any(function(el) {
				return $F(el);
			});

			if (!is_valid){
				// FIXME: the el.up(2) is a total hack because the INPUTs are split up 
				// into two lists. the up(2) goes to a wrapping element around both ULs
				new Insertion.Before(el.up(2), "<div class=\"FormValidationError\">" + msg + "<\/div>");
			}

			return is_valid;
		}.bind(this)
	}

});

/* ------------------------------------------------- END: FormValidation -- */


/* -- BEGIN: DependentRadioButtons ---------------------------------------- */

DP.Widgets.DependentRadioButtons = Class.create();
Object.extend(DP.Widgets.DependentRadioButtons, {
	DEFAULTS : {
		'disable_elements' : true
	}
});
Object.extend(DP.Widgets.DependentRadioButtons.prototype, {
	options : null, // options for DependentRadioButtons instance, copied from DEFAULTS
	inputs : null, // holds the INPUT elements

	// radio_button_elements: an array of INPUT type="radio" DOM elements
	// dependency_config: { 'radio_button_id' : { 'activate' : function(){ }, 'deactivate' : function(){ } } }
	// options: { 'disable_elements' : true }
	initialize : function(radio_button_elements, dependencies, options) {
		// store all of the buttons and options passed in
		this.inputs = $A(radio_button_elements);

		this.dependencies = dependencies;

		// apply the passed-in options
    this.options = Object.extend(Object.clone(DP.Widgets.DependentRadioButtons.DEFAULTS), options || {});

		this.inputs.each(function(el) {
			// attach the event handler
			$(el).observe('click', this.activate.bind(this, el));

			// on initialize, run activate a button if it is checked; then deactivate all unchecked buttons
			if (el.checked) { this.activate(el); }
		}.bind(this));

		this.deactivateAll();

	}, // END: initialize()

	activate : function(input_el) {
		if (!input_el) { return; }

		// check if the activation function exists before running it
		if (this.dependencies[input_el.id] && typeof(this.dependencies[input_el.id].activate) == 'function') {
			var node = this.dependencies[input_el.id].activate();

			if ($(node) && this.options.disable_elements) {
				$$S(node, 'INPUT, SELECT, TEXTAREA, BUTTON').each(function(item) {
					item.disabled = false;
				});
			}
		}

		// run all of the deactivation functions for the other buttons
		for (var input_id in this.dependencies) {
			if ( (input_id != input_el.id) && this.dependencies[input_id] && (typeof(this.dependencies[input_id].activate) == 'function') ) {
				var node = this.dependencies[input_id].deactivate();

				if ($(node) && this.options.disable_elements) {
					$$S(node, 'INPUT, SELECT, TEXTAREA, BUTTON').each(function(item) {
						item.disabled = true;
					});
				}
			}
		}
	}, // END: activate()

	deactivateAll : function() {
		for (var input_id in this.dependencies) {
			if ( !$(input_id).checked &&  (this.dependencies[input_id] && typeof(this.dependencies[input_id].activate) == 'function') ) {
				var node = this.dependencies[input_id].deactivate();

				if ($(node) && this.options.disable_elements) {
					$$S(node, 'INPUT, SELECT, TEXTAREA, BUTTON').each(function(item) {
						item.disabled = true;
					});
				}
			}
		}
	}

});

/* ------------------------------------------ END: DependentRadioButtons -- */


/* -- BEGIN: PrefillForm -------------------------------------------------- */

DP.Widgets.PrefillForm = Class.create();
Object.extend(DP.Widgets.PrefillForm.prototype, {
	CONFIG : { },

	initialize : function(el, data) {
		this.node = el;
		this.data = data;

		this.node.observe('click', this.clickHandler.bind(this, this.node));

		if (el.checked) {
			this.fillForm();
		}

	}, // END: initialize()

	clickHandler : function(checkbox) {
		if (checkbox.checked) {
			this.fillForm();

		} else {
			this.clearForm();
		}
	},

	fillForm : function() {
		for (var field in this.data) {
			var form_el = $(field);
			if (form_el) {
				form_el.disabled = true;
				form_el.value = this.data[field];
			}
		}
	},
	
	clearForm : function() {
		for (var field in this.data) {
			var form_el = $(field);
			if (form_el) {
				form_el.disabled = false;
				form_el.value = "";
			}
		}
	}

});

/* ---------------------------------------------------- END: PrefillForm -- */


/* ======================================================================== */


/* -- BEGIN: FormFieldTitle ----------------------------------------------- */
new DP.Feature({
	Version : "FormFieldTitle;0.1",

	CONFIG : {
		show_title_class : "ShowTitle"
	},

	setupElements : function(scope) {

		// set up the INPUT tags
		$$S(scope, 'INPUT.' + this.CONFIG['show_title_class']).each(function(input_el){
			// skip this whole thing if the element is disabled or if there's no title attribute
			if ( input_el.disabled || 
			    (typeof(input_el.title) == 'undefined') || 
			    (input_el.title == '') ) {
				return;
			}

			this.restoreHelperText.call(input_el);

			// hook up the events
			input_el.observe('focus', this.clearHelperText.bind(input_el), false);
			input_el.observe('blur', this.restoreHelperText.bind(input_el), false);

		}.bind(this)); // END: inputs

		// set up the SELECT tags
		$$S(scope, 'SELECT.' + this.CONFIG['show_title_class']).each(function(select_el){
			// warning: kinda ugly

			// create new option and prepend it tot the options array
			var new_option = "<option value=\"\"" + ((select_el.selectedIndex == 0)	 ? " selected=\"selected\"": "") + ">" + select_el.title + "<\/option>";

			new Insertion.Top(select_el, new_option);
		}); // END: selects

		// set up the TEXTAREA tags
		// FIXME: same as INPUT tags right now... may change
		$$S(scope, 'TEXTAREA.' + this.CONFIG['show_title_class']).each(function(textarea_el){
			// skip this whole thing if the element is disabled or if there's no title attribute
			if ( textarea_el.disabled || 
			    (typeof(textarea_el.title) == 'undefined') || 
			    (textarea_el.title == '') ) {
				return;
			}

			this.restoreHelperText.call(textarea_el);

			// hook up the events
			textarea_el.observe('focus', this.clearHelperText.bind(textarea_el), false);
			textarea_el.observe('blur', this.restoreHelperText.bind(textarea_el), false);
		}.bind(this)); // END: textareas

		// set up the FORM to clear out our titles onsubmit
		$$S(scope, 'FORM').each(function(form_el){
			form_el.observe('submit', this.clearHelperTextOnSubmit.bindAsEventListener(this), false);
		}.bind(this));

	}, // END: setupElements

	public_methods : {

		clearHelperText : function() {
			if (this.value == this.title) {
				this.value = '';
				this.removeClassName(DP.GLOBALS['disabled_class']);
			}
		},

		restoreHelperText : function() {
			if (this.value == '' || this.value == this.title) {
				this.value = this.title;
				this.addClassName(DP.GLOBALS['disabled_class']);
			}
		},

		clearHelperTextOnSubmit : function() {
			var f = Event.element(arguments[0]);

			// loop through all of the inputs in this FORM
			$A($$S(f, 'INPUT')).each(function(input_el) {
				// only process inputs which are ShowTitle enabled
				if (!input_el.hasClassName(this.CONFIG['show_title_class'])) { return; }

				// reset the value if it's equal to the title
				if (input_el.value == input_el.title) { input_el.value = ""; }
			}.bind(this));
		} // END: clearDefaultValues()
		
	}
});
/* ------------------------------------------------- END: FormFieldTitle -- */


/* -- BEGIN: Tabs --------------------------------------------------------- */
new DP.Feature({
	Version : "Tabs;0.1",

	CONFIG : {
		tabset_class : ".TabSet"
	},

	setupElements : function(scope) {

		$$S(scope, this.CONFIG['tabset_class']).each(function(tabset_div){

			var tabset = new DP.Widgets.TabSet(tabset_div);

			if (tabset.tabs && (tabset.tabs.length > 0) ) {
				// store each of the tabs in the tabset in the elements hash so we can access them via HashHandler
				tabset.tabs.each(function(item){
					this.store(item.id, item);
				}.bind(this));

			} else {
				DP.debug("tabset has no tabs!");
			}

		}.bind(this));

	} // END: initialize()
});
/* ----------------------------------------------------------- END: Tabs -- */


/* -- BEGIN: Revealer ----------------------------------------------------- */
new DP.Feature({
	Version : "Revealer;0.1",

	CONFIG : {
		class_name : "RevealList"
	},

	setupElements : function(scope) {

		$$S(scope, "." + this.CONFIG['class_name'] + " DT").each(function(dt){
			// we only care about the first anchor
			var anchor = $$S(dt, 'A')[0];

			if (!anchor) {
				DP.debug("Revealer: no anchor found!");
				return;
			}

			// check and make sure that the HREF has a hash
			var hash = anchor.href.getHash();
			if (!hash) {
				DP.debug(['anchor has no hash!', 'href = ' + anchor.href]);
				return;
			}

			// stuff the revealable element in our list to keep track of it
			this.store(hash, new DP.Widgets.Revealer(anchor));

			// hide the DDs at load
			dt.next('dd').hide();

		}.bind(this));

	} // END: initialize()
});
/* ------------------------------------------------------- END: Revealer -- */


/* -- BEGIN: ToggleBox ---------------------------------------------------- */
new DP.Feature({
	Version : "ToggleBox;0.1",

	CONFIG : {
		header_class_name : "ToggleHeader",
		static_box_class : "Static"
	},

	setupElements : function(scope) {

		// DIV.Toggle( Static)?
		// 	+ DIV.ToggleHeader
		// 	+ DIV.ToggleContent

		// hook up all of the toggle boxes on the page
		$$S(scope, "." + this.CONFIG['header_class_name']).each(function(el){
			// we only care about the first anchor
			var anchor = el.getElementsByTagName('A')[0];

			// sanity check
			if (!anchor) {
				DP.debug("ToggleBox: no anchor found!");
				return false;
			}

			// check and make sure that the HREF has a hash
			// if not, then we consider the ToggleBox to be "Static" (already expanded)
			// FIXME: ensure this design pattern is desired
			var hash = anchor.href.getHash();
			if ($(anchor.parentNode.parentNode.parentNode).hasClassName(this.CONFIG['static_box_class']) || !hash) {
				DP.debug(['ToggleBox: anchor has no hash!', 'href = ' + anchor.href]);
				return false;
			}

			// hide the toggle content at load
			var next_div = el.next('DIV');
			if(!next_div.parentNode.hasClassName('Active'))
			  next_div.hide();

			this.store(hash, new DP.Widgets.ToggleBox(anchor));

		}.bind(this));

	} // END: setupElements()
});
/* ------------------------------------------------------ END: ToggleBox -- */


/* -- BEGIN: TestLinkInterceptor ------------------------------------------ */
new DP.Feature({
	Version : "TestLinkInterceptor;0.1",

	CONFIG : {
		test_url : "DP_TEST",
		unimplemented_msg : "Sorry, this link is not implemented yet."
	},

	setupElements : function(scope) {

		$$S(scope, "A").each(function(anchor) {
			if (anchor.href.toUpperCase().indexOf(this.CONFIG['test_url']) < 0) { return; }

			anchor.observe('click', this.showMessage.bind(this), false);
			anchor.onclick = Prototype.False; // fix for dumb older versions of safari
		}.bind(this));

	}, // END: setupElements()

	public_methods : {

		showMessage : function() {
			alert(this.CONFIG['unimplemented_msg']);
		}
		
	}
});
/* -------------------------------------------- END: TestLinkInterceptor -- */


/* -- BEGIN: FormCanceller ------------------------------------------------ */
new DP.Feature({
	Version : "FormCanceller;0.1",

	CONFIG : {
		link_rel : "CancelForm"
	},

	initialize : function() {
		DP.EventBroker.registerEventsPublisher('togglebox_close', this);
		DP.EventBroker.registerEventsPublisher('row_close', this);
	},

	setupElements : function(scope) {

		// NOTE: this section needs some refactoring

		// set up FORM cancel DOM methods for forms inside ToggleBoxes
		$$S(scope, ".ToggleContent FORM").each(function(f) {
			f.cancel = function() {
				this.dispatchEvent('togglebox_close', { togglebox_id : f.up('DIV.Toggle').id });
			}.bind(this);
		}.bind(this));

		// for in-table editing
		$$S(scope, ".EditCell FORM").each(function(f) {
			f.cancel = function() {
				this.dispatchEvent('row_close', { row_id : f.up('TR').previous("TR").id });
			}.bind(this);
		}.bind(this));

		// set up links to cancel forms
		$$S(scope, "FORM A").each(function(anchor) {

			if (anchor.getAttribute("rel") != this.CONFIG["link_rel"]) { return; }

			anchor.observe('click', this.cancelForm.bindAsEventListener(this), false);
		}.bind(this));

	}, // END: setupElements

	public_methods : {

		cancelForm : function(e) {
			Event.stop(e);

			// this assumes the clicked element is in the FORM we're cancelling
			var f = Event.findElement(e, "FORM");

			f.reset();

			if (typeof(f.cancel) == 'function')
				f.cancel();

			// FIXME: figure out how to add tearDownElements() for the DP.Feature

		} // END: cancelForm
		
	}
});
/* -------------------------------------------------- END: FormCanceller -- */


/* -- BEGIN: TableListingEditor ------------------------------------------- */
new DP.Feature({
	Version : "TableListingEditor;0.1",

	CONFIG : {
		table_class : "Listing"
	},

	setupElements : function(scope) {

		$$S(scope, "TABLE." + this.CONFIG['table_class'] + " TBODY TR").each(function(row) {
			this.store(row.id, new DP.Widgets.Row(row));
		}.bind(this));

	} // END: setupElements()
});
/* --------------------------------------------- END: TableListingEditor -- */


/* -- BEGIN: TagsAndCategories -------------------------------------------- */
new DP.Feature({
	Version : "TagsAndCategories;0.1",

	CONFIG : {
		tag_list : "tag_list",
		tag_class : "tag",
		tag_placeholder : "tag-placeholder",
		suggested_tags : "suggested-tags-list",
		category_list : "category-list",
		observer_update_interval : .5,
		tag_separator : ", ",
		tag_url : new Template("/categories/#{cat_id}/tags/#{content_type}")
	},

	setupElements : function(scope) {

		// bail out if the form doesn't exist
		if (!$(this.CONFIG['tag_list'])) return;

		// figure out what content type we're dealing with
		this.content_type = this._determineContentType($(this.CONFIG['tag_list']).form);
		if (!this.content_type) return;

		// attach event listeners to the categories
		$$S(scope, "#" + this.CONFIG['category_list'] + " LI INPUT").each(function(input){
			input.observe('click', this.categoryClickHandler.bindAsEventListener(this));

			// FIXME: this is not optimnal, as it kicks off n AJAX requests on page load.
			// need a better way of doing this, perhaps chaining the requests
			// also, what to do if a user clicks the category before the tags are loaded?
			this.getTagsForCategory(input);
		
		}.bind(this));

		// monitor the tag input field for changes, so we can update our tag boxes
		new Form.Element.Observer($(this.CONFIG['tag_list']), this.CONFIG['observer_update_interval'], this.deactivateTags.bind(this));

		// temp, as we need the scope of the input as 'this'
		// NOTE: could probalby fix this by using the event to grab the input el
		var CONFIG = this.CONFIG;

		// attach to the blur event so we can clean upp the tag input box
		$(this.CONFIG['tag_list']).observe('blur', function(){
			this.value = (this.value == null) ? "" : this.value.parseTags(CONFIG['tag_separator']).join(CONFIG['tag_separator']);
		});

	}, // END: setupElements()

	public_methods : {
		categoryClickHandler : function() {
			var input = Event.element(arguments[0]);

			var tags = this.tags[input.value];

			if (input.checked) {
				this.populateTags(tags);
			} else {
				this.removeTags(tags);
			}

		}, // END: categoryClickHandler()

		tags : {},
		content_type : null,

		_determineContentType : function(f) {
			var known_content_types = ['Resource', 'Discussion'];
			var regex = /^(\w+)\[/;

			for (var i=0; i < f.elements.length; i++) {
				if (!f.elements[i].name) continue;

				var results = f.elements[i].name.match(regex);
				if (results && results.length && known_content_types.include(results[1].capitalize())) {
					return results[1].capitalize();
				}
			}

			return null;
		},

		getTagsForCategory : function(input) {
			if (!this.content_type) return;

//			alert(this.CONFIG['tag_url'].evaluate({ 'cat_id' : input.value, 'content_type' : this.content_type }));

			new Ajax.Request(this.CONFIG['tag_url'].evaluate({ 'cat_id' : input.value, 'content_type' : this.content_type }), {
				method : "get",

				onSuccess : function(transport, json){
					// NOTE: this is needed because the mime-type doesn't match what prototype is looking for.
					// should be fixed in latest version of prototype.
					this.tags[input.value] = transport.responseText.evalJSON().tags.map(function(tag){
						return tag.name;
					});

					if (input.checked) {
						this.populateTags(this.tags[input.value]);
					}

				}.bind(this),

				onError : function(transport){
					// FIXME: handle errors better
					throw new DP.Errors.AjaxError('bad JSON response');
				}
			});
		
		}, // END: getTagsForCategory()

		getAllTags : function(exclude_cat) {

			var active_tags = {};

			// make sure we have all tags in our tag cache
			$$("#" + this.CONFIG['category_list'] + " LI INPUT").each(function(input){
				if (input.checked) {
					active_tags[input.value] = this.tags[input.value];
				}
			}.bind(this));

			var all_tags = [];
			for (var c in active_tags) {
				// don't include the paased-in cat
				if (c == exclude_cat) continue;

				all_tags = all_tags.concat(this.tags[c]);
			}

			return all_tags.uniq();
		}, // END: getAllTags()

		createTagNode : function(tag) {
			var tag_node = document.createElement('span');
			tag_node.className = this.CONFIG['tag_class'];
			tag_node.appendChild(document.createTextNode(tag));

			return tag_node;
		},

		populateTags : function(new_tag_list) {
			// hide the placeholder text
			$(this.CONFIG['tag_placeholder']).hide();

			// extract current tags from list to array
			var tag_container = $(this.CONFIG['suggested_tags']);
			var suggested_tags = $A(tag_container.immediateDescendants());
			var suggested_tags_text = suggested_tags.pluck('firstChild').pluck('nodeValue');

			// add new tags to array
			var all_tags = suggested_tags_text.concat(new_tag_list).sort().uniq();

			// remove old tags from page
			suggested_tags.invoke('remove');

			// re-create tags and add back into page
			for (var i=-0; i < all_tags.length; i++) {
				var new_tag = this.createTagNode(all_tags[i]);
				Event.observe(new_tag, 'click', this.tagClickHandler.bindAsEventListener(this));
				tag_container.appendChild(new_tag);
				tag_container.appendChild(document.createTextNode(" "));
			}

			// sync up tag list with suggested tags
			this.deactivateTags();
		}, // END: populateTags

		removeTags : function(tags_to_remove) {
			var tag_container = $(this.CONFIG['suggested_tags']);
			var suggested_tags = $A(tag_container.immediateDescendants());

			var all_other_tags = this.getAllTags();

			for (var i=0; i < tags_to_remove.length; i++) {
				if (!all_other_tags.include(tags_to_remove[i])) {
					suggested_tags.find(function(el){
						return (el.firstChild.nodeValue == tags_to_remove[i]);
					}).remove();
				}
			}

			if ($A(tag_container.immediateDescendants()).length < 1)
				$(this.CONFIG['tag_placeholder']).show();
			
		}, // END: removeTags()

		deactivateTags : function() {
			var current_tags = $(this.CONFIG['tag_list']).value.parseTags(this.CONFIG['tag_separator']);

			$(this.CONFIG['suggested_tags']).immediateDescendants().each(function(tag){
				if (current_tags.include(tag.firstChild.nodeValue)) {
					tag.addClassName(DP.GLOBALS['disabled_class']);
				} else {
					tag.removeClassName(DP.GLOBALS['disabled_class']);
				}
			});
		},

		tagClickHandler : function(e) {
			var span = Event.element(e);
			var tag = span.firstChild.nodeValue;

			var tag_input = $(this.CONFIG['tag_list']);
			var current_tags = tag_input.value.parseTags(this.CONFIG['tag_separator']);

			if (current_tags.include(tag)) return;

			current_tags.push(tag);
			tag_input.value = current_tags.join(this.CONFIG['tag_separator']);

			// deactivate newly adaded tag
			span.addClassName(DP.GLOBALS['disabled_class']);
		} // END: tagClickHandler()

	}

});
/* ---------------------------------------------- END: TagsAndCategories -- */


/* -- BEGIN: MeetingFiles ------------------------------------------------- */
// FIXME: all of this logic is not quite right
new DP.Feature({

	Version : "MeetingFiles;0.1",

	CONFIG : {
		event_form_id : "event_form",
		upload_fields : "upload-fields",
		upload_files : "upload-files",
		queued_files : "queued-files",
		input_id_prefix : "upload_file_",
		existing_files : "existing-file-list",
		limit_reached_id : 'upload-limit-reached',
		limit_reached_msg : "You have reached the maximum of five allowed files.",
		max_file_uploads : 5
	},
	
	initialize : function(){
		return;

		DP.EventBroker.registerEventsPublisher('remove_file', this);
	},
	
	setupElements : function(scope){
		return;

		// bail out if the form doesn't exist
		if (!$(this.CONFIG['event_form_id'])) return;

		// check for too many existing files
		var over_limit = (this.countAvailableFiles(scope) <= 0);

		// insert msg if too many files are already uploaded
		if (over_limit) {
			this.createOverLimitMsg();
		}

		// set up the upload fields and hide them
		$$S(scope, "#" + this.CONFIG['upload_fields'])[0].immediateDescendants().each(function(field, index){
			this.store(field.id, new DP.Widgets.FileUploadField(field));

			if ( (index != 0) || over_limit ) field.hide();
		}.bind(this));

		// set up the existing files
		$$S(scope, "#" + this.CONFIG['existing_files'] + " INPUT").each(function(input){
			if (input.checked)
				input.up().previous().addClassName(DP.GLOBALS['deleted_class']);
			
			input.observe('click', function(e){
				var input = Event.element(e);

				if (input.checked)
					input.up().previous().addClassName(DP.GLOBALS['deleted_class']);
				else
					input.up().previous().removeClassName(DP.GLOBALS['deleted_class']);

				this.showNextField();
			}.bind(this));

		}.bind(this));

	}, // END: setupElements()

	public_methods : {
		countAvailableFiles : function(scope) {
			var existing = 0;
			var to_be_deleted = 0;

			$$S(scope, "#" + this.CONFIG['existing_files'] + " INPUT").each(function(input, index){
				existing++;
				if (input.checked) to_be_deleted++;
			});

			var queued_files = $H(this.elements).findAll(function(field, index){
				return !$F(this.CONFIG['input_id_prefix'] + (index + 1)).blank();
			}.bind(this)).length;

			// console.log([
			// 	"existing files = " + existing,
			// 	"queued_files = " + queued_files,
			// 	"to be deleted = " + to_be_deleted
			// ].join("\n"));

			return this.CONFIG['max_file_uploads'] - existing + to_be_deleted - queued_files;
		},

		showNextField : function() {
			// find the next field with nothing in it
			var next_field = $H(this.elements).find(function(field, index){
				return $F(this.CONFIG['input_id_prefix'] + (index + 1)).blank();
			}.bind(this));

//			console.log(next_field);

			if (next_field && next_field.length)
				next_field = next_field[1].field_node;

			var available_files = this.countAvailableFiles();
//			console.log("available_files = " + available_files);

			// and show something
			if (next_field && (available_files > 0)) {
				next_field.show();
				this.removeOverLimitMsg();

			} else if (available_files == 0) {
				if (next_field) next_field.hide();
				this.createOverLimitMsg();

			// force removal of queued files
			} else if (available_files < 0) {
				this.dispatchEvent('remove_file');
			}
		}, // END: showNextField()

		createOverLimitMsg : function() {
			var msg_node = document.createElement('p');
			msg_node.id = this.CONFIG['limit_reached_id'];
			msg_node.className = 'Message';
			msg_node.appendChild(document.createTextNode(this.CONFIG['limit_reached_msg']));

			var limit_reached_node = $(this.CONFIG['limit_reached_id']);
			if (!limit_reached_node) { $(this.CONFIG['upload_files']).appendChild(msg_node); }
		},

		removeOverLimitMsg : function() {
			var limit_reached_node = $(this.CONFIG['limit_reached_id']);
			if (limit_reached_node) { limit_reached_node.remove(); }
		}

	}
});
/* --------------------------------------------------- END: MeetingFiles -- */


/* -- BEGIN: EventMembers ------------------------------------------------- */
new DP.Feature({

	Version : "EventMembers;0.1",

	CONFIG : {
		event_form_id : "event_form",
		group_members_field : "group-members-field",
		group_members_picker : "group-members-picker",
		member_picker_html : [
			"<div id=\"group-members\">",
				"<strong>Invite a Member<\/strong> &nbsp;&nbsp;&nbsp; <span class=\"Link\" id=\"add-all-members\">Add All &raquo;<\/span>",
				"<ul class=\"DHTMLSelect\" id=\"member-list\"><\/ul>",
			"<\/div>",
			"<div id=\"invited-members\">",
				"<h4>Invited Members<\/h4>",
				"<span class=\"Placeholder\" id=\"member-placeholder\">(No members have been invited yet)<\/span>",
			"<\/div>"
		].join("\n"),
		participant_field : "event_participant_list",
		group_members : "group-members",
		add_all_link : "add-all-members",
		member_list : "member-list",
		invited_members : "invited-members",
		person_class : "person",
		remove_person_text : "Remove member from meeting",
		member_placeholder : "member-placeholder"
	},
	
	setupElements : function(scope){
		if (!$(this.CONFIG['event_form_id'])) return;

		$$S(scope, "#" + this.CONFIG['group_members_field'])[0].hide();

		$$S(scope, "#" + this.CONFIG['group_members_picker'])[0].innerHTML = this.CONFIG['member_picker_html'];

		this.populateMemberList();
	},

	public_methods : {
		populateMemberList : function() {
			$A($(this.CONFIG['participant_field']).options).each(function(opt){

				$(this.CONFIG['member_list']).appendChild(this.createGroupMemberNode(opt));

				if (opt.selected) {
					$(this.CONFIG['invited_members']).appendChild(this.createInvitedMemberNode(opt));

					// hide the placeholder text
					$(this.CONFIG['member_placeholder']).hide();
				}
			}.bind(this));

			$(this.CONFIG['add_all_link']).observe('click', this.addAllMembersHandler.bindAsEventListener(this));

		},

		createGroupMemberNode : function(opt) {
			var li_node = document.createElement('li');
			li_node.id = "member_" + opt.value;

			if (opt.selected) li_node.className = "Disabled";

			var span_node = document.createElement('span');
			span_node.className = DP.GLOBALS["link_class"];
			span_node.appendChild(document.createTextNode(opt.text));

			li_node.appendChild(span_node);

			Event.observe(li_node, 'click', this.groupMemberClickHandler.bind(this, li_node, { text : opt.text, value: opt.value }));

			return li_node;
		},

		groupMemberClickHandler : function(li_node, data) {
			// bail out if we're disabled
			if (li_node.hasClassName(DP.GLOBALS['disabled_class'])) return;

			// turn off this LI, we're adding it
			li_node.addClassName(DP.GLOBALS['disabled_class']);

			// loop over the OPTIONs in the SELECT field and select the one we clicked
			$A($(this.CONFIG['participant_field']).options).each(function(opt){
				if (opt.value == data.value) {
					opt.selected = true;
					throw $break;
				}
			});

			// hide the placeholder text
			$(this.CONFIG['member_placeholder']).hide();

			// create the new node, add it to the DOM and highlight it
			new Effect.Highlight($(this.CONFIG['invited_members']).appendChild(this.createInvitedMemberNode(data)), { endcolor : '#F3F3F6' });
		},

		createInvitedMemberNode : function(opt) {
			var member_node = document.createElement('span');
			member_node.className = this.CONFIG["person_class"];
			member_node.appendChild(document.createTextNode(opt.text));
			member_node.appendChild(document.createTextNode(" "));

			var img_node = document.createElement('img');
			img_node.src = DP.GLOBALS['img_path'] + 'action_stop.gif';
			img_node.className = "Icon";
			img_node.title = this.CONFIG['remove_person_text'];

			Event.observe(img_node, 'click', this.removeMemberFromList.bindAsEventListener(this, opt), false);

			member_node.appendChild(img_node);

			// stash the member id in the DOM element for easy removal of member later
			member_node.member_id = opt.value;

			return member_node;
		},

		removeMemberFromList : function(e, data) {

			$A($(this.CONFIG['participant_field']).options).each(function(opt){
				if (opt.value == data.value) {
					opt.selected = false;
					throw $break;
				}
			});

			$('member_' + data.value).removeClassName(DP.GLOBALS['disabled_class']);

			var span = Event.findElement(e, 'span');

			if ($$S(span.parentNode, '.' + this.CONFIG['person_class']).length <= 1) {
				$(this.CONFIG['member_placeholder']).show();
			}

			span.remove();
		},

		addAllMembersHandler : function() {
			// loop over the OPTIONs in the SELECT field
			$A($(this.CONFIG['participant_field']).options).each(function(opt){
				// bail out of the options is already selected
				if (opt.selected) { return; }

				opt.selected = true;
				
				// turn off the LI, we're adding it
				$("member_" + opt.value).addClassName(DP.GLOBALS['disabled_class']);

				// create the new node, add it to the DOM and highlight it
				new Effect.Highlight($(this.CONFIG['invited_members']).appendChild(this.createInvitedMemberNode({ text : opt.text, value: opt.value })), { endcolor : '#F3F3F6' });

			}.bind(this));

			// hide the placeholder text
			$(this.CONFIG['member_placeholder']).hide();

		}

	}
});
/* --------------------------------------------------- END: EventMembers -- */


/* -- BEGIN: HashHandler -------------------------------------------------- */
new DP.Feature({
	Version : "HashHandler;0.1",

	CONFIG : {
		link_rel : "HashHandler"
	},

	initialize : function() {
		// we've only got one hash in a URL, so just run whatever is attached to it
		var hash = window.location.hash.getHash();

		if (hash)
			Event.onReady(this.run.bind(this, hash));
	
	},

	setupElements : function(root_node) {

		// process all of the links which have a rel="HashHandler"
		$$S(root_node, "A").each(function(anchor) {
			// sanity checks
			if (anchor.getAttribute("rel") != this.CONFIG["link_rel"]) {
				return;
			}

			var hash = anchor.href.getHash();
			if (!hash) {
				DP.debug("HashHandler: anchor " + anchor + " doesn't have a hash");
				return;
			}

			// attache the event handler, but do not store it
			anchor.observe('click', this.run.bindAsEventListener(this), false);
		}.bind(this));

	}, // END: setupElements()
	
	public_methods : {

		// this takes an event or an id
		// - events get passed in via onclick of a link
		// - ids get passed in on page load
		run : function() {
			var id = null, item, hash_parts;

			// return if passed nothing
			if (!arguments[0]) {
				return;

 			// get the id of the has via a passed in string
			} else if (typeof(arguments[0]) == "string") {
				id =  arguments[0];

				// get the ID of the hash via an event
			} else {
				var anchor = Event.element(arguments[0]);
				if (anchor.nodeName.toLowerCase() != "a") {
					anchor = anchor.up("a");
				}
				// we shouldn't have to check if the href has a hash, we attached it earlier because it did
				id = anchor.href.getHash();
			}

			DP.debug("HashHandler: attempting to run: '" + id + "'");

			// looking for additional arguments for activate functions after the semicolon (added by JS)
			hash_parts = id.split(";");

			if (item = this.findElement(hash_parts[0])) {
				if (typeof(item.activate) == 'function') {
					var args = "";
					if (hash_parts[1]) {
						item.activate(hash_parts[1]);
						args = " and activated it with '" + hash_parts[1] + "' as an argument";

					} else
						item.activate();

					DP.debug("HashHandler: found hash '" + hash_parts[0] + "' in URL" + args);

				} else {
					DP.debug(["HashHandler: found hash '" + hash_parts[0] + "' in URL, but could not activate it", hash_parts[0] + ".activate = " + item.activate.toString()]);
				}

			}	else {
				DP.debug("HashHandler: found hash '" + hash_parts[0] + "' in URL but could not find associated item in DP.Features.*.elements");
			}

		}, // END: executePageURLHash()

		findElement : function(id) {
			// FIXME: add capability to return multiple hash handlers for one id (across multiple Features)
			// FIXME: this should skip HashHandler.elements, as the items in there reference other Features and do not implemnt 'activate'
			for (var feat in DP.Features) {
				if (Object.keys(DP.Features[feat].elements).include(id)) {
					return DP.Features[feat].elements[id];
				}
			}
		} // END: findElement()

	}
});
/* ---------------------------------------------------- END: HashHandler -- */


/* -- BEGIN: HelpTips ----------------------------------------------------- */
new DP.Feature({
	Version : "HelpTips;0.1",

	CONFIG : {
		help_tip_class : "HelpTip"
	},

	setupElements : function(root_node) {
		$$S(root_node, "SPAN." + this.CONFIG['help_tip_class']).each(function(span){
			this.store(span.id,  new DP.Widgets.HelpTip(span));
		}.bind(this));
	}, // END: setupElements()

	public_methods : { }
});
/* ------------------------------------------------------- END: HelpTips -- */


/* -- BEGIN: AJAX Form Submitter ------------------------------------------ */
new DP.Feature({
	Version : "AJAXFormSubmitter;0.1",

	CONFIG : {
		form_class : "AJAXForm"
	},

	setupElements : function(root_node) {
		$$S(root_node, "FORM." + this.CONFIG['form_class']).each(function(f){
			this.store(f.id,  new DP.Widgets.AJAXForm(f), true);
		}.bind(this));
	}, // END: setupElements()
	
	public_methods : { }
});
/* -------------------------------------------- END: AJAX Form Submitter -- */


/* -- BEGIN: Form Validator ----------------------------------------------- */
new DP.Feature({
	Version : "FormValidator;0.1",

	CONFIG : {
		validation_class : 'ValidateForm'
	},

	setupElements : function(root_node) {
		$$S(root_node, "FORM." + this.CONFIG['validation_class']).each(function(f){
			this.store(f.id,  new DP.Widgets.FormValidation(f));
		}.bind(this));
	}, // END: setupElements()
	
	public_methods : { }
});
/* ------------------------------------------------- END: Form Validator -- */


/* -- BEGIN: Offsite Link ------------------------------------------------- */
new DP.Feature({
	Version : "OffsiteLink;0.1",

	CONFIG : {
		regex : /^http\:\/\/(www\.)?emanagerexchange\.com/i
	},

	setupElements : function(root_node) {
		$$S(root_node, ".WYSIWYGText A").each(function(anchor){
			if (!anchor.href.match(this.CONFIG['regex'])) {
				anchor.target = "_blank";
			}
		}.bind(this));
	}, // END: setupElements()
	
	public_methods : { }
});
/* --------------------------------------------------- END: Offsite Link -- */


/* -- BEGIN: NewsletterPopper ----------------------------------------------- */
new DP.Feature({
	Version : "NewsletterPopper;0.1",

	CONFIG : { },

	setupElements : function(root_node) {
		var newsletter_win = null;

		function openNewsletterWindow(e) {
			Event.stop(e);

			newsletter_win = window.open(this.href, "newsletter_win", "width=700,toolbar=yes,location=yes,status=yes,scrollbars=yes,resizable=yes");

			if (newsletter_win && window.focus) { newsletter_win.focus(); }
		};

		$$S(root_node, "#MainColumn LI A[rel=newsletter]").each(function(anchor) {
			anchor.observe('click', openNewsletterWindow.bind(anchor));
		});

	}, // END: setupElements()
	
	public_methods : { }
});
/* ------------------------------------------------- END: NewsletterPopper -- */


/* -- BEGIN: TimePickerHelper --------------------------------------------- */
new DP.Feature({
	Version : "TimePickerHelper;0.1",

	CONFIG : {
		class_name : "TimePicker"
	},

	setupElements : function(root_node) {
		var elements = $A($$S(root_node, "INPUT." + this.CONFIG['class_name']));
		elements.each(function(input){
			this.store(input.id, new TimePicker(input));
		}.bind(this));

		if (elements.length) {
			elements.pluck("form").uniq().reduce().observe('submit', this.formatTimesForServer.bindAsEventListener(this));
		}

	}, // END: setupElements()
	
	public_methods : {
		formatTimesForServer : function(e){
			$$("." + this.CONFIG['class_name']).each(function(input){
				try {
					input.value = Time.smartParse(input.value).formatSQLTime();
				} catch(e) {
					input.value = "";
				}
			}.bind(this));
		}
	}
});
/* ------------------------------------------------- END: NewsletterPopper -- */
