/* Autohide.js
 * This script is useful for widgets that need to hide components on 'clickElsewhere', i.e.
 * if a click event occurs anywhere but the element in question, it should be hidden. This is
 * particularly useful for custom dropdown menus and tooltip widgets.
 *
 * This relies on a better typeof function than the native typeof keyword. This is implemented as $typeof().
 * Also, this requires arrays to support indexOf(), which at least IE6 does not implement, so we
 * implement one if it is missing from the Array prototype. If there are frameworks that assign
 * a non-function to indexOf on Array.prototype, it will be overridden. If a framework creates and indexOf
 * that does not behave like the native indexOf of JS 1.5, this script will not work.
 *
 * Usage:
 * register(fn, namespace) will take a lambda and a string to be used as a handle.
 * The lambda should take no arguments, and returns either an element or an array of elements.
 *
 * unregister(namespace) unregisters lambda at given namespace
 */

// Check for native or previously implemented Array.indexOf()
if (typeof Array.prototype.indexOf !== 'function') {
	Array.prototype.indexOf = function(item) {
		if (item === undefined) return -1; // no need to go further
		var len = this.length; // cache length
		for (var i = 0; i < len; i++) {
			if (item === this[i]) return i;
		}
		return -1;
	};
}

// Create reliable typeof function
function $typeof(obj) {
	/* improved JS type detection:
	 * native typeof has problems -- typeof null -> 'object' and typeof [] -> 'object'
	 * this aims to return correct values:
	 *     $typeof(null) -> 'null', $typeof([]) -> 'array'
	 */
	var type = typeof obj;
	if (type === 'object') {
		if (!obj) return 'null';
		if (obj instanceof Array || (typeof obj.length === 'number' &&
		                             !obj.propertyIsEnumerable('length') &&
		                             typeof obj.splice === 'function')) {
			return 'array';
		}
	}
	return type;
}

function $isElement(obj) {
	/* this is separate from $typeof() because $typeof() should really
	 * act like a more correct typeof, than to add new functionality.
	 *
	 * This depends on $typeof(), and is a convenience method to determine
	 * whether the object is a DOM element or not.
	 *
	 * $isElement(obj) returns true if Element, false, if not.
	 */
	return ($typeof(obj) === 'object' && typeof obj.nodeName === 'string' && obj.nodeType === 1);
}

var $$object = function(proto) {
	/* this function will create a constructor setting proto parameter as the prototype */
	var klass = function() {
		(this.initialize && typeof this.initialize === 'function') ? this.initialize.apply(this, arguments) : this;
	}
	klass.prototype = proto;
	return klass;
}

/* AutoHide was intended to be a singleton, but one can have multiple AutoHide objects if desired.
 * This constructor makes it simple. Just call var foo = new AutoHide(), pass in options object
 * if overriding defaults is desired. */
var AutoHide = $$object({
	options : {
		action: function(el) {
			el.style.display = 'none';
		}
	},
	initialize: function(options) {
		/* merge options */
		if ($typeof(options) === 'object') {
			for (var k in options) {
				this.options[k] = options[k];
			}
		}
	},
	builders: {}, /* this will be a hash of getter functions */
	elements: [],
	register: function(fn, namespace) {
		if (arguments.length !== 2 ||
		    typeof fn !== 'function' ||
		    typeof namespace !== 'string' ||
		    namespace.match(/^[a-z][0-9a-z_]*$/i) === null) {
			/* namespace must start with a letter, optionally followed by numbers, letters, underscores
			 * and no whitespace */
			return;
		}
		this.builders[namespace] = fn;
	},
	unregister: function(namespace) {
		delete this.builders[namespace];
	},
	registerElements: function(el) {
		if (el === undefined) return;
		var arr = this.elements;
		if ($typeof(el) === 'array') {
			for (var i = 0, len = el.length; i < len; i++) {
				var item = el[i];
				if (arr.indexOf(item) == -1 && $isElement(item)) {
					arr.push(item);
				}
			}
		}
		if ($isElement(el) && arr.indexOf(el) == -1) {
			arr.push(el);
		}
		return el;
	},
	refresh: function() {
		this.elements = []; // dump the current set, let garbage collection have at it!
		var build = this.builders;
		var reg = this.registerElements; // cache this to save scope lookups
		for (var get in build) {
			reg.apply(this, [build[get]()]);
		}
	}
});

document.autoHide = new AutoHide();

// jQuery connector -- TODO: make this agnostic of framework as much as possible (at least make for prototype as well)
jQuery(document).ready(function() {
	jQuery(document).click(function(e) {
		var target = e.target || e.srcElement;
		var array = document.autoHide.elements;
		var len = array.length;
		for (var i = 0; i < len; i++) {
			var item = array[i];
			if (item !== target) $j(item).hide();
		}
	});
});