nextcloud/core/js/snap.js

786 lines
22 KiB
JavaScript
Executable File

/*! 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<settings.minPosition ){
n=settings.minPosition;
}
}
n = parseInt(n, 10);
if(isNaN(n)){
n = 0;
}
if( cache.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 = '';
}
}
},
/**
* Handles all the events that interface with dragging
* @type {Object}
*/
drag: {
/**
* Begins listening for drag events on our element
*/
listen: function() {
cache.translation = 0;
cache.easing = false;
utils.events.addEvent(self.settings.element, utils.eventType('down'), action.drag.startDrag);
utils.events.addEvent(self.settings.element, utils.eventType('move'), action.drag.dragging);
utils.events.addEvent(self.settings.element, utils.eventType('up'), action.drag.endDrag);
},
/**
* Stops listening for drag events on our element
*/
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);
},
/**
* Fired immediately when the user begins to drag the content pane
* @param {Object} e Event object
*/
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.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
}
};
},
/**
* Fired while the user is moving the content pane
* @param {Object} e Event object
*/
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);
}
},
/**
* 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);