568 lines
25 KiB
JavaScript
Executable file
568 lines
25 KiB
JavaScript
Executable file
/*
|
|
* Snap.js
|
|
*
|
|
* Copyright 2013, Jacob Kelley - http://jakiestfu.com/
|
|
* Released under the MIT Licence
|
|
* http://opensource.org/licenses/MIT
|
|
*
|
|
* Github: http://github.com/jakiestfu/Snap.js/
|
|
* Version: 1.9.3
|
|
*/
|
|
/*jslint browser: true*/
|
|
/*global define, module, ender*/
|
|
(function(win, doc) {
|
|
'use strict';
|
|
var Snap = Snap || function(userOpts) {
|
|
var 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,
|
|
slideIntent: 40, // degrees
|
|
minDragDistance: 5
|
|
},
|
|
cache = {
|
|
simpleStates: {
|
|
opening: null,
|
|
towards: null,
|
|
hyperExtending: null,
|
|
halfway: null,
|
|
flick: null,
|
|
translation: {
|
|
absolute: 0,
|
|
relative: 0,
|
|
sinceDirectionChange: 0,
|
|
percentage: 0
|
|
}
|
|
}
|
|
},
|
|
eventList = {},
|
|
utils = {
|
|
hasTouch: ('ontouchstart' in doc.documentElement || win.navigator.msPointerEnabled),
|
|
eventType: function(action) {
|
|
var eventTypes = {
|
|
down: (utils.hasTouch ? 'touchstart' : 'mousedown'),
|
|
move: (utils.hasTouch ? 'touchmove' : 'mousemove'),
|
|
up: (utils.hasTouch ? 'touchend' : 'mouseup'),
|
|
out: (utils.hasTouch ? 'touchcancel' : 'mouseout')
|
|
};
|
|
return eventTypes[action];
|
|
},
|
|
page: function(t, e){
|
|
return (utils.hasTouch && e.touches.length && e.touches[0]) ? e.touches[0]['page'+t] : e['page'+t];
|
|
},
|
|
klass: {
|
|
has: function(el, name){
|
|
return (el.className).indexOf(name) !== -1;
|
|
},
|
|
add: function(el, name){
|
|
if(!utils.klass.has(el, name) && settings.addBodyClasses){
|
|
el.className += " "+name;
|
|
}
|
|
},
|
|
remove: function(el, name){
|
|
if(settings.addBodyClasses){
|
|
el.className = (el.className).replace(name, "").replace(/^\s+|\s+$/g, '');
|
|
}
|
|
}
|
|
},
|
|
dispatchEvent: function(type) {
|
|
if (typeof eventList[type] === 'function') {
|
|
return eventList[type].call();
|
|
}
|
|
},
|
|
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];
|
|
}
|
|
}
|
|
},
|
|
transitionCallback: function(){
|
|
return (cache.vendor==='Moz' || cache.vendor==='ms') ? 'transitionend' : cache.vendor+'TransitionEnd';
|
|
},
|
|
canTransform: function(){
|
|
return typeof settings.element.style[cache.vendor+'Transform'] !== 'undefined';
|
|
},
|
|
deepExtend: function(destination, source) {
|
|
var property;
|
|
for (property in source) {
|
|
if (source[property] && source[property].constructor && source[property].constructor === Object) {
|
|
destination[property] = destination[property] || {};
|
|
utils.deepExtend(destination[property], source[property]);
|
|
} else {
|
|
destination[property] = source[property];
|
|
}
|
|
}
|
|
return destination;
|
|
},
|
|
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: {
|
|
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);
|
|
}
|
|
},
|
|
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);
|
|
}
|
|
},
|
|
prevent: function(e) {
|
|
if (e.preventDefault) {
|
|
e.preventDefault();
|
|
} else {
|
|
e.returnValue = false;
|
|
}
|
|
}
|
|
},
|
|
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;
|
|
}
|
|
},
|
|
action = {
|
|
translate: {
|
|
get: {
|
|
matrix: function(index) {
|
|
|
|
if( !utils.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(',');
|
|
if(matrix.length===16){
|
|
index+=ieOffset;
|
|
}
|
|
return parseInt(matrix[index], 10);
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
},
|
|
easeCallback: function(){
|
|
settings.element.style[cache.vendor+'Transition'] = '';
|
|
cache.translation = action.translate.get.matrix(4);
|
|
cache.easing = false;
|
|
clearInterval(cache.animatingInterval);
|
|
|
|
if(cache.easingTo===0){
|
|
utils.klass.remove(doc.body, 'snapjs-right');
|
|
utils.klass.remove(doc.body, 'snapjs-left');
|
|
}
|
|
|
|
utils.dispatchEvent('animated');
|
|
utils.events.removeEvent(settings.element, utils.transitionCallback(), action.translate.easeCallback);
|
|
},
|
|
easeTo: function(n) {
|
|
|
|
if( !utils.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.animatingInterval = setInterval(function() {
|
|
utils.dispatchEvent('animating');
|
|
}, 1);
|
|
|
|
utils.events.addEvent(settings.element, utils.transitionCallback(), action.translate.easeCallback);
|
|
action.translate.x(n);
|
|
}
|
|
if(n===0){
|
|
settings.element.style[cache.vendor+'Transform'] = '';
|
|
}
|
|
},
|
|
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<settings.minPosition ){
|
|
n=settings.minPosition;
|
|
}
|
|
}
|
|
|
|
n = parseInt(n, 10);
|
|
if(isNaN(n)){
|
|
n = 0;
|
|
}
|
|
|
|
if( utils.canTransform() ){
|
|
var theTranslate = 'translate3d(' + n + 'px, 0,0)';
|
|
settings.element.style[cache.vendor+'Transform'] = theTranslate;
|
|
} else {
|
|
settings.element.style.width = (win.innerWidth || doc.documentElement.clientWidth)+'px';
|
|
|
|
settings.element.style.left = n+'px';
|
|
settings.element.style.right = '';
|
|
}
|
|
}
|
|
},
|
|
drag: {
|
|
listen: function() {
|
|
cache.translation = 0;
|
|
cache.easing = false;
|
|
utils.events.addEvent(settings.element, utils.eventType('down'), action.drag.startDrag);
|
|
utils.events.addEvent(settings.element, utils.eventType('move'), action.drag.dragging);
|
|
utils.events.addEvent(settings.element, utils.eventType('up'), action.drag.endDrag);
|
|
},
|
|
stopListening: function() {
|
|
utils.events.removeEvent(settings.element, utils.eventType('down'), action.drag.startDrag);
|
|
utils.events.removeEvent(settings.element, utils.eventType('move'), action.drag.dragging);
|
|
utils.events.removeEvent(settings.element, utils.eventType('up'), action.drag.endDrag);
|
|
},
|
|
startDrag: function(e) {
|
|
// No drag on ignored elements
|
|
var target = e.target ? e.target : e.srcElement,
|
|
ignoreParent = utils.parentUntil(target, 'data-snap-ignore');
|
|
|
|
if (ignoreParent) {
|
|
utils.dispatchEvent('ignore');
|
|
return;
|
|
}
|
|
|
|
|
|
if(settings.dragger){
|
|
var dragParent = utils.parentUntil(target, settings.dragger);
|
|
|
|
// Only use dragger if we're in a closed state
|
|
if( !dragParent &&
|
|
(cache.translation !== settings.minPosition &&
|
|
cache.translation !== settings.maxPosition
|
|
)){
|
|
return;
|
|
}
|
|
}
|
|
|
|
utils.dispatchEvent('start');
|
|
settings.element.style[cache.vendor+'Transition'] = '';
|
|
cache.isDragging = true;
|
|
cache.hasIntent = null;
|
|
cache.intentChecked = false;
|
|
cache.startDragX = utils.page('X', e);
|
|
cache.startDragY = utils.page('Y', e);
|
|
cache.dragWatchers = {
|
|
current: 0,
|
|
last: 0,
|
|
hold: 0,
|
|
state: ''
|
|
};
|
|
cache.simpleStates = {
|
|
opening: null,
|
|
towards: null,
|
|
hyperExtending: null,
|
|
halfway: null,
|
|
flick: null,
|
|
translation: {
|
|
absolute: 0,
|
|
relative: 0,
|
|
sinceDirectionChange: 0,
|
|
percentage: 0
|
|
}
|
|
};
|
|
},
|
|
dragging: function(e) {
|
|
if (cache.isDragging && settings.touchToDrag) {
|
|
|
|
var thePageX = utils.page('X', e),
|
|
thePageY = utils.page('Y', e),
|
|
translated = cache.translation,
|
|
absoluteTranslation = action.translate.get.matrix(4),
|
|
whileDragX = thePageX - cache.startDragX,
|
|
openingLeft = absoluteTranslation > 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);
|
|
}
|
|
},
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
init = function(opts) {
|
|
if (opts.element) {
|
|
utils.deepExtend(settings, opts);
|
|
cache.vendor = utils.vendor();
|
|
action.drag.listen();
|
|
}
|
|
};
|
|
/*
|
|
* Public
|
|
*/
|
|
this.open = function(side) {
|
|
utils.dispatchEvent('open');
|
|
utils.klass.remove(doc.body, 'snapjs-expand-left');
|
|
utils.klass.remove(doc.body, 'snapjs-expand-right');
|
|
|
|
if (side === 'left') {
|
|
cache.simpleStates.opening = 'left';
|
|
cache.simpleStates.towards = 'right';
|
|
utils.klass.add(doc.body, 'snapjs-left');
|
|
utils.klass.remove(doc.body, 'snapjs-right');
|
|
action.translate.easeTo(settings.maxPosition);
|
|
} else if (side === 'right') {
|
|
cache.simpleStates.opening = 'right';
|
|
cache.simpleStates.towards = 'left';
|
|
utils.klass.remove(doc.body, 'snapjs-left');
|
|
utils.klass.add(doc.body, 'snapjs-right');
|
|
action.translate.easeTo(settings.minPosition);
|
|
}
|
|
};
|
|
this.close = function() {
|
|
utils.dispatchEvent('close');
|
|
action.translate.easeTo(0);
|
|
};
|
|
this.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;
|
|
}
|
|
action.translate.easeTo(to);
|
|
};
|
|
|
|
this.on = function(evt, fn) {
|
|
eventList[evt] = fn;
|
|
return this;
|
|
};
|
|
this.off = function(evt) {
|
|
if (eventList[evt]) {
|
|
eventList[evt] = false;
|
|
}
|
|
};
|
|
|
|
this.enable = function() {
|
|
utils.dispatchEvent('enable');
|
|
action.drag.listen();
|
|
};
|
|
this.disable = function() {
|
|
utils.dispatchEvent('disable');
|
|
action.drag.stopListening();
|
|
};
|
|
|
|
this.settings = function(opts){
|
|
utils.deepExtend(settings, opts);
|
|
};
|
|
|
|
this.state = function() {
|
|
var state,
|
|
fromLeft = action.translate.get.matrix(4);
|
|
if (fromLeft === settings.maxPosition) {
|
|
state = 'left';
|
|
} else if (fromLeft === settings.minPosition) {
|
|
state = 'right';
|
|
} else {
|
|
state = 'closed';
|
|
}
|
|
return {
|
|
state: state,
|
|
info: cache.simpleStates
|
|
};
|
|
};
|
|
init(userOpts);
|
|
};
|
|
if ((typeof module !== 'undefined') && module.exports) {
|
|
module.exports = Snap;
|
|
}
|
|
if (typeof ender === 'undefined') {
|
|
this.Snap = Snap;
|
|
}
|
|
if ((typeof define === "function") && define.amd) {
|
|
define("snap", [], function() {
|
|
return Snap;
|
|
});
|
|
}
|
|
}).call(this, window, document);
|