I have completed writing a responsive menu (it uses the HoverIntent plugin) for my WordPress theme and it looks like it is working fine on desktop/tablets/phones.
You can test the code here. Just make sure that you are testing within Chrome/Opera Developer Toolbar (ctrlshiftI) and then toggle device toolbar (ctrlshiftM) in order to see how it works on tablets/phones.
Is my code bad/inefficient?
jQuery( document ).ready( function ( $ ) {
'use strict';
(function () {
var oldTouchPos,
menuTogglePos,
getFirstLink = $( '.nav a:first' ),
getFirstLinkAttr = getFirstLink.attr( 'href' ),
cachedWindowWidth = $( window ).width();
//This function is responsible for toggling menu Visbility ON/OFF
function toggleMenuVisibility() {
var el = $( '.nav' );
if ( ! isDesktop() ) {
if ( el.is( ':visible' ) ) {
el.css( { 'display': 'none' } );
}
} else {
if ( el.is( ':visible' ) ) {
return false;
} else {
el.css( { 'display': 'block' } );
}
}
}
//This function is responsible for complementing slideUp animation
function decorationUp( $this, minus ) {
$this.prev( 'a' ).css( { 'text-decoration': 'none' } ).children( minus ).not( '.menu-item-description' ).hide( 200 );
}
//Simple function checks if we are on desktop machine (based on CSS "float" property)
function isDesktop() {
return $( '.nav li' ).css( 'float' ) == 'left';
}
//This function is responsible for making first level menu item's same height if description is present (on Desktop machines)
function equalizeHeight_If_Description() {
if ( ! isDesktop() ) {
$( '.nav ul li a' ).removeAttr( 'style' );
} else {
var menuItemDescription = $( 'ul li a .menu-item-description:not("ul ul li a .menu-item-description")' );
if ( menuItemDescription.length ) {
var makeEqualHeight = menuItemDescription.parent().height();
$( '.nav ul li a:not("ul ul li a")' ).not( menuItemDescription.parent( 'a' ) ).css( {
'height': makeEqualHeight + 'px',
'line-height': makeEqualHeight + 'px'
} );
menuItemDescription.parent( 'a' ).css( { 'height': makeEqualHeight + 'px' } );
}
}
}
//Function is responsible for complementing slideDown animation (showing/hiding minus sign, adding/removing href's and adding/removing text-decorations)
function manageHref( thisPrev, getHref, minus ) {
if ( undefined === thisPrev.attr( 'href' ) ) {//If attr is NOT here treat it as a link (allow X Button to animate ,do NOT underline)
thisPrev.children( minus ).show( 290, function() {
thisPrev.attr( 'href', getHref );
if ( '#' == getHref || undefined === getHref ) {
thisPrev.css( { 'text-decoration': 'none' } );
} else {
thisPrev.css( { 'text-decoration': 'underline' } );
}
} );
}
}
//This function is responsible for animating width and height of our menu (On anchor hover... on Desktop machines)
var animateMenuWidth = function() {
var activeClass = $( '.active' );
if ( ! isDesktop() ) {
return false;
}
if ( activeClass.next( 'ul.sub-menu' ).length ) {//Check if next submenu exist. If it does exist do:
var nextSubmenu = activeClass.next( 'ul.sub-menu' );
nextSubmenu
.css( {//Add the following Css properties so we can Read it's values in order to make an animation
'visibility': 'hidden',
'display': 'block'
} );
var getWidth = nextSubmenu.outerWidth( true ),//Get the width of menu
getHeightFirstChild = nextSubmenu.children( 'li:first' ).outerHeight( true ),//Get the height of first child of our menu
getSubmenuHeight = nextSubmenu.outerHeight( true );//Get the height of of menu
nextSubmenu
.removeClass( 'sub-menu' )
.addClass( 'jsub-menu' ).css( {
'visibility': '',
'height': getHeightFirstChild + 'px',
'width': '0',
'z-index': '99999'
} );
nextSubmenu.filter( ':not(:animated)' ).animate( { width: getWidth }, 500, function() {
//animate width of menu,callback animate height
if ( nextSubmenu.children( 'li' ).length > 1 ) {
nextSubmenu.animate( { height: getSubmenuHeight }, 500 );
}
} );
}
};
//Dummy function for hoverIntent plugin
function nullFunc() {
return;
}
//This function is responsible for animating slideDown of our menu on Desktop Layout
var animateMenuDropdown = function() {
var $this = $( this );
if ( ! isDesktop() ) {
return false;
}
if ( $this.children( 'ul:first' ).hasClass( 'jsub-menu' ) ) {//Let's check if "jsub-menu" Class is here
return false;//If it is ("jsub-menu" here) don't SlideDown...
} else {//Else slide down if no class
$this.find( 'ul.sub-menu:first' ).not( ':visible' ).slideDown( 500 );
}
};
//This function is responsible for animating slideUp of our menu on Desktop Layout
var animateUp = function() {
if ( ! isDesktop() ) {
return false;
}
var $this = $( this );
$this.find( 'ul:first' )
.slideUp( 500, function() { //Slide Up our open menu/es,execute callback function
var $this = $( this );
$this.removeClass( 'jsub-menu' ).addClass( 'sub-menu' ).removeAttr( 'style' );//Remove our added Jquery classes and add default Wordpress class "sub-menu"
} );
};
//This function is responsible for animating slideDown of our FIRST menu on Tablet Layout
function slideDownTabletsFirst( $this, minus, getHref ) {
if ( ! isDesktop() ) {
return false;
}
if ( $this.next( '.sub-menu' ).length ) {
$this.removeAttr( 'href' );
$this.next( '.sub-menu' ).slideDown( 300, function() {
var thisPrev = $( this ).prev( 'a' );
manageHref( thisPrev, getHref, minus );
} );
}
if ( $( '.nav ul.sub-menu:visible' ).length > 1 ) {
$( '.sub-menu:not(:animated)' ).slideUp( 300, function() {
var $this = $( this ),
minus = $( '.minus' );
decorationUp( $this, minus );
} );
}
}
//This function is responsible for animating slideDown of our SECOND menu (sub-menu) on Tablet Layout
function slideDownTabletsSecond( $this, minus, getHref ) {
if ( ! isDesktop() ) {
return false;
}
if ( $this.next( '.sub-menu' ).length ) {//Prevent link default behaviour (on first touch don't go to other page )
$this.removeAttr( 'href' );
$this.next( '.sub-menu' ).slideDown( 300, function() {
var thisPrev = $( this ).prev( 'a' );
manageHref( thisPrev, getHref, minus );
} );
}
}
//This function is responsible for animating slideUp/slideDown Menu on Phones Layout
function slideDownPhones( $this, minus, getHref ) {
if ( isDesktop() ) {
return false;
}
if ( $this.next( '.sub-menu' ).length ) {//Prevent link default behaviour (on first touch don't go to other page )
$this.removeAttr( 'href' );
$this.next( '.sub-menu' ).slideDown( 300, function() {
var thisPrev = $( this ).prev( 'a' );
manageHref( thisPrev, getHref, minus );
} );
}
}
//This is MAIN Function it is responsible for animating slideDown/slideUp of our menu on Mobile Layout's (Tablets/Phones)
//Uses functions defined above: decorationUp(),slideDownTabletsFirst(),slideDownTabletsSecond(),slideDownPhones()
function animateMobileMenu() {
$( '.nav ul a:not(.nav ul ul a)' ).on( 'touchstart touchmove', function() {//BIG TABLETS (Desktop Layout) checks if first "ul li a:first" is clicked...
var $this = $( this ),
minus = $( '.minus' ),
getHref = $this.attr( 'href' );
slideDownTabletsFirst( $this, minus, getHref );
} );
$('.nav ul ul a').on( 'touchstart touchmove', function() {
var $this = $( this ),
minus = $( '.minus' ),
getHref = $this.attr( 'href' );
slideDownTabletsSecond( $this, minus, getHref );
} );
$( '.nav a .minus' ).on( 'touchstart touchmove', function( e ) {
e.preventDefault();
e.stopPropagation();
var parentsUntil = $( this ).parentsUntil( 'ul' ).find( 'ul' ),
getFirstAnchor = $( 'ul.jnav a:not(ul.sub-menu a)' );
if ( isDesktop() && ! $( this ).parent().is( getFirstAnchor ) ) {//BIG TABLETS (Desktop Layout) checks if "ul ul li a" is clicked...
parentsUntil.slideUp( 300, function() {
var $this = $( this ),
minus = $( '.minus' );
decorationUp( $this, minus );
} );
} else if ( isDesktop() && $( this ).parent().is( getFirstAnchor ) ) {//BIG TABLETS (Desktop Layout) checks if "ul li a" is clicked...
parentsUntil.slideUp( 300, function() {
var $this = $( this ),
minus = $( '.minus' );
decorationUp( $this, minus );
} );
} else if ( ! isDesktop() ) {//MOBILE PHONES (Mobile Layout)
$( this ).parent( 'a' ).next( '.sub-menu' ).slideUp( 300, function() {
var $this = $( this ),
minus = $( '.minus' );
decorationUp( $this, minus );
} );
}
} );
$( '.nav a' ).on( 'touchstart', function() {
oldTouchPos = $( window ).scrollTop();
} ).on( 'touchend', function() {
var newTouchPos = $( window ).scrollTop(),
$this = $( this ),
minus = $( '.minus' ),
getHref = $this.attr( 'href' );
if ( ( Math.abs( oldTouchPos - newTouchPos ) ) < 3 && ! $this.children( '.minus' ).is( ':visible' ) ) {
slideDownPhones( $this, minus, getHref );
}
} );
$( '.menu-toggle' ).on( 'touchstart', function() {
getFirstLink.attr( 'href', '#' );//Remove Href on first menu Item Just in case the touch begins outside the registered area (user touches BOTH .menu-toggle AND first Link )
menuTogglePos = $( window ).scrollTop();
} ).on( 'touchend', function() {
var newMenuTogglePos = $( window ).scrollTop();
if ( ( Math.abs( menuTogglePos - newMenuTogglePos ) ) < 3 ) {
var navelem = $( '.nav' );
if ( navelem.is( ':animated' ) || $( '.search-text' ).is( ':visible' ) ) {
return false;
} else {
navelem.slideToggle( 500, function() {
getFirstLink.attr( 'href', getFirstLinkAttr );
} );
}
}
} );
}
$( '.nav ul:first ,ul.nav' ).removeClass( 'nav' ).addClass( 'jnav' );//Add jquery Class to our menu (If Menu Is Created via Wordpress Backend)
$( '.nav ul.children' ).removeClass( 'children' ).addClass( 'sub-menu' );//Add jquery Class to our menu (Default menu if no menu is created)
// Execute menu hover states and functions(animate functions,width and height properties in order to make our animations on Desktop Machines)
// Uses HoverIntent plugin
var config = {
sensitivity: 6, // number = sensitivity threshold (must be 1 or higher)
interval: 300, // number = milliseconds for onMouseOver polling interval
over: animateMenuWidth, // function = onMouseOver callback (REQUIRED)
timeout: 500,// number = milliseconds delay before onMouseOut
out: nullFunc// function = onMouseOut callback (EMPTY in order to avoid js errors)
};
var config2 = {
sensitivity: 6, // number = sensitivity threshold (must be 1 or higher)
interval: 300, // number = milliseconds for onMouseOver polling interval
over: animateMenuDropdown, // function = onMouseOver callback (REQUIRED)
timeout: 500, // number = milliseconds delay before onMouseOut
out: animateUp // function = onMouseOut callback
};
if ( "ontouchstart" in document ) {
animateMobileMenu();
} else {//On desktop hover
var subMenuAnchor = $( 'ul.jnav ul.sub-menu a' );
subMenuAnchor.hover( function() {
if ( ! isDesktop() ) {
return false;
}
$( this ).addClass( 'active' );
}, function() {
$( this ).removeClass( 'active' ).removeAttr( 'class' );
} );
subMenuAnchor.hoverIntent( config );
$( 'ul.jnav li' ).hoverIntent( config2 );
//If user is viewing page with resised browser show the Mobile Menu and handle the click's
$( '.menu-toggle' ).on( 'click', function() {
var menuContainer = $( '.nav' );
if ( menuContainer.is( ':animated' ) ) {
return false;
}
menuContainer.slideToggle( 500 );
} );
$('.nav a').click( function() {
var $this = $( this ),
minus = $( '.minus' ),
getHref = $this.attr( 'href' );
slideDownPhones( $this, minus, getHref );
} );
$( '.nav a .minus' ).click( function ( e ) {
e.preventDefault();
e.stopPropagation();
$( this ).parent( 'a' ).next( '.sub-menu' ).slideUp( 300, function() {
var $this = $( this ),
minus = $( '.minus' );
decorationUp( $this, minus );
} );
} );
}
toggleMenuVisibility();
equalizeHeight_If_Description();
$(window).on( "resize", function() {
var newWindowWidth = $( window ).width();
if ( newWindowWidth !== cachedWindowWidth ) { //Prevent toggleMenuVisibility() from running on window scroll (Android/IOS)
// Update the window width for next time
cachedWindowWidth = $( window ).width();
//Do menu visibility toggle
toggleMenuVisibility();
if ( isDesktop() ) { //If desktop remove all
$( '.jnav .sub-menu' ).slideUp( 500 );
$( '.nav ul a' ).css( { 'text-decoration': 'none' } );
$( '.searchbox' ).show();
$( '.minus' ).css( { 'display': 'none' } );
}
}
equalizeHeight_If_Description();
} );
})();
});
1 Answer 1
Ok, let's talk about the elephant in the room: your CSS being in JS. This is going to be very hard to maintain. If you left this code as is, took a 2-week vacation or something, and came back, I bet you will have no energy to untangle this code.
The bulk of what you're trying to achieve can be done in plain CSS. The movements and dimension changes can be done with value transitions. While it's equally daunting to do in CSS, it still makes sense since CSS is built specifically for styling.
$(window).on( "resize", function() { ... } )
One of the disadvantages when doing responsive in JS is you eventually resort to listening to the resize
event. Note that resize fires several times while adjusting the window. This means that your functions fire that number of times as well. If you have functions that are slow, like DOM manipulation, this resize can cause stuttering.
Your code also doesn't serve as documentation. With CSS, especially when written in BEM, you have some degree of being able to figure out what goes where. With this code, you don't even see the structure of the menu.
In other things, labelling things as "desktop", "tablet" and "phone" isn't really ideal. Screen size has nothing to do with the device. A phone can be as large as a 7in tablet. A tablet can be as large as a desktop screen. A 2-in-1 machine can be a desktop as well as a tablet. It's a bad naming scheme use device classification for screen sizes.
Suggesting you try achieving all of this in CSS first, then augment with JS.
Explore related questions
See similar questions with these tags.
isDesktop
within an else statement where the previous if saysif( !isDesktop )
? \$\endgroup\$isDesktop
? The only way you would ever get to that if statement is if!isDesktop
istrue
. If!isDesktop
isfalse
, thenisDesktop
istrue
. \$\endgroup\$