/* *************************************
 * Filename:      lexum-classes.js
 * Version:       1.0
 * Author:        Benoit Giroux
 *                girouxb@lexum.umontreal.ca
 * Last updated:  2009-02-25, 9h30
 *
 ************************************* */
var _language = $$('html')[0].getProperty('lang') || 'en';
var IE6 = Browser.Engine.name == 'trident' && Browser.Engine.version == 4;
var IE7 = Browser.Engine.name == 'trident' && Browser.Engine.version == 5;
var IE = IE6 || IE7;

/*$defined = function(element) {
	return (typeof element != 'undefined');
}

$notDefined = function(element) {
	return !$defined(element);
}

$null = function(element) {
	return (element == null);
}

$notNull = function(element) {
	return !$null(element);
}*/

$clearInlineEvents = function(element) {
	element.removeProperty('onclick');
	element.removeProperty('ondblclick');
	element.removeProperty('onmousedown');
	element.removeProperty('onmousemove');
	element.removeProperty('onmouseout');
	element.removeProperty('onmouseover');
	element.removeProperty('onmouseup');
	element.removeProperty('onchange');
	element.removeProperty('onsubmit');
	element.removeProperty('onreset');
	element.removeProperty('onselect');
	element.removeProperty('onblur');
	element.removeProperty('onfocus');
	element.removeProperty('onkeydown');
	element.removeProperty('onkeypress');
	element.removeProperty('onkeyup');
	element.removeProperty('onload');
	element.removeProperty('onunload');
}

/** *********************************************
  * The Toggler and Togglee classes.
  * Made for easy toggling of the visibility of an
  * element (the Togglee) by clicking another
  * element (the Toggler).
  *
  * The idea is to create a Togglee first:
  * var togglee = new Togglee(element, false, options);
  *
  * The first parameter is the html element that will be toggled
  * The second parameter is whether or not the element will be initially visible
  * The third parameter are the Fx options to be applied when the element is toggled
  *
  * Then you need to create at least one Toggler:
  * var toggler = new Toggler(toggler, togglee, options);
  *
  * The first parameter is the html element that will be the toggler
  * The second parameter is the Togglee created first
  * The third parameter are the options for the Toggler.
  *
  * Then the magic operates and you have a toggler/togglee on your page.
  *
  *********************************************** */

var Toggler = new Class({
	Implements: Options,

	image: null,

	// Default options
	options: {
		action: 'click',         // How to use the Toggler (can be 'click' or 'hover')
		preDelayMs: 0,
		postDelayMs: 0,
		collapsedText: null,     // The (optional) text to display when the Togglee is collapsed (hidden)
		expandedText: null,      // The (optional) text to display when the Togglee is expanded (visible)
		append: false,           // Whether the collapsed and expanded text is appended to the text already present for the toggler.
		input: false,            // Whether the toggler is an input of type "button" (<input type="button"/>)
		togglerImage: null,      // The (optional) image object used for expandedImage and collapsedImage
		expandedImage: null,     // The path to the (optional) image to display when the Togglee is expanded (visible) (often a 'minus' sign)
		collapsedImage: null,    // The path to the (optional) image to display when the Togglee is collapsed (hidden) (often a 'plus' sign)
		imagePosition: 'inside', // The position of the image. Can be "inside", "outside" or "after".
		                         // ("inside" and "outside" imply that the image will be in front ("before") of the Toggler)
		collapsedClass: null,    // The (optional) class to apply to the toggler when the Togglee is collapsed (hidden)
		expandedClass: null      // The (optional) class to apply to the toggler when the Togglee is expanded (visible)
	},

	preTimeout: null,
	postTimeout: null,

	initialize: function(toggler, togglee, options) {
		//this.uniqueId = time();
		this.toggler = $(toggler);
		this.togglee = togglee;
		this.togglee.addToggler(this);
		this.setOptions(options);

		// Add the toggle event on the toggler
		this.addEvents();

		// If the toggler element is staticly positioned, change it to relative so that
		// the prepended images will be absolutely positioned relatively to the toggler.
		if (this.toggler.getStyle('position') == 'static') {
			this.toggler.setStyle('position', 'relative');
		}

		// Initializing the toggler now that everything is set, to put it
		// in sync with the state of its Togglee.
		this.update(this.togglee.isVisible(), true);

	},

	addEvents: function() {
		// The function that will be called when clicked, only if no img or text is set
		if (this.options.collapsedImage == null && this.options.expandedImage == null &&
			this.options.collapsedText == null && this.options.expandedText == null) {

			this.prepareEvents(this.toggler);

			// Put a pointer cursor on the toggler only if it is the element that toggles the Togglee.
			this.toggler.setStyle('cursor', 'pointer');
		}

		// The function that will be called when the toggler gets focus
		var blur = (function() {
			this.toggler.blur();
		}).bind(this);
		this.toggler.addEvent('focus', blur);


		// Since a lot of time, togglers were once links, check if the Toggler
		// has a link as child. If so, remove any potential inline events
		// and as a bonus, cancel the onfocus effect (which will put a dotted
		// border all around the link) by bluring it.
		$clearInlineEvents(this.toggler);
		$each(this.toggler.getChildren(), function(child) {
			$clearInlineEvents(child);
			child.addEvent('focus', function(){
				child.blur();
			});
		});
	},

	prepareEvents: function(firer) {
		//timeoutSelfId = this.uniqueId;
		//timeoutSelf['id' + this.uniqueId] = this;

		var toggle = (function(_e) {
			this.toggle(_e);
		}).bind(this);

		if (this.options.action == 'click') {
			firer.addEvent('click', toggle);
		} else {
			firer.addEvent('mouseenter', function() {
				if (this.postTimeout != null) {
					clearTimeout(this.postTimeout);
					this.postTimeout = null;
				}

				this.preTimeout = setTimeout(function(){
						this.show();
					}.bind(this), this.options.preDelayMs);
			}.bind(this));

			firer.addEvent('mouseleave', function() {
				if (this.preTimeout != null) {
					clearTimeout(this.preTimeout);
					this.preTimeout = null;
				}

				this.postTimeout = setTimeout(function(){
						this.hide();
					}.bind(this), this.options.postDelayMs);
			}.bind(this));
		}
	},

	show: function() {
		if ($defined(this.options.collapsedClass)) {
			this.toggler.removeClass(this.options.collapsedClass);
		}
		if ($defined(this.options.expandedClass)) {
			this.toggler.addClass(this.options.expandedClass);
		}
		this.togglee.show();
	},

	hide: function() {
		if ($defined(this.options.collapsedClass)) {
			this.toggler.addClass(this.options.collapsedClass);
		}
		if ($defined(this.options.expandedClass)) {
			this.toggler.removeClass(this.options.expandedClass);
		}
		this.togglee.hide();
	},

	toggle: function(_e) {
		if ($defined(_e)) {
			_e.preventDefault(); // Prevent the default behavior of the event.
		}

		if (this.togglee.isVisible()) {
			this.hide();
		} else {
			this.show();
		}

		return false;
	},

	// Except for the constructor, this function is only called by the Togglee.
	update: function(toggleeIsVisible, init) {
		if (init) {
			if (toggleeIsVisible) {
				this.setText(this.options.expandedText);
				this.setImg(this.options.expandedImage);
				if ($defined(this.options.collapsedClass)) {
					this.toggler.removeClass(this.options.collapsedClass);
				}
				if ($defined(this.options.expandedClass)) {
					this.toggler.addClass(this.options.expandedClass);
				}
			} else {
				this.setText(this.options.collapsedText);
				this.setImg(this.options.collapsedImage);
				if ($defined(this.options.collapsedClass)) {
					this.toggler.addClass(this.options.collapsedClass);
				}
				if ($defined(this.options.expandedClass)) {
					this.toggler.removeClass(this.options.expandedClass);
				}
			}
		} else {
			if (toggleeIsVisible) {
				this.updateText(this.options.expandedText);
				this.updateImg(this.options.expandedImage);
			} else {
				this.updateText(this.options.collapsedText);
				this.updateImg(this.options.collapsedImage);
			}
		}
	},

	// Update optional context relative text on the Toggler.
	// (i.e. 'open' when closed, and 'close' when opened).
	setText: function(text) {
		if ($defined(text)) {
			// There are three possibilities on where to put the text
			if (this.options.append) {
				// If the text is appended to the original text
				var span = this.toggler.getElement('span.togglerText');

				if (!$defined(span)) {
					span = new Element('span', {
						'class': 'togglerText',
						'styles': {
							'cursor': 'pointer',
							'font-size': '.65em',
							'margin': '0 1em',
							'vertical-align': '.2em'
						}
					});

					this.toggler.grab(span);

					var myself = this;
					var toggle = (function(_e) {
						this.toggle(_e);
					}).bind(this);

					if (this.options.action == 'click') {
						span.addEvent('click', toggle);
					} else {
						span.addEvent('mouseenter', function() {
							this.show();
						}.bind(myself));
						span.addEvent('mouseleave', function() {
							this.hide();
						}.bind(myself));
					}
				}

				span.set('text', text);


			} else if (this.options.input) {
				// if the text is applied to an input (i.e. an <input type="button"> element)
				this.toggler.set('value', text);

				var myself = this;
				var toggle = (function(_e) {
					this.toggle(_e);
				}).bind(this);

				if (this.options.action == 'click') {
					this.toggler.addEvent('click', toggle);
				} else {
					this.toggler.addEvent('mouseenter', function() {
						this.show();
					}.bind(myself));
					this.toggler.addEvent('mouseleave', function() {
						this.hide();
					}.bind(myself));
				}
				this.toggler.setStyle('cursor', 'pointer');
			} else {
				// else, the text simply replaces whatever was already present
				this.toggler.set('text', text);

				var myself = this;
				var toggle = (function(_e) {
					this.toggle(_e);
				}).bind(this);

				if (this.options.action == 'click') {
					this.toggler.addEvent('click', toggle);
				} else {
					this.toggler.addEvent('mouseenter', function() {
						this.show();
					}.bind(myself));
					this.toggler.addEvent('mouseleave', function() {
						this.hide();
					}.bind(myself));
				}
				this.toggler.setStyle('cursor', 'pointer');
			}
		}
	},

	// Update optional context relative text on the Toggler.
	// (i.e. 'open' when closed, and 'close' when opened).
	updateText: function(text) {
		if ($defined(text)) {
			// There are three possibilities on where to put the text
			if (this.options.append) {
				// If the text is appended to the original text
				var span = this.toggler.getElement('span.togglerText');
				span.set('text', text);
			} else if (this.options.input) {
				// if the text is applied to an input (i.e. an <input type="button"> element)
				this.toggler.set('value', text);
			} else {
				// else, the text simply replaces whatever was already present
				this.toggler.set('text', text);
			}
		}
	},

	// Update optional context relative image on the Toggler.
	// (i.e. a '+' image to expand a collapsed Togglee and a '-' image to collapse an expanded Togglee)
	setImg: function(imgSrc) {
		if ($defined(imgSrc)) {
			//var img = this.toggler.getElement('img.togglerImage');
			var img = this.image;

			// The function to position the image correctly. Will be called once the image is loaded.
			var positionImage = (function() {
				if (this.options.imagePosition == 'inside' || this.options.imagePosition == 'outside') {
					img.setStyle('left', -1.5* img.getSize().x);
					img.setStyle('bottom', this.toggler.getSize().y/2 - img.getSize().y/2);
					if (this.options.imagePosition == 'inside') {
						this.toggler.setStyle('paddingLeft', 1.5* img.getSize().x);
						img.setStyle('left', '1px');
					}

					// Reduces the effect on IE6, but that the best we can do for now.
					if (window.IE6) {
						this.toggler.setStyle('paddingLeft', 0);
						img.setStyle('marginRight', '.3em');
						img.setStyle('position', 'relative');
						img.setStyle('bottom', 0);
						img.setStyle('left', '1px');
					}
				}

			}).bind(this);

			// At the page load, the IMG tag won't be in the DOM. Create it.
			if (!$defined(img)) {
				img = null;
				if (this.options.togglerImage != null) {
					img = this.options.togglerImage;
					if (!img.hasClass('togglerImage')) {
						img.addClass('togglerImage');
					}
					img.setStyle('cursor', 'pointer');
				} else {
					img = new Element('img', {
						'styles': {
							'cursor': 'pointer'
						}
					});
					img.addClass('togglerImage');

					if (this.options.imagePosition == 'inside' || this.options.imagePosition == 'outside') {
						img.setStyle('position', 'absolute');
						this.toggler.grab(img, 'top');
					} else {
						img.setStyle('marginLeft', '.35em');
						img.setStyle('verticalAlign', 'middle');
						this.toggler.grab(img, 'bottom');
					}
				}

				img.addEvent('load', function() {
					positionImage();
				});

				var myself = this;
				var toggle = (function(_e) {
					this.toggle(_e);
				}).bind(this);

				if (this.options.action == 'click') {
					img.addEvent('click', toggle);
				} else {
					img.addEvent('mouseenter', function() {
						this.show();
					}.bind(myself));
					img.addEvent('mouseleave', function() {
						this.hide();
					}.bind(myself));
				}

				this.image = img;
			}

			// Set the image. Once the image will be loaded, it will fire the 'load' event
			img.set('src', imgSrc);
		}
	},

	// Update optional context relative image on the Toggler.
	// (i.e. a '+' image to expand a collapsed Togglee and a '-' image to collapse an expanded Togglee)
	updateImg: function(imgSrc) {
		if ($defined(imgSrc)) {
			this.image.set('src', imgSrc);
		}
	}
});

var Togglee = new Class({
	Implements: [Chain, Options],

	// Default Fx options
	options: {
		duration: 0,                // Default time in milliseconds for the transition. Defaults to zero, which means instant show/hide.
		link: 'ignore',
		mode: 'vertical',
		transition: 'sine:in:out',
		scrollToOnClose: null,
		floatOver: false,        // Set the togglee to float over the page's content (like in a menu).
		maxWidth: null,          // Max width of the togglee if it is configure to float over the page's content.
		preshow: function(){},   // Method to call before show begins.
		postshow: function(){},  // Method to call after show is done.
		prehide: function(){},   // Method to call before hide begins.
		posthide: function(){}   // Method to call after hide is done.
	},

	initialize: function(target, visible, effectOptions) {
		this.target = $(target);
		this.visible = visible;
		this.togglers = [];
		this.setOptions(effectOptions);
		//this.options.link = 'ignore'; // Overriding whatever it could have been set to.
		this.effect = new Fx.Slide(this.target, this.options);

		// Add an event to change state only once the transition is completed.
		var myself = this;
		this.effect.addEvent('complete', function() {myself.toggleState();});

		// If the Togglee is initially hidden, it needs to be set
		// by bypassing the real hide method to prevent toggling the display
		if (!visible) {
			// Thus, the target is explicitly hidden with its slide option
			this.target.get('slide').hide(this.options.mode);
			// To fix a CSS bug in IE, we also need to make the target invisible, unfortunatly
			// The bug happens if there is another Toggler/Togglee inside this Togglee
			this.target.setStyle('display', 'none');
		}

		// Add padding to zero paddings, to force margins of inside elements to be contained
		// within the boundaries of this Togglee. Else these margins are not accounted for
		// and the Togglee can overflow its area of visibility, resulting in hidden parts.
		if (this.target.getStyle('padding-top') == '0px') {
			this.target.setStyle('padding-top', '1px');
		}
		if (this.target.getStyle('padding-bottom') == '0px') {
			this.target.setStyle('padding-bottom', '1px');
		}
		if (this.target.getStyle('padding-left') == '0px') {
			this.target.setStyle('padding-left', '1px');
		}
		if (this.target.getStyle('padding-right') == '0px') {
			this.target.setStyle('padding-right', '1px');
		}

		// Sometimes you may want to have the Togglee slide in and out without affecting the layout (appearing 'over' the rest of the page)
		// To do so, the container created by Mootools needs to have its position set to 'absolute' instead of 'static'.
		if (this.options.floatOver) {
			this.target.getParent().setStyle('position', 'absolute');
			this.target.getParent().setStyle('left', '0');
			// Since the togglee is now absolutely positioned, we have to do something to control its witdh. I still don't know exactly
			// how this should be done.
			if (this.options.maxWidth == null) {
				var childrenCount = this.target.getChildren().length;
				var childrenLineHeight = this.target.getChildren().getFirst().getStyle('lineHeight')[0].toFloat();
				var subElementsHeight = this.target.offsetHeight;

				while (subElementsHeight > (childrenCount + 0.5) * 2 * childrenLineHeight &&
						this.options.maxWidth < this.target.getParent().getParent().offsetWidth * 3) {
					this.options.maxWidth += 20;
					this.target.getParent().setStyle('width', this.options.maxWidth);
					subElementsHeight = this.target.offsetHeight;
				}
			}
		}

		this.target.getParent().addClass('toggleeContainer');
	},

	// This function will always be called once the transition is done
	toggleState: function() {
		if (this.effect.open) {
			this.visible = true;
		} else {
			this.visible = false;
		}
		this.updateTogglers();
	},

	show: function() {
		var myself = this;
		// Within the following chain, 'this' refers to 'this.effect'
		this.effect.chain(function() {
			myself.options.preshow();
			this.callChain();
		}).chain(function() {
			myself.target.setStyle('display', 'block');
			this.slideIn().chain(function() {
				myself.target.getParent().setStyle('height', 'auto');
			});
		}).chain(function() {
			myself.options.postshow();
			this.callChain();
		});
		this.effect.callChain();
	},

	hide: function() {
		var myself = this;
		// Within the following chain, 'this' refers to 'this.effect'
		this.effect.chain(function() {
			myself.options.prehide();
			this.callChain();
		}).chain(function() {
			this.slideOut().chain(function() {
				myself.target.setStyle('display', 'none');
			});

			if ($chk(myself.options.scrollToOnClose)) {
				var positionY = myself.options.scrollToOnClose.getPosition().y;

				new Fx.Scroll(window).start(0, positionY);
			}

		}).chain(function() {
			myself.options.posthide();
			this.callChain();
		});
		this.effect.callChain();
	},

	// Whether this Togglee is visible or not.
	isVisible: function() {
		return this.visible;
	},

	// Adds a Toggler to the list of Togglers acting on this Togglee.
	addToggler: function(toggler) {
		this.togglers.push(toggler);
	},

	// Update all Togglers so that they are always all in sync
	// with the state of this Togglee on which they act on.
	updateTogglers: function() {
		var myself = this;

		this.togglers.each(function(toggler) {
			toggler.update(myself.isVisible());
		});
	}
});

/** *********************************************
  * The LabeledInput
  *
  *********************************************** */
var LabeledInput = new Class({
	// The input itself
	input: null,

	/**
	 * label: an element whose inner text is the label for the input.
	 * input: an input element of type text.
	 */
	initialize: function(label, input) {
		label.setStyle('display', 'none');
		input.store('label', label.get('text'));
		this.input = input;

		if (this.input.value == '' || this.input.value == this.input.retrieve('label')) {
			this.input.addClass('emptyInput');
			this.input.value = this.input.retrieve('label');
		}

		this.input.addEvent('focus', (function() {
			this.gainFocus();
		}).bind(this));

		this.input.addEvent('blur', (function() {
			this.loseFocus();
		}).bind(this));
	},

	loseFocus: function() {
		if (this.emptyInput()) {
			this.input.value = this.input.retrieve('label');
			this.input.addClass('emptyInput');
		}
	},

	gainFocus: function() {
		if (this.input.value == this.input.retrieve('label')) {
			this.input.value = '',
			this.input.removeClass('emptyInput');
		}
	},

	emptyInput: function() {
		if (this.input.value == '') {
			return true;
		}

		return false;
	}
});

/** *********************************************
 * The Tabber class.
 *
 *
 *********************************************** */
var Tabber = new Class({
	Implements: Options,

	tabs: null,
	activeTabIndex: null,
	contents: null,

	options: {
		activeClass: 'activeTab',
		activeTabIndex: 0
	},

	initialize: function(tabs, contents, options) {
		this.setOptions(options);
		this.tabs = tabs;
		this.contents = contents;

		this.initTabs(tabs);
		this.hideAllContents();

		this.activeTabIndex = this.options.activeTabIndex;
		this.showTab(this.activeTabIndex);
	},

	initTabs: function(tabs) {
		$each(tabs, function(tab) {
			tab.addEvent('click', (function() {
				this.hideTab(this.activeTabIndex);
				this.activeTabIndex = this.tabs.indexOf(tab);
				this.showTab(this.activeTabIndex);

			}).bind(this));

			tab.setStyle('cursor', 'pointer');
		}, this);
	},

	hideAllContents: function() {
		this.contents.setStyle('display', 'none');
	},

	showTab: function(index) {
		this.tabs[index].addClass(this.options.activeClass);
		this.contents[index].setStyle('display', 'block');
	},

	hideTab: function(index) {
		this.tabs[index].removeClass(this.options.activeClass);
		this.contents[index].setStyle('display', 'none');
	}
});

var TextToSelect = new Class({
	Implements: Options,

	input: null,
	select: null,
	deleteLink: null,

	options: {
		selectSize: 5
	},

	initialize: function(input, select, deleteLink, options) {
		this.setOptions(options);
		this.input = $(input);
		this.select = $(select);
		this.deleteLink = deleteLink;
		this.form = this.input.form;

		this.initInput();
		this.initSelect();
		this.initDeleteLink();
		this.initForm();
	},

	initInput: function() {
		this.input.addEvent('keydown', (function(e) {
			if (e.key == 'enter') {
				e.preventDefault();
				if (this.input.value != '') {
					this.addToSelect(this.input, this.select);
				}
			}
		}).bind(this));

		var addButton = new Element('button', {
			'text': '\u00BB'
		});

		addButton.addEvent('click', (function(e) {
			e.preventDefault();
			if (this.input.value != '') {
				this.addToSelect(this.input, this.select);
			}
		}).bind(this));

		addButton.inject(this.input, 'after');
	},

	addToSelect: function(input, select) {
		var present = false;

		$each(select.getElements('option'), function(option) {
			if (option.value.clean() == input.value.clean()) {
				present = true;
			}
		});

		if (!present) {
			select.grab(new Element('option', {
				'value': this.input.value.clean(),
				'text': this.input.value.clean()
			}));

			input.value = '';
		}

	},

	initSelect: function() {
		if (this.select.getProperty('multiple') == '') {
			this.select.setProperty('multiple', 'multiple');
		}

		this.select.setProperty('size', this.options.selectSize);
	},

	initDeleteLink: function() {
		this.deleteLink.addEvent('click', (function(_e) {
			_e.preventDefault();
			this.deleteSelected();
		}).bind(this));
	},

	initForm: function() {
		this.form.addEvent('submit', (function(_e) {
			this.select.getElements('option').set('selected', true);
		}).bind(this));
	},

	deleteSelected: function() {
		this.select.getSelected().destroy();
	}
});


/** *********************************************
 * The Menu class.
 *
 *
 *********************************************** */
var Menu = new Class({
	Implements: Options,

	options: {
		hoverClass: 'menuHover'
	},

	initialize: function(list, options) {
		this.setOptions(options);
		this.menu = list;

		this.initList();
	},

	initList: function() {
		var listElements = this.menu.getChildren();
		var myself = this;

		$each(listElements, function(listElement) {
			var subElements = listElement.getElement('ul');
			if (subElements) {
				var subMenuTogglee = new Togglee(subElements, false, {
					duration: 100,
					link: 'cancel',
					floatOver: true,
					transition: 'pow:out'/*,
					maxWidth: maxWidth + 'px'*/
				});


				var subMenuToggler = new Toggler(listElement, subMenuTogglee, {
					expandedClass: myself.options.hoverClass,
					action: 'hover',
					preDelayMs: 150,
					postDelayMs: 150
				});
			}
		});
	}
})

var Popup = new Class({
	Implements: Options,

	options: {
		alwaysRaised: 'yes',
		channelmode: 'no',
		directories: 'no',
		fullscreen: 'no',
		height: 450,
		left: 200,
		location: 'no',
		menubar: 'no',
		name: '',
		resizable: 'yes',
		scrollbars: 'yes',
		status: 'yes',
		titlebar: 'yes',
		toolbar: 'yes',
		top: 50,
		width: 350
	},

	initialize: function(url, options) {
		this.setOptions(options);
		this.parent = window;
		this.url = url;
	},

	buildOptions: function() {
		var opt =
			'channelmode=' + this.options.channelmode + ',' +
			'directories=' + this.options.directories + ',' +
			'fullscreen=' + this.options.fullscreen + ',' +
			'height=' + this.options.height + ',' +
			'left=' + this.options.left + ',' +
			'location=' + this.options.location + ',' +
			'menubar=' + this.options.menubar + ',' +
			'name=' + this.options.name + ',' +
			'resizable=' + this.options.resizable + ',' +
			'scrollbars=' + this.options.scrollbars + ',' +
			'status=' + this.options.status + ',' +
			'titlebar=' + this.options.titlebar + ',' +
			'toolbar=' + this.options.toolbar + ',' +
			'top=' + this.options.top + ',' +
			'width=' + this.options.width;

		return opt;
	},

	pop: function() {
		this.window = this.parent.open(this.url, this.options.name, this.buildOptions());
	},

	close: function() {
		this.window.close();
	}
});

var ModalDialog = new Class({
	Implements: Options,

	options: {
		center: 'yes',
		dialogHeight: 450,
		dialogLeft: 200,
		dialogTop: 50,
		dialogWidth: 350,
		edge: 'sunken',
		name: '',
		help: 'no',
		resizable: 'yes',
		scroll: 'yes',
		status: 'yes'
	},

	initialize: function(url, options) {
		//this.
		this.parent = window;
		this.url = url;
	},

	buildOptions: function() {
		var opt =
			'center=' + this.options.center + ',' +
			'dialogHeight=' + this.options.dialogHeight + ',' +
			'dialogLeft=' + this.options.dialogLeft + ',' +
			'dialogTop=' + this.options.dialogTop + ',' +
			'dialogWidth=' + this.options.dialogWidth + ',' +
			'edge=' + this.options.edge + ',' +
			'help=' + this.options.help + ',' +
			'resizable=' + this.options.resizable + ',' +
			'scroll=' + this.options.scroll + ',' +
			'status=' + this.options.status;

		return opt;
	},

	pop: function() {
		this.window = this.parent.open(this.url, this.buildOptions());
	},

	close: function() {
		this.window.close();
	}
});

/** *********************************************
  * The Bubbler class.
  *
  *
  *********************************************** */
var Bubbler = new Class({
	Implements: Options,

	mandatoryOptions: {
		opacity: '0',
		position: 'absolute'
	},

	options: {
		// Default styling options for the bubble
		styles: {
			backgroundColor: '#ffed7a',
			border: '1px solid #ffdc00',
			color: '#000',
			fontFamily: 'inherit',
			fontSize: '.8em',
			padding: '.25em',
			textAlign: 'justify',
			maxWidth: '40%'
		},

		// Default styling options for the bubble, when configured as footnote.
		footnoteStyles: {
			backgroundColor: '#ffed7a',
			border: '1px solid #ffdc00',
			bottom: 0,
			color: '#000',
			fontFamily: 'inherit',
			fontSize: '.8em',
			left: 0,
			padding: '.25em',
			right: 0
		},

		// The text of the bubble. Optional, as the text could also come from the 'title' attribute of the bubbler element, in which case no text has to be given.
		bubbleText: null,
		// The element containing the bubble. If null, the container is the body of the page and the offset will serve for placing the bubble relative to the container
		container: null,
		// Optional CSS class to style the bubble. If specified, the default styling options (footnote or not) won't apply.
		cssClass: null,
		// If set to true, the offset options will be ignored. The bubble will appear as a box at the bottom of the window.
		footnote: false,
		// Maximum opacity for the fade in of the bubble (between 0 and 1, 0 being completely transparent and 1 being completely opaque).
		maxOpacity: 1,
		// Default offset of the bubble relative to the mouse cursor or the container (if the container is specified).
		offset: {x: 15, y: 12},
		// Reference of the offset, in CSS terms. It also determines its direction. Accepted terms are 'left' and 'right' for x and 'top' and 'bottom' for y.
		offsetReference: {x: 'left', y: 'bottom'}
	},

	initialize: function(originalElement, options) {
		this.bubbler = $(originalElement);
		this.setOptions(options);
		this.bubble = this.createBubble();

		if (!$defined(this.options.container)) {
			this.container = $(document.body);
		} else {
			this.container = $(this.options.container);

			if (this.container.getStyle('position') == 'static') {
				this.container.setStyle('position', 'relative');
			}
		}

		if (this.bubble.get('html') != '') {
			this.addEvents();
		}
	},

	addEvents: function() {
			var myself = this;
			this.bubbler.addEvent('mouseenter', function(event) {
				myself.showBubble(event);
			});

			this.bubbler.addEvent('mouseleave', function(event) {
				myself.hideBubble();
			});

	},

	createBubble: function() {
		var div = new Element('div');
		// If a class was given in the options, use the class to style the
		// bubble instead of default styling options.
		if ($defined(this.options.cssClass)) {
			div.setProperty('class', this.options.cssClass);
		} else {
			if (this.options.footnote) {
				div.setStyles(this.options.footnoteStyles);
			} else {
				div.setStyles(this.options.styles);
			}
		}

		// Mandatory options for the bubble, to ensure correct behavior of the bubble.
		div.setStyles(this.mandatoryOptions);
		if (!$defined(this.options.bubbleText)) {
			div.set('html', this.bubbler.getProperty('title'));
		} else {
			div.set('html', this.options.bubbleText);
		}

		// Precaution, to make sure nothing else pops-up than our bubble
		this.bubbler.removeProperty('title');

		return div;
	},

	/*
	 * Make the bubble appear.
	 */
	showBubble: function(event) {
		// Put the bubble in the DOM first, then we'll be able to calculate its position
		// and ajust it.
		this.bubble.inject(this.container, 'bottom');

		if (!this.options.footnote) {
			if (!$defined(this.options.container)) {
				// Calculates coordinates from the mouse pointer
				var newCoordinates = this.getUpdatedMouseCoordinates(event);
				this.bubble.setStyle('position', 'fixed');
				this.bubble.setStyle('left', newCoordinates.x);
				this.bubble.setStyle('top', newCoordinates.y);
			} else {
				// Calculates coordinates from the specified container, which is by default the normal cartesian origin: bottom left corner
				var newCoordinates = this.getUpdatedCoordinates();
				this.bubble.setStyle(this.options.offsetReference.x, newCoordinates.x);
				this.bubble.setStyle(this.options.offsetReference.y, newCoordinates.y);
			}


		}

		this.bubble.fade(this.options.maxOpacity);
	},

	/*
	 * Make the bubble disappear.
	 */
	hideBubble: function() {
		// Fade the bubble out and remove it from the DOM.
		var bubble = this.bubble;
		this.bubble.fade(0).get('tween').chain(function() {
			bubble.dispose();
		});
	},

	/*
	 * Get the coordinates of the position where the bubble needs to be in relation to the mouse position.
	 */
	getUpdatedMouseCoordinates: function(event) {
		// This is the position of the right-side of the bubble
		var calculatedX
			= event.client.x          // the x position of the mouse pointer
			+ this.options.offset.x    // the horizontal distance from the bubble to the mouse pointer
			+ this.bubble.getSize().x // the width of the bubble

		// This is the position of the top-side of the bubble
		var calculatedY
			= event.client.y          // the y position of the mouse pointer
			- this.options.offset.y    // the vertical distance from the bubble to the mouse pointer
			- this.bubble.getSize().y // the height of the bubble

		// Check whether or not the bubble's right side will go beyond the right of the document
		var correctedX = calculatedX > $(document).getSize().x ?
			event.client.x - this.options.offset.x - this.bubble.getSize().x :
			event.client.x + this.options.offset.x;

		// Check whether or not the bubble's top side will go beyond the top of the document
		var correctedY = calculatedY < 0 ?
			event.client.y + this.options.offset.y :
			event.client.y - this.options.offset.y - this.bubble.getSize().y;

		return {x: correctedX, y: correctedY};
	},

	getUpdatedCoordinates: function() {
		return {x: this.options.offset.x, y: this.options.offset.y};
	}
});

/** *********************************************
  * The Logger class.
  * Not the be-all-end-all logger, but at least
  * allows for debug messages a lot less obstrusive
  * than traditionnal 'alert()' calls, and it works
  * for all browsers.
  *
  * The logger will work if the website is in debug
  * mode, that is, by passing 'true' to the constructor.
  * Else, no debug will occur except in Firebug's
  * console, if present.
  *********************************************** */
var Logger = new Class({
		initialize: function(isDebug) {
			this.debug = isDebug;
		},

		log: function(text) {
			// Always log if Firebug is present, since it is subtle and non-obstrusive and quite useful.
			if ($defined(console) && $defined(console.log)) {
				console.log(text);
			}

			// Only use the logger if in debug mode.
			if (this.debug) {
				var logArea = $('logArea');
				var logAreaLi = new Element('li');
				var logAreaLiPre = new Element('pre', {
					'styles': {
						'margin': '0'
					}
				});
				logAreaLiPre.set('text', text);
				logAreaLi.grab(logAreaLiPre);


				if (!$defined(logArea)) {
					logArea = new Element('div', {
						'id': 'logArea',
						'styles': {
							'background-color': '#c6453f',
							'bottom': '0',
							'color': '#fff',
							'font-family': 'monospace',
							'font-size': '',
							'height': '60px',
							'left': '0',
							'overflow': 'auto',
							'padding': '12px 256px 12px 12px',
							'position': 'fixed',
							'right': '0',
							'z-index': '1000'
						}
					});

					logAreaTitle = new Element('h1', {
						'styles': {
							'color': '#fff',
							'bottom:': '30px',
							'font-size': '24px',
							'font-weight': 'bold',
							'position': 'fixed',
							'right': '36px'
						}
					});
					logAreaTitle.set('text', 'DEBUG CONSOLE');

					logArea.addEvent('click', function() {
						this.destroy();
					});

					var logAreaUl = new Element('ul', {
						'styles': {
							'margin': '0 .5em',
							'padding': '0 .5em'
						}
					});

					logAreaUl.grab(logAreaLi);
					logArea.grab(logAreaTitle);
					logArea.grab(logAreaUl);

					$(document.body).grab(logArea);
				} else {
					logArea.getElement('ul').grab(logAreaLi);
				}

			}
		}
	}
);

