Swipe Side Navigation Drawer With Hammer.js

I was surprised that I couldn't find a side navbar/ navigation drawer for Bootstrap that fit what I needed. Since everything is mobile now, I assumed swipe open examples would be everywhere, but I didn't find much (at least nothing I liked). I looked into jQuery-Mobile, but the framework is quite large at 196kB minified and gzipped (though I now see they have a download builder, that might be a good way to go). I saw on a forum that Hammer.js had the swipe gesture at only 7.34kB minified and gzipped!

First thing first, I made my sidebar for my music page:

<div class="sideNavBlack" id="sideNavMenu">
    <div id="sideNavTop"></div>
    <div id="sideNavMiddle">
        <a class="js-scroll-trigger" href="#page-top" id="sideNavImg">
            <img class="img-fluid img-profile rounded-circle mx-auto mb-4" src="{% static 'img/uke-sm.jpg' %}" alt="" id="image-span">
        </a>
    </div>
    <div id="sideNavBottom">
        <ul class="navbar-nav" id="sideNavLinks">
        {% for artist in artists %}
            <li class="nav-item">
                <a class="nav-link sideNavListItem js-scroll-trigger" href="#{{ artist|slugify }}">{{ artist }}</a>
            </li>
        {% endfor %}
        </ul>
    </div>
</div>

I wanted the bar hidden on smaller screens, so you see left: -280px; for screens smaller than 991px. I also had some problems with resizing windows (hiding toobars on scroll), so to "fix" that, I just made the div larger than the viewheight (height: 120vh;). The resulting CSS is:

@media (max-width: 991px) {

    #sideNavMenu {
        position: fixed;
        top: 0;
        left: -280px;
        width: 280px;
        height: 120vh;
        overflow-x: hidden;
        transition: 0.3s;
        z-index: 1000;
    }
}

@media (min-width: 992px) {
    #sideNavMenu {
        position: fixed;
        top: 0;
        left: 0;
        width: 280px;
        height: 120vh;
        overflow-x: hidden;
        transition: 0.3s;
        z-index: 1000;
    }
}

Note: The code above does not include any of the styling inside my sidebar

Now that the sidebar is there and auto-hides when the windows shrinks, we need it to swipe out. Let's create a new javascript file, sidenav.js:

let myElement = document.getElementById('page');

// create a simple instance
// by default, it only adds horizontal recognizers
let mc = new Hammer(myElement, { inputClass: Hammer.TouchInput, cssProps: {userSelect: 'auto',}});

let menu = $('#sideNavMenu');
let sideMenu = document.getElementById("sideNavMenu");

This sets the variable myElement to the page element "page" (which is the first div in my HTML), sets the Hammer.js constructor mc, and sets the variables menu and sideMenu to access the sideNavMenu element in the corresponding js and jquery respectively. The docs say to use var mc = new Hammer(myElement); but when I did that, I was no longer able to select any text on the page. I found online, to add {inputClass: Hammer.TouchInput, cssProps: {userSelect: 'auto',}. I assume (since I didn't see it in the docs) that it disables userSelect by default as to not interfere with other input events, but setting it to 'auto' works well for me.

Next, we handle the appropriate events:

// listen to events...
mc.on("swipeleft swiperight", _.debounce(function(ev) {
    if (isMobileWidth()) {
        if (ev.type == "swipeleft") {
            sideMenu.style.left = "-280px";
        }
        else if (ev.type == "swiperight") {
            sideMenu.style.left = "0";
        }
    }
}, 100));

Where the function isMobileWidth() verifies the width via css:

//html
<div id="mobile-indicator"></div>

//js
function isMobileWidth() {
    return $('#mobile-indicator').is(':visible');
}

//css
#mobile-indicator {
    display: none;
}

@media (max-width: 991px) {
    #mobile-indicator {
        display: block;
    }
}

And _.debounce is my attempt to reduce CPU usage and prevent double triggers (though I'm pretty sure it's unnecessary) using the Underscore.js library.

Next, I wanted to show the menu on mouse over on desktop:

$(window).on('mousemove', mouseMoveHandler);

function mouseMoveHandler(e) {
    if (isMobileWidth()) {
        if (e.pageX < 20 || menu.is(':hover')) {
            // Show the menu if mouse is within 20 pixels
            // from the left or we are hovering over it
            sideMenu.style.left = "0";
        }
        else {
            // Hide the menu if the mouse is further than 20 pixels
            // from the left and it is not hovering over the menu
            // and we aren't already scheduled to hide it
            sideMenu.style.left = "-280px";
        }
    }
}

Here, the menu is open if the mouse is within 20px of the left side of the screen or if the mouse is hovering over an already open menu. Otherwise it closes.

To handle the other fringe cases, I added:

function closeSideNav() {
    if (isMobileWidth()) {
        sideMenu.style.left = "-280px";
    }
}

$(window).resize(_.debounce(function(){
        if (!isMobileWidth()){
            //get rid of js styling and let css do its thing
            sideMenu.style.left = null;
        }
    }, 100)
);

$(document).mouseleave(function () {
    if (isMobileWidth()) {
        sideMenu.style.left = "-280px";
    }
});

closeSideNav() is added to the drawer links to close the drawer (onclick="closeSideNav()").

$(window).resize clears the js styling to let the css do its thing since it is already handling hiding the element.

$(document).mouseleave closes the menu if the mouse leaves the page.

Now, let's include Underscore.js, Hammer.js, and sidenav.js in the HTML. With Django, this looks like:

{% block scripts %}
    {{ block.super }}
    <script src="{% static 'js/underscore-min.js' %}"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
    <script src="{% static 'js/sidenav.js' %}"></script>
{% endblock %}

Note: I don't want the side navbar on ALL pages, hence the {% block scripts %}

There is a lot going on right there. It was lot of trial and error for me to catch the fringe cases, but if anyone finds a bug or knows a way to reduce the CPU usage, please let me know in the comments below!

Sept. 5, 2018, 3:17 p.m.
Load Comments