8d6a2feb |
/*!
* meny 1.4
* http://lab.hakim.se/meny
* MIT licensed
*
* Created by Hakim El Hattab (http://hakim.se, @hakimel)
*/
(function( root, factory ) {
if( typeof define === 'function' && define.amd ) {
// AMD module
define( factory );
} else {
// Browser global
root.Meny = factory();
}
}(this, function () {
// Date.now polyfill
if( typeof Date.now !== 'function' ) Date.now = function() { return new Date().getTime(); };
var Meny = {
// Creates a new instance of Meny
create: function( options ) {
return (function(){
// Make sure the required arguments are defined
if( !options || !options.menuElement || !options.contentsElement ) {
throw 'You need to specify which menu and contents elements to use.';
}
// Make sure the menu and contents have the same parent
if( options.menuElement.parentNode !== options.contentsElement.parentNode ) {
throw 'The menu and contents elements must have the same parent.';
}
// Constants
var POSITION_T = 'top',
POSITION_R = 'right',
POSITION_B = 'bottom',
POSITION_L = 'left';
// Feature detection for 3D transforms
var supports3DTransforms = 'WebkitPerspective' in document.body.style ||
'MozPerspective' in document.body.style ||
'msPerspective' in document.body.style ||
'OPerspective' in document.body.style ||
'perspective' in document.body.style;
// Default options, gets extended by passed in arguments
var config = {
width: 300,
height: 300,
position: POSITION_L,
threshold: 40,
angle: 30,
overlap: 6,
transitionDuration: '0.5s',
transitionEasing: 'ease',
gradient: 'rgba(0,0,0,0.20) 0%, rgba(0,0,0,0.65) 100%)',
mouse: true,
touch: true
};
// Cache references to DOM elements
var dom = {
menu: options.menuElement,
contents: options.contentsElement,
wrapper: options.menuElement.parentNode,
cover: null
};
// State and input
var indentX = dom.wrapper.offsetLeft,
indentY = dom.wrapper.offsetTop,
touchStartX = null,
touchStartY = null,
touchMoveX = null,
touchMoveY = null,
isOpen = false,
isMouseDown = false;
// Precalculated transform and style states
var menuTransformOrigin,
menuTransformClosed,
menuTransformOpened,
menuStyleClosed,
menuStyleOpened,
contentsTransformOrigin,
contentsTransformClosed,
contentsTransformOpened,
contentsStyleClosed,
contentsStyleOpened;
var originalStyles = {},
addedEventListeners = [];
// Ongoing animations (for fallback mode)
var menuAnimation,
contentsAnimation,
coverAnimation;
configure( options );
/**
* Initializes Meny with the specified user options,
* may be called multiple times as configuration changes.
*/
function configure( o ) {
// Extend the default config object with the passed in
// options
Meny.extend( config, o );
setupPositions();
setupWrapper();
setupCover();
setupMenu();
setupContents();
bindEvents();
}
/**
* Prepares the transforms for the current positioning
* settings.
*/
function setupPositions() {
menuTransformOpened = '';
contentsTransformClosed = '';
menuAngle = config.angle;
contentsAngle = config.angle / -2;
switch( config.position ) {
case POSITION_T:
// Primary transform:
menuTransformOrigin = '50% 0%';
menuTransformClosed = 'rotateX( ' + menuAngle + 'deg ) translateY( -100% ) translateY( '+ config.overlap +'px )';
contentsTransformOrigin = '50% 0';
contentsTransformOpened = 'translateY( '+ config.height +'px ) rotateX( ' + contentsAngle + 'deg )';
// Position fallback:
menuStyleClosed = { top: '-' + (config.height-config.overlap) + 'px' };
menuStyleOpened = { top: '0px' };
contentsStyleClosed = { top: '0px' };
contentsStyleOpened = { top: config.height + 'px' };
break;
case POSITION_R:
// Primary transform:
menuTransformOrigin = '100% 50%';
menuTransformClosed = 'rotateY( ' + menuAngle + 'deg ) translateX( 100% ) translateX( -2px ) scale( 1.01 )';
contentsTransformOrigin = '100% 50%';
contentsTransformOpened = 'translateX( -'+ config.width +'px ) rotateY( ' + contentsAngle + 'deg )';
// Position fallback:
menuStyleClosed = { right: '-' + (config.width-config.overlap) + 'px' };
menuStyleOpened = { right: '0px' };
contentsStyleClosed = { left: '0px' };
contentsStyleOpened = { left: '-' + config.width + 'px' };
break;
case POSITION_B:
// Primary transform:
menuTransformOrigin = '50% 100%';
menuTransformClosed = 'rotateX( ' + -menuAngle + 'deg ) translateY( 100% ) translateY( -'+ config.overlap +'px )';
contentsTransformOrigin = '50% 100%';
contentsTransformOpened = 'translateY( -'+ config.height +'px ) rotateX( ' + -contentsAngle + 'deg )';
// Position fallback:
menuStyleClosed = { bottom: '-' + (config.height-config.overlap) + 'px' };
menuStyleOpened = { bottom: '0px' };
contentsStyleClosed = { top: '0px' };
contentsStyleOpened = { top: '-' + config.height + 'px' };
break;
default:
// Primary transform:
menuTransformOrigin = '100% 50%';
menuTransformClosed = 'translateX( -100% ) translateX( '+ config.overlap +'px ) scale( 1.01 ) rotateY( ' + -menuAngle + 'deg )';
contentsTransformOrigin = '0 50%';
contentsTransformOpened = 'translateX( '+ config.width +'px ) rotateY( ' + -contentsAngle + 'deg )';
// Position fallback:
menuStyleClosed = { left: '-' + (config.width-config.overlap) + 'px' };
menuStyleOpened = { left: '0px' };
contentsStyleClosed = { left: '0px' };
contentsStyleOpened = { left: config.width + 'px' };
break;
}
}
/**
* The wrapper element holds the menu and contents.
*/
function setupWrapper() {
// Add a class to allow for custom styles based on
// position
Meny.addClass( dom.wrapper, 'meny-' + config.position );
originalStyles.wrapper = dom.wrapper.style.cssText;
dom.wrapper.style[ Meny.prefix( 'perspective' ) ] = '800px';
dom.wrapper.style[ Meny.prefix( 'perspectiveOrigin' ) ] = contentsTransformOrigin;
}
/**
* The cover is used to obfuscate the contents while
* Meny is open.
*/
function setupCover() {
if( dom.cover ) {
dom.cover.parentNode.removeChild( dom.cover );
}
dom.cover = document.createElement( 'div' );
// Disabled until a falback fade in animation is added
dom.cover.style.position = 'absolute';
dom.cover.style.display = 'block';
dom.cover.style.width = '100%';
dom.cover.style.height = '100%';
dom.cover.style.left = 0;
dom.cover.style.top = 0;
dom.cover.style.zIndex = 1000;
dom.cover.style.visibility = 'hidden';
dom.cover.style.opacity = 0;
// Silence unimportant errors in IE8
try {
dom.cover.style.background = 'rgba( 0, 0, 0, 0.4 )';
dom.cover.style.background = '-ms-linear-gradient('+ config.position +','+ config.gradient;
dom.cover.style.background = '-moz-linear-gradient('+ config.position +','+ config.gradient;
dom.cover.style.background = '-webkit-linear-gradient('+ config.position +','+ config.gradient;
}
catch( e ) {}
if( supports3DTransforms ) {
dom.cover.style[ Meny.prefix( 'transition' ) ] = 'all ' + config.transitionDuration +' '+ config.transitionEasing;
}
dom.contents.appendChild( dom.cover );
}
/**
* The meny element that folds out upon activation.
*/
function setupMenu() {
// Shorthand
var style = dom.menu.style;
switch( config.position ) {
case POSITION_T:
style.width = '100%';
style.height = config.height + 'px';
break;
case POSITION_R:
style.right = '0';
style.width = config.width + 'px';
style.height = '100%';
break;
case POSITION_B:
style.bottom = '0';
style.width = '100%';
style.height = config.height + 'px';
break;
case POSITION_L:
style.width = config.width + 'px';
style.height = '100%';
break;
}
originalStyles.menu = style.cssText;
style.position = 'fixed';
style.display = 'block';
style.zIndex = 1;
if( supports3DTransforms ) {
style[ Meny.prefix( 'transform' ) ] = menuTransformClosed;
style[ Meny.prefix( 'transformOrigin' ) ] = menuTransformOrigin;
style[ Meny.prefix( 'transition' ) ] = 'all ' + config.transitionDuration +' '+ config.transitionEasing;
}
else {
Meny.extend( style, menuStyleClosed );
}
}
/**
* The contents element which gets pushed aside while
* Meny is open.
*/
function setupContents() {
// Shorthand
var style = dom.contents.style;
originalStyles.contents = style.cssText;
if( supports3DTransforms ) {
style[ Meny.prefix( 'transform' ) ] = contentsTransformClosed;
style[ Meny.prefix( 'transformOrigin' ) ] = contentsTransformOrigin;
style[ Meny.prefix( 'transition' ) ] = 'all ' + config.transitionDuration +' '+ config.transitionEasing;
}
else {
style.position = style.position.match( /relative|absolute|fixed/gi ) ? style.position : 'relative';
Meny.extend( style, contentsStyleClosed );
}
}
/**
* Attaches all input event listeners.
*/
function bindEvents() {
if( 'ontouchstart' in window ) {
if( config.touch ) {
Meny.bindEvent( document, 'touchstart', onTouchStart );
Meny.bindEvent( document, 'touchend', onTouchEnd );
}
else {
Meny.unbindEvent( document, 'touchstart', onTouchStart );
Meny.unbindEvent( document, 'touchend', onTouchEnd );
}
}
if( config.mouse ) {
Meny.bindEvent( document, 'mousedown', onMouseDown );
Meny.bindEvent( document, 'mouseup', onMouseUp );
Meny.bindEvent( document, 'mousemove', onMouseMove );
}
else {
Meny.unbindEvent( document, 'mousedown', onMouseDown );
Meny.unbindEvent( document, 'mouseup', onMouseUp );
Meny.unbindEvent( document, 'mousemove', onMouseMove );
}
}
/**
* Expands the menu.
*/
function open() {
if( !isOpen ) {
isOpen = true;
Meny.addClass( dom.wrapper, 'meny-active' );
dom.cover.style.height = dom.contents.scrollHeight + 'px';
dom.cover.style.visibility = 'visible';
// Use transforms and transitions if available...
if( supports3DTransforms ) {
// 'webkitAnimationEnd oanimationend msAnimationEnd animationend transitionend'
Meny.bindEventOnce( dom.wrapper, 'transitionend', function() {
Meny.dispatchEvent( dom.menu, 'opened' );
} );
dom.cover.style.opacity = 1;
dom.contents.style[ Meny.prefix( 'transform' ) ] = contentsTransformOpened;
dom.menu.style[ Meny.prefix( 'transform' ) ] = menuTransformOpened;
}
// ...fall back on JS animation
else {
menuAnimation && menuAnimation.stop();
menuAnimation = Meny.animate( dom.menu, menuStyleOpened, 500 );
contentsAnimation && contentsAnimation.stop();
contentsAnimation = Meny.animate( dom.contents, contentsStyleOpened, 500 );
coverAnimation && coverAnimation.stop();
coverAnimation = Meny.animate( dom.cover, { opacity: 1 }, 500 );
}
Meny.dispatchEvent( dom.menu, 'open' );
}
}
/**
* Collapses the menu.
*/
function close() {
if( isOpen ) {
isOpen = false;
Meny.removeClass( dom.wrapper, 'meny-active' );
// Use transforms and transitions if available...
if( supports3DTransforms ) {
// 'webkitAnimationEnd oanimationend msAnimationEnd animationend transitionend'
Meny.bindEventOnce( dom.wrapper, 'transitionend', function() {
Meny.dispatchEvent( dom.menu, 'closed' );
} );
dom.cover.style.visibility = 'hidden';
dom.cover.style.opacity = 0;
dom.contents.style[ Meny.prefix( 'transform' ) ] = contentsTransformClosed;
dom.menu.style[ Meny.prefix( 'transform' ) ] = menuTransformClosed;
}
// ...fall back on JS animation
else {
menuAnimation && menuAnimation.stop();
menuAnimation = Meny.animate( dom.menu, menuStyleClosed, 500 );
contentsAnimation && contentsAnimation.stop();
contentsAnimation = Meny.animate( dom.contents, contentsStyleClosed, 500 );
coverAnimation && coverAnimation.stop();
coverAnimation = Meny.animate( dom.cover, { opacity: 0 }, 500, function() {
dom.cover.style.visibility = 'hidden';
Meny.dispatchEvent( dom.menu, 'closed' );
} );
}
Meny.dispatchEvent( dom.menu, 'close' );
}
}
/**
* Unbinds Meny and resets the DOM to the state it
* was at before Meny was initialized.
*/
function destroy() {
dom.wrapper.style.cssText = originalStyles.wrapper
dom.menu.style.cssText = originalStyles.menu;
dom.contents.style.cssText = originalStyles.contents;
if( dom.cover && dom.cover.parentNode ) {
dom.cover.parentNode.removeChild( dom.cover );
}
Meny.unbindEvent( document, 'touchstart', onTouchStart );
Meny.unbindEvent( document, 'touchend', onTouchEnd );
Meny.unbindEvent( document, 'mousedown', onMouseDown );
Meny.unbindEvent( document, 'mouseup', onMouseUp );
Meny.unbindEvent( document, 'mousemove', onMouseMove );
for( var i in addedEventListeners ) {
this.removeEventListener( addedEventListeners[i][0], addedEventListeners[i][1] );
}
addedEventListeners = [];
}
/// INPUT: /////////////////////////////////
function onMouseDown( event ) {
isMouseDown = true;
}
function onMouseMove( event ) {
// Prevent opening/closing when mouse is down since
// the user may be selecting text
if( !isMouseDown ) {
var x = event.clientX - indentX,
y = event.clientY - indentY;
switch( config.position ) {
case POSITION_T:
if( y > config.height ) {
close();
}
else if( y < config.threshold ) {
open();
}
break;
case POSITION_R:
var w = dom.wrapper.offsetWidth;
if( x < w - config.width ) {
close();
}
else if( x > w - config.threshold ) {
open();
}
break;
case POSITION_B:
var h = dom.wrapper.offsetHeight;
if( y < h - config.height ) {
close();
}
else if( y > h - config.threshold ) {
open();
}
break;
case POSITION_L:
if( x > config.width ) {
close();
}
else if( x < config.threshold ) {
open();
}
break;
}
}
}
function onMouseUp( event ) {
isMouseDown = false;
}
function onTouchStart( event ) {
touchStartX = event.touches[0].clientX - indentX;
touchStartY = event.touches[0].clientY - indentY;
touchMoveX = null;
touchMoveY = null;
Meny.bindEvent( document, 'touchmove', onTouchMove );
}
function onTouchMove( event ) {
touchMoveX = event.touches[0].clientX - indentX;
touchMoveY = event.touches[0].clientY - indentY;
var swipeMethod = null;
// Check for swipe gestures in any direction
if( Math.abs( touchMoveX - touchStartX ) > Math.abs( touchMoveY - touchStartY ) ) {
if( touchMoveX < touchStartX - config.threshold ) {
swipeMethod = onSwipeRight;
}
else if( touchMoveX > touchStartX + config.threshold ) {
swipeMethod = onSwipeLeft;
}
}
else {
if( touchMoveY < touchStartY - config.threshold ) {
swipeMethod = onSwipeDown;
}
else if( touchMoveY > touchStartY + config.threshold ) {
swipeMethod = onSwipeUp;
}
}
if( swipeMethod && swipeMethod() ) {
event.preventDefault();
}
}
function onTouchEnd( event ) {
Meny.unbindEvent( document, 'touchmove', onTouchMove );
// If there was no movement this was a tap
if( touchMoveX === null && touchMoveY === null ) {
onTap();
}
}
function onTap() {
var isOverContent = ( config.position === POSITION_T && touchStartY > config.height ) ||
( config.position === POSITION_R && touchStartX < dom.wrapper.offsetWidth - config.width ) ||
( config.position === POSITION_B && touchStartY < dom.wrapper.offsetHeight - config.height ) ||
( config.position === POSITION_L && touchStartX > config.width );
if( isOverContent ) {
close();
}
}
function onSwipeLeft() {
if( config.position === POSITION_R && isOpen ) {
close();
return true;
}
else if( config.position === POSITION_L && !isOpen ) {
open();
return true;
}
}
function onSwipeRight() {
if( config.position === POSITION_R && !isOpen ) {
open();
return true;
}
else if( config.position === POSITION_L && isOpen ) {
close();
return true;
}
}
function onSwipeUp() {
if( config.position === POSITION_B && isOpen ) {
close();
return true;
}
else if( config.position === POSITION_T && !isOpen ) {
open();
return true;
}
}
function onSwipeDown() {
if( config.position === POSITION_B && !isOpen ) {
open();
return true;
}
else if( config.position === POSITION_T && isOpen ) {
close();
return true;
}
}
/// API: ///////////////////////////////////
return {
configure: configure,
open: open,
close: close,
destroy: destroy,
isOpen: function() {
return isOpen;
},
/**
* Forward event binding to the menu DOM element.
*/
addEventListener: function( type, listener ) {
addedEventListeners.push( [type, listener] );
dom.menu && Meny.bindEvent( dom.menu, type, listener );
},
removeEventListener: function( type, listener ) {
dom.menu && Meny.unbindEvent( dom.menu, type, listener );
}
};
})();
},
/**
* Helper method, changes an element style over time.
*/
animate: function( element, properties, duration, callback ) {
return (function() {
// Will hold start/end values for all properties
var interpolations = {};
// Format properties
for( var p in properties ) {
interpolations[p] = {
start: parseFloat( element.style[p] ) || 0,
end: parseFloat( properties[p] ),
unit: ( typeof properties[p] === 'string' && properties[p].match( /px|em|%/gi ) ) ? properties[p].match( /px|em|%/gi )[0] : ''
};
}
var animationStartTime = Date.now(),
animationTimeout;
// Takes one step forward in the animation
function step() {
// Ease out
var progress = 1 - Math.pow( 1 - ( ( Date.now() - animationStartTime ) / duration ), 5 );
// Set style to interpolated value
for( var p in interpolations ) {
var property = interpolations[p];
element.style[p] = property.start + ( ( property.end - property.start ) * progress ) + property.unit;
}
// Continue as long as we're not done
if( progress < 1 ) {
animationTimeout = setTimeout( step, 1000 / 60 );
}
else {
callback && callback();
stop();
}
}
// Cancels the animation
function stop() {
clearTimeout( animationTimeout );
}
// Starts the animation
step();
/// API: ///////////////////////////////////
return {
stop: stop
};
})();
},
/**
* Extend object a with the properties of object b.
* If there's a conflict, object b takes precedence.
*/
extend: function( a, b ) {
for( var i in b ) {
a[ i ] = b[ i ];
}
},
/**
* Prefixes a style property with the correct vendor.
*/
prefix: function( property, el ) {
var propertyUC = property.slice( 0, 1 ).toUpperCase() + property.slice( 1 ),
vendors = [ 'Webkit', 'Moz', 'O', 'ms' ];
for( var i = 0, len = vendors.length; i < len; i++ ) {
var vendor = vendors[i];
if( typeof ( el || document.body ).style[ vendor + propertyUC ] !== 'undefined' ) {
return vendor + propertyUC;
}
}
return property;
},
/**
* Adds a class to the target element.
*/
addClass: function( element, name ) {
element.className = element.className.replace( /\s+$/gi, '' ) + ' ' + name;
},
/**
* Removes a class from the target element.
*/
removeClass: function( element, name ) {
element.className = element.className.replace( name, '' );
},
/**
* Adds an event listener in a browser safe way.
*/
bindEvent: function( element, ev, fn ) {
if( element.addEventListener ) {
element.addEventListener( ev, fn, false );
}
else {
element.attachEvent( 'on' + ev, fn );
}
},
/**
* Removes an event listener in a browser safe way.
*/
unbindEvent: function( element, ev, fn ) {
if( element.removeEventListener ) {
element.removeEventListener( ev, fn, false );
}
else {
element.detachEvent( 'on' + ev, fn );
}
},
bindEventOnce: function ( element, ev, fn ) {
var me = this;
var listener = function() {
me.unbindEvent( element, ev, listener );
fn.apply( this, arguments );
};
this.bindEvent( element, ev, listener );
},
/**
* Dispatches an event of the specified type from the
* menu DOM element.
*/
dispatchEvent: function( element, type, properties ) {
if( element ) {
var event = document.createEvent( "HTMLEvents", 1, 2 );
event.initEvent( type, true, true );
Meny.extend( event, properties );
element.dispatchEvent( event );
}
},
/**
* Retrieves query string as a key/value hash.
*/
getQuery: function() {
var query = {};
location.search.replace( /[A-Z0-9]+?=([\w|:|\/\.]*)/gi, function(a) {
query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
} );
return query;
}
};
return Meny;
}));
|