bs_base-8.x-1.x-dev/js/navbar.js
js/navbar.js
/**
* @file
*
* Navigation bar improvements and tweaks.
*
* The biggest addition is support for 3 levels of depth for small screens.
*/
(function ($, Drupal, drupalSettings) {
'use strict';
// Lets expose some usefull methods to the public.
Drupal.bs_base = Drupal.bs_base ? Drupal.bs_base : {};
Drupal.bs_base.navbar = Drupal.bs_base.navbar ? Drupal.bs_base.navbar : {};
/**
* Toggle all dropdown menu items in active path.
*
* @param $navbar
* Navigation bar jQuery element.
* @param state
* Pass true or false to specifically set the aria-expanded state. If not
* used aria-expanded state value is toggled.
*/
var toggleActivePath = function($navbar, state) {
$navbar.find('.dropdown > a.is-active').each(function () {
toggleDropdown($(this), state);
});
};
/**
* Toggle dropdown for a given dropdown toggle and it parent.
*
* @param $dropdownToggle
* @param state
* Pass true or false to specifically set the aria-expanded state. If not
* used aria-expanded state value is toggled.
*/
var toggleDropdown = function($dropdownToggle, state) {
$($dropdownToggle.parents('.dropdown').get(0)).toggleClass('show');
// If state is passed use it, otherwise toggle the state.
var newState = state !== null && typeof(state) !== 'undefined' ? state : ($dropdownToggle.attr('aria-expanded') === 'false' ? 'true' : 'false');
$dropdownToggle.attr('aria-expanded', newState);
$dropdownToggle.siblings('.dropdown-menu').toggleClass('show');
};
/**
* Find open dropdown items and close them in a given dropdown parent.
*
* @param $dropdownParent
*/
var closeDropdowns = function($dropdownParent) {
$dropdownParent.find('> .dropdown.show').each(function () {
var $this = $(this);
$this.removeClass('show');
$this.find('> a').attr('aria-expanded', 'false');
$this.find('> .dropdown-menu').removeClass('show');
});
};
/**
* Check is navbar shown.
*
* @param $element
* Element that belongs to a navbar.
* @return {boolean}
* True if navbar collapse is shown, false if not.
*/
Drupal.bs_base.navbar.isNavbarCollapseShow = function($element) {
var $navbarCollapse;
if ($element.hasClass('navbar-collapse')) {
$navbarCollapse = $element;
}
else {
$navbarCollapse = $element.parents('.navbar-collapse');
}
// If parent .navbar-collapse has show class this means that we are
// in small view where responsive navbar is visible.
// canvas-slid is used for off canvas navigation
// @todo can we maybe normalize this so show class is used in both cases?
if ($navbarCollapse.hasClass('show') || $navbarCollapse.hasClass('canvas-slid')) {
return true;
}
return false;
};
/**
* Follow the link.
*
* @param $link
* jQuery element with href and optional target attribute.
*/
Drupal.bs_base.navbar.followLink = function($link) {
// Load a page in browser and disable propagation of event.
if ($link.attr('target')) {
window.open($link.attr('href'), $link.attr('target'));
}
else {
window.location = $link.attr('href');
}
};
/**
* Open sub-navigation items on hover.
*/
function subNavigationOnHover($navbar) {
var $navbarWraper = $navbar.parents('.navbar-wrapper');
// Stores all top level navbar nav menu items.
var menuItems = [];
// Stores initial top level active menu item.
var initialActiveMenu = null;
// Does menu item already have second-level-expanded CSS class.
let hasSecondLevelExpanded = false;
// Activate menu item.
function activateMenuItem(item) {
if (item.active) {
return;
}
item.active = true;
// We are attaching show CSS class because we are using it later to detect
// a case of clicking on menu item which already have open submenu - the
// case when we are following the link.
item.item.addClass('active show');
item.itemLink.addClass('active is-active').attr('aria-expanded', true);
if ($navbarWraper.hasClass('second-level-expanded')) {
hasSecondLevelExpanded = true;
}
else {
$navbarWraper.addClass('second-level-expanded');
}
}
// Deactivate menu item.
function deactivateMenuItem(item) {
if (!item.active) {
return;
}
item.active = false;
item.item.removeClass('active show');
item.itemLink.removeClass('active is-active').attr('aria-expanded', false);
if (!hasSecondLevelExpanded) {
$navbarWraper.removeClass('second-level-expanded');
}
}
// Deactivate all menu items.
function deactivateMenuItems(skipMenuItem) {
// Remove active class from all other dropdown items.
for (var i = 0; i < menuItems.length; ++i) {
if ((!skipMenuItem || i !== skipMenuItem.index) && menuItems[i].dropdown && menuItems[i].active) {
deactivateMenuItem(menuItems[i]);
}
}
}
// Get menu item from passed DOM element. Support second level menu items
// also.
function getItem(node) {
var link = node;
if (node.nodeName == 'SPAN') {
// If target is menu item span link child then lets switch to parent.
link = node.parentElement;
}
var itemIndex = Array.prototype.indexOf.call($navbar[0].children, link.parentElement);
if (itemIndex == -1) {
// Check is this second level and if yes find a parent item.
var parentDropdown = $(node).parents('.nav-item.dropdown').get(0);
if (parentDropdown) {
itemIndex = Array.prototype.indexOf.call($navbar[0].children, parentDropdown);
}
}
return itemIndex != -1 ? menuItems[itemIndex] : null;
}
// Initialize top level menu items data.
$navbar.find('> .nav-item').each(function (index) {
var $item = $(this);
var menuItem = {
index: index,
item: $item,
itemLink: $item.find('> a'),
active: $item.hasClass('active'),
dropdown: $item.hasClass('dropdown'),
};
if (menuItem.active) {
menuItem.initiallyActive = true;
initialActiveMenu = menuItem;
}
menuItems.push(menuItem);
});
// Hover in.
var hoverIn = function (e) {
var menuItem = getItem(e.target);
if (menuItem) {
deactivateMenuItems(menuItem);
if (menuItem.dropdown && !menuItem.active) {
activateMenuItem(menuItem);
}
}
};
// Hover out.
var hoverOut = function (e) {
var menuItem = getItem(e.target);
if (!menuItem) {
return;
}
// We process hover out only when we are leaving navbar. In that case
// we will activate initially active menu item.
var parent = $(e.relatedTarget).parents('.navbar-nav');
if (!parent.length) {
if (menuItem.active && !menuItem.initiallyActive) {
deactivateMenuItem(menuItem);
}
if (initialActiveMenu && !initialActiveMenu.active) {
deactivateMenuItems();
activateMenuItem(initialActiveMenu);
}
}
};
// Attach hover processing to all top level menu items.
for (var i = 0; i < menuItems.length; ++i) {
// Use jQuery.hoverIntent plugin if it exist.
if ($.fn.hoverIntent) {
// Attach hover intent over for switching between navbar items.
// We want fast 100ms for switching between items because we don't
// want user to wait when hovering over navbar items.
menuItems[i].item.hoverIntent({
over: hoverIn,
out: $.noop,
timeout: 100
});
// Attach hover intent out for moving outside of navbar.
// Slow 2000ms hover out from navbar completely because we want to
// give user a chance to return.
menuItems[i].item.hoverIntent({
over: $.noop,
out: hoverOut,
timeout: 2000
});
}
else {
menuItems[i].item.hover(hoverIn, hoverOut);
}
}
}
$(function () {
if (drupalSettings.bs_base.navbar_type) {
$('.navbar-nav').each(function () {
var $navbar = $(this);
var $navbarCollapse = $navbar.parents('.navbar-collapse');
// Visit navbar open dropdown click behavior - click on parent open
// submenu link will visit a parent link.
if (drupalSettings.bs_base.navbar_opened_submenu_behavior === 'visit') {
$navbar.find('.dropdown').each(function () {
var $dropdown = $(this);
var $link = $dropdown.find('> a');
$link.click(function (e) {
// If menu dropdown is shown (expanded) follow the link.
// This allows us to use expanded dropdown toggle links as regular
// navigation elements.
if ($dropdown.hasClass('show') ||
// Or if we are on the first level dropdown menu and second
// level horizontal option is on, and we are not on responsive
// menu we should follow the menu and stop processing of a
// click, so we don't show second level in dropdown menu.
(this.dataset.menuLevel === '0' && drupalSettings.bs_base.navbar_type === 'second-level-horizontal' && !Drupal.bs_base.navbar.isNavbarCollapseShow($navbarCollapse)) ||
// Or if we are on second level and navbar type is second level
// dropdown and not in responsive menu then make sure we always
// follow the click on a link. With this we are sure we also
// cover cases when second level has third level presented in
// which case click on second level will not work because it
// will open third level which we do not show.
// Problem here is that this will not work with close dropdown
// option, but this is an edge case and generally this whole
// case can be avoided if you do not expand third level menus
// or just remove them.
(this.dataset.menuLevel === '1' && drupalSettings.bs_base.navbar_type === 'second-level-dropdown' && !Drupal.bs_base.navbar.isNavbarCollapseShow($navbarCollapse))) {
Drupal.bs_base.navbar.followLink($link);
return false;
}
});
});
}
// Second level horizontal with show on hover option.
if (drupalSettings.bs_base.navbar_type === 'second-level-horizontal' && drupalSettings.bs_base.navbar_onhover) {
// Initialize hover event (open sub navigation) on menu items.
// Do not initialize hover events when responsive menu is active. This
// will avoid various problems like that on touch display the tap
// effect will cast first hover and then click event.
// Detecting actually are we on responsive menu or not is tricky. One
// solution is to check is navbar visible - if yes we assume that we
// are on not on responsive menu, and we can init hover effects.
if ($navbarCollapse.is(':visible')) {
subNavigationOnHover($navbar);
}
// On big screens, we need to stop any click activity for the first
// level. It can happen that if the user does a very fast click after
// entering the dropdown link then BS dropdown will be activated. In
// this case, we will show the second level on hover, but when hover
// is done user will then see BS dropdown menu which got activated
// from BS js dropdown code.
$navbar.find('> .dropdown > a').click(function () {
if (!Drupal.bs_base.navbar.isNavbarCollapseShow($navbarCollapse)) {
return false;
}
});
}
});
}
// Toggle active path on first sidebar show.
// @TODO - note that this can still fail if we have multiple toggler buttons
// on page and user use multiple of them on same page. But this should not
// happen hopefully or we need to refactor this more with additional state
// var.
$('.navbar-toggler').each(function () {
var $navbar = $(this.dataset.target).find('.navbar-nav');
this.addEventListener('click', function () {
toggleActivePath($navbar, true);
}, {once: true});
});
if (drupalSettings.bs_base.navbar_offcanvas_type) {
// Small screen third level support for dropdown.
// Taken and fixed from http://stackoverflow.com/a/18682698.
// @todo - we should support second level the same way as we support third
// level, but Bootstrap dropdown.js is making problems here (stealing the
// click and reloading the page probably). This is not ideal and should be
// improved.
$('.navbar-nav').each(function () {
var $navbar = $(this);
$navbar.find('.dropdown .dropdown > a').each(function () {
var $this = $(this);
//var $parent = $($this.parents('.dropdown-menu').get(0));
$this.on('click', function (event) {
// If we are on big screen then don't continue.
if (!Drupal.bs_base.navbar.isNavbarCollapseShow($navbar)) {
return;
}
// Avoid following the href location when clicking.
event.preventDefault();
// Avoid having the menu to close when clicking.
event.stopPropagation();
// Close first any open dropdown in the same parent.
// @todo - this is commented for now to avoid user focus losing when
// we have open long menus and closing them will trigger viewport
// vertical scrolling.
//closeDropdowns($parent);
// Open the one we clicked on.
toggleDropdown($this);
});
});
});
// Offcanvas close link support.
var $offcanvas = $('.offcanvas');
$('.offcanvas-close-link').click(function () {
$offcanvas.offcanvas('hide');
// Our close link has hash for href. Clicking on hash link will scroll
// page to the top. We disable propagation of click event to stop this.
return false;
});
// On page unload we will hide offcanvas navigation if it's open. This
// will prevent Firefox and maybe Safari to show offcanvas navigation when
// using browser history back button.
var pageUnloadCalled = false;
var pageUnload = function (e) {
// Lets not call this twice for browsers that are not iOS.
if (pageUnloadCalled) {
return;
}
pageUnloadCalled = true;
var offcanvasData = $offcanvas.data('bs.offcanvas');
if (offcanvasData && offcanvasData.state === 'slid') {
$offcanvas.offcanvas('hide');
}
};
// Although beforeunload is supported on iOS it will not be called there.
// Apple documentation suggest using pagehide to support back and forward
// browser buttons.
window.addEventListener('beforeunload', pageUnload);
window.addEventListener('pagehide', pageUnload);
}
});
$(document).on('hide.bs.dropdown', function (e) {
// Don't hide dropdown navigation menus which are in sidebar navigation.
// This will prevent hiding open submenus in sidebar navigation when we want
// to open a new sub navigation - the rest will stay open.
if (e.target.classList.contains('nav-item') && Drupal.bs_base.navbar.isNavbarCollapseShow($(e.target))) {
e.preventDefault();
}
});
})(jQuery, Drupal, drupalSettings);
