FFFGateway-Monitoring/js/responsive-menu.js

1001 lines
33 KiB
JavaScript

/**
* Responsive Menu
* Version: 0.2.1
* URL: private
* Description: A drop-down responsive Menu for responsive layouts
* Requires: jQuery
* Optional: Modernizr
* Author: jbowyers
* Copyright: 2014-2015 jbowyers
* License: This file is part of Responsive Menu.
* Responsive Menu is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Responsive Menu is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/
*/
;(function( $, window, document, Math, undefined ) {
'use strict';
var pluginName = 'rMenu';
/**
* The plugin
* @param {object} el - The menu container typically a nav element
* @param {object} options - plugin options object litteral
* @returns {Plugin}
* @constructor
*/
var Plugin = function( el, options ) {
// Clone this object
var o = this;
/**
* Initialize option defaults and set options =============================
* @type {{minWidth: string, toggleSel: string, menuSel: string, menuItemsSel: string, transitionSpeed: number, animateBool: string, accelerateBool: string}}
*/
o.optionsInit = {
/**
* Minimum width for expanded layout in pixels - String Should match media query in css file
* Must be in pixels and include px units if not using Modernizr.
* @default '769px'
*/
minWidth: '769px',
/**
* The opening and closing speed of the menus in milliseconds
* @default 400
*/
transitionSpeed: 400,
/**
* The jQuery easing function - used with jQuery transitions
* @default 'swing'
* @options 'swing', 'linear'
*/
jqueryEasing: 'swing',
/**
* The CSS3 transitions easing function - used with CSS3 transitions
* @default 'ease'
*/
css3Easing: 'ease',
/**
* Use button as Toggle Link - instead of text
* @default true
*/
toggleBtnBool: true,
/**
* The Toggle Link selector
* @default '.rm-toggle'
*/
toggleSel: '.rm-toggle',
/**
* The menu/sub-menu selector
* @default 'ul'
*/
menuSel: 'ul',
/**
* The menu items selector
* @default 'li'
*/
menuItemsSel: 'li',
/**
* Use CSS3 animation/transitions Boolean
* @default true
* Do not use animation/transitions: false
*/
animateBool: true,
/**
* Force GPU Acceleration Boolean
* @default false
* Do not force: false
*/
accelerateBool: false,
/**
* the setup complete callback function
* @default 'false'
*/
setupCallback: false,
/**
* the tabindex start value - integer
* @default 1
*/
tabindexStart: 1,
/**
* Use development mode - outputs information to console
* @default false
*/
developmentMode: false
};
o.options = $.extend( {}, o.optionsInit, options );
// Define public objects and vars =========================================
// Toggle Link object
o.tButton = $( o.options.toggleSel );
// The class applied to the toggle Link element to make it a button
o.tButtonClass = 'rm-button';
// The class applied to the toggle Link element when it is visible
o.tButtonShowClass = 'rm-show';
// The class applied to the toggle Link element when it is visible
o.tButtonActiveClass = 'rm-active';
// Nav element object - contains the menus
o.el = $( el );
// The class the plugin adds to the nav element
o.navElementClass = 'rm-nav';
// Container object - contains everything - the Nav element and Toggle Link
o.container = o.el.parent();
// The class the plugin adds to the container of the nav element
o.containerClass = 'rm-container';
// The class applied to container element to trigger expanded layout
o.expandedClass = 'rm-layout-expanded';
// The class applied to container element to trigger contracted layout
o.contractedClass = 'rm-layout-contracted';
// The class that is removed from the toggle and nav element when JS is supported
o.noJSClass = 'rm-nojs';
// All menu elements
o.menus = o.el.find( o.options.menuSel );
// The class applied to all menu elements
o.menuClass = 'rm-menu';
// Top level menu object - contains the menus
o.topMenu = o.el.children( o.options.menuSel );
// The class the plugin adds to the top menu element
o.topMenuClass = 'rm-top-menu';
// The class applied to menu/sub-menu element when menu is expanded
o.menuExpandedClass = 'rm-menu-expanded';
// The class applied to menu/sub-menu element when menu is hidden
o.menuHiddenClass = 'accessibly-hidden';
// The class the plugin adds to the menu elements when calculating height
o.menuCalcClass = 'rm-calculate';
// The class applied to all menu items
o.menuItemClass = 'rm-menu-item';
// The focused parent element
o.itemFocused = false;
// The class applied to menu items that contain a sub-menu
o.parentClass = 'rm-parent';
// The class applied to a menu item when its menu is expanded
o.itemHoverClass = 'rm-hover';
// The class applied to the first menu item
o.itemFirst = 'rm-first';
// The class applied to the last menu item
o.itemLast = 'rm-last';
// The class applied to the second to last menu item
o.item2ndLast = 'rm-2nd-last';
// The CSS3 animate class variable
o.animateClass = 'rm-css-animate';
// The CSS3 animate Boolean
o.animateBool = o.options.animateBool;
// The GPU accelerate class variable
o.accelerateClass = 'rm-accelerate';
// The GPU accelerate Boolean
o.accelerateBool = o.options.accelerateBool;
// The touchmove Boolean - did a touchmove event just occur
o.touchMoveBool = false;
// Resize and Pause hover event timer function
o.timer = false;
// The window width - used to verify a window width change
o.windowWidth = $( window ).width();
/**
* Initiate plugin =========================================
* @returns {Plugin}
*/
o.init = function() { // Should only be called once
// Set up the plugin
o.setup();
// Window event handlers
$( window ).on({
// Reset on screen resize
'resize': function() {
// Test if width has resized - as opposed to height
if ($( window ).width() !== o.windowWidth) {
// Update the window width
o.windowWidth = $( window ).width();
// Adjust layout
clearTimeout( o.timer );
o.timer = setTimeout( o.adjust, 500 );
}
}
});
// Run setupCallback function
if ( typeof( o.options.setupCallback ) === "function" ) {
o.options.setupCallback();
}
return this;
};
/**
* Setup plugin ============================================================
* @returns {Plugin}
*/
o.setup = function() { // Can be called again to reset plugin
// Add the container class to the nav element's parent element
o.container.addClass( o.containerClass );
// add rm-button class if using button
if ( o.options.toggleBtnBool ) {
o.tButton.addClass( o.tButtonClass );
} else {
o.tButton.removeClass( o.tButtonClass );
}
// Remove o.noJSClass class and add click event to Toggle Link
o.tButton
.removeClass( o.noJSClass )
.off( 'mousedown.rm focusin.rm click.rm' )
// Use mousedown and focus to trigger toggle
.on( 'mousedown.rm focusin.rm', tButtonFocus )
// Disable click events
.on( 'click.rm', tButtonClick )
.attr( 'tabindex', 0 )
;
// Add menu class and make submenus accessibly hidden
o.menus
.addClass( o.menuClass )
.attr( 'aria-hidden', 'false' )
.hide();
// Add top menu class
o.topMenu.addClass( o.topMenuClass );
// Adjust o.animateBool
if ( o.animateBool ) { // using CSS3 transitions
// Check if transitions and acceleration are supported
if ( typeof Modernizr !== 'undefined' ) { // Test with Modernizr
if ( !Modernizr.csstransitions ) {
o.animateBool = false;
o.accelerateBool = false;
} else if ( !Modernizr.csstransforms3d ) {
o.accelerateBool = false;
}
} else if ( !transitionsSupported() ) {
o.animateBool = false;
o.accelerateBool = false;
} else if ( !transform3DSupported() ) {
o.accelerateBool = false;
}
} else {
o.accelerateBool = false;
}
// Add animate and accelerate classes if CSS3 animation
if ( o.animateBool ) {
o.menus.addClass( o.animateClass );
if ( o.accelerateBool ) {
o.menus.addClass( o.accelerateClass );
}
}
// Add and remove classes and click events
o.el
.removeClass( o.noJSClass )
.addClass( o.navElementClass )
.off( 'focusin.rm focusout.rm click.rm touchend.rm touchmove.rm' )
// Use focus to trigger menu item focus/hover behaviour
.on( 'focusin.rm', o.options.menuItemsSel, itemFocus )
// De-focus menu on focus out
.on( 'focusout.rm', o.topMenu, menuBlur )
// Use click and touchend to trigger click behaviour
.on( 'click.rm touchend.rm', o.options.menuItemsSel, itemClick )
// Set touchMoveBool to true on touchmove event
.on( 'touchmove.rm', o.options.menuItemsSel, touchMove )
.find( o.options.menuItemsSel )
.each( function(i) {
var $el = $( this );
$el
.addClass( o.menuItemClass)
.children( 'a' ).attr( 'tabindex', 0 )
;
if ( $el.is( ':first-child') ) {
$el.addClass( o.itemFirst );
}
if ( $el.is( ':last-child') ) {
$el.addClass( o.itemLast )
.prev().addClass( o.item2ndLast );
}
})
.addBack()
.removeClass( o.parentClass )
.has( o.options.menuSel )
.addClass( o.parentClass )
;
// Apply initial layout and adjustments
o.adjust();
return this;
};
/**
* Adjust plugin ============================================================
* @param {String} minWidth - the min-width value (including units)
* minWidth must be in pixels if not using Modernizr. Should match media query in css file
*/
o.adjust = function( minWidth ) {
// Get the breakpoint minimum width
minWidth = typeof minWidth !== 'undefined' ? minWidth : o.options.minWidth;
// Check browser width - set menu layout
if ( typeof Modernizr !== 'undefined' && Modernizr.mq('only all') ) { // MQs supported - Test with Modernizr
if ( o.options.developmentMode ) {
console.log( 'Modernizr: MQ supported' );
}
if ( !Modernizr.mq( '( min-width: ' + minWidth + ' )' ) ) {
o.layoutContracted();
} else {
o.layoutExpanded();
}
} else { // Unable to detect MQ support - Test width using outerWidth - less reliable
if ( o.options.developmentMode ) {
console.log( 'unable to detect MQ support' );
}
if ( $( window ).outerWidth() < parseInt( minWidth ) ) {
o.layoutContracted();
} else {
o.layoutExpanded();
}
}
};
// External Helper Functions ===============================================
/**
* Contracted layout
* @returns {Plugin}
*/
o.layoutContracted = function() {
if ( !o.container.hasClass( o.contractedClass ) ) { // not contracted
// Contract any expanded siblings and their children
menuBlur( { 'type': 'layoutContracted' } );
// Apply Contracted class
o.container
.removeClass( o.expandedClass )
.addClass( o.contractedClass )
.find( '.' + o.itemHoverClass ).removeClass( o.itemHoverClass );
if ( o.animateBool ) { // using CSS3 transitions
// Recalculate menu heights
o.calculateHeights();
}
// Remove hover events
o.el.off( 'mouseenter.le mouseleave.le' );
// Show Toggle Link and setup topMenu
o.tButton.addClass( o.tButtonShowClass );
if ( !o.tButton.hasClass( o.tButtonActiveClass ) ) { // topMenu not active
// Hide topMenu
o.topMenu
.addClass( o.menuHiddenClass )
.show()
.removeClass( o.menuExpandedClass )
;
} else { // topMenu is active
// Show topMenu
o.topMenu
.removeClass( o.menuHiddenClass )
.show()
.addClass( o.menuExpandedClass );
if ( o.animateBool ) { // Using CSS3 transitions
o.topMenu
.css({
'max-height': 'none'
})
;
}
}
}
if ( o.options.developmentMode ) {
console.log( 'responsive-menu: contracted layout' );
}
return this;
};
/**
* Expanded layout
* @returns {Plugin}
*/
o.layoutExpanded = function() {
if ( !o.container.hasClass( o.expandedClass ) ) { // not expanded
// Contract any expanded siblings and their children
menuBlur( { 'type': 'layoutExpanded' } );
// Apply expanded class to container
o.container
.removeClass( o.contractedClass )
.addClass( o.expandedClass )
.find( '.' + o.itemHoverClass ).removeClass( o.itemHoverClass );
if ( o.animateBool ) { // using CSS3 transitions
// Recalculate menu heights
o.calculateHeights();
}
// Re-apply mouse events
o.el.off( 'mouseenter.le mouseleave.le' )
// Add mouseenter to all menu items to trigger focus
.on( 'mouseenter.le', o.options.menuItemsSel, itemFocus )
// Add mouseleave to trigger focus when re-entering parent of expanded menu
.on( 'mouseleave.le', o.options.menuItemsSel, itemLeave )
// Add mouseleave on topmenu to trigger menu blur
.on( 'mouseleave.le', o.topMenu, menuBlur )
;
// Show Menu - Hide Toggle Link
o.tButton.removeClass( o.tButtonShowClass );
o.topMenu.removeClass( o.menuHiddenClass )
.show()
.addClass( o.menuExpandedClass );
if ( o.animateBool ) { // Using CSS3 transitions
o.topMenu
.css({
'max-height': 'none',
'overflow': 'visible'
})
;
}
}
if ( o.options.developmentMode ) {
console.log( 'responsive-menu: expanded layout' );
}
return this;
};
/**
* Calculate the heights of each submenu and store in data object, reset styles
* Used when CSS3 transitions are enabled
* @returns {Plugin}
*/
o.calculateHeights = function() {
// Unstyle menus to original state to measure heights and then reapply styles
o.menus
.addClass( o.menuCalcClass )
.removeClass( o.menuExpandedClass )
.attr( 'style', '' )
.show( 0 );
// Reselect to force application of styles
o.menus.each( function () {
var $el = $( this );
$el
.data( 'height', $el.height() )
;
})
.css( {
'max-height': '0'
})
.removeClass( o.menuCalcClass )
;
return this;
};
/**
* Toggle visibility of entire menu
* @param {Object} el - The toggle Link element
*/
o.toggleMenu = function( el ) {
// Contract all sub-menus
contract( o.topMenu );
if ( !o.topMenu.hasClass( o.menuHiddenClass ) ) { // topMenu is visible
// Hide topMenu
$( el ).removeClass( o.tButtonActiveClass );
contract( o.container );
} else { // menu is hidden
// Show topMenu
$( el ).addClass( o.tButtonActiveClass );
o.topMenu.removeClass( o.menuHiddenClass );
if ( o.animateBool ) { // Using CSS3 transitions
o.topMenu.css( 'max-height', '0' );
} else { // Use jQuery animation
o.topMenu.hide( 0 );
}
expand( o.el );
}
};
// internal Event Handler Functions ===============================================
/**
* Toggle Btn focus and mousedown event handler
* @param {event} e - event object
*/
var tButtonFocus = function( e ) {
e.stopPropagation();
var $el = $( e.target );
clearTimeout( o.timer );
o.timer = setTimeout( function () {
o.toggleMenu( e.target );
}, 100 );
};
/**
* Toggle Btn Click event handler
* @param {event} e - event object
*/
var tButtonClick = function( e ) {
e.preventDefault();
e.stopPropagation();
};
/**
* Item click and touchend event handler
* @param {event} e - event object
*/
var itemClick = function( e ) {
var $el = $( e.currentTarget );
e.stopPropagation();
if ( ( $el.hasClass( o.itemHoverClass ) || !$el.hasClass( o.parentClass ) ) && !o.touchMoveBool ) {
location.href = $el.children( 'a' ).attr('href');
menuBlur( e );
} else if ( e.type !== 'touchend' ) {
e.preventDefault();
}
o.touchMoveBool = false;
};
/**
* Menu item focus and mouseenter event handler -
* Triggers: focus, mouseenter
* @param {event} e - event object
*/
var itemFocus = function( e ) {
// get current target before it changes
var $el = $( e.currentTarget );
e.stopPropagation();
// Add focus if item does not have focus
if ( e.type !== 'focusin' ) {
$el.children( 'a' ).not( ':focus' ).focus();
}
o.itemFocused = $el;
clearTimeout( o.timer );
o.timer = setTimeout( function () {
// Expand topmenu if toggle button is active and menu is contracted
if ( o.tButton.hasClass( o.tButtonShowClass ) && !o.tButton.hasClass( o.tButtonActiveClass )) {
o.toggleMenu( o.tButton.get(0) );
}
// Expand menu
if ( $el.hasClass( o.parentClass ) ) {
if ( !$el.hasClass( o.itemHoverClass ) ) {
// Contract any expanded siblings and their children
contract( $el.parent() );
expand( $el );
}
} else {
// Contract any expanded siblings and their children
contract( $el.parent() );
}
}, 100 );
};
/**
* Touchmove event handler
* @param {event} e - event object
*/
var touchMove = function( e ) {
o.touchMoveBool = true;
};
/**
* Topmenu mouseleave and foucusout event handler
* Triggers: mouseleave, focusout
* @param {event} e - event object
*/
var menuBlur = function( e ) {
// Define event type if e is undefined
e = e || { 'type': 'callback' };
clearTimeout( o.timer );
o.timer = setTimeout( function () {
if ( o.itemFocused ) {
o.itemFocused.children( 'a' ).blur();
o.itemFocused = false;
}
contract( o.topMenu );
}, 100 );
};
/**
* Sub-menu item mouseleave event handler - used with expanded layout
* Triggers: mouseleave
* @param {event} e - event object
*/
var itemLeave = function( e ) {
// get current target before it changes
var $el = $( e.currentTarget );
clearTimeout( o.timer );
o.timer = setTimeout( function () {
// Focus the parent element of the expanded menu
$el.parent().parent().children( 'a' ).focus();
}, 100 );
};
/**
* The CSS3 Transition End Contract event handler - used to add call-back functions to CSS3 transitions
* @param {event} e - event object
*/
var transitionEndContract = function( e ) {
if ( e.originalEvent.propertyName === 'max-height' ) {
var $el = $( e.currentTarget );
e.stopPropagation();
// Menu Contracted
$el
.css( {
'transition': '',
'max-height': '0',
'overflow': 'hidden'
} )
.removeClass( o.menuExpandedClass )
.off( 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd' )
.parent().find( '.' + o.itemHoverClass ).addBack().removeClass( o.itemHoverClass )
;
if ( $el.hasClass( o.topMenuClass ) ) { // is topMenu
// accessibly hide topMenu
$el
.addClass( o.menuHiddenClass )
.show( 0 );
}
// Scroll to expanded menu
scrollMenu( o.itemFocused );
}
};
/**
* The CSS3 Transition End Expand event handler - used to add call-back functions to CSS3 transitions
* @param {event} e - event object
*/
var transitionEndExpand = function( e ) {
if ( e.originalEvent.propertyName === 'max-height' ) {
var $el = $( e.currentTarget );
e.stopPropagation();
// Menu expanded
$el
.removeClass( o.menuHiddenClass )
.css( {
'transition': '',
'max-height': 'none',
'overflow': 'visible'
} )
.addClass( o.menuExpandedClass )
.off( 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd' )
;
$el.parent( '.' + o.parentClass ).addClass( o.itemHoverClass );
// Scroll to expanded menu
scrollMenu( o.itemFocused );
}
};
// Internal Helper Functions ===============================================
/**
* Contract sub-menus
* @param {Object} $parent - The parent element of the menu Item initiating the event
*/
var contract = function( $parent ) {
var $menus = $parent.find( o.options.menuSel );
if ( o.animateBool ) { // Using CSS3 transitions
// Set max-height to height of each expanded menu
$menus.each( function(){
var $el = $( this );
if ( $el.height() !== 0 ) {
$el
.css({
'max-height': $el.height(),
'transition': 'max-height ' + String( o.options.transitionSpeed / 1000 ) + 's ' + o.options.css3Easing,
'overflow': 'hidden'
})
.on( 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', transitionEndContract )
;
} else {
$menus.not( $el );
}
});
// Must force a redraw so transition will occur
$menus.hide(0).show(0);
// Contract menu
$menus
.css({
'max-height': '0'
})
.removeClass( o.menuExpandedClass )
;
} else { // Use jQuery animation
// Contract menus
$menus.each( function() {
var $el = $( this );
if ( $el.height() !== 0 ) {
$el
.slideUp( o.options.transitionSpeed, o.options.jqueryEasing, function () {
$el
.css( 'overflow', 'visible' )
.removeClass( o.menuExpandedClass )
.parent( '.' + o.parentClass )
.removeClass( o.itemHoverClass )
;
if ( $el.hasClass( o.topMenuClass ) ) {
o.topMenu.addClass( o.menuHiddenClass );
}
// Scroll to expanded menu
scrollMenu( o.itemFocused );
})
;
}
});
}
};
/**
* Expand sub-menu
* @param {Object} $el - The menu Item initiating the event
*/
var expand = function( $el ) {
// Define menu
var $menu = $el.children( o.options.menuSel );
// Remove hover class from siblings
$el.siblings( '.' + o.itemHoverClass )
.removeClass( o.itemHoverClass );
if ( o.animateBool ) { // Using CSS3 transitions
// Expand menu
$menu
.css({
'transition': 'max-height ' + String( o.options.transitionSpeed / 1000 ) + 's ' + o.options.css3Easing,
'max-height': $menu.data('height')
})
.on( 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', transitionEndExpand )
;
} else { // Use jQuery animation
// Expand menu
$menu
.slideDown( o.options.transitionSpeed, o.options.jqueryEasing, function() {
$el.addClass( o.itemHoverClass );
$menu
.addClass( o.menuExpandedClass )
.css( 'overflow','visible' )
;
console.log('jquery expand');
// Scroll to expanded menu
scrollMenu( o.itemFocused );
})
;
}
};
// initialize ----------------------------------------------------------------
o.init( el );
return this;
};
/**
* Create plugin obects
* @param {Object} options - Plugin options
* @returns {*}
*/
$.fn[ pluginName ] = function( options ) {
// Return collection of elements
return this.each( function() {
var $el = $( this );
if ( !$el.data( pluginName ) ) {
$el.data( pluginName, new Plugin( this, options ) );
}
});
};
// Out of Scope Private functions ==================================================
/**
* Test for transform3d support
* @returns {boolean}
*/
var transform3DSupported = function() {
var el = document.createElement('p'),
has3d,
transforms = {
'webkitTransform':'-webkit-transform',
'OTransform':'-o-transform',
'msTransform':'-ms-transform',
'MozTransform':'-moz-transform',
'transform':'transform'
};
// Add it to the body to get the computed style
document.body.insertBefore(el, null);
for(var t in transforms){
if( el.style[t] !== undefined ){
el.style[t] = 'translate3d(1px,1px,1px)';
has3d = window.getComputedStyle(el).getPropertyValue(transforms[t]);
}
}
document.body.removeChild(el);
return (has3d !== undefined && has3d.length > 0 && has3d !== "none");
};
/**
* Test for CSS3 transitions support
* @returns {boolean}
*/
var transitionsSupported = function() {
var b = document.body || document.documentElement,
s = b.style,
p = 'transition';
if (typeof s[p] === 'string') { return true; }
// Tests for vendor specific prop
var v = ['Moz', 'webkit', 'Webkit', 'Khtml', 'O', 'ms'];
p = p.charAt(0).toUpperCase() + p.substr(1);
for (var i=0; i<v.length; i++) {
if (typeof s[v[i] + p] === 'string') { return true; }
}
return false;
};
/**
* Scroll Menu into viewport if off screen
* @returns {boolean}
*/
var scrollMenu = function( $el ) {
if ( $el.length ) {
var viewTop = $( window ).scrollTop();
var viewBottom = viewTop + $( window ).height();
var boundsTop = $el.offset().top;
var boundsBottom = boundsTop + $el.outerHeight();
if ( boundsBottom > viewBottom || boundsTop < viewTop ) {
$( 'html, body' ).animate( { scrollTop: boundsTop }, 'slow' );
}
}
};
})( jQuery, window, document, Math );