Search code examples
javascripthtmlcsstouchmouse

How can I get the side menu bar to move and react instantly according to mouse position?


Problem

The side menubar is moving at 1 pixel per frame and catches up with the position of the mouse. When releasing the left mouse button, the menu stays there and CSS does not do the transitioning according to whether 'cbMenu' is checked or not. The browsers I've tried that have this behavior is in Firefox and Chrome.

Also, if I move my mouse too fast to the right, the left position of the menu will exceed the left side of the browser window, which is 0, and it is equivalent to setting the menu's left property greater than 0 in CSS. The menu won't move since the "onmousemove" event is slow to react. Sometimes the menu moves very fast but not always.

Code

HTML

The code below is actually written in PHP, so I'm only showing the top half.

  <div class="header_menu_outer">
    <input type="checkbox" id="cbMenu" class="cbmenu" name=""
      value="" />
    <div id="mainmenu" class="header_menu_inner">
      <header id="header" class="header">
        <label for="cbMenu" class="menuicon_label">&#9776;
        </label>
        <h1 class="header_h1">Grayson Peddie's Course Notes</h1>
      </header>
      
      <nav id="nav" class="nav">
        <ul class="nav_menu">
          <li class="nav_menu_li">
            <a class="nav_menu_a" href="/">Home</a>
          </li>
          <?php for($i = 0; $i < count($cnmdirs); $i++) { ?>
          <li class="nav_menu_li">
            <a class="nav_menu_a" href="/<?=$cnmdirs[$i] ?>"><?=$cnmNames[$i]["title"] ?></a>
          </li>
          <?php } ?>
        </ul>
      </nav>
    </div>
  </div>

  <main id="content" class="content" role="main">
    <div class="inner_header">
      <label for="cbMenu" class="menuicon_label">&#9776;
      </label>
      <h2 class="inner_header_h2"><?=$pageTitle ?></h2>
    </div>

The closing tag for '' is in a different PHP file in MVC (model, view, and controller).

CSS

@media screen and (max-width: 1200px ) {
  #cbMenu:checked + .header_menu_inner { left: 0; transition: left 0.5s linear; }
  #cbMenu + .header_menu_inner { transition: left 0.5s linear; }
  .menuicon { display: table-cell; width: 2.5em; line-height: 2.5em; text-align: center; }
  body {
    grid-template-columns: 0 auto;
  }
  .header_menu_inner { left: -400px; z-index: 10; }
}

Javascript

var mouseCX = 0; // Store the position of the mouse coordinate in X.
var mainmenu = document.getElementById("mainmenu");
var content  = document.getElementById("content");
var cbMenu = document.getElementById("cbMenu");
var menu_offsetX = 0;
var mouseDown = false;

// Get the current x-position of the mouse and store in mouseCX. "C" is "client."
document.body.onmousemove = function(event) {
    // Get the position of the mouse or touch point.
    mouseCX = (typeof event.touches !== "undefined") ? event.touches[0].clientX :
        event.clientX;
    // Only if the finger on the touchscreen or the left mouse button is held down.
    if(mouseDown)
    {
        // Don't exceed the browser's left X coordinate of 0 or higher.
        if(mainmenu.offsetLeft <= 0)
        {
            // Move the side menu according to the stored distance
            // between the mouse position and initial menu position,
            // along with the current position of the mouse or touch.
            mainmenu.style.left = (mouseCX - menu_offsetX) + 'px';
        }
    }
}

var beginMenuMovement = function()
{
    // For the first line, if the side menu bar is open and if the mouse cursor is
    // not positioned over the side menu bar, don't perform the rest of the code.
    // Or in the second line, if the menu bar is not opened and starts from the center
    // of the screen, then again, do not perform instructions in the onmousedown event.
    if((cbMenu.checked && !this.matches('#mainmenu')) ||
       (!cbMenu.checked && mouseCX > 100))
        return false;
    
    // The web page must be within 1200 pixels in width as the menu
    // is hidden by default.
    if(window.innerWidth < 1200)
    {
        mouseDown = true;
        // If the menu's left position is -400 pixels, then the
        // absolute value is 400 pixels. Add in the position of
        // the mouse in X coordinate to get the distance between
        // the menu and mouse position.
        menu_offsetX = Math.abs(mainmenu.offsetLeft) + mouseCX;
    }
}

mainmenu.onmousedown = beginMenuMovement;
content.onmousedown = beginMenuMovement;
mainmenu.ontouchstart = beginMenuMovement;
content.ontouchstart = beginMenuMovement;

var endMenuMovement = function()
{
    if(window.innerWidth < 1200)
    {
        mouseDown = false;
        
        // The entire width of the menu would be 400 pixels. Divide by 2 to
        // get 200 pixels at a negative value. If the position of the menu
        // is less than -200 pixels, the cbMenu should be left unchecked and
        // should transition back to -400 pixels. If the left position is
        // greater than -200 pixels, the cbMenu should be checked and the
        // CSS transition should begin transitioning to 0.
        // Related in CSS: transition: left 0.5s linear
        menu_minWidth = (mainmenu.offsetWidth / 2) - mainmenu.offsetWidth;
        if(mainmenu.offsetLeft > menu_minWidth)
        {
            cbMenu.checked = true;
        }
        else
        {
            cbMenu.checked = false;
        }
    }
}

mainmenu.onmouseup = endMenuMovement;
content.onmouseup = endMenuMovement;
mainmenu.ontouchend = endMenuMovement;
content.ontouchend = endMenuMovement;

Expected Behavior

I want the menu to react instantly according to the position of the mouse cursor.

HTML Code Explained:

The slideout menu uses a checkbox trick for those who have JavaScript turned off. If a user clicks in a hamburger label (☰), the menu will slide out until the user closes the menu by clicking in the hamburger label. This is useful if a user who browses the Internet uses NoScript in Firefox as there can be bad/malicious scripts floating in the Internet. The only functionality a user won't be able to do without Javascript is swipe from left to right to open the menu from the side of a touchscreen.

Note:

No jQuery code please. The jQuery library is bloated in file size, so I want to keep my script small. Sure, jQuery is great and easy to use, but many people might forget how big the jQuery library is.

Also, I've included a touch tag, although I'm only focusing on using a mouse. I have implemented a single touch functionality, but I couldn't get the menu to slide out.

Question

In addition to my question from the title of my thread regarding how to get the menu to react in according to the position of the mouse, how can I get the code for "onmouseup" to behave similar to CSS transition?


Solution

  • Intro

    I've made changes to CSS and JavaScript for enabling transform and disabling CSS transitions when JavaScript is turned on or when a user allows the domain/IP address in NoScript extension for Firefox. By using transform, sliding the menu in and out is a lot faster with translateX() than calling mainmenu.style.left every time when mousemove event is fired constantly. Plus, I've also learned to play CSS3 animations at a specific point to an end. What follows is a code for CSS and JavaScript.

    Code

    CSS

    @media screen and (max-width: 1200px ) {
      #cbMenu:checked + .header_menu_inner { left: 0; transition: left 0.5s linear; }
      #cbMenu + .header_menu_inner { transition: left 0.5s linear; }
      .menuicon { display: table-cell; width: 2.5em; line-height: 2.5em; text-align: center; }
      body {
        grid-template-columns: 0 auto;
      }
      #mainmenu { left: -360px; z-index: 10; transform: translateX(0);
                  box-shadow: 0 2em 2em 2em rgba(0,0,0,0.5); }
    }
    

    What changed in CSS is a change from .header_menu_inner to #mainmenu and the new addition of "transform" which sets the translation in X coordinate to 0. JavaScript code removes the .header_menu_inner class in order to prevent CSS transitions from occurring.

    JS

    var mouseCX = 0; // Store the position of the mouse coordinate in X.
    var prevMouseCX = 0; // Store the previous mouse coordinate in X.
    
    // HTML Elements
    var mainmenu = document.getElementById("mainmenu");
    mainmenu.classList.remove("header_menu_inner");
    var content  = document.getElementById("content");
    var cbMenu = document.getElementById("cbMenu");
    
    var mouseDown = false;
    var distanceCX = 0;
    
    window.onresize = (() => {
        // Skip the check if the browser window is greater than 1200 pixels
        // and the cbMenu check box is not checked.
        if(window.innerWidth > 1200 && cbMenu.checked)
        {
            cbMenu.checked = false;
            var matrixValues = menu_getTransformMatrix();
            menu_slideLeft(matrixValues[4]);
        }   
    }); 
    
    // If the menu button is toggled, open the menu. Else, close the menu.
    cbMenu.onchange = function(event) {
        var matrixValues = menu_getTransformMatrix();
        if(cbMenu.checked) menu_slideRight(matrixValues[4]);
        else menu_slideLeft(matrixValues[4]);
    }
    
    // Get the current x-position of the mouse and store in mouseCX. "C" is "client."
    // This allows the user to slide the side menu in and out.
    var menuInMotion = function(event) {
        // If using a touch screen, this is a one-finger operation. Otherwise, the
        // user is using a mouse.
        mouseCX = (typeof event.touches !== "undefined") ? event.touches[0].clientX :
            event.clientX;
        if(mouseDown) {
            // Math.min(Math.max(min, val), max)
            // Increase or decrease the distance between the left edge of the screen
            // and where the mouse cursor is located.
            distanceCX += mouseCX - prevMouseCX;
            mainmenu.style.transform = 'translateX('
                + Math.min(Math.max(0, distanceCX), mainmenu.offsetWidth) + 'px)';
        }
        // Store the previous position of the mouse cursor. This will be used to
        // calculate the direction the mouse cursor is going.
        prevMouseCX = mouseCX;
    }
    
    document.body.onmousemove = menuInMotion;
    document.body.ontouchmove = menuInMotion;
    
    // User wants to slide the menu out from the side or slide the menu
    // back towards the left. Start moving the menu.
    var beginMenuMovement = function()
    {
        // If the menu is open, only allow the user to swipe inside the menu bar.
        // Otherwise, if the menu is closed, the user can only slide out from the
        // left edge of the web page.
        // Only move the menu if the width of the viewport is 1200 pixels or less.
        if(window.innerWidth <= 1200
            && ((cbMenu.checked && this.matches('#mainmenu')) || mouseCX < 20))
        {
            mouseDown = true;
            content.style.userSelect = "none";
        }
    }
    
    
    mainmenu.onmousedown = beginMenuMovement;
    content.onmousedown = beginMenuMovement;
    mainmenu.ontouchstart = beginMenuMovement;
    content.ontouchstart = beginMenuMovement;
    
    // User has finished moving the side menu bar.
    var endMenuMovement = function()
    {
        // Same for when the user releases the left mouse button.
        if(window.innerWidth <= 1200)
        {
            mouseDown = false;
            // Get the translation values for the side bar menu.
            var matrixValues = menu_getTransformMatrix();
            // If the user slides the menu as far to the left,
            // the side menu bar is closed (unchecked).
            menu_minWidth = mainmenu.offsetWidth / 2;
            if(matrixValues[4] > menu_minWidth)
            {
                cbMenu.checked = true;
                menu_slideRight(matrixValues[4]);
            }
            else
            {
                cbMenu.checked = false;
                menu_slideLeft(matrixValues[4]);
            }
            content.style.userSelect = "";
        }
    }
    
    mainmenu.onmouseup = endMenuMovement;
    content.onmouseup = endMenuMovement;
    mainmenu.ontouchend = endMenuMovement;
    content.ontouchend = endMenuMovement;
    
    // The two functions below this comment is for performing animations and for
    // holding at the start or end position of the side menu bar's left coordinate.
    function menu_slideRight(matrixValues)
    {
        // Play the animation that starts from where the user releases the mouse
        // button and slide to the end, which equals the width of the menu.
        // Source: https://developer.mozilla.org/en-US/docs/Web/API/Element/animate
        mainmenu.animate([
            { transform: 'translateX(' + matrixValues + 'px)' },
            { transform: 'translateX(' + mainmenu.offsetWidth + 'px)' }],
            { duration: 500 });
        // Set the new transform position to the width of the menu.
        mainmenu.style.transform = 'translateX(' + mainmenu.offsetWidth + 'px)';
        distanceCX = mainmenu.offsetWidth;
    }
    
    function menu_slideLeft(matrixValues)
    {
        mainmenu.animate([
            { transform: 'translateX(' + matrixValues + 'px)' },
            { transform: 'translateX(0px)' }],
            { duration: 500 });
        mainmenu.style.transform = 'translateX(0px)';
        distanceCX = 0;
    }
    
    // Source: https://zellwk.com/blog/css-translate-values-in-javascript/
    function menu_getTransformMatrix() {
        var matrix = window.getComputedStyle(mainmenu).transform;
        return matrix.match(/matrix.*\((.+)\)/)[1].split(', ');
    }
    

    An explanation of how my code works is in the comments marked with //.

    Did It Work?

    So far, swiping in from the left works great as long as JavaScript is enabled. If JavaScript is disabled, at least I can provide fallback to just opening and closing a side menu bar. The CSS3 animation proves very useful as I don't have to do a for loop. And I did not know I can dynamically create animations!

    Pitfalls

    The swiping from left to right didn't work in Android well when using Firefox and Chrome. Plus, in Chrome for Android, the swiping from left edge of the screen causes conflict with the back/forward gestures. https://www.neowin.net/news/chrome-adds-swipe-gestures-on-android-for-going-back-and-forth-through-browser-history/

    And yes, GNOME 3 desktop environment for a touchscreen laptop also conflicts with GNOME's Overview gesture. If I swipe from the left edge of the screen, instead of opening a side menu bar, the swipe-from-left-edge gesture activates the overview which shows something quite similar to macOS's Expose, but with dynamic workspaces to the right of the screen and application icons to the left. I wanted to share information about the GNOME 3 desktop environment for those who have never ventured into Linux.

    Summary

    In short, adding support for swiping from the left edge of the screen is probably not the best idea.

    But hey! At least I can share my code and my findings for those who want to try it out.