﻿/*

fn.icattMenu


Hoe werkt het menu
De structuur is flexibel maar met default settings moet deze zijn:

<ul>
<li><a>m1</a></li>
<li><a>m2</a>
<ul>
<li><a>m2.1</a></li>
<li><a>m2.2</a></li>
<li><a>m3</a></li>
</ul>
</li>
<li><a>m3</a></li>
</ul>
    
De uitgangsstijl voor elk element wordt in de .css geregeld. Met welke classes of hoe dat gebeurt is niet van belang voor dit script

Via settings wordt bepaald op welke elmenenten de mousenter en mouseleave triggers worden geplaatst. 

On mouseenter worden menu elementen die niet open zijn geopend. Andere open elementen sluiten eerst.
On mouseenter worden menu elementen die niet uitgelicht zijn uitgelicht. Andere uitgelichte elementen worden eerst gedimd.

//TODO: optimalisatie door lijst van open en uitgelichte elementen bij te houden en alleen die weer uit te zetten
    
De open en sluit stijlen kunnen voor elk level van het menu worden ingesteld.
De highlighted en dimmed stijlen kunnen ook voor elk level in het menu worden ingesteld.
    
On mouseenter en mouseleave wordt met een instelbare vertraging de closed en dimmed stijlen voor dat level toegepast.
    
Bij elke mouseenter en mouseleave worden alle 'hangende' acties weer gecancelled, die zijn inmiddels verlopen. 
Snel heen en weer bewegen geeft dus geen flakkerend menu.
    
Het menu kent een openStyle en een closedStyle voor elk level in het menu.
Het menu kent een hiStyle en een loStyle voor elk level in het menu.

Het menu houdt bij op welke elementen style attributen worden gedefinieerd. 
Deze style attributen worden allemaal verwijderd bij het verlaten van het menu zodat het altijd
weer in de oorspronkelijke staat terugkeert
    
    
*/

(function($) {

    $.fn.icattMenu = function(options) {

        //debugger

		var SKINROOT_TOKEN = "[SkinRoot]",
			APPROOT_TOKEN = "[AppRoot]";
			
        var $menu = this;

        var menuElement = $menu.get(0);

        // variable to keep track of all elements for which a style attribute is set by the menu engine
        // because on leaving the menu, all style attributes must be cleared...
        menuElement.$styledElements = $([])

        // if selector has no or more the one element to act upon... ignore.
        if ($menu.length != 1) return;

        if (!$.fn.icattMenu.menuCount) $.fn.icattMenu.menuCount = 0;

        $.fn.icattMenu.menuCount++

        menuElement.menuId = $.fn.icattMenu.menuCount
        
        $.fn.icattMenu.defaults = {
            activeClass: 'm_active'
            , applicationRoot: "/"                      // Absolute virtual path for the application. Used to substitute [AppRoot] token in styles containing urls
            , skinRoot: "/"								// Absolute virtual path for the Skin. Used to substitute [SkinRoot] token in styles containing urls
            , leaveDelay: 1000                          // milliseconds delay before mouseleave is executed
            , enterDelay: 100                           // milliseconds delay before mousenter is executed
            , triggerSelector: 'li > a'                 //jQuery selector used in $menu.find(selector) to select the elements that trigger the mouseenter and mouseleave events
            , getItems: function($triggerElements) {    //function that selects the menuitem elements relative to the trigger element
                return $triggerElements.closest("li")
            }
            // Each element of the styles array correspons to a menu level
            //
            // For each level an array of style rules is defined
            //
            // A rules array is inherited by the levels below, so holes in the array are allowed
            // A hole is defined by a null element.
            // Only the top level is required...
            //
            // An empty object, defened as {} cleares all inline  styles on the element
            // A style rule object has a selector property that contains a jQuery selector expression
            // This selector is used to find the elements to apply the styles to, relative to the menuitem element.
            // The selector may eighter be:
            //      A function taking the $menuItem as a parameter and returning a jQuery collection or,
            //      any jQuery selector or,
            //      null.
            //
            // An empty selector expression (== null) selects the item element itself.
            // 
            // example selector function: function($item){return $item.closest("a")}
            //
            //
            //
            , styles: [
            //Start of array with styleDefenitions
                 [ 
                    {
                        selector: function($this) {
                            return $this.children("a")
                        }
                        , hiStyle: { background: '#66CC00', color: '#000000' }
                        , loStyle: { background: 'none', color: '#000000' }
                        , openStyle: null
                        , closedStyle: null
                    } // end style rule
                   ,{
                       selector: function($this) {
                           return $this.find(" > ul, > ul > li") //child menu
                       }
                        , hiStyle: null
                        , loStyle: null
                        , openStyle: { display: 'block' }
                        , closedStyle: { display: 'block' }
                    } // end style rule
                 ]
                /////////////////////////                 
                // end level 1 rule array
                /////////////////////////
                , [ 
                    {
                        selector: function($this) {
                            return $this.children("a")
                        }
                        , hiStyle: { background: 'none', color: '#000000' }
                        , loStyle: { background: 'none', color: '#000000' }
                        , openStyle: null
                        , closedStyle: null
                    } // end style rule
                   ,{ // rule for ul element
                       selector: function($this) {
                           return $this.find(" > ul, > ul > li, > ul > li > a") //child menu
                       }
                        , hiStyle: null
                        , loStyle: null
                        , openStyle: { display: 'block' }
                        , closedStyle: { display: 'block', background: 'none' } //background 'none' noodzakelijk omdat deze ook bij display none toch nog getoond wordt in IE!!
                    } // end style rule
                 ]
                /////////////////////////                 
                // end level 2 rule array
                /////////////////////////
                , [ //Start LEVEL 3
                    {
                        selector: null //null selector selects $this == itemElement
                        , hiStyle: { background: 'url('+SKINROOT_TOKEN+'images/m_tabright02.gif) top right no-repeat' }
                        , loStyle: { background: 'url('+SKINROOT_TOKEN+'images/m_tabright01.gif) top right no-repeat', color: '#000000' }
                        , openStyle: null
                        , closedStyle: null
                    } // end style rule
                   , {
                       selector: function($this) {
                           return $this.children("a")
                       }
                        , loStyle: { background: 'url('+SKINROOT_TOKEN+'images/m_tableft01.gif) top left no-repeat', color: '#000000' }
                        , hiStyle: { background: 'url('+SKINROOT_TOKEN+'images/m_tableft02.gif) top left no-repeat', color: '#000000' }
                        , openStyle: null
                        , closedStyle: null
                    } // end style rule
                 ]
                /////////////////////////                 
                // end level 3 rule array
                /////////////////////////                 
                , null //example empty rule array                

            ] // end styles array

           
            //possible future settings:
            //highlight parents = 0|1
            //highlight selected = 0|1
            //open selected = 0|1

        }

        // deal with settings
        var settings = $.fn.extend($.fn.icattMenu.defaults, options)
        //debugger
        // replace tokens in style
        
        SKINROOT_TOKEN = SKINROOT_TOKEN.replace("\[","\\[").replace("\]","\\]")
        APPROOT_TOKEN = APPROOT_TOKEN.replace("\[","\\[").replace("\]","\\]")
        var skinExp = new RegExp(SKINROOT_TOKEN,"ig")
        var appExp = new RegExp(APPROOT_TOKEN,"ig")
        
       
        for(var i=0,j=settings.styles.length;i<j;i++){
            var levelRules = settings.styles[i]
            if (levelRules){            
                for (k=0,l=levelRules.length;k<l;k++){
                    var rule = levelRules[k]
                    _replaceTokenInRuleStyles(rule,skinExp,settings.skinRoot)
                    _replaceTokenInRuleStyles(rule,appExp,settings.appRoot)
                }
            }
        }
        
        /////////////////////////////////////////////////////////////
        function _replaceTokenInRuleStyles(rule,tokenRegExp,tokenValue){
        //debugger
            if (rule){
                _replaceTokenInStyleObject(rule.hiStyle,tokenRegExp,tokenValue)
                _replaceTokenInStyleObject(rule.loStyle,tokenRegExp,tokenValue)
                _replaceTokenInStyleObject(rule.openStyle,tokenRegExp,tokenValue)
                _replaceTokenInStyleObject(rule.closeStyle,tokenRegExp,tokenValue)
            }
        }

        function _replaceTokenInStyleObject(style,tokenRegExp,tokenValue){
            if (style) {
                for (var name in style){
                    var styleValue = style[name]
                    style[name] = styleValue ? styleValue.replace(tokenRegExp,tokenValue):null
                }
            }
        }

        /////////////////////////////////////////////////////////////
        function _applyStyleRules($coll, action) {

            //TODO apply style bottom up: lowest element in $coll first.

            $coll.each(function() {

                //get the level of the li element.
                var level = this.itemLevel;
                var $item = $(this);

                //get the style rules for the level
                if (!this.styleRules)
                    this.styleRules = _getStyleDefenitions(settings.styles, level)

                var styleRules = this.styleRules;

                var $this = $(this);

                for (var i = 0, j = styleRules.length; i < j; i++) {
                    var rule = styleRules[i];

                    if (action === "lo" && rule.loStyle)
                        _applyStyle($this, rule.selector, rule.loStyle);
                    if (action === "hi" && rule.hiStyle)
                        _applyStyle($this, rule.selector, rule.hiStyle);
                    if (action === "open" && rule.openStyle)
                        _applyStyle($this, rule.selector, rule.openStyle);
                    if (action === "close" && rule.closedStyle)
                        _applyStyle($this, rule.selector, rule.closedStyle);
                }


            }) // end coll.each function

            return $coll;
        }; // end function _applyStyleRules

        /////////////////////////////////////////////////////////////
        function _applyStyle($this, selector, style) {

            var $item

            if (style) {
                if (jQuery.isFunction(selector))
                    $item = selector($this);
                else if (selector === null)
                    $item = $this;
                else
                    $item = $this.find(selector);

                $item.css(style);
                menuElement.$styledElements = menuElement.$styledElements.add($item)
                
            }

            return $this;

        } // end function _applyStyle

        /////////////////////////////////////////////////////////////
        function _getClassByLevel(ar, level) {

            //if not specified, take the lowest level above
            if (!ar.length)
                return ar;

            var index = (level > (ar.length)) ? ar.length - 1 : level - 1

            //zero level must always be defined
            while (!ar[index] && index >= 0) { index-- }

            return ar[index]

        }

        /////////////////////////////////////////////////////////////
        function _getStyleDefenitions(ar, level) {
            var index = (level > (ar.length - 1)) ? ar.length - 1 : level - 1;

            while (!ar[index] && index > 0) {
                index--;
            }
            return ar[index];
        }

        /////////////////////////////////////////////////////////////
        function _getStyleMap(ar, level) {
            //            //if not specified, take the lowest level above
            //            level = (level > (ar.length-1)) ? ar.length-1 , level
            //            
            //            //zero level must always be defined
            //            while (!ar[level] && level > 0) {level--}
            //            
            //            return ar[level]
            //            
        }

        /////////////////////////////////////////////////////////////
        function _getItemLevel(el) {
            // als menu element bovenste UL is, dan...
            var level = menuElement.tagName == "UL" ? 1 : 0

            while (el.parentNode && el !== menuElement) {

                if (el.tagName && el.tagName == "UL") level++;

                el = el.parentNode;

            }

            return level;
        }

        /////////////////////////////////////////////////////////////
        /////////////////////////////////////////////////////////////


        //If menu is already defined, break it off...
        if (menuElement.$triggerElements) return $menu;

        //var menuIndex = $.fn.icattMenu.registry.push(this) - 1;
        menuElement.$triggerElements = $menu.find(settings.triggerSelector);

        //        //closure to hide stuff
        //        (function(){
        //            //get the corresponding item elements...
        //            debugger
        //            var itemElements = [];
        //            menuElement.$triggerElements.each(function(){
        //                itemElements.push( $(this).closest("li").get(0) );
        //            })
        //            menuElement.$itemElements = menuElement.$triggerElements.closest("li")
        //                    
        //        })();

        //debugger
        menuElement.$itemElements = settings.getItems(menuElement.$triggerElements)

        //menuElement.$openItems = $menu.find("li.m_active").not("li.m_selected")

        //menuElement.$selectedItem = $menu.find("li.m_selected")

        //menuElement.$hiItems = menuElement.$openItems.add(menuElement.$selectedItem).children("a")

        menuElement.$itemElements.each(function() {
            //Set $menu, level data on the element.
            //Persist reference to menu on each active element...
            this.$menu = $menu;

            //menuLevel
            this.itemLevel = _getItemLevel(this, $menu);
        })

        //Bind mouseenter and mouseleave events of the LI elements.
        menuElement.$triggerElements
			.mouseenter(function(event) {
			    //debugger

			    //Stop the event from bubbling UP to its parents
			    event.stopPropagation();

			    //cancel all scheduled events for the current menu...
			    $.fn.icattScheduler.cancel(menuElement.menuId);

			    var $itemElement = this.$itemElement
			    
			    //scheduel the following actions:
			    var fAction = function() {

			        //create a jQuery object for the element raising the event.
			       

			        //first close and dim all active elements that are not a parent
			        var $coll = menuElement.$itemElements.not($itemElement.parents().andSelf());


			        _applyStyleRules($coll, 'lo');
			        _applyStyleRules($coll, 'close');

			        //then activate this: open en highlight
			        _applyStyleRules($itemElement, 'open');
			        _applyStyleRules($itemElement, 'hi');


			    }
			    
			    if (settings.enterDelay > 0 ) 
    			    $.fn.icattScheduler.schedule(fAction, settings.enterDelay, menuElement.menuId);
    			else
    			    fAction();


			})
			.mouseleave(function(event) {
			    //debugger

			    //scheduel the following actions:
			$.fn.icattScheduler.schedule(function() {

			        menuElement.$styledElements.removeAttr("style")
			        menuElement.$styledElements = $([])

			    }, settings.leaveDelay,menuElement.menuId);
			}

		).each(function() {
		    // In this context, 'this' refers to the DOM element in the jQuery result array
		    var triggerElement = this;

		    //Persist reference to menu on each active element...
		    this.$menu = $menu;

		    //menuLevel
		    this.itemLevel = _getItemLevel(this, $menu);

		    //$itemElement
		    this.$itemElement = $(this).closest("li");

		});

        //always return the original jQuery object.
        return this;

    };


})(jQuery);

