media/js/base/protocol/protocol-menu.js (239 lines of code) (raw):
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// Copied from Protocol, to be backported along with nav updates.
(function () {
'use strict';
var MzpMenu = {};
var _menuOpen = false;
var _hoverTimeout;
var _hoverTimeoutDelay = 150;
var _mqWideNav;
var _wideBreakpoint = '768px';
var _options = {
onMenuOpen: null,
onMenuClose: null,
onMenuButtonClose: null
};
/**
* Opens a menu panel.
* @param {Object} el - DOM element (`.mzp-c-menu-category.mzp-js-expandable`)
* @param {Boolean} animate - show animation when menu panel opens.
*/
MzpMenu.open = function (el, animate) {
if (animate) {
el.classList.add('mzp-is-animated');
}
el.classList.add('mzp-is-selected');
_menuOpen = true; // For checking menu state on keyup.
el.querySelector('.c-menu-title').setAttribute('aria-expanded', true);
if (typeof _options.onMenuOpen === 'function') {
_options.onMenuOpen(el);
}
};
/**
* Closes all currently open menu panels.
* Note: on small screens more than one menu can be open at the same time.
*/
MzpMenu.close = function () {
var current = document.querySelectorAll(
'.c-menu-category.mzp-is-selected'
);
for (var i = 0; i < current.length; i++) {
// The following classes must be removed in the correct order
// to work around a bug in bedrock's classList polyfill for IE9.
// https://github.com/mozilla/bedrock/issues/6221 :/
current[i].classList.remove('mzp-is-selected');
current[i].classList.remove('mzp-is-animated');
current[i]
.querySelector('.c-menu-title')
.setAttribute('aria-expanded', false);
}
_menuOpen = false; // For checking menu state on keyup.
if (typeof _options.onMenuClose === 'function' && current.length > 0) {
_options.onMenuClose();
}
return current.length > 0;
};
MzpMenu.onDocumentKeyUp = function (e) {
if (e.keyCode === 27 && _menuOpen) {
MzpMenu.close();
}
};
/**
* Menu panel close button `click` event handler.
* @param {Object} e - Event object.
*/
MzpMenu.onCloseButtonClick = function (e) {
e.preventDefault();
if (typeof _options.onMenuButtonClose === 'function') {
_options.onMenuButtonClose();
}
MzpMenu.close();
};
/**
* Toggles the open/closed state of a menu panel.
* @param {Object} el - DOM element (`.mzp-c-menu-category.mzp-js-expandable`)
*/
MzpMenu.toggle = function (el) {
var state = el.classList.contains('mzp-is-selected') ? true : false;
if (!state) {
MzpMenu.open(el);
} else {
// The following classes must be removed in the correct order
// to work around a bug in bedrock's classList polyfill for IE9.
// https://github.com/mozilla/bedrock/issues/6221 :/
el.classList.remove('mzp-is-selected');
el.classList.remove('mzp-is-animated');
el.querySelector('.c-menu-title').setAttribute(
'aria-expanded',
false
);
if (typeof _options.onMenuClose === 'function') {
_options.onMenuClose();
}
}
};
/**
* Menu `mouseenter` event handler.
* Opens the menu only when hover intent is shown.
* Animates only if a menu panel is not already open.
* @param {Object} e - Event object.
*/
MzpMenu.onMouseEnter = function (e) {
clearTimeout(_hoverTimeout);
_hoverTimeout = setTimeout(function () {
var current = MzpMenu.close();
var animate = current ? false : true;
MzpMenu.open(e.target, animate);
}, _hoverTimeoutDelay);
};
/**
* Menu `mouseleave` event handler.
* Closes the menu only when hover intent is shown.
*/
MzpMenu.onMouseLeave = function () {
clearTimeout(_hoverTimeout);
_hoverTimeout = setTimeout(function () {
MzpMenu.close();
}, _hoverTimeoutDelay);
};
/**
* Menu `focusout` event handler.
* Closes the menu when focus moves to an alement outside of the currently open panel.
*/
MzpMenu.onFocusOut = function () {
var self = this;
/**
* After an element loses focus, `document.activeElement` will always be `body` before
* moving to the next element. A `setTimeout` of `0` circumvents this issue as it
* re-queues the JavaScript to run at the end of the current excecution.
*/
setTimeout(function () {
// If the menu is open and the newly focused element is not a child, then call close().
if (
!self.contains(document.activeElement) &&
self.classList.contains('mzp-is-selected')
) {
MzpMenu.close();
}
}, 0);
};
/**
* Menu link `click` event handler for wide viewports.
* Closes any currently open menu panels before opening the selected one.
* @param {Object} e - Event object.
*/
MzpMenu.onClickWide = function (e) {
e.preventDefault();
MzpMenu.close();
MzpMenu.open(e.target.parentNode);
};
/**
* Menu link `click` event handler for small viewports.
* Toggles the currently selected menu open open/close state.
* @param {Object} e - Event object.
*/
MzpMenu.onClickSmall = function (e) {
e.preventDefault();
MzpMenu.toggle(e.target.parentNode);
};
/**
* Convenience function for checking `matchMedia` state.
* @return {Boolean}
*/
MzpMenu.isWideViewport = function () {
return _mqWideNav.matches;
};
/**
* Toggle desktop/mobile navigation using `matchMedia` event handler.
*/
MzpMenu.handleState = function () {
_mqWideNav = matchMedia('(min-width: ' + _wideBreakpoint + ')');
function menuBind(mq) {
MzpMenu.close();
if (mq.matches) {
MzpMenu.unbindEventsSmall();
MzpMenu.bindEventsWide();
} else {
MzpMenu.unbindEventsWide();
MzpMenu.bindEventsSmall();
}
}
if (window.matchMedia('all').addEventListener) {
// evergreen
_mqWideNav.addEventListener('change', menuBind, false);
} else if (window.matchMedia('all').addListener) {
// IE fallback
_mqWideNav.addListener(menuBind);
}
if (MzpMenu.isWideViewport()) {
MzpMenu.bindEventsWide();
} else {
MzpMenu.bindEventsSmall();
}
};
/**
* Bind events for wide viewports.
*/
MzpMenu.bindEventsWide = function () {
var items = document.querySelectorAll(
'.c-menu-category.mzp-js-expandable'
);
var link;
var close;
for (var i = 0; i < items.length; i++) {
items[i].addEventListener(
'mouseenter',
MzpMenu.onMouseEnter,
false
);
items[i].addEventListener(
'mouseleave',
MzpMenu.onMouseLeave,
false
);
items[i].addEventListener('focusout', MzpMenu.onFocusOut, false);
link = items[i].querySelector('.c-menu-title');
link.addEventListener('click', MzpMenu.onClickWide, false);
close = items[i].querySelector('.c-menu-button-close');
close.addEventListener('click', MzpMenu.onCloseButtonClick, false);
}
// close with escape key
document.addEventListener('keyup', MzpMenu.onDocumentKeyUp, false);
};
/**
* Unbind events for wide viewports.
*/
MzpMenu.unbindEventsWide = function () {
var items = document.querySelectorAll(
'.c-menu-category.mzp-js-expandable'
);
var link;
var close;
for (var i = 0; i < items.length; i++) {
items[i].removeEventListener(
'mouseenter',
MzpMenu.onMouseEnter,
false
);
items[i].removeEventListener(
'mouseleave',
MzpMenu.onMouseLeave,
false
);
items[i].removeEventListener('focusout', MzpMenu.onFocusOut, false);
link = items[i].querySelector('.c-menu-title');
link.removeEventListener('click', MzpMenu.onClickWide, false);
close = items[i].querySelector('.c-menu-button-close');
close.removeEventListener(
'click',
MzpMenu.onCloseButtonClick,
false
);
}
document.removeEventListener('keyup', MzpMenu.onDocumentKeyUp, false);
};
/**
* Bind events for small viewports.
*/
MzpMenu.bindEventsSmall = function () {
var items = document.querySelectorAll(
'.c-menu-category.mzp-js-expandable .c-menu-title'
);
for (var i = 0; i < items.length; i++) {
items[i].addEventListener('click', MzpMenu.onClickSmall, false);
}
};
/**
* Unbind events for small viewports.
*/
MzpMenu.unbindEventsSmall = function () {
var items = document.querySelectorAll(
'.c-menu-category.mzp-js-expandable .c-menu-title'
);
for (var i = 0; i < items.length; i++) {
items[i].removeEventListener('click', MzpMenu.onClickSmall, false);
}
};
/**
* Set initial ARIA menu panel states.
*/
MzpMenu.setAria = function () {
var items = document.querySelectorAll(
'.c-menu-category.mzp-js-expandable .c-menu-title'
);
for (var i = 0; i < items.length; i++) {
items[i].setAttribute('aria-expanded', false);
}
};
/**
* Enhances the menu for 1st class JS support.
*/
MzpMenu.enhanceJS = function () {
var menu = document.querySelectorAll('.c-menu');
for (var i = 0; i < menu.length; i++) {
menu[i].classList.remove('mzp-is-basic');
menu[i].classList.add('mzp-is-enhanced');
}
};
/**
* Basic feature detect for 1st class menu JS support.
*/
MzpMenu.isSupported = function () {
if (typeof window.MzpSupports !== 'undefined') {
return (
window.MzpSupports.matchMedia && window.MzpSupports.classList
);
} else {
return false;
}
};
/**
* Initialize menu.
* @param {Object} options - configurable options.
*/
MzpMenu.init = function (options) {
if (typeof options === 'object') {
for (var i in options) {
if (options.hasOwnProperty.call(i)) {
_options[i] = options[i];
}
}
}
if (MzpMenu.isSupported()) {
MzpMenu.handleState();
MzpMenu.setAria();
MzpMenu.enhanceJS();
}
};
window.MzpMenu = MzpMenu;
})();