/** * 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 viewbottom || boundstop < viewtop ) { $( 'html, body' ).animate( { scrolltop: boundstop }, 'slow' ); } } }; })( jquery, window, document, math );