/** * sp-slidemenu.js * * @version 0.1.0 * @url https://github.com/be-hase/sp-slidemenu * * Copyright 2013 be-hase.com. * Licensed under the MIT License: * http://www.opensource.org/licenses/mit-license.php */ ;(function(window, document, undefined) { "use strict"; var div, PREFIX, support, gestureStart, EVENTS, ANIME_SPEED, SLIDE_STATUS, SCROLL_STATUS, THRESHOLD, EVENT_MOE_TIME, rclass, ITEM_CLICK_CLASS_NAME, defaults; div = document.createElement('div'); PREFIX = ['webkit', 'moz', 'o', 'ms']; support = SpSlidemenu.support = {}; support.transform3d = hasProp([ 'perspectiveProperty', 'WebkitPerspective', 'MozPerspective', 'OPerspective', 'msPerspective' ]); support.transform = hasProp([ 'transformProperty', 'WebkitTransform', 'MozTransform', 'OTransform', 'msTransform' ]); support.transition = hasProp([ 'transitionProperty', 'WebkitTransitionProperty', 'MozTransitionProperty', 'OTransitionProperty', 'msTransitionProperty' ]); support.addEventListener = 'addEventListener' in window; support.msPointer = window.navigator.msPointerEnabled; support.cssAnimation = (support.transform3d || support.transform) && support.transition; support.touch = 'ontouchend' in window; EVENTS = { start: { touch: 'touchstart', mouse: 'mousedown' }, move: { touch: 'touchmove', mouse: 'mousemove' }, end: { touch: 'touchend', mouse: 'mouseup' } }; defaults = { main : "#main", button : ".menu-button", slidemenu : ".slidemenu", slidemenu_header : ".slidemenu-header", slidemenu_body : ".slidemenu-body", slidemenu_content : ".slidemenu-content", direction : 'left' }; gestureStart = false; if (support.addEventListener) { document.addEventListener('gesturestart', function() { gestureStart = true; }); document.addEventListener('gestureend', function() { gestureStart = false; }); } ANIME_SPEED = { slider: 300, scrollOverBack: 400 }; SLIDE_STATUS = { close: 0, open: 1, progress: 2 }; THRESHOLD = 10; EVENT_MOE_TIME = 50; rclass = /[\t\r\n\f]/g; ITEM_CLICK_CLASS_NAME = 'menu-item'; /* [MEMO] SpSlidemenu properties which is not function is ... -- element -- element: main element: slidemenu element: button element: slidemenuBody element: slidemenuContent element: slidemenuHeader -- options -- bool: disableCssAnimation bool: disabled3d -- animation -- bool: useCssAnimation bool: use3d -- slide -- int: slideWidth string: htmlOverflowX string: bodyOverflowX int: buttonStartPageX int: buttonStartPageY -- scroll -- bool: scrollTouchStarted bool: scrollMoveReady int: scrollStartPageX int: scrollStartPageY int: scrollBasePageY int: scrollTimeForVelocity int: scrollCurrentY int: scrollMoveEventCnt int: scrollAnimationTimer int: scrollOverTimer int: scrollMaxY */ function SpSlidemenu(options) { if (this instanceof SpSlidemenu) { return this.init(options); } else { return new SpSlidemenu(options); } } SpSlidemenu.prototype.init = function(options) { var _this = this; for(var k in defaults) { if (!options.hasOwnProperty(k)){ options[k] = defaults[k]; } } // find and set element. _this.setElement(options); if (!_this.main || !_this.slidemenu || !_this.button || !_this.slidemenuBody || !_this.slidemenuContent) { throw new Error('Element not found. Please set correctly.'); } _this.disableCssAnimation = (options.disableCssAnimation === undefined) ? false : options.disableCssAnimation; _this.disable3d = (options.disable3d === undefined) ? false : options.disable3d; _this.direction = 'left'; if (options.direction === 'right') { _this.direction = 'right'; } // animation _this.useCssAnimation = support.cssAnimation; if (_this.disableCssAnimation === true) { _this.useCssAnimation = false; } _this.use3d = support.transform3d; if (_this.disable3d === true) { _this.use3d = false; } // slide _this.slideWidth = (getDimentions(_this.slidemenu)).width; _this.main.SpSlidemenuStatus = SLIDE_STATUS.close; _this.htmlOverflowX = ''; _this.bodyOverflowX = ''; // scroll _this.scrollCurrentY = 0; _this.scrollAnimationTimer = false; _this.scrollOverTimer = false; // set default style. _this.setDefaultStyle(); // bind some method for callback. _this.bindMethods(); // add event addTouchEvent('start', _this.button, _this.buttonTouchStart, false); addTouchEvent('move', _this.button, blockEvent, false); addTouchEvent('end', _this.button, _this.buttonTouchEnd, false); addTouchEvent('start', _this.slidemenuContent, _this.scrollTouchStart, false); addTouchEvent('move', _this.slidemenuContent, _this.scrollTouchMove, false); addTouchEvent('end', _this.slidemenuContent, _this.scrollTouchEnd, false); _this.slidemenuContent.addEventListener('click', _this.itemClick, false); // window size change window.addEventListener('resize', debounce(_this.setHeight, 100), false); return _this; }; SpSlidemenu.prototype.bindMethods = function() { var _this, funcs; _this = this; funcs = [ 'setHeight', 'slideOpen', 'slideOpenEnd', 'slideClose', 'slideCloseEnd', 'buttonTouchStart', 'buttonTouchEnd', 'mainTouchStart', 'scrollTouchStart', 'scrollTouchMove', 'scrollTouchEnd', 'scrollInertiaMove', 'scrollOverBack', 'scrollOver', 'itemClick' ]; funcs.forEach(function(func) { _this[func] = bind(_this[func], _this); }); }; SpSlidemenu.prototype.setElement = function(options) { var _this = this; for(var k in options) { if (typeof options[k] !== "string") { return false; } } _this.main = document.querySelector(options.main); _this.slidemenu = document.querySelector(options.slidemenu); _this.button = document.querySelector(options.button); _this.slidemenuBody = document.querySelector(options.slidemenu_body); _this.slidemenuContent = document.querySelector(options.slidemenu_content); _this.slidemenuHeader = document.querySelector(options.slidemenu_header); }; SpSlidemenu.prototype.setDefaultStyle = function() { var _this = this; if (support.msPointer) { _this.slidemenuContent.style.msTouchAction = 'none'; } _this.setHeight(); if (_this.useCssAnimation) { setStyles(_this.main, { transitionProperty: getCSSName('transform'), transitionTimingFunction: 'ease-in-out', transitionDuration: ANIME_SPEED.slider + 'ms', transitionDelay: '0ms', transform: _this.getTranslateX(0) }); setStyles(_this.slidemenu, { transitionProperty: 'visibility', transitionTimingFunction: 'linear', transitionDuration: '0ms', transitionDelay: ANIME_SPEED.slider + 'ms' }); setStyles(_this.slidemenuContent, { transitionProperty: getCSSName('transform'), transitionTimingFunction: 'ease-in-out', transitionDuration: '0ms', transitionDelay: '0ms', transform: _this.getTranslateY(0) }); } else { setStyles(_this.main, { position: 'relative', left: '0px' }); setStyles(_this.slidemenuContent, { top: '0px' }); } }; SpSlidemenu.prototype.setHeight = function(event) { var _this, browserHeight; _this = this; browserHeight = getBrowserHeight(); setStyles(_this.main, { minHeight: browserHeight + 'px' }); setStyles(_this.slidemenu, { height: browserHeight + 'px' }); }; SpSlidemenu.prototype.buttonTouchStart = function(event) { var _this = this; event.preventDefault(); event.stopPropagation(); switch(_this.main.SpSlidemenuStatus) { case SLIDE_STATUS.progress: break; case SLIDE_STATUS.open: case SLIDE_STATUS.close: _this.buttonStartPageX = getPage(event, 'pageX'); _this.buttonStartPageY = getPage(event, 'pageY'); break; } }; SpSlidemenu.prototype.buttonTouchEnd = function(event) { var _this = this; event.preventDefault(); event.stopPropagation(); if (_this.shouldTrigerNext(event)) { switch(_this.main.SpSlidemenuStatus) { case SLIDE_STATUS.progress: break; case SLIDE_STATUS.open: _this.slideClose(event); break; case SLIDE_STATUS.close: _this.slideOpen(event); break; } } }; SpSlidemenu.prototype.mainTouchStart = function(event) { var _this = this; event.preventDefault(); event.stopPropagation(); _this.slideClose(event); }; SpSlidemenu.prototype.shouldTrigerNext = function(event) { var _this = this, buttonEndPageX = getPage(event, 'pageX'), buttonEndPageY = getPage(event, 'pageY'), deltaX = Math.abs(buttonEndPageX - _this.buttonStartPageX), deltaY = Math.abs(buttonEndPageY - _this.buttonStartPageY); return deltaX < 20 && deltaY < 20; }; SpSlidemenu.prototype.slideOpen = function(event) { var _this = this, toX; if (_this.direction === 'left') { toX = _this.slideWidth; } else { toX = -_this.slideWidth; } _this.main.SpSlidemenuStatus = SLIDE_STATUS.progress; //set event addTouchEvent('move', document, blockEvent, false); // change style _this.htmlOverflowX = document.documentElement.style['overflowX']; _this.bodyOverflowX = document.body.style['overflowX']; document.documentElement.style['overflowX'] = document.body.style['overflowX'] = 'hidden'; if (_this.useCssAnimation) { setStyles(_this.main, { transform: _this.getTranslateX(toX) }); setStyles(_this.slidemenu, { transitionProperty: 'z-index', visibility: 'visible', zIndex: '1' }); } else { animate(_this.main, _this.direction, toX, ANIME_SPEED.slider); setStyles(_this.slidemenu, { visibility: 'visible' }); } // set callback setTimeout(_this.slideOpenEnd, ANIME_SPEED.slider + EVENT_MOE_TIME); }; SpSlidemenu.prototype.slideOpenEnd = function() { var _this = this; _this.main.SpSlidemenuStatus = SLIDE_STATUS.open; // change style if (_this.useCssAnimation) { } else { setStyles(_this.slidemenu, { zIndex: '1' }); } // add event addTouchEvent('start', _this.main, _this.mainTouchStart, false); }; SpSlidemenu.prototype.slideClose = function(event) { var _this = this; _this.main.SpSlidemenuStatus = SLIDE_STATUS.progress; //event removeTouchEvent('start', _this.main, _this.mainTouchStart, false); // change style if (_this.useCssAnimation) { setStyles(_this.slidemenu, { transitionProperty: 'visibility', visibility: 'hidden', zIndex: '-1' }); setTimeout( function() { setStyles(_this.main, { transform: _this.getTranslateX(0) }); }, 50); } else { animate(_this.main, _this.direction, 0, ANIME_SPEED.slider); setStyles(_this.slidemenu, { zIndex: '-1' }); } // set callback setTimeout(_this.slideCloseEnd, ANIME_SPEED.slider + EVENT_MOE_TIME); }; SpSlidemenu.prototype.slideCloseEnd = function() { var _this = this; _this.main.SpSlidemenuStatus = SLIDE_STATUS.close; // change style document.documentElement.style['overflowX'] = _this.htmlOverflowX; document.body.style['overflowX'] = _this.bodyOverflowX; if (_this.useCssAnimation) { } else { setStyles(_this.slidemenu, { visibility: 'hidden' }); } // set event removeTouchEvent('move', document, blockEvent, false); }; SpSlidemenu.prototype.scrollTouchStart = function(event) { var _this = this; if (gestureStart) { return; } if (_this.scrollOverTimer !== false) { clearTimeout(_this.scrollOverTimer); } _this.scrollCurrentY = _this.getScrollCurrentY(); if (_this.useCssAnimation) { setStyles(_this.slidemenuContent, { transitionTimingFunction: 'ease-in-out', transitionDuration: '0ms', transform: _this.getTranslateY(_this.scrollCurrentY) }); } else { _this.stopScrollAnimate(); setStyles(_this.slidemenuContent, { top: _this.scrollCurrentY + 'px' }); } _this.scrollOverTimer = false; _this.scrollAnimationTimer = false; _this.scrollTouchStarted = true; _this.scrollMoveReady = false; _this.scrollMoveEventCnt = 0; _this.scrollMaxY = _this.calcMaxY(); _this.scrollStartPageX = getPage(event, 'pageX'); _this.scrollStartPageY = getPage(event, 'pageY'); _this.scrollBasePageY = _this.scrollStartPageY; _this.scrollTimeForVelocity = event.timeStamp; _this.scrollPageYForVelocity = _this.scrollStartPageY; _this.slidemenuContent.removeEventListener('click', blockEvent, true); }; SpSlidemenu.prototype.scrollTouchMove = function(event) { var _this, pageX, pageY, distY, newY, deltaX, deltaY; _this = this; if (!_this.scrollTouchStarted || gestureStart) { return; } pageX = getPage(event, 'pageX'); pageY = getPage(event, 'pageY'); if (_this.scrollMoveReady) { event.preventDefault(); event.stopPropagation(); distY = pageY - _this.scrollBasePageY; newY = _this.scrollCurrentY + distY; if (newY > 0 || newY < _this.scrollMaxY) { newY = Math.round(_this.scrollCurrentY + distY / 3); } _this.scrollSetY(newY); if (_this.scrollMoveEventCnt % THRESHOLD === 0) { _this.scrollPageYForVelocity = pageY; _this.scrollTimeForVelocity = event.timeStamp; } _this.scrollMoveEventCnt++; } else { deltaX = Math.abs(pageX - _this.scrollStartPageX); deltaY = Math.abs(pageY - _this.scrollStartPageY); if (deltaX > 5 || deltaY > 5) { _this.scrollMoveReady = true; _this.slidemenuContent.addEventListener('click', blockEvent, true); } } _this.scrollBasePageY = pageY; }; SpSlidemenu.prototype.scrollTouchEnd = function(event) { var _this, speed, deltaY, deltaTime; _this = this; if (!_this.scrollTouchStarted) { return; } _this.scrollTouchStarted = false; _this.scrollMaxY = _this.calcMaxY(); if (_this.scrollCurrentY > 0 || _this.scrollCurrentY < _this.scrollMaxY) { _this.scrollOverBack(); return; } deltaY = getPage(event, 'pageY') - _this.scrollPageYForVelocity; deltaTime = event.timeStamp - _this.scrollTimeForVelocity; speed = deltaY / deltaTime; if (Math.abs(speed) >= 0.01) { _this.scrollInertia(speed); } }; SpSlidemenu.prototype.scrollInertia = function(speed) { var _this, directionToTop, maxTo, distanceMaxTo, stopTime, canMove, to, duration, speedAtboundary, nextTo; _this = this; if (speed > 0) { directionToTop = true; maxTo = 0; } else { directionToTop = false; maxTo = _this.scrollMaxY; } distanceMaxTo = Math.abs(_this.scrollCurrentY - maxTo); speed = Math.abs(750 * speed); if (speed > 1000) { speed = 1000; } stopTime = speed / 500; canMove = (speed * stopTime) - ((500 * Math.pow(stopTime, 2)) / 2); if (canMove <= distanceMaxTo) { if (directionToTop) { to = _this.scrollCurrentY + canMove; } else { to = _this.scrollCurrentY - canMove; } duration = stopTime * 1000; _this.scrollInertiaMove(to, duration, false); } else { to = maxTo; speedAtboundary = Math.sqrt((2 * 500 * distanceMaxTo) + Math.pow(speed, 2)); duration = (speedAtboundary - speed) / 500 * 1000; _this.scrollInertiaMove(to, duration, true, speedAtboundary, directionToTop); } }; SpSlidemenu.prototype.scrollInertiaMove = function(to, duration, isOver, speed, directionToTop) { var _this = this, stopTime, canMove; _this.scrollCurrentY = to; if (_this.useCssAnimation) { setStyles(_this.slidemenuContent,{ transitionTimingFunction: 'cubic-bezier(0.33, 0.66, 0.66, 1)', transitionDuration: duration + 'ms', transform: _this.getTranslateY(to) }); } else { _this.scrollAnimate(to, duration); } if (!isOver) { return; } stopTime = speed / 7500; canMove = (speed * stopTime) - ((7500 * Math.pow(stopTime, 2)) / 2); if (directionToTop) { to = _this.scrollCurrentY + canMove; } else { to = _this.scrollCurrentY - canMove; } duration = stopTime * 1000; _this.scrollOver(to, duration); }; SpSlidemenu.prototype.scrollOver = function(to, duration) { var _this; _this = this; _this.scrollCurrentY = to; if (_this.useCssAnimation) { setStyles(_this.slidemenuContent,{ transitionTimingFunction: 'cubic-bezier(0.33, 0.66, 0.66, 1)', transitionDuration: duration + 'ms', transform: _this.getTranslateY(to) }); } else { _this.scrollAnimate(to, duration); } _this.scrollOverTimer = setTimeout(_this.scrollOverBack, duration); }; SpSlidemenu.prototype.scrollOverBack = function() { var _this, to; _this = this; if (_this.scrollCurrentY >= 0) { to = 0; } else { to = _this.scrollMaxY; } _this.scrollCurrentY = to; if (_this.useCssAnimation) { setStyles(_this.slidemenuContent,{ transitionTimingFunction: 'ease-out', transitionDuration: ANIME_SPEED.scrollOverBack + 'ms', transform: _this.getTranslateY(to) }); } else { _this.scrollAnimate(to, ANIME_SPEED.scrollOverBack); } }; SpSlidemenu.prototype.scrollSetY = function(y) { var _this = this; _this.scrollCurrentY = y; if (_this.useCssAnimation) { setStyles(_this.slidemenuContent,{ transitionTimingFunction: 'ease-in-out', transitionDuration: '0ms', transform: _this.getTranslateY(y) }); } else { _this.slidemenuContent.style.top = y + 'px'; } }; SpSlidemenu.prototype.scrollAnimate = function(to, transitionDuration) { var _this = this; _this.stopScrollAnimate(); _this.scrollAnimationTimer = animate(_this.slidemenuContent, 'top', to, transitionDuration); }; SpSlidemenu.prototype.stopScrollAnimate = function() { var _this = this; if (_this.scrollAnimationTimer !== false) { clearInterval(_this.scrollAnimationTimer); } }; SpSlidemenu.prototype.itemClick = function(event) { var elem = event.target || event.srcElement; if (hasClass(elem, ITEM_CLICK_CLASS_NAME)) { this.slideClose(); } }; SpSlidemenu.prototype.calcMaxY = function(x) { var _this, contentHeight, bodyHeight, headerHeight; _this = this; contentHeight = _this.slidemenuContent.offsetHeight; bodyHeight = _this.slidemenuBody.offsetHeight; headerHeight = 0; if (_this.slidemenuHeader) { headerHeight = _this.slidemenuHeader.offsetHeight; } if (contentHeight > bodyHeight) { return - (contentHeight - bodyHeight + headerHeight); } else { return 0; } }; SpSlidemenu.prototype.getScrollCurrentY = function() { var ret = 0; if (this.useCssAnimation) { getStyle(window.getComputedStyle(this.slidemenuContent, ''), 'transform').split(',').forEach(function(value){ var number = parseInt(value, 10); if (!isNaN(number) && number !== 0 && number !== 1) { ret = number; } }); } else { var number = parseInt(getStyle(window.getComputedStyle(this.slidemenuContent, ''), 'top'), 10); if (!isNaN(number) && number !== 0 && number !== 1) { ret = number; } } return ret; }; SpSlidemenu.prototype.getTranslateX = function(x) { var _this = this; return _this.use3d ? 'translate3d(' + x + 'px, 0px, 0px)' : 'translate(' + x + 'px, 0px)'; }; SpSlidemenu.prototype.getTranslateY = function(y) { var _this = this; return _this.use3d ? 'translate3d(0px, ' + y + 'px, 0px)' : 'translate(0px, ' + y + 'px)'; }; //Utility Function function hasProp(props) { return some(props, function(prop) { return div.style[prop] !== undefined; }); } function upperCaseFirst(str) { return str.charAt(0).toUpperCase() + str.substr(1); } function some(ary, callback) { var i, len; for (i = 0, len = ary.length; i < len; i++) { if (callback(ary[i], i)) { return true; } } return false; } function setStyle(elem, prop, val) { var style = elem.style; if (!setStyle.cache) { setStyle.cache = {}; } if (setStyle.cache[prop] !== undefined) { style[setStyle.cache[prop]] = val; return; } if (style[prop] !== undefined) { setStyle.cache[prop] = prop; style[prop] = val; return; } some(PREFIX, function(_prefix) { var _prop = upperCaseFirst(_prefix) + upperCaseFirst(prop); if (style[_prop] !== undefined) { //setStyle.cache[prop] = _prop; style[_prop] = val; return true; } }); } function setStyles(elem, styles) { var style, prop; for (prop in styles) { if (styles.hasOwnProperty(prop)) { setStyle(elem, prop, styles[prop]); } } } function getStyle(style, prop) { var ret; if (style[prop] !== undefined) { return style[prop]; } some(PREFIX, function(_prefix) { var _prop = upperCaseFirst(_prefix) + upperCaseFirst(prop); if (style[_prop] !== undefined) { ret = style[_prop]; return true; } }); return ret; } function getCSSName(prop) { var ret; if (!getCSSName.cache) { getCSSName.cache = {}; } if (getCSSName.cache[prop] !== undefined) { return getCSSName.cache[prop]; } if (div.style[prop] !== undefined) { getCSSName.cache[prop] = prop; return prop; } some(PREFIX, function(_prefix) { var _prop = upperCaseFirst(_prefix) + upperCaseFirst(prop); if (div.style[_prop] !== undefined) { ret = '-' + _prefix + '-' + prop; return true; } }); getCSSName.cache[prop] = ret; return ret; } function bind(func, context) { var nativeBind, slice, args; nativeBind = Function.prototype.bind; slice = Array.prototype.slice; if (func.bind === nativeBind && nativeBind) { return nativeBind.apply(func, slice.call(arguments, 1)); } args = slice.call(arguments, 2); return function() { return func.apply(context, args.concat(slice.call(arguments))); }; } function blockEvent(event) { event.preventDefault(); event.stopPropagation(); } function getDimentions(element) { var previous, key, properties, result; previous = {}; properties = { position: 'absolute', visibility: 'hidden', display: 'block' }; for (key in properties) { previous[key] = element.style[key]; element.style[key] = properties[key]; } result = { width: element.offsetWidth, height: element.offsetHeight }; for (key in properties) { element.style[key] = previous[key]; } return result; } function getPage(event, page) { return event.changedTouches ? event.changedTouches[0][page] : event[page]; } function addTouchEvent(eventType, element, listener, useCapture) { useCapture = useCapture || false; if (support.touch) { element.addEventListener(EVENTS[eventType].touch, listener, useCapture); } else { element.addEventListener(EVENTS[eventType].mouse, listener, useCapture); } } function removeTouchEvent(eventType, element, listener, useCapture) { useCapture = useCapture || false; if (support.touch) { element.removeEventListener(EVENTS[eventType].touch, listener, useCapture); } else { element.removeEventListener(EVENTS[eventType].mouse, listener, useCapture); } } function hasClass(elem, className) { className = " " + className + " "; if (elem.nodeType === 1 && (" " + elem.className + " ").replace(rclass, " ").indexOf(className) >= 0 ) { return true; } return false; } function animate(elem, prop, to, transitionDuration) { var begin, from, duration, easing, timer; begin = +new Date(); from = parseInt(elem.style[prop], 10); to = parseInt(to, 10); duration = parseInt(transitionDuration, 10); easing = function(time, duration) { return -(time /= duration) * (time - 2); }; timer = setInterval(function() { var time, pos, now; time = new Date() - begin; if (time > duration) { clearInterval(timer); now = to; } else { pos = easing(time, duration); now = pos * (to - from) + from; } elem.style[prop] = now + 'px'; }, 10); return timer; } function getBrowserHeight() { if ( window.innerHeight ) { return window.innerHeight; } else if ( document.documentElement && document.documentElement.clientHeight !== 0 ) { return document.documentElement.clientHeight; } else if ( document.body ) { return document.body.clientHeight; } return 0; } function debounce(func, wait, immediate) { var timeout, result; return function() { var context = this, args = arguments; var later = function() { timeout = null; if (!immediate) result = func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) result = func.apply(context, args); return result; }; } window.SpSlidemenu = SpSlidemenu; })(window, window.document);