/**
 * @fileoverview activator.js
 * Widget.Activator activates controls with highlighters, selection, and en/disabling functionality.<br />
 * Requires Prototype (http://www.prototypejs.org) 1.6 or later<br /><br />
 *
 * Copyright (c) 2007 - 2008 Marc Heiligers (marc@eternal.co.za) http://www.eternal.co.za<br /><br />
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.<br /><br />
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.<br /><br />
 *
 * Change History:<br />
 * Version 0.2.00: 11 Oct 2008<br />
 * - Prepared for release, updated docs and demo pages<br />
 * - Added MIT license<br />
 * Version 0.1.15: 8 Oct 2008<br />
 * - BREAKING CHANGE: the event memo is no longer the element but now contains the following properties:<br />
 * 		element - the element<br />
 * 		activator - the activator<br />
 * 		event - the event that triggered this event<br />
 * Version 0.1.14: 5 Oct 2008<br />
 * - Fixed bug where elements were still firing mouseout when disabled<br />
 * Version 0.1.13: 8 Jun 2008<br />
 * - Updated setEnabled so that it removes the active and hover styles if you disable an element<br />
 * Version 0.1.12: 18 Apr 2008<br />
 * - Updated apply so that it ensures that selected and enabled elements remain so<br />
 * Version 0.1.11: 17 Mar 2008<br />
 * - Fixed a bug in setSelected and setEnabled where the default was not true<br />
 * Version 0.1.10: 12 Mar 2008<br />
 * - Added click event<br />
 * Version 0.1.9: 7 Mar 2008<br />
 * - Added this.selector to save the original selector<br />
 * - Added apply method which removes all and reapplies the original selector<br />
 * Version 0.1.8: 3 Mar 2008<br />
 * - Moved event name options into their own events options<br />
 * Version 0.1.7: 25 Feb 2008<br />
 * - Fixed a bug where the elements array was not correctly compacted after remove<br />
 * Version 0.1.6: 23 Feb 2008<br />
 * - getElements will return all elements if passed null or *<br />
 * - The above affects methods like remove: remove() or remove("*") will remove all elements<br />
 * - Selector in constructor is now optional<br />
 * - Minor improvement in constructor - just call add instead of calling add for each element returned from $$(selector) which add does anyway<br />
 * - Fixed a bug in add where it would incorrectly try to add a selector string passed in<br />
 * - Moved className options into a classNames object<br />
 * Version 0.1.5: 18 Feb 2008<br />
 * - Fixed a bugs in selectAll and enableAll where the default was not true<br />
 * Version 0.1.4: 19 Jan 2008<br />
 * - Fixed a minor bug in add<br />
 * - Added possibility for selector to be null - TODO: paramater mashing<br />
 * Version 0.1.3: 10 Dec 2007<br />
 * - Added mousedownEvent and mouseupEvent names to options<br />
 * Version 0.1.2: 19 Oct 2007<br />
 * - Added handlers array and updated destroy method to stopObserving all event handlers<br />
 * Version 0.1.1: 18 Oct 2007<br />
 * - Improved #isElement method to allow for sliding doors<br />
 * - Turned event names into properties of Widget.Activator (Widget.Activator.mousedownEvent and Widget.Activator.mouseupEvent)<br />
 * Version 0.1.0: 14 Oct 2007<br />
 * - Initial version
 */
if(typeof Widget == "undefined") Widget = {};
 /**
 * @class Widget.Activator
 * @version 0.1.13
 * @author Marc Heiligers marc@eternal.co.za http://www.eternal.co.za
 */
Widget.Activator = Class.create(/** @scope Widget.Activator **/{
	/**
	 * The Activator class constructor.
	 * @constructor Activator
	 * @param {string} [selector] A CSS selector string which is defines one or more elements to be activated
	 * @param {object} [options] An object hash of options.
	 */
	initialize: function(selector, options) {
		if(selector && !Object.isString(selector)) {
			options = selector;
			selector = null;
		}

		/**
		 * The default options.classNames.normals object.
		 * @class
		 * @param {string} [normal] The className to be applied to all activated elements. (default: null)
		 * @param {string} [hover] The className to be applied when the element is hovered. (default: "hover")
		 * @param {string} [active] The className to be applied when the element is active (mouse down). (default: "active")
		 * @param {string} [selected] The className to be applied when the element is selected. (default: "selected")
		 * @param {string} [disbaled] The className to be applied when the element is disabled. (default: "disabled")
		 */
		var classNames = Object.extend({
			normal: null,
			hover: "hover",
			active: "active",
			selected: "selected",
			disabled: "disabled"
		}, options ? options.classNames || {} : {});

		/**
		 * The default options.classNames.normals object.
		 * @class
		 * @param {string} [mouseover] The name of the mouse over event. (default: Widget.Activator.mouseoverEvent = "activator:mouseover")
		 * @param {string} [mouseout] The name of the mouse out event. (default: Widget.Activator.mouseoutEvent = "activator:mouseout")
		 * @param {string} [mousedown] The name of the mouse down event. (default: Widget.Activator.mousedownEvent = "activator:mousedown")
		 * @param {string} [mouseup] The name of the mouse up event. (default: Widget.Activator.mouseupEvent = "activator:mouseup")
		 * @param {string} [click] The name of the click event. (default: Widget.Activator.clickEvent = "activator:click")
		 */
		var events = Object.extend({
			mouseover: Widget.Activator.mouseoverEvent,
			mouseout: Widget.Activator.mouseoutEvent,
			mousedown: Widget.Activator.mousedownEvent,
			mouseup: Widget.Activator.mouseupEvent,
			click: Widget.Activator.clickEvent
		}, options ? options.events || {} : {});

		/**
		 * The default options object.
		 * @class
		 * @param {object} [classNames] The classNames object to be applied to all activated elements. (default: null)
		 * @param {object} [events] The event names to be used. (default: null)
		 * @param {string} [container] An element that contains all activated elements, and that is the source of all activator events. If null, document.body is used. (default: null)
		 * @param {bool} [singleSelect] If true, only one element can be selected at a time. (default: false)
		 * @param {bool} [extendElements] If true, all activated elements are extended with #getActivator(), #isSelected(), #setSelected(bool), #isEnabled(), and #setEnabled(bool) methods. (default: false)
		 */
		this.options = Object.extend({
			container: null,
			singleSelect: false,
			extendElements: false
		}, options || {});
		this.options.classNames = classNames;
		this.options.events = events;

		this.mouseoverListener = this.mouseover.bindAsEventListener(this);
		this.mouseoutListener = this.mouseout.bindAsEventListener(this);
		this.mousedownListener = this.mousedown.bindAsEventListener(this);
		this.mouseupListener = this.mouseup.bindAsEventListener(this);
		this.clickListener = this.click.bindAsEventListener(this);
		if(this.options.container) {
			this.options.container = $(this.options.container);
			this.options.container.observe("mouseover", this.mouseoverListener);
			this.options.container.observe("mouseout", this.mouseoutListener);
			this.options.container.observe("mousedown", this.mousedownListener);
			this.options.container.observe("mouseup", this.mouseupListener);
			this.options.container.observe("click", this.clickListener);
		}

		this.elements = [];
		if(selector) {
			this.add(selector);
			this.selector = selector;
		}

		this.handlers = [];
	},
	/**
	 * Removes all elements and then adds elements based on the selector, or the original selector if none is passed.
	 * @param {string|Array|element} elements A CSS selector string, array of elements, or single element to add.
	 */
	apply: function(elements) {
		var s = this.getSelected();
		var d = this.getDisabled();
		this.remove("*");
		this.add(elements ? elements : this.selector);
		this.setSelected(s);
		this.setEnabled(d, false);
	},
	/**
	 * Add elements to the activator.
	 * @param {string|Array|element} elements A CSS selector string, array of elements, or single element to add.
	 */
	add: function(elements) {
		if(Object.isString(elements)) return this.add($$(elements));
		if(Object.isArray(elements)) {
			elements.each(function(e) {
				this.add(e);
			}, this);
			return;
		}
		var element = $(elements);
		if(this.options.container) {
			var t = element.ancestors().find(function(a) {
				return a == this.options.container;
			}.bind(this));
			if(!t) {
				throw Widget.Activator.notContainerChild.interpolate({ element: element.identify(), container: this.options.container.identify() });
			}
		} else {
			element.observe("mouseover", this.mouseoverListener);
			element.observe("mouseout", this.mouseoutListener);
			element.observe("mousedown", this.mousedownListener);
			element.observe("mouseup", this.mouseupListener);
			element.observe("click", this.clickListener);
		}
		if(this.options.classNames.normal) element.addClassName(this.options.classNames.normal);
		this.elements.push(element);
		element[Widget.Activator.activatorAttribute] = this;
		element[Widget.Activator.selectedAttribute] = false;
		element[Widget.Activator.enabledAttribute] = true;
		if(this.options.extendElements) {
			element[Widget.Activator.getActivatorFunction] = function() { return this[Widget.Activator.activatorAttribute]; };
			element[Widget.Activator.setSelectedFunction] = function(selected) { this.getActivator().setSelected(this, selected); };
			element[Widget.Activator.isSelectedFunction] = function() { return this.getActivator().isSelected(this); };
			element[Widget.Activator.setEnabledFunction] = function(enabled) { this.getActivator().setEnabled(this, enabled); };
			element[Widget.Activator.isEnabledFunction] = function() { return this.getActivator().isEnabled(this); };
		}
	},
	/**
	 * Remove elements from the activator.
	 * @param {string|Array|number|element} elements A CSS selector string, array of elements, index of an element, or single element to remove.
	 */
	remove: function(elements) {
		this.getElements(elements).each(function(e) {
			if(!this.options.container) {
				e.stopObserving("mouseover", this.mouseoverListener);
				e.stopObserving("mouseout", this.mouseoutListener);
				e.stopObserving("mousedown", this.mousedownListener);
				e.stopObserving("mouseup", this.mouseupListener);
				e.stopObserving("click", this.clickListener);
			}
			$w("activatorAttribute selectedAttribute enabledAttribute getActivatorFunction setSelectedFunction isSelectedFunction setEnabledFunction isEnabledFunction").each(function(a) {
				//Common.log(Widget.Activator[a] + " " + Object.isUndefined(e[Widget.Activator[a]]));
				//if(!Object.isUndefined(e[Widget.Activator[a]])) delete e[Widget.Activator[a]];
				e[Widget.Activator[a]] = null;
			});
			/*delete e[Widget.Activator.activatorAttribute];
			delete e[Widget.Activator.selectedAttribute];
			delete e[Widget.Activator.enabledAttribute];
			if(this.options.extendElements) {
				delete e[Widget.Activator.getActivatorFunction];
				delete e[Widget.Activator.setSelectedFunction];
				delete e[Widget.Activator.isSelectedFunction];
				delete e[Widget.Activator.setEnabledFunction];
				delete e[Widget.Activator.isEnabledFunction];
			}*/
			e.removeClassName(this.options.classNames.hover);
			e.removeClassName(this.options.classNames.active);
			e.removeClassName(this.options.classNames.selected);
			e.removeClassName(this.options.classNames.disabled);
			if(this.options.classNames.normal) e.removeClassName(this.options.classNames.normal);
			this.elements[this.elements.indexOf(e)] = null;
		}.bind(this));
		this.elements = this.elements.compact();
	},
	/**
	 * The mouseover event handler
	 * @private
	 */
	mouseover: function(event) {
		var e = this.isElement(event.element());
		if(e && !e[Widget.Activator.disabledAttribute]) {
			e.addClassName(this.options.classNames.hover);
			this.fire(this.options.events.mouseover, e, event);
		}
	},
	/**
	 * The mouseout event handler
	 * @private
	 */
	mouseout: function(event) {
		var e = this.isElement(event.element());
		if(e && !e[Widget.Activator.disabledAttribute]) {
			e.removeClassName(this.options.classNames.hover);
			e.removeClassName(this.options.classNames.active);
			this.fire(this.options.events.mouseout, e, event);
		}
	},
	/**
	 * The mousedown event handler
	 * @private
	 */
	mousedown: function(event) {
		var e = this.isElement(event.element());
		if(e && !e[Widget.Activator.disabledAttribute]) {
			e.addClassName(this.options.classNames.active);
			this.fire(this.options.events.mousedown, e, event);
		}
	},
	/**
	 * The mouseup event handler
	 * @private
	 */
	mouseup: function(event) {
		var e = this.isElement(event.element());
		if(e) e.removeClassName(this.options.classNames.active);
		if(e && !e[Widget.Activator.disabledAttribute]) {
			this.fire(this.options.events.mouseup, e, event);
		}
	},
	/**
	 * The click event handler
	 * @private
	 */
	click: function(event) {
		var e = this.isElement(event.element());
		if(e && !e[Widget.Activator.disabledAttribute]) {
			this.fire(this.options.events.click, e, event);
		}
	},
	/**
	 * Set an event handler for an activator event. The Activator class fires mousedown and mouseup events.
	 * Event handlers are attached to the options.container if there is one, and document.body if not.
	 * If there are multiple Activators without containers, all event handlers will receive messages for
	 * all the activators. You can test to see if the element is part of this activator using the isElement method.
	 * The element for which the event was fired is in event.memo.
	 * @param {string} eventName The name of the event. Use Widget.Activator.mousedownHandler and Widget.Activator.mouseupHandler constants.
	 * @param {function} elements Your event handler function.
	 */
	observe: function(eventName, handler) {
		if(this.options.container) {
			this.options.container.observe(eventName, handler);
		} else {
			$(document).observe(eventName, handler);
		}
		this.handlers.push({ eventName: eventName, handler: handler });
		return this;
	},
	/**
	 * Remove an event handler for an activator event.
	 * @param {string} eventName The name of the event. Use Widget.Activator.mousedownHandler and Widget.Activator.mouseupHandler constants.
	 * @param {function} elements Your event handler function.
	 */
	stopObserving: function(eventName, handler) {
		if(this.options.container) {
			this.options.container.stopObserving(eventName, handler);
		} else {
			$(document).stopObserving(eventName, handler);
		}
		return this;
	},
	/**
	 * Fires an activator event.
	 * @private
	 */
	fire: function(eventName, element, event) {
		var memo = {
			activator: this,
			element: element,
			event: event
		}
		if(this.options.container) {
			this.options.container.fire(eventName, memo);
		} else {
			$(document).fire(eventName, memo);
		}
		return this;
	},
	/**
	 * Gets an element from the activator by index or id.
	 * @param {number|string} element The index or id of the element to get.
	 * @returns {element} The element if found, null otherwise.
	 */
	getElement: function(element) {
		if(Object.isNumber(element)) {
			return this.elements[element];
		}
		return this.isElement(element);
	},
	/**
	 * Gets activator elements by index or CSS selector.
	 * @param {number|string|Array} elements The index, CSS selector, or Array of element ids or indices to get.
	 * @returns {Array} The array elements found.
	 */
	getElements: function(elements) {
		if(elements === null || elements === "*") return this.getElements(this.elements);
		if(Object.isNumber(elements)) return [ this.elements[elements] ];
		if(Object.isString(elements)) return this.getElements($$(elements));
		if(Object.isArray(elements)) {
			var es = [];
			elements.each(function(a) {
				var e = this.getElement(a);
				if(e) es.push(e);
			}.bind(this));
			return es;
		}
		if(this.isElement(elements)) return [ $(elements) ];
		return [];
	},
	/**
	 * Determines if an element or one of it's parent elements is member of the activator's list of elements.
	 * @param {element} element The element to test.
	 * @returns {element} The element or parent element that is a member of the activator's list of elements if found, null otherwise.
	 */
	isElement: function(element) {
		if(!element || element == this.options.container || element == document.body) {
			return null;
		}
		element = $(element);
		var e = this.elements.find(function(e) {
			return e == element;
		});
		if(!e) {
			if(!element.up) return null;
			return this.isElement(element.up());
		}
		return e;
	},
	/**
	 * (De)Selects one or more elements (and deselects all other elements if the singleSelect option is true).
	 * @param {number|string|Array} elements The index, CSS selector, or Array of element ids or indices to select.
	 * @param {bool} [selected] If true, the elements are selected; they are deselected otherwise. (default: true)
	 */
	setSelected: function(elements, selected) {
		var se = (Object.isUndefined(selected) || selected);
		var fn = se ? Element.addClassName : Element.removeClassName;
		if(se && this.options.singleSelect) {
			this.selectAll(false);
		}
		this.getElements(elements).each(function(e) {
			fn(e, this.options.classNames.selected);
			e[Widget.Activator.selectedAttribute] = se;
		}, this);
	},
	/**
	 * Determines if an element is selected.
	 * @param {number|string} element The index or id of the element to get.
	 * @returns {bool} True if the element is selected, false otherwise.
	 */
	isSelected: function(element) {
		return this.getElement(element)[Widget.Activator.selectedAttribute];
	},
	/**
	 * Gets all selected elements.
	 * @returns {Array|element} An array of selected elements if the singleSelect option is false. If the singleSelect option is true, returns the selected element if any, null otherwise.
	 */
	getSelected: function() {
		var es = [];
		this.elements.each(function(e) {
			if(e[Widget.Activator.selectedAttribute]) es.push(e);
		});
		if(this.options.singleSelect) {
			return es.length > 0 ? es[0] : null;
		}
		return es;
	},
	/**
	 * (Dis)Enables one or more elements.
	 * @param {number|string|Array} elements The index, CSS selector, or Array of element ids or indices to enable.
	 * @param {bool} [enabled] If true, the elements are enabled; they are disabled otherwise. (default: true)
	 */
	setEnabled: function(elements, enabled) {
		var en = (Object.isUndefined(enabled) || enabled);
		var fn = en ? Element.removeClassName : Element.addClassName;
		this.getElements(elements).each(function(e) {
			fn(e, this.options.classNames.disabled);
			e[Widget.Activator.disabledAttribute] = !en;
			if(!en) {
				e.removeClassName(this.options.classNames.hover);
				e.removeClassName(this.options.classNames.active);
			}
		}.bind(this));
	},
	/**
	 * Determines if an element is enabled.
	 * @param {number|string} element The index or id of the element to get.
	 * @returns {bool} True if the element is enabled, false otherwise.
	 */
	isEnabled: function(element) {
		return !this.getElement(element)[Widget.Activator.disabledAttribute];
	},
	/**
	 * Gets all disabled elements.
	 * @returns {Array} An array of disabled elements.
	 */
	getDisabled: function() {
		var es = [];
		this.elements.each(function(e) {
			if(e[Widget.Activator.disabledAttribute]) es.push(e);
		});
		return es;
	},
	/**
	 * (De)Selects all elements.
	 * @param {bool} [selected] If true, the elements are selected; they are deselected otherwise. (default: true)
	 */
	selectAll: function(selected) {
		var se = (Object.isUndefined(selected) || selected);
		this.elements.each(function(e) {
			this.setSelected(e, se);
		}.bind(this));
	},
	/**
	 * (Dis)Enables all elements.
	 * @param {bool} [enabled] If true, the elements are enabled; they are disabled otherwise. (default: true)
	 */
	enableAll: function(enabled) {
		var en = (Object.isUndefined(enabled) || enabled);
		this.elements.each(function(e) {
			this.setEnabled(e, en);
		}.bind(this));
	},
	/**
	 * Destroys the activator. Removes all event handlers and extended methods.
	 */
	destroy: function() {
		this.remove(this.elements);
		this.handlers.each(function(h) {
			this.stopObserving(h.eventName, h.handler);
		}.bind(this));
		if(this.options.container) {
			this.options.container.stopObserving("mouseover", this.mouseoverListener);
			this.options.container.stopObserving("mouseout", this.mouseoutListener);
			this.options.container.stopObserving("mousedown", this.mousedownListener);
			this.options.container.stopObserving("mouseup", this.mouseupListener);
			this.options.container.stopObserving("click", this.clickListener);
		}
		this.handlers.length = 0;
		this.elements.length = 0;
	}
});
/**
 * Constants
 */
Object.extend(Widget.Activator, {
	/**
	 * Template for error message displayed when an element is not a child of the container.
	 */
	notContainerChild: "Element #{element} is not a child of the container #{container}.",
	/**
	 * Name of element attribute for the activator.
	 */
	activatorAttribute: "activator",
	/**
	 * Name of element attribute for selected.
	 */
	selectedAttribute: "activatorSelected",
	/**
	 * Name of element attribute for disabled.
	 */
	disabledAttribute: "activatorDisabled",
	/**
	 * Name of element attribute for getActivator function if elements are extended.
	 */
	getActivatorFunction: "getActivator",
	/**
	 * Name of element attribute for setSelected function if elements are extended.
	 */
	setSelectedFunction: "setSelected",
	/**
	 * Name of element attribute for isSelected function if elements are extended.
	 */
	isSelectedFunction: "isSelected",
	/**
	 * Name of element attribute for setEnabled function if elements are extended.
	 */
	setEnabledFunction: "setEnabled",
	/**
	 * Name of element attribute for isEnabled function if elements are extended.
	 */
	isEnabledFunction: "isEnabled",
	/**
	 * Name of mouseover event fired by the activator.
	 */
	mouseoverEvent: "activator:mouseover",
	/**
	 * Name of mouseout event fired by the activator.
	 */
	mouseoutEvent: "activator:mouseout",
	/**
	 * Name of mousedown event fired by the activator.
	 */
	mousedownEvent: "activator:mousedown",
	/**
	 * Name of mouseup event fired by the activator.
	 */
	mouseupEvent: "activator:mouseup",
	/**
	 * Name of click event fired by the activator.
	 */
	clickEvent: "activator:click"
});