/*
 * Dynamisches JavaScript Menu.
 *
 * Copyright artundweise GmbH, 2009.
 *
 * Autor: Alf Werder
 *
 * Jede Benutzung, Veränderung und Vervielfältigung dieses Codes ist nur mit
 * ausdrücklicher und schriftlicher Genehmigung der artundweise GmbH gestattet.
 *
 *
 * Hinweise:
 *
 * Ein *Menu* ist ein beliebiger HTML-Element-Knoten, der eine beliebige Azahl
 * von MenuItems enthält. Ein *MenuItem* ist ein HTML-Element-Knoten, der mit
 * der CSS-Marker-Klasse 'menu-item' versehen ist. MenuItems müssen nicht
 * direkte Kinder des Menus sein.
 *
 * Ein MenuItem kann ein *SubMenu* enthalten. Ein SubMenu ist ein
 * HTML-Element-Knoten, der mit der CSS-Marker-Klasse 'sub-menu' versehen ist.
 * Das SubMenu müss wiederum kein direktes Kind des MenuItems sein.
 *
 * Es gibt zwei Arten von SubMenus: *DefaultSubMenus* und *PopupSubMenus*. Der
 * Unterschied besteht in der dynamischen Positionierung. Während die Position
 * eines DefaultSubMenus nicht beeinflusst wird, werden PoupSubMenus per
 * JavaScript dynamisch positioniert.
 *
 * Eine PopupSubMenu muss mit der zusätzlichen CSS-Marker-Klasse
 * 'sub-menu-type-popup' ausgezeichnet werden. Ein solches PopupSubMenu
 * wird initial aus dem DOM entfernt und dynamisch wieder eingefügt, wenn es
 * angezeigt wird.
 *
 * Beim Anzeigen eines PopupSubMenus wird ein Div-Container erzeugt, der als
 * direktes Kind von document.body in den DOM eingefügt wird. Das PopupSubMenu
 * wird dann in diesen sog. Wrapper eingefügt. Der Wrapper wird absolut
 * positioniert. Da das PoupSubMenu selbst ein Kind des absolut positionierten
 * Wrappers ist, kann dieses mit { position: relative; top: ...; left: ...; }
 * realitiv zum Wrapper verschoben werden.
 *
 * Die Psoition des Wrapper wird von den CSS-Marker-Klassen 'popup-align-top'
 * und 'popup-align-left' gesteuert. Trägt das PopupSubMenu die Klasse
 * 'popup-align-top', so liegt die linke obere Ecke des Wrappers einen Pixel
 * links von der rechten oberen Ecke des MenuItems. Trägt das PopupSubMenu die
 * Klasse 'popup-align-left', so liegt die linke obere Ecke des Wrappers einen
 * Pixel unter der linken unteren Ecke des MenuItems.
 *
 * Jedes MenuItem ist entweder 'collapsed' oder 'expanded'. Gleichlautende
 * CSS-Klassen werden gemäß des Zustands dynamisch vergeben und können zum
 * "stylen" des Menus verwendet werden. Zu bachten ist, das auch ein MenuItem
 * ohne SubMenu die Zustände 'expanded' und 'collapsed' kennt, je nach dem,
 * ob der Besucher mit dem Mauszeiger auf das MenuItem zeigt, oder nicht.
 *
 * Unter dem initialen Zustand des Menus versteht man den Zustand, in dem sich
 * das Menu unmittebar nach dem Rendern der Seite zeigt. Üblicherweise
 * reflektiert dieser Zustand den Ort, an dem sich der Benutzer gerade innerhalb
 * der Site befindet. Dieser Initiale Zustand wird immer wieder eingenommen,
 * wenn der Besucher den Mauszeiger ganz aus dem Menu herausbewegt.
 *
 * Zur Markierung des initialen Zustands wird ein MenuItem (das, was den
 * aktuellen Ort bezeichnet) mit der CSS-Marker-Klasse 'current-item' versehen.
 * Bei der Initialisierung werden jetzt das CurrentItem und all seine Väter
 * und Großväter mit der CSS-Klasse 'selected' versehen. Außerdem wird das Menu
 * so aufgeklappt, dass der Pfad zum CurrentItem sichtbar wird. Allerdings nur
 * so weit, wie kein PopupSubMenu aufgeklappt werden muss.
 *
 */

var jsmen = {};

jsmen.DEFAULT_OPTS = $H({
	debug: false,
	cssClassExpanded: 'expanded',
	cssClassCollapsed: 'collapsed',
	cssClassPointed: 'pointed',
	cssClassMenuItem: 'menu-item',
	delay: 250,
	cssClassSelected: 'selected',
	cssClassPopupAlignTop: 'popup-align-top',
	cssClassPopupAlignLeft: 'popup-align-left',
	
	/* Do not remove this marker! */
	defaultsAlreadyMerged: true
});

jsmen.mergeOpts = function(opts) {
	return jsmen.DEFAULT_OPTS.merge(opts).toObject();
}

jsmen.log = function(message) {
	if (console) {
		console.log(message);
	}
}

jsmen.Pointable = Class.create({
	initialize: function(element, opts) {
		this.element = $(element);
		this.opts = jsmen.mergeOpts(opts);
		this.pointed = false;
		
		if(this.element != null) {
			this.element.observe("mouseover", this.onMouseOver.bindAsEventListener(this));
			this.element.observe("mouseout", this.onMouseOut.bindAsEventListener(this));
		}
	},
	
	isPointed: function() {
		return this.pointed;
	},

	onMouseOver: function(ev) {
		if (this.opts.debug) {
			jsmen.log("Mouse in --> " + this.element.inspect());
		}
		
		if (this.pendingMouseOut) this.pendingMouseOut = false
		this.setPointed(true);
	},

	onMouseOut: function(ev) {
		if (this.opts.debug) {
			jsmen.log("Mouse out <-- " + this.element.inspect());
		}

		this.pendingMouseOut = true;
		this.handleDeferredMouseOut.bind(this).defer();
	},
	
	handleDeferredMouseOut: function() {
		if (this.pendingMouseOut) {
			this.pendingMouseOut = false;
			this.setPointed(false)
		}
	},
	
	setPointed: function(pointed) {
		if (this.pointed = pointed) {
			this.onMouseEnter();
		} else {
			this.onMouseLeave();
		}
	},
	
	onMouseEnter: function() { /* Overwrite to handle event. */ },
	onMouseLeave: function() { /* Overwrite to handle event. */ }
});

jsmen.MenuItem = Class.create(jsmen.Pointable, {
	initialize: function($super, menu, element, opts) {
		$super(element, opts);
		this.menu = menu;
		this.initialised = false;

		if (this.initiallyExpanded = this.isExpanded()) {
			this.initSubMenu();
		}
	},
	
	initSubMenu: function() {
		if (!this.initialised) {
			var el = this.element.down('.sub-menu');
			if (el) { this.subMenu = this.createSubMenu(el, this.opts); }
			this.initialised = true;
		}
	},
	
	hasPopupSubMenu: function() {
		return this.subMenu? this.subMenu.isPopup(): false;
	},
	
	createSubMenu: function(el, opts) {
		if (el.hasClassName('sub-menu-type-popup')) {
			return new jsmen.PopupMenu(this, el, opts);
		} else {
			return new jsmen.DefaultSubMenu(this, el, opts);
		}
	},
	
	isCollapsed: function() {
		return this.element.hasClassName(this.opts.cssClassCollapsed);
	},
	
	isExpanded: function() {
		return this.element.hasClassName(this.opts.cssClassExpanded);
	},
	
	isExpanding: function() {
		return this.expandingTimeout != null;
	},
	
	isExpandedOrExpanding: function() {
		return this.isExpanded() || this.isExpanding();
	},
	
	isSelected: function() {
		return this.selectedItem;
	},
	
	setCollapsed: function() {
		if (!this.isCollapsed() && !this.hasPointedDescendant()) {
			this.element.removeClassName(this.opts.cssClassExpanded);
			this.element.addClassName(this.opts.cssClassCollapsed);
			if (this.subMenu) this.subMenu.hide();
			this.menu.onItemCollapsed(this);
		}
	},
	
	setExpanded: function() {
		if (!this.isExpanded()) {
			this.element.removeClassName(this.opts.cssClassCollapsed);
			this.element.addClassName(this.opts.cssClassExpanded);
			if (this.subMenu) this.subMenu.show();
			this.menu.onItemExpanded(this);
		}
	},
	
	setInitialState: function() {
		if (this.initiallyExpanded) {
			if (this.subMenu && !this.subMenu.isPopup()) this.subMenu.setInitialState();
			this.setExpanded();
		} else {
			this.setCollapsed();
		}
	},

	collapse: function() {
		if (this.expandingTimeout) {
			window.clearTimeout(this.expandingTimeout);
			this.expandingTimeout = null;
		}
		
		var self = this;
		this.collapsingTimeout = window.setTimeout(function() {
			self.collapsingTimeout = null;
			self.setCollapsed();
		}, this.opts.delay);
	},

	expand: function() {
		this.initSubMenu();
		
		if (this.collapsingTimeout) {
			window.clearTimeout(this.collapsingTimeout);
			this.collapsingTimeout = null;
		}
		
		var self = this;
		this.expandingTimeout = window.setTimeout(function() {
			self.expandingTimeout = null;
			self.setExpanded();
		}, this.opts.delay);
	},

	onMouseEnter: function() {
		this.element.toggleClassName(this.opts.cssClassPointed);
		this.expand();
	},

	onMouseLeave: function() {
		this.element.toggleClassName(this.opts.cssClassPointed);
		this.collapse();
	},
	
	hasPointedDescendant: function() {
		var pointedDescendant = this.isPointed();
		if (!pointedDescendant &&
			this.subMenu &&
			this.subMenu.hasPointedDescendant()) {
			
			pointedDescendant = true;
		}
		return pointedDescendant;
	}
});
	
jsmen.collectItems = function(menu, el, items, opts) {
	el.childElements().each(function(e) {
		if (e.hasClassName(opts.cssClassMenuItem)) {
			items.push(new jsmen.MenuItem(menu, e, opts));
		} else {
			jsmen.collectItems(menu, e, items, opts);
		}
	});
}

jsmen.Menu = Class.create(jsmen.Pointable, {
	initialize: function($super, element, opts) {
		this.initializing = true;
		$super(element, opts);
		this.items = new Array();
		if(this.element != null) {
			jsmen.collectItems(this, this.element, this.items, this.opts);
		}
		this.initializing = false;
	},
	
	hasExpandedItem: function() {
		var expanded = false;
		this.items.each(function(item) { if (item.isExpanded()) expanded = true; });
		return expanded;
	},
	
	hasExpandedOrExpandingItem: function() {
		var expandedOrExpanding = false;
		this.items.each(function(item) { if (item.isExpandedOrExpanding()) expandedOrExpanding = true; });
		return expandedOrExpanding;
	},
	
	isSelected: function() {
		var selected = false;
		this.items.each(function(item) {if (item.isSelected()) selected = true; });
		return selected;
	},
	
	onItemExpanded: function(item) {
		this.collapseOthers(item);
	},
	
	collapseOthers: function(item) {
		this.items.each(function(i) {
			if (i !== item) i.setCollapsed();
		});
	},

	onItemCollapsed: function(item) {
		if (!this.initializing && !this.hasExpandedOrExpandingItem()) {
			this.setInitialState();
		}
	},
	
	setInitialState: function() {
		this.items.each(function(item) { item.setInitialState(); });
	},
	
	hasPointedDescendant: function() {
		var pointedDescendant = this.isPointed();
		if (!pointedDescendant) {
			this.items.each(function(item) { 
				if (item.hasPointedDescendant()) pointedDescendant = true;
			});
		}
		return pointedDescendant;
	},
	
	isPopup: function() { return false; }
});

jsmen.SubMenu = Class.create(jsmen.Menu, {
	initialize: function($super, menuItem, element, opts) {
		$super(element, opts);
		this.menuItem = menuItem;
	},

	onMouseEnter: function() {
		this.menuItem.expand()
	},

	onMouseLeave: function() {
		this.menuItem.collapse();
	},
	
	onItemExpanded: function(item) {
		if (this.menuItem) this.menuItem.setExpanded();
		this.collapseOthers(item);
	},

	onItemCollapsed: function(item) {
		if (this.menuItem) this.menuItem.setCollapsed();
	},
	
	show: function() { /* Overwrite in sub class */ },
	
	hide: function() { /* Overwrite in sub class */ },
	
	setAllItemsCollapsed: function() {
		this.items.each(function(item) { item.setCollapsed(); });
	}
});

jsmen.DefaultSubMenu = Class.create(jsmen.SubMenu, {
	initialize: function($super, menuItem, element, opts) {
		$super(menuItem, element, opts);
	},

	show: function() {
		this.element.show();
	},
	
	hide: function() {
		this.setAllItemsCollapsed();
		this.element.hide();
	}
});

jsmen.PopupMenu = Class.create(jsmen.SubMenu, {
	initialize: function($super, menuItem, element, opts) {
		$super(menuItem, element, opts);
		
		this.element.remove();
	},

	show: function() {
		var ofs = this.menuItem.element.cumulativeOffset();
		var dim = this.menuItem.element.getDimensions();
		
		var style;
		if (this.element.hasClassName(this.opts.cssClassPopupAlignTop)) {
		 	style = $H({
				position: 'absolute',
				top: ofs.top + 'px',
				left: ofs.left + dim.width + 'px'
			});
		} else {
		 	style = $H({
				position: 'absolute',
				top: ofs.top + dim.height + 'px',
				left: ofs.left + 'px'
			});
		}
		
		if (!this.wrapper) {
			this.wrapper = new Element('div');
			this.wrapper.insert(this.element);
			Element.insert(window.document.body, this.wrapper);
		} else {
			this.wrapper.show();
		}
		
		this.wrapper.setStyle(style.toObject());
		
		if (this.opts.debug) {
			jsmen.log("Showing popup menu with style " + style.inspect());
		}
	},
	
	hide: function() {
		this.setAllItemsCollapsed();
		if (this.wrapper) this.wrapper.hide();
	},

	isPopup: function() { return true; }
});