Skip to content

Instantly share code, notes, and snippets.

@jmyrland
Last active July 12, 2019 10:15
Show Gist options
  • Save jmyrland/5450222 to your computer and use it in GitHub Desktop.
Save jmyrland/5450222 to your computer and use it in GitHub Desktop.
Off canvas menu with touch handles.

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'
});
<!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>
"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);
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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment