1
\$\begingroup\$

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();

} );    

})();

});
\$\endgroup\$
  • \$\begingroup\$ Why are you checking isDesktop within an else statement where the previous if says if( !isDesktop )? \$\endgroup\$ – Trojan404 Aug 12 '16 at 18:09
  • \$\begingroup\$ If !Desktop then hide the menu else show the menu... \$\endgroup\$ – Dejo Dekic Aug 12 '16 at 18:18
  • \$\begingroup\$ Yes, but why do you need to check if it is isDesktop? The only way you would ever get to that if statement is if !isDesktop is true. If !isDesktop is false, then isDesktop is true. \$\endgroup\$ – Trojan404 Aug 12 '16 at 18:20
  • \$\begingroup\$ Good catch ,THX look's like that was redundant code :) \$\endgroup\$ – Dejo Dekic Aug 12 '16 at 18:50
2
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.