e.stopPropagation();
  • if this is a mouse click, we do not want to interrupt normal navigation if there is no panel it should act like a regular link, always.

            if (this.$panel.length === 0 || e.pointerType === 'mouse') {
                return;
            }
  • determine if this event has bubbled

            if (this.$panel.length > 0) {
                var $closestPanel = $(e.target).closest('.menu-panel');
                if ($closestPanel.length > 0 && $closestPanel[0] === this.$panel[0]) {
  • This event bubbled from a child panel’s link (a leaf menu item). It doesn’t have a Panel object of its own, so it should act like a regular link.

                    return;
                }
            }
  • At this point we know this is a touch event, and we want to toggle the panel.

  • If the menu is closed, or if it’s open and we want a click to hide the menu and not navigate, then prevent navigation

            if (!this.options.navigateOnHoverPress || !this.isOpen)
            {
                e.preventDefault();
                e.preventClick();
            }
            
            if (this.transitioning) {
                return;
            }
    
            if (this.isTopLevel) {
                this.menu.clickHoverActivated = true;
            }
    
            this[this.isOpen ? 'hide' : 'show'](e);
    
            if (this.isTopLevel) {
                this.menu.ignoreDocumentClick = +(new Date());
            }
        };
  • force this panel to hide itself

        Panel.prototype.forceHide = function (event) {
            this.menu.clickHoverActivated = false;
            this.hide(event);
        };
    
        /**
         * Creates a menu "group" from a jQuery collection of top-level
         * menu items. This will be called once for each element in the
         * top level jQuery object's collection.
         *
         * Menus are actually the "root" panel, and contain an array of sub panels.
         * Each Panel contains the actual menu items (links).
         */
        var Menu = function ($topLevelItems, options) {
            var me = this;
    
            this.options = options;
  • Assign a CSS class to help distinguish between top-level and sub menus.

            this.$topLevelItems = $topLevelItems.addClass('menu-item-top');
  • array of all Panel instances within this menu

            this.panels = [];
  • array of other jQuery elements adjacent to this menu (i.e. on the same menu-bar)

            this.siblings = this.options.siblings.map(function (i, sibling) {
                return $(sibling).data('dropDownMenu');
            });
  • The menu panel hierarchy root element

            this.rootMenu = {
                parent: null,
                children: []
            };
  • When a top menu button is clicked, the menu begins to react to hover events (mimics Windows/MacOS menus).

            this.clickHoverActivated = false;
  • Used to signal the document click handler that the click came from the menu, so it should be ignored.

            this.ignoreDocumentClick = false;
  • Create a Panel instance for each menu panel, store in an array

            this.$topLevelItems.find('.menu-panel').each(function (i, panelElement) {
                me.panels.push(new Panel(me, panelElement));
            });
  • Top level items without a submenu need a Panel instance as well, to interact with other top level items.

            this.$topLevelItems.not('.menu-item-with-submenu').each(function (i, panelElement) {
                me.panels.push(new Panel(me, null, panelElement));
            });
  • Build a tree representing the parent/child relationships in the menu

            $.each(this.panels, function (i, panel) {
                panel.resolveParent();
            });
  • Set up rollovers on the menu items. Note: This does not include the top level items with submenus, which have different rules for rollovers. jQuery.find() only includes decendants, no the current set, which is the top level menu items.

    we use hoverDelay because it internally filters out touch-based pointer events

            this.$topLevelItems.find('.menu-item').hoverDelay(
                function () {
                    highlightMenuItem($(this), true);
                },
                function () {
                    highlightMenuItem($(this), false);
                }
            )
            .each(function () {
                highlightMenuItem($(this), false);
            });
  • Bind a handler to close the menu if it is clicked off.

            $(document).on('click', $.proxy(this.onDocumentClick, this));
        };
  • Hides all menus and submenus

        Menu.prototype.hideAllPanels = function (event) {
            this.ignoreDocumentClick = +(new Date());
    
            $.each(this.rootMenu.children, function (i, child) {
                child.forceHide(event);
            });
        };
  • On any document click outside of a menu, we close all other menus on the page. This is the click handler attached to the document click event to do so.

        Menu.prototype.onDocumentClick = function documentClickHandler(e) {
  • Check a flag which indicates the click is from the menu itself

            if (this.ignoreDocumentClick) {
  • store the time difference, we only listen to this prevention if it’s within 200ms of the original event to prevent errors

                var timeDiff = +(new Date()) - this.ignoreDocumentClick;
    
                this.ignoreDocumentClick = false;
    
                if (timeDiff < 200) {
                    return;
                }
            }
    
            this.hideAllPanels(e);
        };
  • Dropdown menu plugin. The jQuery element collection should include top-level container elements which in turn contain items with class “menu-item”. Each item in the jQuery collection becomes a menu “group”.

        $.fn.dropDownMenu = function (options) {
            if (this.length === 0) {
                return this;
            }
  • Map skin from the registered skin collection

            var _skin = _skins[options.skin];
            if (!_skin) {
                throw new Error('Invalid dropDownMenu skin: ' + options.skin);
            }
  • Merge explicit options with skin and defaults

            var o = $.extend({}, _defaults, _skin || {}, options);
  • Resolve conflicting options

            if (!o.showOnHover) {
                o.linksWithSubmenusEnabled = false;
            }
  • save the jquery selection to get siblings

            var selection = $(this);
  • Loop through each menu container and create a menu ‘group’.

            this.each(function () {
  • Find all the top-level menu items within the container.

                var $topLevelItems = findUntil($(this), function (elem, results) {
                    if ($(elem).hasClass('menu-item')) {
                        results.push(elem);
                        return false;
                    }
    
                    return true;
                });
    
                var menu = new Menu(
                    $topLevelItems,
                    $.extend({}, o, {
                        siblings: selection.not(this)
                    })
                );
    
                $(this).data('dropDownMenu', menu);
                _menus.push(menu);
            });
  • Allow the jQuery chain to remain unbroken.

            return this;
        };
    
        var _menus = [];
  • Hide all other menus

        function hideOtherMenus(ignoreMenu, e) {
            $.each(_menus, function (i, menu) {
                if (!ignoreMenu || ignoreMenu !== menu) {
                    menu.hideAllPanels(e);
                }
            });
        }
    })(jQuery);