All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.resources.primefaces.menu.menu.plainmenu.js Maven / Gradle / Ivy

There is a newer version: 14.0.7
Show newest version

/**
 * __PrimeFaces PlainMenu Widget__
 * 
 * Menu is a navigation component with sub menus and menu items.
 * 
 * @prop {JQuery} menuitemLinks DOM elements with the links of each menu item.
 * @prop {string} stateKey Name of the HTML5 Local Store that is used to store the state of this plain menu (expanded / collapsed
 * menu items).
 * @prop {string[]} collapsedIds A list with the ID of each menu item (with children) that is collapsed.
 * 
 * @interface {PrimeFaces.widget.PlainMenuCfg} cfg The configuration for the {@link  PlainMenu| PlainMenu widget}.
 * You can access this configuration via {@link PrimeFaces.widget.BaseWidget.cfg|BaseWidget.cfg}. Please note that this
 * configuration is usually meant to be read-only and should not be modified.
 * @extends {PrimeFaces.widget.MenuCfg} cfg
 * 
 * @prop {boolean} cfg.toggleable `true` if grouped items can be toggled (expanded / collapsed), or `false` otherwise.
 * @prop {boolean} cfg.statefulGlobal When enabled, menu state is saved globally across pages. If disabled then state 
 * is stored per view/page.
 */
PrimeFaces.widget.PlainMenu = PrimeFaces.widget.Menu.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.menuitemLinks = this.jq.find('.ui-menuitem-link:not(.ui-state-disabled)');

        this.bindEvents();
        this.bindToggleable();
        this.bindOverlay();
    },

    /**
     * Binds the necessary events for the menu if it is toggleable. This includes setting up the state management
     * by initializing the storage key and restoring the state from storage.
     * @private
     */
    bindToggleable: function() {
        if (!this.cfg.toggleable) return;

        this.cfg.statefulGlobal = Boolean(this.cfg.statefulGlobal);
        this.collapsedIds = [];
        this.createStorageKey();
        this.restoreState();

        var $this = this;
        var headers = this.jq.find('> .ui-menu-list > .ui-widget-header');
        
        headers.on('mouseover.menu', function() {
            $(this).addClass('ui-state-hover');
        }).on('mouseout.menu', function() {
            $(this).removeClass('ui-state-hover');
        }).on('click.menu', function(e) {
            var header = $(this);
            var icon = header.find('> h3 > .ui-icon');

            if (icon.hasClass('ui-icon-triangle-1-s')) {
                $this.collapseSubmenu(header, true);
            } else {
                $this.expandSubmenu(header, true);
            }

            PrimeFaces.clearSelection();
            e.preventDefault();
        });
    },

    /**
     * Binds overlay-specific event handlers if the overlay configuration is enabled.
     * This includes hiding the menu on certain key presses or clicks, and managing focus.
     * @private
     */
    bindOverlay: function() {
        var $this = this;

        if (this.cfg.overlay) {
            // Hide menu when any menu item link is clicked
            this.menuitemLinks.on("click", function() {
                $this.hide();
            });

            // Handle keyboard navigation for overlay
            this.trigger.on('keydown.ui-menu', function(e) {
                switch (e.key) {
                    case 'ArrowDown':
                        if (!$this.jq.is(':visible')) {
                            $this.show();
                        }
                        e.preventDefault();
                        break;

                    case 'Tab':
                        if ($this.jq.is(':visible')) {
                            $this.hide();
                        }
                        break;
                }
            });
        } else {
            // Handle focus events for the menu
            this.jq.off('focusout.menu focusin.menu').on({
                "focusout.menu": function(e) {
                    if (!e.relatedTarget || !$this.jq.has(e.relatedTarget).length) {
                        $this.resetFocusState();
                    }
                },
                "focusin.menu": function(e) {
                    if (e.relatedTarget && !$this.jq.has(e.relatedTarget).length) {
                        $this.focus($this.menuitemLinks.filter(':not([disabled])').first(), e);
                    }
                }
            });
        }
    },
 
    /**
     * Binds event handlers to menu item links for interaction via mouse and keyboard.
     * This includes setting the initial focus, handling mouse enter and leave, click events,
     * and keyboard navigation using arrow keys, space, and enter.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        // Set the first focusable menu item
        this.resetFocusState();

        // Bind mouse and click events to focus items
        this.menuitemLinks.on("mouseenter.menu click.menu", function(e) {
            $this.focus($(this), e);
        }).on("mouseleave.menu", function(e) {
            $this.unfocus($(this), e);
        });

        // Bind keyboard navigation events
        this.menuitemLinks.on('keydown.menu', function(e) {
            var currentLink = $this.menuitemLinks.filter('.ui-state-active:first');
            switch (e.code) {
                case 'Home':
                case 'PageUp':
                    $this.navigateMenu(e, currentLink, 'prev', 'last');
                    e.preventDefault();
                    break;
                case 'End':
                case 'PageDown':
                    $this.navigateMenu(e, currentLink, 'next', 'last');
                    e.preventDefault();
                    break;
                case 'ArrowUp':
                    $this.navigateMenu(e, currentLink, 'prev', 'first');
                    e.preventDefault();
                    break;
                case 'ArrowDown':
                    $this.navigateMenu(e, currentLink, 'next', 'first');
                    e.preventDefault();
                    break;
                case 'Space':
                case 'Enter':
                case 'NumpadEnter':
                    currentLink.trigger('click');
                    PrimeFaces.utils.openLink(e, currentLink);
                    break;
                case 'Escape':
                    $this.hide();
                    if ($this.cfg.overlay) {
                        $this.trigger.trigger('focus');
                    }
                    break;
            }
        });
    },

    /**
     * Resets the focus state of the menu.
     * This method sets the first focusable menu item and removes hover and active states for non-overlay menus.
     * @private
     */
    resetFocusState: function() {
        // Set the first focusable menu item
        this.resetFocus(true);

        // plain menu does not have hover and active states
        if (!this.cfg.overlay) {
            this.menuitemLinks.removeClass('ui-state-hover ui-state-active');
        }
    },

    /**
     * Navigates the menu items in the specified direction ('prev' or 'next').
     * @param {JQuery.TriggeredEvent} event - The event that triggered the focus.
     * @param {JQuery} currentLink The currently focused menu item link.
     * @param {string} direction The direction to navigate ('prev' or 'next').
     * @param {string} firstOrLast The first or last item to navigate to ('first' or 'last').
     * @private
     */
    navigateMenu: function(event, currentLink, direction, firstOrLast) {
        var targetItem = currentLink.parent()[direction + 'All']('.ui-menuitem:not(:has(.ui-state-disabled)):' + firstOrLast);
        if (targetItem.length) {
            this.unfocus(currentLink, event);
            this.focus(targetItem.children('.ui-menuitem-link'), event);
        }
    },

    /**
     * Create the key where the state for this component is stored.  By default it is stored per view. Override this 
     * method to change the behavior to be global.
     */
    createStorageKey: function() {
        this.stateKey = PrimeFaces.createStorageKey(this.id, 'PlainMenu', this.cfg.statefulGlobal);
    },

    /**
     * Collapses the given sub menu so that the children of that sub menu are not visible anymore.
     * @param {JQuery} header Menu item with children to collapse.
     * @param {boolean} [stateful] `true` if the new state of this menu (which items are collapsed and expanded) should
     * be saved (in an HTML5 Local Store), `false` otherwise. 
     */
    collapseSubmenu: function(header, stateful) {
        var items = header.nextUntil('li.ui-widget-header');

        header.attr('aria-expanded', false)
            .find('> h3 > .ui-icon').removeClass('ui-icon-triangle-1-s').addClass('ui-icon-triangle-1-e');

        items.filter('.ui-submenu-child').hide();

        if (stateful) {
            this.collapsedIds.push(header.attr('id'));
            this.saveState();
        }
    },

    /**
     * Expands the given sub menu so that the children of that sub menu become visible.
     * @param {JQuery} header Menu item with children to expand.
     * @param {boolean} [stateful] `true` if the new state of this menu (which items are collapsed and expanded) should
     * be saved (in an HTML5 Local Store), `false` otherwise. 
     */
    expandSubmenu: function(header, stateful) {
        var items = header.nextUntil('li.ui-widget-header');

        header.attr('aria-expanded', true)
            .find('> h3 > .ui-icon').removeClass('ui-icon-triangle-1-e').addClass('ui-icon-triangle-1-s');

        items.filter('.ui-submenu-child').show();

        if (stateful) {
            var id = header.attr('id');
            this.collapsedIds = $.grep(this.collapsedIds, function(value) {
                return (value !== id);
            });
            this.saveState();
        }
    },

    /**
     * Saves the current state (expanded / collapsed menu items) of this plain menu. Used to preserve the state during
     * AJAX updates as well as between page reloads. The state is stored in an HTML5 Local Store.
     * @private
     */
    saveState: function() {
        localStorage.setItem(this.stateKey, this.collapsedIds.join(','));
    },

    /**
     * Restores that state as stored by `saveState`. Usually called after an AJAX update and on page load.
     * @private
     */
    restoreState: function() {
        var collapsedIdsAsString = localStorage.getItem(this.stateKey);

        if (collapsedIdsAsString) {
            this.collapsedIds = collapsedIdsAsString.split(',');

            for (let collapsedId of this.collapsedIds) {
                if (collapsedId) {
                    this.collapseSubmenu($(PrimeFaces.escapeClientId(collapsedId).replace(/\|/g, "\\|")), false);
                }
            }
        }
    },

    /**
     * Clear the saved state (collapsed / expanded menu items) of this plain menu.
     * @private
     */
    clearState: function() {
        localStorage.removeItem(this.stateKey);
    }

});




© 2015 - 2024 Weber Informatics LLC | Privacy Policy