/*! Snap.js v2.0.0-rc1 */ (function(win, doc) { 'use strict'; // Our export var Namespace = 'Snap'; // Our main toolbelt var utils = { /** * Deeply extends two objects * @param {Object} destination The destination object * @param {Object} source The custom options to extend destination by * @return {Object} The desination object */ extend: function(destination, source) { var property; for (property in source) { if (source[property] && source[property].constructor && source[property].constructor === Object) { destination[property] = destination[property] || {}; utils.extend(destination[property], source[property]); } else { destination[property] = source[property]; } } return destination; } }; /** * Our Snap global that initializes our instance * @param {Object} opts The custom Snap.js options */ var Core = function( opts ) { var self = this; /** * Our default settings for a Snap instance * @type {Object} */ var settings = self.settings = { element: null, dragger: null, disable: 'none', addBodyClasses: true, hyperextensible: true, resistance: 0.5, flickThreshold: 50, transitionSpeed: 0.3, easing: 'ease', maxPosition: 266, minPosition: -266, tapToClose: true, touchToDrag: true, clickToDrag: true, slideIntent: 40, // degrees minDragDistance: 5 }; /** * Stores internally global data * @type {Object} */ var cache = self.cache = { isDragging: false, simpleStates: { opening: null, towards: null, hyperExtending: null, halfway: null, flick: null, translation: { absolute: 0, relative: 0, sinceDirectionChange: 0, percentage: 0 } } }; var eventList = self.eventList = {}; utils.extend(utils, { /** * Determines if we are interacting with a touch device * @type {Boolean} */ hasTouch: ('ontouchstart' in doc.documentElement || win.navigator.msPointerEnabled), /** * Returns the appropriate event type based on whether we are a touch device or not * @param {String} action The "action" event you're looking for: up, down, move, out * @return {String} The browsers supported event name */ eventType: function(action) { var eventTypes = { down: (utils.hasTouch ? 'touchstart' : settings.clickToDrag ? 'mousedown' : ''), move: (utils.hasTouch ? 'touchmove' : settings.clickToDrag ? 'mousemove' : ''), up: (utils.hasTouch ? 'touchend' : settings.clickToDrag ? 'mouseup': ''), out: (utils.hasTouch ? 'touchcancel' : settings.clickToDrag ? 'mouseout' : '') }; return eventTypes[action]; }, /** * Returns the correct "cursor" position on both browser and mobile * @param {String} t The coordinate to retrieve, either "X" or "Y" * @param {Object} e The event object being triggered * @return {Number} The desired coordiante for the events interaction */ page: function(t, e){ return (utils.hasTouch && e.touches.length && e.touches[0]) ? e.touches[0]['page'+t] : e['page'+t]; }, klass: { /** * Checks if an element has a class name * @param {Object} el The element to check * @param {String} name The class name to search for * @return {Boolean} Returns true if the class exists */ has: function(el, name){ return (el.className).indexOf(name) !== -1; }, /** * Adds a class name to an element * @param {Object} el The element to add to * @param {String} name The class name to add */ add: function(el, name){ if(!utils.klass.has(el, name) && settings.addBodyClasses){ el.className += " "+name; } }, /** * Removes a class name * @param {Object} el The element to remove from * @param {String} name The class name to remove */ remove: function(el, name){ if(utils.klass.has(el, name) && settings.addBodyClasses){ el.className = (el.className).replace(name, "").replace(/^\s+|\s+$/g, ''); } } }, /** * Dispatch a custom Snap.js event * @param {String} type The event name */ dispatchEvent: function(type) { if( typeof eventList[type] === 'function') { return eventList[type].apply(); } }, /** * Determines the browsers vendor prefix for CSS3 * @return {String} The browsers vendor prefix */ vendor: function(){ var tmp = doc.createElement("div"), prefixes = 'webkit Moz O ms'.split(' '), i; for (i in prefixes) { if (typeof tmp.style[prefixes[i] + 'Transition'] !== 'undefined') { return prefixes[i]; } } }, /** * Determines the browsers vendor prefix for transition callback events * @return {String} The event name */ transitionCallback: function(){ return (cache.vendor==='Moz' || cache.vendor==='ms') ? 'transitionend' : cache.vendor+'TransitionEnd'; }, /** * Determines if the users browser supports CSS3 transformations * @return {[type]} [description] */ canTransform: function(){ return typeof settings.element.style[cache.vendor+'Transform'] !== 'undefined'; }, /** * Determines an angle between two points * @param {Number} x The X coordinate * @param {Number} y The Y coordinate * @return {Number} The number of degrees between the two points */ angleOfDrag: function(x, y) { var degrees, theta; // Calc Theta theta = Math.atan2(-(cache.startDragY - y), (cache.startDragX - x)); if (theta < 0) { theta += 2 * Math.PI; } // Calc Degrees degrees = Math.floor(theta * (180 / Math.PI) - 180); if (degrees < 0 && degrees > -180) { degrees = 360 - Math.abs(degrees); } return Math.abs(degrees); }, events: { /** * Adds an event to an element * @param {Object} element Element to add event to * @param {String} eventName The event name * @param {Function} func Callback function */ addEvent: function addEvent(element, eventName, func) { if (element.addEventListener) { return element.addEventListener(eventName, func, false); } else if (element.attachEvent) { return element.attachEvent("on" + eventName, func); } }, /** * Removes an event to an element * @param {Object} element Element to remove event from * @param {String} eventName The event name * @param {Function} func Callback function */ removeEvent: function addEvent(element, eventName, func) { if (element.addEventListener) { return element.removeEventListener(eventName, func, false); } else if (element.attachEvent) { return element.detachEvent("on" + eventName, func); } }, /** * Prevents the default event * @param {Object} e The event object */ prevent: function(e) { if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; } } }, /** * Searches the parent element until a specified attribute has been matched * @param {Object} el The element to search from * @param {String} attr The attribute to search for * @return {Object|null} Returns a matched element if it exists, else, null */ parentUntil: function(el, attr) { var isStr = typeof attr === 'string'; while (el.parentNode) { if (isStr && el.getAttribute && el.getAttribute(attr)){ return el; } else if(!isStr && el === attr){ return el; } el = el.parentNode; } return null; } }); var action = self.action = { /** * Handles translating the elements position * @type {Object} */ translate: { get: { /** * Returns the amount an element is translated * @param {Number} index The index desired from the CSS3 values of translate3d * @return {Number} The amount of pixels an element is translated */ matrix: function(index) { if( !cache.canTransform ){ return parseInt(settings.element.style.left, 10); } else { var matrix = win.getComputedStyle(settings.element)[cache.vendor+'Transform'].match(/\((.*)\)/), ieOffset = 8; if (matrix) { matrix = matrix[1].split(','); // Internet Explorer likes to give us 16 fucking values if(matrix.length===16){ index+=ieOffset; } return parseInt(matrix[index], 10); } return 0; } } }, /** * Called when the element has finished transitioning */ easeCallback: function(fn){ settings.element.style[cache.vendor+'Transition'] = ''; cache.translation = action.translate.get.matrix(4); cache.easing = false; if(cache.easingTo===0){ utils.klass.remove(doc.body, 'snapjs-right'); utils.klass.remove(doc.body, 'snapjs-left'); } if( cache.once ){ cache.once.call(self, self.state()); delete cache.once; } utils.dispatchEvent('animated'); utils.events.removeEvent(settings.element, utils.transitionCallback(), action.translate.easeCallback); }, /** * Animates the pane by the specified amount of pixels * @param {Number} n The amount of pixels to move the pane */ easeTo: function(n, cb) { if( !cache.canTransform ){ cache.translation = n; action.translate.x(n); } else { cache.easing = true; cache.easingTo = n; settings.element.style[cache.vendor+'Transition'] = 'all ' + settings.transitionSpeed + 's ' + settings.easing; cache.once = cb; utils.events.addEvent(settings.element, utils.transitionCallback(), action.translate.easeCallback); action.translate.x(n); } if(n===0){ settings.element.style[cache.vendor+'Transform'] = ''; } }, /** * Immediately translates the element on its X axis * @param {Number} n Amount of pixels to translate */ x: function(n) { if( (settings.disable==='left' && n>0) || (settings.disable==='right' && n<0) ){ return; } if( !settings.hyperextensible ){ if( n===settings.maxPosition || n>settings.maxPosition ){ n=settings.maxPosition; } else if( n===settings.minPosition || n 0, translateTo = whileDragX, diff; // Shown no intent already if((cache.intentChecked && !cache.hasIntent)){ return; } if(settings.addBodyClasses){ if((absoluteTranslation)>0){ utils.klass.add(doc.body, 'snapjs-left'); utils.klass.remove(doc.body, 'snapjs-right'); } else if((absoluteTranslation)<0){ utils.klass.add(doc.body, 'snapjs-right'); utils.klass.remove(doc.body, 'snapjs-left'); } } if (cache.hasIntent === false || cache.hasIntent === null) { var deg = utils.angleOfDrag(thePageX, thePageY), inRightRange = (deg >= 0 && deg <= settings.slideIntent) || (deg <= 360 && deg > (360 - settings.slideIntent)), inLeftRange = (deg >= 180 && deg <= (180 + settings.slideIntent)) || (deg <= 180 && deg >= (180 - settings.slideIntent)); if (!inLeftRange && !inRightRange) { cache.hasIntent = false; } else { cache.hasIntent = true; } cache.intentChecked = true; } if ( (settings.minDragDistance>=Math.abs(thePageX-cache.startDragX)) || // Has user met minimum drag distance? (cache.hasIntent === false) ) { return; } utils.events.prevent(e); utils.dispatchEvent('drag'); cache.dragWatchers.current = thePageX; // Determine which direction we are going if (cache.dragWatchers.last > thePageX) { if (cache.dragWatchers.state !== 'left') { cache.dragWatchers.state = 'left'; cache.dragWatchers.hold = thePageX; } cache.dragWatchers.last = thePageX; } else if (cache.dragWatchers.last < thePageX) { if (cache.dragWatchers.state !== 'right') { cache.dragWatchers.state = 'right'; cache.dragWatchers.hold = thePageX; } cache.dragWatchers.last = thePageX; } if (openingLeft) { // Pulling too far to the right if (settings.maxPosition < absoluteTranslation) { diff = (absoluteTranslation - settings.maxPosition) * settings.resistance; translateTo = whileDragX - diff; } cache.simpleStates = { opening: 'left', towards: cache.dragWatchers.state, hyperExtending: settings.maxPosition < absoluteTranslation, halfway: absoluteTranslation > (settings.maxPosition / 2), flick: Math.abs(cache.dragWatchers.current - cache.dragWatchers.hold) > settings.flickThreshold, translation: { absolute: absoluteTranslation, relative: whileDragX, sinceDirectionChange: (cache.dragWatchers.current - cache.dragWatchers.hold), percentage: (absoluteTranslation/settings.maxPosition)*100 } }; } else { // Pulling too far to the left if (settings.minPosition > absoluteTranslation) { diff = (absoluteTranslation - settings.minPosition) * settings.resistance; translateTo = whileDragX - diff; } cache.simpleStates = { opening: 'right', towards: cache.dragWatchers.state, hyperExtending: settings.minPosition > absoluteTranslation, halfway: absoluteTranslation < (settings.minPosition / 2), flick: Math.abs(cache.dragWatchers.current - cache.dragWatchers.hold) > settings.flickThreshold, translation: { absolute: absoluteTranslation, relative: whileDragX, sinceDirectionChange: (cache.dragWatchers.current - cache.dragWatchers.hold), percentage: (absoluteTranslation/settings.minPosition)*100 } }; } action.translate.x(translateTo + translated); } }, /** * Fired when the user releases the content pane * @param {Object} e Event object */ endDrag: function(e) { if (cache.isDragging) { utils.dispatchEvent('end'); var translated = action.translate.get.matrix(4); // Tap Close if (cache.dragWatchers.current === 0 && translated !== 0 && settings.tapToClose) { utils.dispatchEvent('close'); utils.events.prevent(e); action.translate.easeTo(0); cache.isDragging = false; cache.startDragX = 0; return; } // Revealing Left if (cache.simpleStates.opening === 'left') { // Halfway, Flicking, or Too Far Out if ((cache.simpleStates.halfway || cache.simpleStates.hyperExtending || cache.simpleStates.flick)) { if (cache.simpleStates.flick && cache.simpleStates.towards === 'left') { // Flicking Closed action.translate.easeTo(0); } else if ( (cache.simpleStates.flick && cache.simpleStates.towards === 'right') || // Flicking Open OR (cache.simpleStates.halfway || cache.simpleStates.hyperExtending) // At least halfway open OR hyperextending ) { action.translate.easeTo(settings.maxPosition); // Open Left } } else { action.translate.easeTo(0); // Close Left } // Revealing Right } else if (cache.simpleStates.opening === 'right') { // Halfway, Flicking, or Too Far Out if ((cache.simpleStates.halfway || cache.simpleStates.hyperExtending || cache.simpleStates.flick)) { if (cache.simpleStates.flick && cache.simpleStates.towards === 'right') { // Flicking Closed action.translate.easeTo(0); } else if ( (cache.simpleStates.flick && cache.simpleStates.towards === 'left') || // Flicking Open OR (cache.simpleStates.halfway || cache.simpleStates.hyperExtending) // At least halfway open OR hyperextending ) { action.translate.easeTo(settings.minPosition); // Open Right } } else { action.translate.easeTo(0); // Close Right } } cache.isDragging = false; cache.startDragX = utils.page('X', e); } } } }; // Initialize if (opts.element) { utils.extend(settings, opts); cache.vendor = utils.vendor(); cache.canTransform = utils.canTransform(); action.drag.listen(); } }; utils.extend(Core.prototype, { /** * Opens the specified side menu * @param {String} side Must be "left" or "right" */ open: function(side, cb) { utils.dispatchEvent('open'); utils.klass.remove(doc.body, 'snapjs-expand-left'); utils.klass.remove(doc.body, 'snapjs-expand-right'); if (side === 'left') { this.cache.simpleStates.opening = 'left'; this.cache.simpleStates.towards = 'right'; utils.klass.add(doc.body, 'snapjs-left'); utils.klass.remove(doc.body, 'snapjs-right'); this.action.translate.easeTo(this.settings.maxPosition, cb); } else if (side === 'right') { this.cache.simpleStates.opening = 'right'; this.cache.simpleStates.towards = 'left'; utils.klass.remove(doc.body, 'snapjs-left'); utils.klass.add(doc.body, 'snapjs-right'); this.action.translate.easeTo(this.settings.minPosition, cb); } }, /** * Closes the pane */ close: function(cb) { utils.dispatchEvent('close'); this.action.translate.easeTo(0, cb); }, /** * Hides the content pane completely allowing for full menu visibility * @param {String} side Must be "left" or "right" */ expand: function(side){ var to = win.innerWidth || doc.documentElement.clientWidth; if(side==='left'){ utils.dispatchEvent('expandLeft'); utils.klass.add(doc.body, 'snapjs-expand-left'); utils.klass.remove(doc.body, 'snapjs-expand-right'); } else { utils.dispatchEvent('expandRight'); utils.klass.add(doc.body, 'snapjs-expand-right'); utils.klass.remove(doc.body, 'snapjs-expand-left'); to *= -1; } this.action.translate.easeTo(to); }, /** * Listen in to custom Snap events * @param {String} evt The snap event name * @param {Function} fn Callback function * @return {Object} Snap instance */ on: function(evt, fn) { this.eventList[evt] = fn; return this; }, /** * Stops listening to custom Snap events * @param {String} evt The snap event name */ off: function(evt) { if (this.eventList[evt]) { this.eventList[evt] = false; } }, /** * Enables Snap.js events */ enable: function() { utils.dispatchEvent('enable'); this.action.drag.listen(); }, /** * Disables Snap.js events */ disable: function() { utils.dispatchEvent('disable'); this.action.drag.stopListening(); }, /** * Updates the instances settings * @param {Object} opts The Snap options to set */ settings: function(opts){ utils.extend(this.settings, opts); }, /** * Returns information about the state of the content pane * @return {Object} Information regarding the state of the pane */ state: function() { var state, fromLeft = this.action.translate.get.matrix(4); if (fromLeft === this.settings.maxPosition) { state = 'left'; } else if (fromLeft === this.settings.minPosition) { state = 'right'; } else { state = 'closed'; } return { state: state, info: this.cache.simpleStates }; } }); // Assign to the global namespace this[Namespace] = Core; }).call(this, window, document);