META-INF.resources.primefaces.menu.menu.tieredmenu.js Maven / Gradle / Ivy
/**
* __PrimeFaces TieredMenu Widget__
*
* TieredMenu is used to display nested submenus with overlays.
*
* @typedef {"hover" | "click"} PrimeFaces.widget.TieredMenu.ToggleEvent Allowed event types for toggling a tiered menu.
*
* @prop {boolean} [active] Whether the menu is currently active.
* @prop {JQuery | null} [activeitem] The active menu item, if any.
* @prop {JQuery | null} [lastFocusedItem] The last root menu that had focus, if any.
* @prop {boolean} [itemClick] Set to `true` an item was clicked and se to `false` when the user clicks
* outside the menu.
* @prop {JQuery} links DOM element with all links for the menu entries of this tiered menu.
* @prop {JQuery} rootLinks DOM element with all links for the root (top-level) menu entries of this tiered menu.
* @prop {number} [timeoutId] Timeout ID, used for the animation when the menu is shown.
* @prop {boolean} isRTL Whether the writing direction is set to right-to-left.
* @prop {boolean} isVertical Whether component is vertical orientation like TieredMenu.
* @prop {boolean} isHorizontal Whether component is horizontal orientation like MenuBar.
*
* @interface {PrimeFaces.widget.TieredMenuCfg} cfg The configuration for the {@link TieredMenu| TieredMenu 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.autoDisplay Defines whether the first level of submenus will be displayed on mouseover or not.
* When set to `false`, click event is required to display this tiered menu.
* @prop {number} cfg.showDelay Number of milliseconds before displaying menu. Default to 0 immediate.
* @prop {number} cfg.hideDelay Number of milliseconds before hiding menu, if 0 not hidden until document.click.
* @prop {PrimeFaces.widget.TieredMenu.ToggleEvent} cfg.toggleEvent Event to toggle the submenus.
*/
PrimeFaces.widget.TieredMenu = PrimeFaces.widget.Menu.extend({
/**
* @override
* @inheritdoc
* @param {PrimeFaces.PartialWidgetCfg} cfg
*/
init: function(cfg) {
this._super(cfg);
this.cfg.toggleEvent = this.cfg.toggleEvent || 'hover';
this.cfg.showDelay = this.cfg.showDelay || 0;
this.cfg.hideDelay = this.cfg.hideDelay || 0;
this.tabIndex = this.cfg.tabIndex || "0";
this.links = this.jq.find('a.ui-menuitem-link:not(.ui-state-disabled)');
this.rootLinks = this.jq.find('> ul.ui-menu-list > .ui-menuitem > .ui-menuitem-link');
this.isRTL = this.jq.hasClass('ui-menu-rtl');
this.isVertical = this.jq.find('ul.ui-menu-list').attr('aria-orientation') === 'vertical';
this.isHorizontal = !this.isVertical;
this.bindEvents();
},
/**
* Sets up all event listeners required by this widget.
* @protected
*/
bindEvents: function() {
this.bindItemEvents();
this.bindKeyEvents();
this.bindDocumentHandler();
this.bindFocusEvents();
},
/**
* Sets up all event listeners for the mouse events on the menu entries (`click` / `hover`).
* @protected
*/
bindItemEvents: function() {
if (this.cfg.toggleEvent === 'click' || PrimeFaces.env.isTouchable(this.cfg))
this.bindClickModeEvents();
else if (this.cfg.toggleEvent === 'hover')
this.bindHoverModeEvents();
},
/**
* Sets up all event listners required for focus interactions.
* @protected
*/
bindFocusEvents: function() {
var $this = this;
// make first focusable
var firstLink = this.links.filter(':not([disabled])').first();
firstLink.attr("tabindex", $this.tabIndex);
this.resetFocus(true);
firstLink.removeClass('ui-state-hover ui-state-active');
this.links.on("mouseenter.tieredFocus click.tieredFocus", function() {
var $link = $(this),
$menuitem = $link.parent();
$this.deactivate($menuitem);
$link.trigger('focus');
}).on("focusin.tieredFocus", function() {
var menuitem = $(this).parent();
$this.highlight(menuitem);
});
},
/**
* Sets up all event listeners when `toggleEvent` is set to `hover`.
* @protected
*/
bindHoverModeEvents: function() {
var $this = this;
this.links.on("mouseenter.tieredHover", function() {
var link = $(this),
menuitem = link.parent();
if ($this.cfg.autoDisplay || $this.active) {
$this.activate(menuitem);
}
else {
$this.highlight(menuitem);
}
}).on("mouseleave.tieredHover", function() {
// clear timeout of possible delayed show event if mouseleave is fired before showDelay was over
if (($this.cfg.autoDisplay || $this.active) && $this.cfg.showDelay > 0 && $this.timeoutId) {
clearTimeout($this.timeoutId);
}
});
this.rootLinks.on("click.tieredHover", function() {
var link = $(this),
menuitem = link.parent(),
submenu = menuitem.children('ul.ui-menu-child');
$this.itemClick = true;
if (submenu.length === 1) {
if (submenu.is(':visible')) {
$this.active = false;
$this.deactivate(menuitem);
}
else {
$this.active = true;
$this.highlight(menuitem);
$this.showSubmenu(menuitem, submenu);
}
}
});
this.links.filter('.ui-submenu-link').on("click", function(e) {
$this.itemClick = true;
e.preventDefault();
});
this.jq.on("mouseleave", function(e) {
$this.deactivateAndReset(e);
});
},
/**
* Sets up all event listeners when `toggleEvent` is set to `click`.
* @protected
*/
bindClickModeEvents: function() {
var $this = this;
this.links.filter('.ui-submenu-link').on('click.tieredClick', function(e) {
var link = $(this),
menuitem = link.parent(),
submenu = menuitem.children('ul.ui-menu-child');
$this.itemClick = true;
if (submenu.length) {
if (submenu.is(':visible')) {
$this.deactivate(menuitem);
}
else {
$this.showSubmenu(menuitem, submenu);
}
}
e.preventDefault();
}).on('mousedown.tieredClick', function(e) {
e.stopPropagation();
});
},
/**
* Sets up all event listners required for keyboard interactions.
* @protected
*/
bindKeyEvents: function() {
var $this = this;
this.links.on('keydown.tieredMenu', function(e) {
var link = $(this),
menuitem = link.parent(),
isRootLink = false;
// menubar is horizontal and is only one we care about if this is a root link
if ($this.isHorizontal) {
isRootLink = !menuitem.closest('ul').hasClass('ui-menu-child')
}
// Helper functionto navigate to a menu item
function navigateTo(item) {
if (item.length && item.children('a.ui-menuitem-link').length) {
$this.deactivate(menuitem);
$this.activate(item, false);
}
}
// Helper function to close the submenu if its open
function closeSubmenu() {
var submenu = $('.ui-submenu-link[aria-expanded="true"]').last().parent();
if (submenu.length === 1) {
$this.deactivate(menuitem);
$this.deactivate(submenu);
$this.activate(submenu, false);
}
}
// Helper function to open the submenu if its open
function openSubmenu() {
if (menuitem.hasClass('ui-menu-parent')) {
$this.activate(menuitem);
}
}
// Helper function close and hide the menu and refocus original trigger
function closeMenuPanel() {
if ($this.cfg.overlay) {
$this.reset();
$this.hide();
// re-focus original trigger if there is one
if ($this.trigger && $this.trigger.length > 0) {
$this.trigger.trigger('focus');
}
}
}
switch (e.code) {
case 'Home':
case 'PageUp':
navigateTo(menuitem.prevAll('.ui-menuitem:last'));
e.preventDefault();
break;
case 'End':
case 'PageDown':
navigateTo(menuitem.nextAll('.ui-menuitem:last'));
e.preventDefault();
break;
case 'ArrowUp':
var navigatedTo = menuitem.prevAll('.ui-menuitem:first');
if ((isRootLink || navigatedTo.length === 0) && !$this.isVertical) {
closeSubmenu();
}
else {
navigateTo(navigatedTo);
}
e.preventDefault();
break;
case 'ArrowDown':
if (isRootLink) {
openSubmenu();
}
else {
navigateTo(menuitem.nextAll('.ui-menuitem:first'));
}
e.preventDefault();
break;
case 'ArrowRight':
if (isRootLink) {
navigateTo(menuitem.nextAll('.ui-menuitem:first'));
}
else if ($this.isRTL) {
closeSubmenu();
}
else {
openSubmenu();
}
e.preventDefault();
break;
case 'ArrowLeft':
if (isRootLink) {
navigateTo(menuitem.prevAll('.ui-menuitem:first'));
}
else if ($this.isRTL) {
openSubmenu();
}
else {
closeSubmenu();
}
e.preventDefault();
break;
case 'Space':
case 'Enter':
case 'NumpadEnter':
if (menuitem.hasClass('ui-menu-parent')) {
$this.activate(menuitem);
} else {
link.trigger('click');
PrimeFaces.utils.openLink(e, link);
closeMenuPanel();
}
e.preventDefault();
break;
case 'Escape':
if ($this.cfg.overlay) {
closeMenuPanel();
} else {
closeSubmenu();
}
e.preventDefault();
break;
case 'Tab':
$this.reset();
break;
default:
break;
}
});
},
/**
* Registers a delegated event listener for a mouse click on a menu entry.
* @protected
*/
bindDocumentHandler: function() {
var $this = this,
clickNS = 'click.' + this.id;
$(document.body).off(clickNS).on(clickNS, function() {
if ($this.itemClick) {
$this.itemClick = false;
return;
}
$this.reset();
});
this.addDestroyListener(function() {
$(document.body).off(clickNS);
});
},
/**
* Deactivates a menu item so that it cannot be clicked and interacted with anymore.
* @param {JQuery} menuitem Menu item (`LI`) to deactivate.
* @param {boolean} [animate] `true` to animate the transition to the disabled state, `false` otherwise.
*/
deactivate: function(menuitem, animate) {
var $this = this;
this.activeitem = null;
menuitem.removeClass('ui-menuitem-active ui-menuitem-highlight');
var $link = menuitem.children('a.ui-menuitem-link');
this.unfocus($link);
var activeSibling = menuitem.siblings('.ui-menuitem-active');
if (activeSibling.length) {
activeSibling.find('li.ui-menuitem-active').each(function() {
$this.deactivate($(this));
});
$this.deactivate(activeSibling);
}
var submenu = menuitem.children('ul.ui-menu-child');
if (submenu.length > 0) {
$link.attr('aria-expanded', 'false');
if (animate)
submenu.fadeOut('fast');
else
submenu.hide();
}
},
/**
* Activates a menu item so that it can be clicked and interacted with.
*
* @param {JQuery} menuitem - The menu item to activate.
* @param {boolean} [showSubMenu=true] - If false, only focuses the menu item without showing the submenu.
*/
activate: function(menuitem, showSubMenu = true) {
this.highlight(menuitem);
// if this is a root menu item.
if (menuitem.parent().is('ul.ui-menu-list:not(.ui-menu-child)')) {
this.lastFocusedItem = menuitem;
}
// focus the menu item when activated
this.focus(menuitem.children('a.ui-menuitem-link'));
if (showSubMenu) {
var submenu = menuitem.children('ul.ui-menu-child');
if (submenu.length == 1) {
this.showSubmenu(menuitem, submenu);
}
}
},
/**
* Highlights the given menu item by applying the proper CSS classes and focusing the associated link.
*
* @param {JQuery} menuitem - The menu item to highlight.
*/
highlight: function(menuitem) {
this.activeitem = menuitem;
menuitem.addClass('ui-menuitem-active ui-menuitem-highlight');
menuitem.children('a.ui-menuitem-link').addClass('ui-state-hover');
},
/**
* Shows the given submenu of a menu item.
* @param {JQuery} menuitem A menu item (`LI`) with children.
* @param {JQuery} submenu A child of the menu item.
*/
showSubmenu: function(menuitem, submenu) {
var pos = {
my: this.isRTL ? 'right top' : 'left top',
at: this.isRTL ? 'left top' : 'right top',
of: menuitem,
collision: 'flipfit'
};
submenu.css('z-index', PrimeFaces.nextZindex())
.show()
.position(pos);
//avoid queuing multiple runs
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.timeoutId = PrimeFaces.queueTask(function() {
submenu.css('z-index', PrimeFaces.nextZindex())
.show()
.position(pos);
var $link = menuitem.children('a.ui-menuitem-link');
$link.attr('aria-expanded', 'true');
submenu.find('a.ui-menuitem-link:focusable:first').trigger('focus');
}, this.cfg.showDelay);
},
/**
* Deactivates all items and resets the state of this widget to its orignal state such that only the top-level menu
* items are shown.
*/
reset: function() {
var $this = this;
this.active = false;
this.jq.find('li.ui-menuitem-active').each(function() {
$this.deactivate($(this), true);
});
this.resetFocus(!this.lastFocusedItem);
if (this.lastFocusedItem) {
this.lastFocusedItem.children('a.ui-menuitem-link').attr('tabindex', $this.tabIndex);
}
this.links.removeClass('ui-state-active ui-state-hover');
},
/**
* Deactivates the current active menu item and resets the menu state after a delay.
* Optionally stops the propagation of the event.
*
* @param {Event} [e] - The event object (optional).
*/
deactivateAndReset: function(e) {
var $this = this;
// Deactivate the current active menu item if it exists
if (this.activeitem) {
this.deactivate(this.activeitem);
}
// If hideDelay is configured, reset the menu state after the delay
if (this.cfg.hideDelay > 0) {
this.timeoutId = PrimeFaces.queueTask(() => {
$this.reset();
}, this.cfg.hideDelay);
}
else {
this.reset();
}
// Stop the propagation of the event if the event object is provided
if (e) {
e.stopPropagation();
}
}
});
© 2015 - 2024 Weber Informatics LLC | Privacy Policy