/*** TREE AND PERMISSIONS LIBRARY ***/

/*** Flash {{{
 * The Flash class exposes a generic API for displaying messages
 * on the page for the user to read. Like the Rails "flash" session
 * variable. */
var Flash = Class.create();
Flash.prototype = {
	initialize: function(container) {
		this.msgbox = $(container);
		this.msgbox.className += " jt_gen_flash";
		this.msgbox.className = this.msgbox.className.gsub(/\s+/, ' ');
		this.msgbox.hide();
		this.options = {
			hideDelay: 0,
			floating: false,
			zIndex: 200
		}
		Object.extend(this.options, arguments[1] || {});

		if(this.options.floating) {
			if(!this.options.hideDelay) this.options.hideDelay = 5;
			this.msgbox.setStyle({ position: 'absolute', zIndex: this.options.zIndex });
		}
	},

	setMessage: function(message) {
		this.msgbox.innerHTML = message;
	},

	place: function() {
		Position.Center(this.msgbox);
	},

	show: function() {
		this.placeRef = this.place.bind(this);
		if(this.options.floating) {
			this.place();
			Event.observe(window, 'scroll', this.placeRef);
			Event.observe(window, 'resize', this.placeRef);
		}

		if(arguments[0] && typeof(arguments[0]).toLowerCase() == 'string') {
			this.setMessage(arguments[0]);
		}
		this.msgbox.show();
		new Effect.Highlight(this.msgbox, { duration: 2.0 });
		if(this.options.floating) this.place();
		
		if(this.options.hideDelay) {
			window.setTimeout(function() {
				this.hide();
			}.bind(this), this.options.hideDelay*1000);
		}
	},

	hide: function() {
		//this.msgbox.hide();
		new Effect.Fade(this.msgbox, { duration: 0.5 });

		if(this.options.floating) {
			Event.stopObserving(window, 'scroll', this.placeRef);
			Event.stopObserving(window, 'resize', this.placeRef);
		}
	}
} // }}}
/*** Permissions {{{
 * The Permissions class creates a dialog for editing permissions
 * of a node. */
var Permissions = Class.create();
Permissions.prototype = {
	/*** initialize() {{{
	 * The constructor method. */
	initialize: function(node, context) {
		this.node = node;
		this.context = context;
		this.changes = new Hash();
		this.options = {};
		this.click_targets = [];
		this.save_buttons = [];
		Object.extend(this.options, arguments[2] || {});

		if(!$('permissions_overlay')) {
			var po = new Element('div', { id: 'permissions_overlay'});
			po.className = 'jt_gen_overlay';
			po.style.display = 'none';
			po.setOpacity(0.7);
			$(document.body).insert({ bottom: po });
		}
		if(!$('permissions_dialog')) {
			var pd = new Element('div', { id: 'permissions_dialog' });
			pd.className = 'jt_gen_dialog permissions_dialog';
			pd.style.display = 'none';
			$(document.body).insert({ bottom: pd });
		}
		if(!$('dialog_waiting_widget')) {
			var dww = new Element('div', { id: 'dialog_waiting_widget' });
			var i = new Element('img', { src: '/images/loading.gif' });
			i.style.margin = '30px';
			dww.insert({ bottom: i });
			$(document.body).insert({ bottom: dww });
		}
		this.waiting = $('dialog_waiting_widget');
		this.dialog = $('permissions_dialog');
		this.overlay = $('permissions_overlay');
	}, // }}}
	/*** feedback() {{{
	 * Give the user a message of some kind. */
	feedback: function(message) {
		if(this.options.flash) {
			this.options.flash.show(message);
		} else {
			alert(message);
		}
	}, // }}}
	/*** wait() {{{
	 * Show a little waiting thing. Could be useful. */
	wait: function() {
		this.place();
		this.overlay.show();
		this.waiting.show();

		this.placeRef = this.place.bind(this);
		Event.observe(window, 'scroll', this.placeRef);
		Event.observe(window, 'resize', this.placeRef);
	}, // }}}
	/*** unWait() {{{
	 * The opposite of wait(). */
	unWait: function() {
		if(this.overlay && this.overlay.visible()) {
			this.overlay.hide();
		}
		if(this.waiting && this.waiting.visible) {
			this.waiting.hide();
		}
		Event.stopObserving(window, 'scroll', this.placeRef);
		Event.stopObserving(window, 'resize', this.placeRef);
	}, // }}}
	/*** place() {{{
	 * Positions and sizes the permissions summary box on the
	 * screen. Should be called when the show() function is
	 * called, and also when the window is resized. */
	place: function() {
		var vpd = document.viewport.getDimensions();
		this.overlay.style.width = vpd.width + 'px';
		this.overlay.style.height = vpd.height + 'px';
		var so = document.viewport.getScrollOffsets();
		this.overlay.style.left = so[0] + 'px';
		this.overlay.style.top = so[1] + 'px';
		var win = Position.getWindowSize();
		this.dialog.setStyle({
			width: win.width-200 + 'px',
			height: win.height-200 + 'px'
		});
		Position.Center(this.waiting, { zIndex: 300 });
		Position.Center(this.dialog, { zIndex: 200 });
	}, // }}}
	/*** show() {{{
	 * Creates the dialog for displaying the permissions summary,
	 * and requests the active permissions from the server. */
	show: function() {
		/* Always empty out the container and place the loading
		 * widget before beginning. */
		this.dialog.innerHTML = '';
		this.wait();

		new Ajax.Request('/network/functions/security_util.cfm', {
			method: 'post',
			parameters: { action: 'get', node: this.node },
			onSuccess: function(transport, json, opt) {
				var json = transport.response.responseJSON;
				this.unWait();
				if(json.status && parseInt(json.status) == 1 && json.data) {
					this.populate(json.data);
					this.overlay.show();
					this.dialog.show();
				} else {
					this.feedback('An error was encountered while returning the permissions information: ' + json.messages.join(', '));
				}
			}.bind(this),
			onFailure: function() {
				this.unWait();
			}.bind(this)
		});
		
		this.place();
		//new Effect.Appear(this.dialog, { duration: 0.5 } );
	}, // }}}
	/*** click_handler() {{{
	 * Handle clicks on checkboxes within the permissions dialog. */
	clickHandler: function(e) {
		var el = Event.element(e);
		if(el._action && el._action.action) {
			if(el._action.action != 'check') {
				Event.stop(e);
			}
			switch(el._action.action) {
				case 'check':
					// Get a number instead of a boolean.
					var access = (el.checked) ? 1 : 0;
					// Local copy of the context object attached to the checkbox.
					var p = el._action.permission;
					// Extended to include the current access value.
					Object.extend(p, { access: access });

					// If this box was changed before...
					if(this.changes.keys().include(el.id)) {
						// Don't include it in changes anymore (it is now
						// reverted to its original value).
						this.changes.unset(el.id);
					} else {
						// Otherwise, record it.
						this.changes.set(el.id, p);
					}
					break;

				case 'save':
					this.save();
					break;

				case 'cancel':
					this.hide();
					break;

				case 'inherit':
					this.resetInheritance(el._action.permission.node, el._action.permission.secr_node);
					break;

				default:
					this.feedback('An incorrect action was provided, or no action was given!');
					break;
			}
		}
	}, // }}}
	/*** addEventHandlers() {{{
	 * Add the event handlers. Obviously. */
	addEventHandlers: function() {
		/* Scrolling and resizing */
		this.placementHandler = this.place.bind(this);
		Event.observe(window, 'scroll', this.placementHandler);
		Event.observe(window, 'resize', this.placementHandler);

		this.clickHandlerRef = this.clickHandler.bindAsEventListener(this);
		this.click_targets.each(function(id) {
			Event.observe(id, 'click', this.clickHandlerRef);
		}.bind(this));
	}, // }}}
	/*** removeEventHandlers() {{{
	 * You have to be able to remove them so you can stop observing before
	 * you destroy the elements... This prevents MEMORY LEAKS.
	 * I'm looking at YOU, Internet Explorer. */
	removeEventHandlers: function() {
		/* Scrolling and resizing */
		Event.stopObserving(window, 'scroll', this.placementHandler);
		Event.stopObserving(window, 'resize', this.placementHandler);
		
		/* Clicking on checkboxes */
		this.click_targets.each(function(id) {
			Event.stopObserving(id, 'click', this.clickHandlerRef);
		}.bind(this));
	}, // }}}
	/*** populate() {{{
	 * Fill up the dialog with juicy information. */
	populate: function(json) {
		/*** HEADING */
		this.dialog.insert({ bottom: new Element('h2').update('Permissions for '+this.context.name) });

		/*** ACTUAL PERMISSIONS TABLE */

		// List of checkboxes.
		this.cb = [];

		/* Secured nodes */
		if(json[0] && json[0].name && json[0].id) {
			for(var p in json) {
				if(json[p].name && json[p].id && json[p].permissions) {
					var permission_box = new Element('div');
					permission_box.className = 'permission_matrix_container';
					permission_box.insert({ bottom: new Element('h3').update(json[p].name) });
					/* Domains */
					var tbody = new Element('tbody');
					for(var domain in json[p].permissions) {
						var f = new Element('tr');
						var e = new Element('td').update('&nbsp;');
						e.className = 'empty';
						f.insert({ bottom: e });

						/* The heading row lists each action, e.g. "Use", "Manage", "Delete".
						 * The key is the name of the action and the value takes the form
						 * "1:Use some kind of stuff" where the 1 is {0,1} indicating the permission
						 * setting, followed by a colon, followed by a description of what it means. */
						for(var q in json[p].permissions[domain]) {
							e = new Element('td').update(this.clean(q));
							if(v = json[p].permissions[domain][q].match(/\d:(.*)/)) e.title = v[1];
							e.className = 'action';
							f.insert({ bottom: e });
						}
						tbody.insert({ bottom: f });

						f = new Element('tr');
						e = new Element('th').update(this.clean(domain));
						e.className = 'domain';
						f.insert({ bottom: e });

						/* Actions */
						for(var q in json[p].permissions[domain]) {
							var g = new Element('td');
							g.className = 'access';
							var id = (domain+'_'+q+this.node+'_'+json[p].id).toLowerCase().gsub(/ /,'');
							var h = new Element('input', { id: id, type: 'checkbox', checked: !!parseInt(json[p].permissions[domain][q].match(/^\d/)[0]) });
							h._action = {
								action: 'check',
								permission: {
									node: this.node,
									secr_node: json[p].id,
									domain: domain,
									action: q
								}
							}
							h._permission = {
								node: this.node,
								secr_node: json[p].id,
								domain: domain,
								action: q
							}
							/* Store the IDs of the checkboxes so we can add event listeners
							 * once they've all been appended to the DOM. You can't really
							 * observe something that doesn't exist yet. */
							this.cb.push(id);
							this.click_targets.push(id);
							g.insert({ bottom: h });
							f.insert({ bottom: g });
						}
						tbody.insert({ bottom: f });
					}

					var d = new Element('table');
					d.className = 'permissions_matrix_table';
					d.insert({ bottom: tbody });
					permission_box.insert({ bottom: d });
					
					var reset = new Element('a', { href: '#', id: 'reset_inheritance_'+json[p].id });
					reset.insert({ bottom: new Element('img', { src: '/images/icons/fffm/action_refresh_blue.gif' }) });
					reset.insert({ bottom: '&nbsp;Reset inheritable permissions' });
					reset.className = 'reset_inheritance_link';
					reset._action = {
						action: 'inherit',
						permission: {
							node: this.node,
							secr_node: json[p].id
						}
					}
					this.click_targets.push('reset_inheritance_'+json[p].id);

					permission_box.insert({ bottom: reset });
					this.dialog.insert({ bottom: permission_box });

					/*** BUTTONS ***/
					var saveButton = new Element('button', { id: 'perms_command_save'+json[p].id }).update('Save Changes');
					saveButton._action = { action: 'save' };
					this.click_targets.push('perms_command_save'+json[p].id);
					var cancelLink = new Element('a', { id: 'perms_command_cancel'+json[p].id, href: '#' }).update('Cancel');
					cancelLink._action = { action: 'cancel' };
					this.click_targets.push('perms_command_cancel'+json[p].id);
					var buttons = new Element('div', { 'class': 'perms_command_buttons' });
					buttons.insert({ bottom: saveButton });
					buttons.insert({ bottom: '&nbsp;or&nbsp;' });
					buttons.insert({ bottom: cancelLink });
					this.dialog.insert({ bottom: buttons });
				}
			}
		} else {
			/* No permissions to display */
			this.dialog.insert({ bottom: '<p>There are no permissions to display.</p>' });

			/*** BUTTONS ***/
			var cancelLink = new Element('a', { id: 'perms_command_cancel', href: '#' }).update('Close this window');
			cancelLink._action = { action: 'cancel' };
			this.click_targets.push('perms_command_cancel');
			var buttons = new Element('div', { 'class': 'perms_command_buttons' });
			buttons.insert({ bottom: cancelLink });
			this.dialog.insert({ bottom: buttons });
		}

		this.addEventHandlers();
	}, // }}}
	/*** clean() {{{
	 * Clean up the permissions domain and action strings for
	 * display. */
	clean: function(s) {
		if(typeof s != 'string') return '';
		return s.gsub(/_/, ' ').toLowerCase().capitalize();
	}, // }}}
	/*** hide() {{{
	 * Hide the permissions summary dialog. */
	hide: function() {
		this.removeEventHandlers();
		this.dialog.innerHTML = '';
		this.dialog.hide();
		this.overlay.hide();
	}, // }}}
	/*** save() {{{
	 * Save changed permissions to the database. */
	save: function() {
		this.hide();
		if(this.changes.keys().length) {
			this.wait();
			var all_changes = [];
			this.changes.keys().each(function(e) {
				all_changes.push(this.changes.get(e));
			}.bind(this));

			new Ajax.Request('/network/functions/security_util.cfm', {
				method: 'post',
				parameters: {
					action: 'set',
					json: Object.toJSON(all_changes)
				},
				onSuccess: function(transport) {
					this.unWait();
					var ret = transport.response.responseJSON;
					if(ret && ret.status && parseInt(ret.status) == 1) {
						this.feedback('Permissions have been saved successfully.');
					}
				}.bind(this),
				onFailure: function() {

				}
			});
		} else {
			this.feedback('Permissions changes have been saved successfully.');
		}
	}, // }}}
	/*** resetInheritance() {{{
	 * Remove the permissions for a particular node on a particular secr_node,
	 * thereby restoring that node's inherited permissions for that secr_node. */
	resetInheritance: function(node, secr_node) {
		this.wait();
		new Ajax.Request('/network/functions/security_util.cfm', {
			method: 'post',
			parameters: { action: 'inherit', node: node, secr_node: secr_node },
			onSuccess: function(transport) {
				var ret = transport.response.responseJSON;
				if(ret && ret.status && parseInt(ret.status) == 1) {
					this.hide();
					this.show();
				} else {
					this.unWait();
					this.hide();
					this.feedback('There was an error saving permissions. Please try again in a moment.');
				}
			}.bind(this),
			onFailure: function() { }
		});
	}, // }}}
	/*** add() {{{
	 * Creates the dialog for adding a new set of permissions for a
	 * particular secured node and requests the possible permissions
	 * from the server. */
	add: function() {
	} // }}}
}; // }}}
/*** Tree {{{
 * The Tree class creates a collapsable tree structure. */
var Tree = Class.create();
Tree.prototype = {
	/*** initialize() {{{
	 * The constructor method. */
	initialize: function(tree, container) {
		if(typeof Prototype == 'undefined')
			throw('Tree requires the prototype.js library to be loaded.');
		if(typeof Effect == 'undefined')
			throw('Tree requires the Scriptaculous effects library (effects.js) to be loaded.');
		this.tree = tree;
		this.expanded = [];
		this.json = {};
		this.container = $(container);
		this.drags = [];
		this.drops = [];
		this.click_targets = [];
		this.selectedNode;
		this.helpers = new Hash();
		this.options = {
			treeUrl: '/network/functions/hierarchy_util.cfm',
			treeUrlParms: {},
			imgPlus: '/images/plus.gif',
			imgMinus: '/images/minus.gif',
			expandRoot: false,
			manage: {
				move:			false,
				rename:			false,
				permissions:	false,
				select:			false,
				create:			false,
				del:			false
			},
			hideCommandText: false,
			hideHelperText: false,
			confirmDelete: true,
			onSelect: function() { return true },
			json: null,
			flash: null,
			helpers: null,
			helperFilters: {},
			helperIcons: {},
			showHelpersOnRootNode: false,
			icons: {
				'r_members.recruiter_id':'/images/icons/fffm/icon_user.gif',
				'secr_node.node_id':'/images/icons/fffm/folder.gif',
				'secr_owner.owner_id':'/images/icons/fffm/folder.gif',
				'root':'/images/icons/fffm/folder.gif'
			},
			manageIcons: {
				'del':'/images/icons/fffm/action_stop.gif',
				'permissions':'/images/icons/fffm/page_security.gif',
				'rename':'/images/icons/fffm/page_edit.gif'
			},
			modalWaiting: true,
			loadingThrobber: true,
			onLoadSetThrobber: true,
			emptyMessage: 'Your tree does not contain any nodes.'
		};
		Object.extend(this.options, arguments[2] || {});
		this.options.helperFilters = $H(this.options.helperFilters);

		if((typeof(this.options.manage)).match(/object/i)) {
			this.options.manage = Object.extend({
				move:			false,
				rename:			false,
				permissions:	false,
				del:			false,
				select:			false,
				create:			false
			}, this.options.manage);
		} else {
			this.options.manage = {
				move:			!!this.options.manage,
				rename:			!!this.options.manage,
				permissions:	!!this.options.manage,
				del:			!!this.options.manage,
				select:			!!this.options.manage,
				create:			!!this.options.manage
			}
			if(this.options.manage.move) this.options.manage.select = false;
		}

		if(this.options.manage.move && typeof Droppables == 'undefined')
			throw('Tree requires the Scriptaculous drag and drop library (dragdrop.js) to be loaded.');

		if(typeof(this.options.imgPlus).match(/string/i)) {
			var tmp = new Image();
			tmp.src = this.options.imgPlus;
			this.options.imgPlus = tmp;
		}
		if(typeof(this.options.imgMinus).match(/string/i)) {
			var tmp = new Image();
			tmp.src = this.options.imgMinus;
			this.options.imgMinus = tmp;
		}

		if(this.options.json && typeof(this.options.json).match(/string/)) {
			this.options.json = eval('('+this.options.json+')');
		}

		/* Create the loading overlay. */
		var e = new Element('div', { id: 'tree_overlay'});
		e.className = 'jt_gen_overlay';
		e.style.display = 'none';
		e.setOpacity(0.7);
		$(document.body).insert({ bottom: e });
		this.overlay = $('tree_overlay');

		this.getTree();
	}, // }}}
	/*** unSelect() {{{
	 * Remove any current selection. */
	unSelect: function() {
		if(!this.options.manage.select) return null;
		if(this.selectedNode) {
			$(this.selectedNode).className = $(this.selectedNode).className.gsub(/selected/, '').replace(/^\s\s*/, '').replace(/\s\s*$/, '');
			this.selectedNode = null;
		}
	}, // }}}
	/*** selected() {{{
	 * Sort of a "getter" for the node in the tree that is selected. */
	selected: function() {
		if(!this.options.manage.select || !this.selectedNode) return false;
		return $(this.selectedNode)._node || false;
		//return this.selectedNode.match(/_(\d+)/)[1];
	}, // }}}
	/*** getTree() {{{
	 * Retrieve the JSON object representing the entire tree. */
	getTree: function() {
		if(this.options.loadingThrobber) this.wait();
		if(this.options.json) {
			this.json = this.options.json;
			this.options.json = null;
			this.clearOutput();
			this.buildTree();
			this.applyClickHandlers();
			if(this.options.loadingThrobber) this.unWait();
			this.options.loadingThrobber = this.options.onLoadSetThrobber;
		} else {
			var parms = Object.extend({ action: 'hierarchyGet', tree: this.tree }, this.options.treeUrlParms || {});
			new Ajax.Request(this.options.treeUrl, {
				method: 'post',
				parameters: parms,
				onSuccess: function(transport, json, opt) {
					this.options.loadingThrobber = this.options.onLoadSetThrobber;
					var rt = transport.response.responseText || '';
					if(rt.match(/login/i)) this.feedback('You must be logged in to see this information. Your session may have expired. Please refresh this page.');

					var ret = transport.response.responseJSON;
					if(ret.status && parseInt(ret.status) == 1 && ret.data) {
						this.json = ret.data;
						this.clearOutput();
						this.buildTree();
						this.applyClickHandlers();
						if(this.options.loadingThrobber) this.unWait();
					} else {
						if(this.options.loadingThrobber) this.unWait();
						this.feedback(ret.messages.join(', '));
					}
				}.bind(this),
				onFailure: function() {
					this.options.loadingThrobber = this.options.onLoadSetThrobber;
				}
			});
		}
	}, // }}}
	/*** feedback() {{{
	 * Give the user a message of some kind. */
	feedback: function(message) {
		if(this.options.flash) {
			this.options.flash.show(message);
		} else {
			alert(message);
		}
	}, // }}}
	/*** wedge() {{{
	 * This function exists solely to stop IE from processing
	 * drag and selectstart events during dragging of elements */
	wedge: function(e) {
		return false;
	}, // }}}
	/*** getSubTree() {{{
	 * Retrieve a sub-tree (for clients, etc.) and place the results into the
	 * appropriate pre-defined container object. */
	getSubTree: function() {
		var node_id = arguments[0] || null;
		var node_value = arguments[1] || null;
		var level = arguments[2] || 0;

		if(node_id && node_value && level != null) {
			var p = new Element('div', { 'class':'subtree_waiting' }).setStyle({ padding: '10px', marginLeft: level*20+'px' });
			p.insert({ bottom: new Element('img', { src: '/images/waiting.gif'}) });
			p.insert({ bottom: '&nbsp;Loading...' });
			$('group_'+node_id+'_container').innerHTML = '';
			$('group_'+node_id+'_container').insert({ bottom: p });

			new Ajax.Request(this.options.treeUrl, {
				method: 'post',
				parameters: { action: 'hierarchyGet', tree: node_value, id: node_id, level: level },
				onSuccess: function(transport, json, opt) {
					var json = transport.response.responseJSON;
					if(json.status && parseInt(json.status) == 1 && json.data && json.data[0].members) {
						$('group_'+opt.parameters.id+'_container').innerHTML = '';
						this.buildTree(json.data[0].members, 'group_'+opt.parameters.id+'_container', (opt.parameters.level + 1), opt.parameters.tree);
						this.applyClickHandlers();
					} else if(parseInt(json.status) == 0 && json.messages) {
						this.feedback(json.messages.join(', '));
					}
				}.bind(this),
				onFailure: function() {

				}
			});
		}

	}, // }}}
	/*** clickHandler() {{{
	 * Handle click events on links and things that perform actions. */
	clickHandler: function(e) {
		var oe = e;
		var e = Event.element(e);
		Event.stop(oe);

		if(e._action) {
			switch(e._action.action) {
				case "toggle":
					this.nodeToggle(e._action.node.id);
					break;

				case "load_toggle":
					e._action.action = 'toggle';

					this.nodeToggle(e._action.node.id);

					this.getSubTree(e._action.node.id, e._action.node.value, e._action.node.level);
					break;

				case "delete":
					if(!this.options.confirmDelete || confirm('Are you sure you want to permanently delete this item?'))
						this.doAction('nodeDelete', { tree: this.tree, node: e._action.node });
					break;

				case "permissions":
					var opt = {};
					if(this.options.flash) Object.extend(opt, { flash: this.options.flash });
					this.perms = new Permissions(e._action.node, e._action, opt);
					this.perms.show();
					break;

				case "select":
					if(this.selectedNode && this.selectedNode == e.id) {
						$(this.selectedNode).className = $(this.selectedNode).className.gsub(/selected/, '').replace(/^\s\s*/, '').replace(/\s\s*$/, '');
						this.selectedNode = null;
					} else {
						if(this.options.onSelect(e._node)) {
							if(this.selectedNode)
								$(this.selectedNode).className = $(this.selectedNode).className.gsub(/selected/, '').replace(/^\s\s*/, '').replace(/\s\s*$/, '');
							e.className += ' selected';
							this.selectedNode = e.id;
						}
					}
					break;

				case "helper":
					if(e._action.helper_id) {
						if(this.helpers.keys().include(e._action.helper_id)) {
							this.helpers.get(e._action.helper_id)(e._action.node);
						}
					}
					break;

				default:
					this.feedback('Incorrect action specified!');
					break;
			}
		}
	}, // }}}
	/*** applyClickHandlers() {{{
	 * Loop through the queue of elements that receive clicks and
	 * apply the click handler to them. */
	applyClickHandlers: function() {
		if(this.click_targets.length) {
			this.clickHandlerRef = this.clickHandler.bindAsEventListener(this);

			this.click_targets.each(function(e) {
				if(!$(e)._watched) {
					$(e)._watched = true;
					Event.observe($(e), 'click', this.clickHandlerRef);
				}
			}.bind(this));
		}
	}, // }}}
	/*** doAction() {{{
	 * Perform a live server action. This function handles all
	 * the ajax calls to move/delete/rename nodes in the tree, and
	 * to refresh it. */
	doAction: function(action, parms) {
		switch(action) {
			case 'nodeMove':
				if(parms.tree && parms.node && parms.parent) {
					this.wait();
					Object.extend(parms, {
						action: 'nodeMove'
					});

					/* Move the node. */
					new Ajax.Request(this.options.treeUrl, {
						method: 'post',
						parameters: parms,
						onSuccess: function(transport, j, opt) {
							var json = transport.response.responseJSON;
							if(json.status && parseInt(json.status) == 1) {
								this.expanded.push(opt.parameters.parent);
								this.getTree();
							} else if(json.status && parseInt(json.status) == 0 && json.messages) {
								this.unWait();
								this.feedback(json.messages.join(', '));
							}
						}.bind(this),
						onFailure: function() {
							this.feedback('The nodeMove procedure did not return successfully.');
						}.bind(this)
					});
				}
			break;

			case 'nodeDelete':
				if(parms.tree && parms.node) {
					this.wait();
					Object.extend(parms, {
						action: 'nodeDelete'
					});
					/* Delete the node */
					new Ajax.Request(this.options.treeUrl, {
						method: 'post',
						parameters: parms,
						onSuccess: function(t, j, o) {
							var json = t.response.responseJSON;
							if(json.status && parseInt(json.status) == 1) {
								this.getTree();
							} else if(json.status && parseInt(json.status) == 0 && json.messages) {
								this.unWait();
								this.feedback(json.messages);
							}
						}.bind(this),
						onFailure: function() {
							this.feedback('The nodeDelete procedure did not return successfully.');
						}.bind(this)
					});
				}
			break;

			case 'nodeAdd':
			break;
		}
	}, // }}}
	/*** clearOutput() {{{
	 * Destroy drag and drop objects within the output container
	 * and then empty it. */
	clearOutput: function() {
		this.drops.each(function(e) {
			Droppables.remove(e);
		});
		this.drops = [];
		this.drags.each(function(e) {
			e.destroy();
		});
		this.click_targets = [];
		this.container.innerHTML = '';
	}, // }}}
	/*** wait() {{{
	 * Show a little waiting thing. Could be useful. */
	wait: function() {
		if(!$('tree_waiting_widget')) {
			var dww = document.createElement('div');
			dww.setAttribute('id', 'tree_waiting_widget');
			var i = document.createElement('img');
			i.setAttribute('src','/images/loading.gif');
			i.style.margin = '30px';
			dww.appendChild(i);
		}

		$(document.body).insert({ bottom: dww });

		this.overlay.show();
		$('tree_waiting_widget').show();
		this.placeWaiting();

		this.placeWaitingRef = this.placeWaiting.bind(this);
		Event.observe(window, 'scroll', this.placeWaitingRef);
		Event.observe(window, 'resize', this.placeWaitingRef);
	}, // }}}
	/*** unWait() {{{
	 * The opposite of wait(). */
	unWait: function() {
		this.overlay.hide();
		if($('tree_waiting_widget') && $('tree_waiting_widget').visible) {
			$('tree_waiting_widget').hide();
		}
		Event.stopObserving(window, 'scroll', this.placeWaitingRef);
		Event.stopObserving(window, 'resize', this.placeWaitingRef);
	}, // }}}
	/*** placeWaiting() {{{
	 * Position the waiting widget and overlay in the proper place. */
	placeWaiting: function() {
		// Only if it exists...
		if(this.options.modalWaiting) {
			if($('tree_waiting_widget')) Position.Center($('tree_waiting_widget'));

			var vpd = document.viewport.getDimensions();
			this.overlay.style.width = vpd.width + 'px';
			this.overlay.style.height = vpd.height + 'px';

			var so = document.viewport.getScrollOffsets();
			this.overlay.style.left = so[0] + 'px';
			this.overlay.style.top = so[1] + 'px';
		} else {
			if($('tree_waiting_widget')) {
				var tww = $('tree_waiting_widget');
				tww.style.top	= (this.container.viewportOffset()[1]+this.container.cumulativeScrollOffset()[1]) + ((this.container.getHeight() - tww.getHeight()) / 2) + 'px';
				tww.style.left	= (this.container.viewportOffset()[0]+this.container.cumulativeScrollOffset()[0]) + ((this.container.getWidth() - tww.getWidth()) / 2) + 'px';
			}
			Element.clonePosition(this.overlay, this.container);
		}
	}, // }}}
	/*** nodeToggle() {{{
	 * Toggle the expanded and collapsed state of a node
	 * with children. */
	nodeToggle: function() {
		var jid = arguments[0] || 0;
		var e = $('group_' + jid + '_container');
		var p = $('group_img_' + jid);
		if(e.visible()) {
			e.hide();
			p.src = this.options.imgPlus.src;
			if(this.expanded.include(jid)) {
				this.expanded = this.expanded.select(function(e) { return e != jid });
			}
		} else {
			e.show();
			p.src = this.options.imgMinus.src;
			if(!this.expanded.include(jid)) {
				this.expanded.push(jid);
			}
		}
		return false;
	}, // }}}
	/*** buildTree() {{{
	 * Generate the tree structure and populate the output container. */
	buildTree: function() {
		var json = arguments[0] || this.json;
		var container = $(arguments[1]) || $(this.container);
		var level = arguments[2] || 0;
		var clientTree = arguments[3] || null;

		if(json[0] && $H(json[0]).keys().include('id')) {
			// Only if it's the root node, cause it to be expanded.
			if(level == 0 && this.options.expandRoot) this.expanded.push(json[0].id);

			for(x in json) {
				if(json[x].name) {
					/* It came to my attention that occasionally the name can be a number, such as when a
					 * company name is actually a number. In this case, the JSON conversion function that
					 * supplies the tree to us will NOT have quoted the value, because numbers don't need
					 * to be quoted in JSON. But we need this value to be a string so that we are able to
					 * perform matching, regex, etc. on it. Thus, += ''
					 */
					json[x].name += '';
					if(json[x] && json[x].name && json[x].type) {
						children = (json[x].members[0]) ? true : false;
						if(json[x].type == 'secr_owner.owner_id') clientTree = json[x].value;

						// Create the base element that represents a group node.
						var temp = new Element('div', { id: 'group_' + json[x].id + '-' + json[x].parent });
						temp.className = 'node';
						temp.style.marginLeft = level * 20 + 'px';
						if(x % 2 == 1 && json[x].type == 'r_members.recruiter_id'){
							temp.style.backgroundColor = '#EDEDED';
						}

						/* If this node is not the root, there should be some commands.
						 * We add the commands before the other contents of the node because they
						 * have to be floated to the right, so they actually have to come first
						 * in the DOM. */
						var cls = [];

						if(this.options.helpers && (level > 0 || (level == 0 && this.options.showHelpersOnRootNode))) {
							for(link in this.options.helpers) {
								var proceed = true;
								/* If there is a filter for this helper link */
								if(this.options.helperFilters.keys().include(link)) {
									for(test in this.options.helperFilters.get(link)) {
										//proceed = true;
										if(json[x][test]) {
											if(!this.options.helperFilters.get(link)[test].match(json[x][test])) proceed = false;
										}
									}
								} else {
									proceed = true;
								}

								if(proceed) {
									/* Warning! This won't actually make unique link IDs if someone happens
									 * to specify a number of helper links all with the same display name. */
									var clid = 'ch_' + link.gsub(/[^a-z]/i, '') + '_' + json[x].id;
									var cl = new Element('a', { id: clid, href: '#', 'class':'node_helper_link', title: link });
									if(this.options.helperIcons[link]) {
										var cli = new Element('img', { src: this.options.helperIcons[link] });
										cli._action = {
											action: 'helper',
											helper_id: clid,
											node: {
												id: json[x].id || 0,
												name: json[x].name || '',
												description: json[x].description || '',
												type: json[x].type || 'secr_node.node_id',
												"parent": json[x].parent || this.tree,
												value: json[x].value || 0,
												tree: this.tree,
												clientTree: clientTree
											}
										}
										cl.insert({ bottom: cli });
									}
									if(!this.options.hideHelperText) {
										cl.insert({ bottom: '&nbsp;'+link });
									}
									cl._action = {
										action: 'helper',
										helper_id: clid,
										node: {
											id: json[x].id || 0,
											name: json[x].name || '',
											description: json[x].description || '',
											type: json[x].type || 'secr_node.node_id',
											"parent": json[x].parent || this.tree,
											value: json[x].value || 0,
											tree: this.tree,
											clientTree: clientTree
										}
									}
									this.helpers.set(clid, this.options.helpers[link]);
									this.click_targets.push(clid);
									cls.push(cl);
									cls.push('&nbsp;|&nbsp;');
								}
							}

							cls.pop();
						} else if(cls.length) {
							cls.pop();
						}

						/*** PERMISSIONS COMMAND (command link, cl) ***/
						if(level > 0 && this.options.manage.permissions) {
							if(cls.length) {
								cls.push('&nbsp;|&nbsp;');
							}
							var cl = new Element('a', { id: 'node_perm_'+json[x].id, href: '#', 'class':'node_command_link', title: 'Permissions' })
							var cli = new Element('img', { src: this.options.manageIcons.permissions, 'class':'node_perm_icon' });
							cli._action = { action: 'permissions', name: json[x].name, node: json[x].id };
							cl.insert({ bottom: cli });
							if(!this.options.hideCommandText) cl.insert({ bottom: '&nbsp;permissions' });
							cl._action = { action: 'permissions', name: json[x].name, node: json[x].id };
							this.click_targets.push(cl.id);

							cls.push(cl);
						}

						if(level > 0 && this.options.manage.rename && !json[x].name.match(/administrator/i)) {
							if(cls.length) cls.push('&nbsp;|&nbsp;');

							var cl = new Element('a', { id: 'node_rename_'+json[x].id, href: '#', 'class':'node_command_link', title: 'Rename' });
							var cli = new Element('img', { src: this.options.manageIcons.rename, 'class':'node_rename_icon' });
							cli._action = { action: 'rename', name: json[x].name, node: json[x].id };
							cl.insert({ bottom: cli });
							if(!this.options.hideCommandText) cl.insert({ bottom: '&nbsp;Delete' });
							cl._action = { action: 'rename', name: json[x].name, node: json[x].id };
							this.click_targets.push(cl.id);

							cls.push(cl);
						}

						/*** DELETE COMMAND (command link, cl) ***/
						if(level > 0 && this.options.manage.del && !json[x].name.match(/administrator/i)) {
							if(cls.length) cls.push('&nbsp;|&nbsp;');

							var cl = new Element('a', { id: 'node_del_'+json[x].id, href: '#', 'class':'node_command_link', title: 'Delete' });
							var cli = new Element('img', { src: this.options.manageIcons.del, 'class':'node_del_icon' });
							cli._action = { action: 'delete', name: json[x].name, node: json[x].id };
							cl.insert({ bottom: cli });
							if(!this.options.hideCommandText) cl.insert({ bottom: '&nbsp;Delete' });
							cl._action = { action: 'delete', name: json[x].name, node: json[x].id };
							this.click_targets.push(cl.id);

							cls.push(cl);
						}

						if(cls.length) {
							/* Container for all commands (command container, cc) */
							var cc = new Element('div');
							cc.className = 'node_commands';
							cls.each(function(cl) { cc.insert({ bottom: cl }) });
							temp.insert({ bottom: cc });
						}

						/* If it does have members, we need an expand/contract switch
						 * with click event handling and things. */
						if(children || json[x].type == 'secr_owner.owner_id') {
							// Create the link element.
							temp2 = new Element('a', { href: '#', id: 'node_toggle_link_'+json[x].id });
							temp2.className = 'node_link';

							temp2._action = {
								action: ((json[x].type == 'secr_owner.owner_id' && !this.expanded.include(json[x].id)) ? 'load_toggle' : 'toggle'),
								node: {
									id: json[x].id || 0,
									name: json[x].name || '',
									description: json[x].description || '',
									type: json[x].type || 'secr_node.node_id',
									"parent": json[x].parent || this.tree,
									value: json[x].value || 0
								}
							}
							this.click_targets.push(temp2.id);

							// Create the image element and place it inside the link element.
							temp3 = new Element('img', { id: 'group_img_' + json[x].id });

							temp3._action = {
								action: ((json[x].type == 'secr_owner.owner_id' && !this.expanded.include(json[x].id)) ? 'load_toggle' : 'toggle'),
								node: {
									id: json[x].id || 0,
									name: json[x].name || '',
									description: json[x].description || '',
									type: json[x].type || 'secr_node.node_id',
									"parent": json[x].parent || this.tree,
									value: json[x].value || 0,
									level: level
								}
							}
							//this.click_targets.push(temp3.id);
							
							temp3.className = 'node_link_img';
							if(this.expanded.include(json[x].id)) {
								temp3.setAttribute('src', this.options.imgMinus.src);
							} else {
								temp3.setAttribute('src', this.options.imgPlus.src);
							}
							//temp3.setAttribute('id', 'group_img_' + json[x].id);
							temp3.style.align = 'middle';

							// Glue them together.
							temp2.insert({ bottom: temp3 });
							temp.insert({ bottom: temp2 });
						} else {
							// If there are no children, create a blank space.
							/* I have placed the style inline here because IE suddenly stopped
							 * applying the style associated with the class and I don't know why. */
							temp.insert({ bottom: new Element('div', {
								'class':'node_spacer',
								style: 'float: left; width: 9px; height: 18px;'
							}).update('&nbsp;') });
						}

						/* Apply these node properties to the group thing, too, so it can
						 * be a drop target and know what's going on. */
						temp._node = {
							id: json[x].id || 0,
							name: json[x].name || '',
							description: json[x].description || '',
							type: json[x].type || 'secr_node.node_id',
							"parent": json[x].parent || this.tree,
							value: json[x].value || 0,
							clientTree: clientTree
						}

						// Place the icon and text node (node name) into the element.
						// Image (folder/person/etc.)
						tempimg = new Element('img', { src: this.options.icons[json[x].type] });
						tempimg.className = 'group_icon';
						// Node name
						temp2 = new Element('div', { id: 'group_name_' + json[x].id + '-' + json[x].parent });
						temp2.insert({ bottom: tempimg });
						temp2.insert({ bottom: '&nbsp;'+json[x].name });
						temp2._node = {
							id: json[x].id || 0,
							name: json[x].name || '',
							description: json[x].description || '',
							type: json[x].type || 'secr_node.node_id',
							"parent": json[x].parent || this.tree,
							value: json[x].value || 0,
							clientTree: clientTree
						}
						temp2.className = 'node_text';
						if(this.options.manage.move && level > 0) temp2.className += ' draggable';
						else if(this.options.manage.select || !this.options.manage.move) temp2.className += ' clickable';

						temp.insert({ bottom: temp2 });

						/* Stick it all onto the main output element. The "container,"
						 * we call it. */
						container.insert({ bottom: temp });

						/* If management is on, make the group labels draggable and the group containers
						 * droppable. If management is off, make the group labels selectable. */
						if(this.options.manage.move) {
							/* If this new node is not the root node of the hierarchy (i.e. its
							 * level is greater than zero), then make it draggable. */
							if(level > 0) {
								var d = new Draggable('group_name_' + json[x].id + '-' + json[x].parent, {
									onStart: function() {
										Event.observe(document.body, 'drag', this.wedge, false);
										Event.observe(document.body, 'startselect', this.wedge, false);
									}.bind(this),
									onEnd: function() {
										Event.stopObserving(document.body, 'drag', this.wedge, false);
										Event.stopObserving(document.body, 'startselect', this.wedge, false);
									}.bind(this),
									revert: true,
									starteffect: '',
									endeffect: ''
								});
								this.drags.push(d);
							}

							/* All group nodes are drop targets. */
							var dropper_proto = function() {
								var e = arguments[0];
								var d = arguments[1];
								var drag_id = e.id.match(/_(\d*)-/)[1];
								var drop_id = d.id.match(/_(\d*)-/)[1];
								var drag_parent = e.id.match(/-(\d*)$/)[1];

								/* Check to see if this thing is being dropped onto its own parent
								 * OR ITSELF. */
								if(drag_parent != drop_id && drag_id != drop_id) {
									// A warning for moving entire groups.
									var answer = true;
									if(answer) {
										this.doAction('nodeMove', { tree: this.tree, node: drag_id, parent: drop_id, from_tree: e._node.clientTree, to_tree: d._node.clientTree });
									}
								}
							}
							var dropper = dropper_proto.bind(this);

							Droppables.add('group_' + json[x].id + '-' + json[x].parent, {
								hoverclass: 'grp_active',
								onDrop: dropper
							});
							this.drops.push($('group_' + json[x].id + '-' + json[x].parent));
						} else if(this.options.manage.select) {
							var e = $('group_name_' + json[x].id + '-' + json[x].parent);
							e._action = { action: 'select', name: json[x].name, node: json[x].id };
							this.click_targets.push(e.id);
						}

						if(children || json[x].type == 'secr_owner.owner_id') {
							/* Create the container that will hold all of this group's members.
							 * We need a div container so we can show and hide them. */
							temp = new Element('div', { id: 'group_' + json[x].id + '_container' });
							//temp = document.createElement('div');
							//temp.setAttribute('id', 'group_' + json[x].id + '_container');
							if(!this.expanded.include(json[x].id)) temp.style.display = 'none';
							container.insert({ bottom: temp });
							//container.appendChild(temp);
						}

						if(children) this.buildTree(json[x].members, 'group_' + json[x].id + '_container', (level + 1), clientTree);
						else if(json[x].type == 'secr_owner.owner_id' && this.expanded.include(json[x].id)) {
							this.getSubTree(json[x].id, json[x].value, level);
						}
					}
				}
			}
		} else {
			var em = new Element('div', { 'class':'empty_message' });
			em.setStyle({ marginLeft: level*20+'px', padding: '10px' });
			em.update(this.options.emptyMessage);
			container.insert({ bottom: em });
		}
	} // }}}
}; // }}}
