ravivit9
3/12/2014 - 11:37 AM

.readme.md

body, html {
    height: 100%;
    overflow-x:hidden;
}

#outer-wrapper {
    width: 100%;
    height: 100%;
    position: relative;

    -webkit-transition:all .3s ease-in-out;
    -moz-transition:all .3s ease-in-out;
    -o-transition:all .3s ease-in-out;
    transition:all .3s ease-in-out;
}

.show-left-menu #outer-wrapper {
    /*left: 200px;*/

    -webkit-transform: translate3d(200px,0,0);
    -moz-transform: translate3d(200px,0,0);
    -ms-transform: translate3d(200px,0,0);
    -o-transform: translate3d(200px,0,0);
    transform: translate3d(200px,0,0);
}

.show-right-menu #outer-wrapper {
    /*left: -200px;*/

    -webkit-transform: translate3d(-200px,0,0);
    -moz-transform: translate3d(-200px,0,0);
    -ms-transform: translate3d(-200px,0,0);
    -o-transform: translate3d(-200px,0,0);
    transform: translate3d(-200px,0,0);
}

.off-canvas-menu {
    width: 200px;
    height:100%;
    background: #333;
    position: absolute;
    top:0;

    -webkit-box-shadow: inset -1.5em 0 1.5em -0.75em rgba(0, 0, 0, 0.25);
    -moz-box-shadow: inset -1.5em 0 1.5em -0.75em rgba(0, 0, 0, 0.25);
    box-shadow: inset -1.5em 0 1.5em -0.75em rgba(0, 0, 0, 0.25);

    padding-top: 80px;
    -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
}

nav#left-menu {
    left: -200px;
}
nav#right-menu {
    left: 100%;
}

#inner-wrapper {
    overflow: hidden;
    min-height: 100%;

    padding-top: 80px;
    -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
}

#outer-wrapper .navbar.navbar-fixed-top {
    position: absolute;
}

.navbar.navbar-fixed-top .btn.btn-navbar.off-canvas-menu-toggle {
    display: block;
    height: 40px;
    width: 40px;
    padding: 10px;
    background: #444;
    margin: 0;
    border-radius: 0;
    border-right: 1px solid #000;
}

@media (max-width: 767px) {
    body {
		padding-left: 0;
		padding-right: 0;
	}
	
	#inner-wrapper {
		padding-left: 20px;
		padding-right: 20px;
	}
	
	.navbar-fixed-top, .navbar-fixed-bottom, .navbar-static-top {
		margin-left: 0;
		margin-right: 0;
	}
}
"use strict";

(function(window) {

    window.OffCanvasMenuController = function(options){

        options = options || {};

        this.$menu = options.$menu;
        this.menuExpandedClass = options.menuExpandedClass;

        // Escape if the menu is not found.
        if(this.$menu.length == 0 || !this.menuExpandedClass)
            return;

        this.$menuToggle = options.$menuToggle || [];
        this.$menuExpandedClassTarget = options.$menuExpandedClassTarget || $('body');
        this.position = options.position || 'left';

        this.$wrapper = options.wrapper || $('#outer-wrapper');
        this.wrapper = this.$wrapper[0];

        this.dragHandleOffset = options.dragHandleOffset || this.$menuToggle.outerWidth();
        this.expandedWidth = this.$menu.outerWidth();

        if(this.$menuToggle.length > 0){
            var self = this;

            // Set up toggle button:
            this.$menuToggle.click(function(){
                var method = !self.$menuExpandedClassTarget.hasClass(self.menuExpandedClass) ? 'addClass' : 'removeClass';
                self.$menuExpandedClassTarget[method](self.menuExpandedClass);
            });
        }

        // add event listeners
        if (this.wrapper.addEventListener) {
            this.wrapper.addEventListener('touchstart', this, false);
            this.wrapper.addEventListener('touchmove', this, false);
            this.wrapper.addEventListener('touchend', this, false);
            this.wrapper.addEventListener('touchcancel', this, false);
        }

    }

    window.OffCanvasMenuController.prototype = {

        start: null,

        handleEvent: function (e) {
            switch (e.type) {
                case 'touchstart': this.onTouchStart(e); break;
                case 'touchmove':  this.onTouchMove(e); break;
                case 'touchcancel':
                case 'touchend': this.onTouchEnd(e); break;
            }
        },

        currentPosition: function(){
            return this.position == 'left' ? this.$menu.offset().left + this.expandedWidth
                : this.$menu.offset().left;
        },

        inBounds: function(position){
            return (this.position == 'left' && position >= 0 && position <= this.expandedWidth) ||
                (position >= -this.expandedWidth && position <= 0);
        },

        onTouchStart: function(e){

            var pageX = e.touches[0].pageX;

            // Escape if invalid start touch position
            if(this.currentPosition() - this.dragHandleOffset > pageX ||
                this.currentPosition() + this.dragHandleOffset < pageX)
                return;

            this.start = {
                startingX: this.currentPosition(),

                // get touch coordinates for delta calculations in onTouchMove
                pageX: pageX,
                pageY: e.touches[0].pageY
            };

            // reset deltaX
            this.deltaX = this.$wrapper.position().left;

            // used for testing first onTouchMove event
            this.isScrolling = undefined;

            // set transition time to 0 for 1-to-1 touch movement
            this.wrapper.style.MozTransitionDuration = this.wrapper.style.webkitTransitionDuration = 0;

            e.stopPropagation();
        },

        onTouchMove: function(e){

            // Escape if invalid start or not in bounds:
            if(!this.start)
                return;

            this.deltaX = e.touches[0].pageX - this.start.pageX;

            // determine if scrolling test has run - one time test
            if (typeof this.isScrolling == 'undefined') {
                this.isScrolling = !!(this.isScrolling || Math.abs(this.deltaX) < Math.abs(e.touches[0].pageY - this.start.pageY));
            }

            // if user is not trying to scroll vertically
            if (!this.isScrolling) {

                // prevent native scrolling
                e.preventDefault();

                var newPos = this.position == 'left' ? this.start.startingX + this.deltaX
                    : this.deltaX - ($(window).width() - this.start.startingX);

                if(!this.inBounds(newPos))
                    return;

                // translate immediately 1-to-1
                this.wrapper.style.MozTransform = this.wrapper.style.webkitTransform = 'translate3d(' + newPos + 'px,0,0)';

                e.stopPropagation();
            }


        },

        onTouchEnd: function(e){

            // Escape if invalid start:
            if(!this.start)
                return;

            // if not scrolling vertically
            if (!this.isScrolling) {

                // determine if swipe will trigger open/close menu
                var isOpeningMenu = (this.position == 'left' && this.deltaX > 0) ||
                    (this.position == 'right' && this.deltaX < 0);

                // Reset styles
                this.$wrapper.attr('style', '');

                // open/close menu:
                var method = isOpeningMenu ? 'addClass' : 'removeClass';
                this.$menuExpandedClassTarget[method](this.menuExpandedClass);
            }

            // Reset start object:
            this.start = null;

            e.stopPropagation();
        }

    }
})(window);
<!DOCTYPE html>
<html>
<head>
  <title>Off canvas menu example</title>
  <link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
  <link href="./styles.css" rel="stylesheet">
  
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
  <script src="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script>
  <script src="./off-canvas-menu.js"></script>
  
  <script>
    "use strict";
    
    $(document).ready(function(){
    
        new OffCanvasMenuController({
            $menu: $('#left-menu'),
            $menuToggle: $('#left-menu-toggle'),
            menuExpandedClass: 'show-left-menu',
            position: 'left'
        });
    
        new OffCanvasMenuController({
            $menu: $('#right-menu'),
            $menuToggle: $('#right-menu-toggle'),
            menuExpandedClass: 'show-right-menu',
            position: 'right'
        });
    });
  </script>
</head>
<body>

    <div id="outer-wrapper">
        <div id="inner-wrapper">


          <nav id="left-menu" class="off-canvas-menu">
            <ul>
              <li>
                <a href="#">Chapter 1</a>
              </li>
              <li>
                <a href="#">Chapter 2</a>
              </li>
              <li>
                <a href="#">Chapter 3</a>
              </li>
              <li>
                <a href="#">Chapter 4</a>
              </li>
              <li>
                <a href="#">Chapter 5</a>
              </li>
            </ul>
          </nav>

          <nav id="right-menu" class="off-canvas-menu">
            <ul>
              <li>
                <a href="#">Chapter 1</a>
              </li>
              <li>
                <a href="#">Chapter 2</a>
              </li>
              <li>
                <a href="#">Chapter 3</a>
              </li>
              <li>
                <a href="#">Chapter 4</a>
              </li>
              <li>
                <a href="#">Chapter 5</a>
              </li>
            </ul>
          </nav>

            <div class="navbar navbar-inverse navbar-fixed-top">
              <div class="navbar-inner">
                <div class="pull-left">
                  <button type="button" id="left-menu-toggle" class="btn btn-navbar off-canvas-menu-toggle">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                  </button>
                </div>
                <div class="pull-right">
                  <button type="button" id="right-menu-toggle" class="btn btn-navbar off-canvas-menu-toggle">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                  </button>
                </div>
              </div>
            </div>

            <div class="container">

                <div class="span12">
                
                  <h1>Hey hey hey!</h1>

                  This example is based on elements of <a href="http://coding.smashingmagazine.com/2013/01/15/off-canvas-navigation-for-responsive-website/">this post.</a>

                </div>
            </div>
        </div>
    </div>

</body>
</html>

Off canvas menu with touch handles.

View an example here.

This example is based on elements of this post.

Incliudes a OffCanvasMenuController to handle touch events bound to the off canvas menu. For example when swiping from the outer left side to the right, the left off canvas menu is dragged along.

Example usage

new OffCanvasMenuController({
    $menu: $('#left-menu'),
    $menuToggle: $('#left-menu-toggle'),
    menuExpandedClass: 'show-left-menu',
    position: 'left'
});